diff --git a/webv4/pages/001-forum.xjs b/webv4/pages/001-forum.xjs
index 85ace20d863e7fc622e88cba12f8de87f007fb4b..0f2f3eadc23c250d19a13bda74d2497de1b191df 100644
--- a/webv4/pages/001-forum.xjs
+++ b/webv4/pages/001-forum.xjs
@@ -1,6 +1,6 @@
-<!--HIDDEN:New Forum-->
+<!--HIDDEN:XJS Forum-->
 
-<script type="text/javascript" src="./js/forum.js"></script>
+<script type="text/javascript" src="./js/xjs-forum.js"></script>
 
 <?xjs
     load(settings.web_lib + 'request.js');
@@ -10,12 +10,67 @@
 
 <!-- To do: Search (forum, group, sub, or thread), Sortation and data-attributes -->
 
+<div data-loading-template hidden>
+	<li class="list-group-item striped">
+		<? locale.write('loading_message', 'main'); ?>
+		<span data-spinning-cursor></span>
+	</li>
+</div>
+
+<!-- Viewing a thread (list of messages) -->
 <? if (Request.has_param('sub') && Request.has_param('thread')) { ?>
 
-    <?xjs
-        var offset = Request.has_param('offset') ? parseInt(Request.get_param('offset')) : 0;
-        var thread = getMessageThreads(Request.get_param('sub'), settings.max_messages).thread[Request.get_param('thread')];
-    ?>
+    <? var thread = getMessageThreads(Request.get_param('sub'), settings.max_messages).thread[Request.get_param('thread')]; ?>
+	
+	<script type="text/javascript" src="./js/graphics-converter.js"></script>
+	<script type="text/javascript" src="./js/avatars.js"></script>
+	
+	<div hidden>
+		<li id="forum-message-template" class="list-group-item striped">
+			<a data-message-anchor></a>
+			<h4>
+				<strong data-message-subject></strong>
+			</h4>
+			<div class="clearfix">
+				<div class="pull-left" style="margin-right:1em;" data-avatar></div>
+				<div>
+					<div data-badge-unread hidden>
+						<span class="badge unread">
+							<? locale.write('label_new_message'); ?>
+						</span>
+					</div>
+					<? locale.write('label_message_from'); ?> 
+					<strong data-message-from></strong><span data-message-from-address></span>
+					<br />
+					<? locale.write('label_message_to'); ?> 
+					<strong data-message-to></strong>
+					<br />
+					<? locale.write('label_message_date'); ?> 
+					<strong data-message-date></strong>
+				</div>
+			</div>
+			<div class="message" data-message-body></div>
+			<div class="clearfix">
+				<button class="btn btn-default icon" title="<? locale.write('label_delete'); ?>" aria-label="<? locale.write('label_delete'); ?>">
+					<span class="glyphicon glyphicon-trash"></span>
+				</button>
+				<a class="btn btn-default icon" title="<? locale.write('label_direct_link'); ?>" aria-label="<? locale.write('label_direct_link'); ?>" href="#" data-direct-link>
+					<span class="glyphicon glyphicon-link"></span>
+				</a>
+				<button class="btn btn-default icon" title="<? locale.write('label_reply'); ?>" aria-label="<? locale.write('label_reply'); ?>">
+					<span class="glyphicon glyphicon-comment"></span>
+				</button>
+				<button class="btn-uv btn btn-default icon" data-button-upvote>
+					<span title="Upvotes" class="glyphicon glyphicon-arrow-up"></span>
+					<span title="Upvotes" data-upvote-count>0</span>
+				</button>
+				<button class="btn-dv btn btn-default icon" data-button-downvote>
+					<span title="Downvotes" class="glyphicon glyphicon-arrow-down"></span>
+					<span title="Downvotes" data-downvote-count>0</span>
+				</button>
+			</div>
+		</li>
+	</div>
 
     <ol class="breadcrumb">
         <li>
@@ -40,19 +95,185 @@
         </li>
     </ol>
 
-    <div id="jump-unread-container" style="margin-bottom:1em;">
-        <a class="btn btn-default" id="jump-unread" title="Jump to first unread message" href="#">
-            <span class="glyphicon glyphicon-star"></span>
+    <div id="jump-unread-container" style="margin-bottom:1em;" hidden>
+        <a class="btn btn-default" id="jump-unread" title="<? locale.write('button_jump_to_unread'); ?>" href="#">
+            <? locale.write('label_new_message'); ?>
         </a>
     </div>
 
-    <div id="forum-list-container" class="list-group">
+	<div id="forum-list-container" class="list-group"></div>
+	<div id="forum-load-more-messages" class="text-center" hidden>
+		<button id="load-more-messages" class="btn btn-primary" aria-label="<? locale.write('button_load_more', 'main'); ?>" title="<? locale.write('button_load_more', 'main'); ?>" onclick="listMessages('<? Request.write_param('sub'); ?>', '<? Request.write_param('thread'); ?>', <? write(settings.page_size); ?>, true)">
+			<? locale.write('button_load_more', 'main'); ?>
+		</button>
+	</div>
 
-    </div>
+	<script type="text/javascript">
+		listMessages('<? Request.write_param('sub'); ?>', <? Request.write_param('thread'); ?>, <? write(settings.page_size); ?>);
+		// Should subscribe here for updates to the thread; need to add some functions to lib/forum.js, lib/events/forum.js, root/js/xjs-forum.js
+	</script>
+	
+	<!-- get n messages of thread and append to forum-list-container -->
 
+<!-- Viewing a sub (list of threads) -->
 <? } else if (Request.has_param('sub') && msg_area.sub[Request.get_param('sub')] !== undefined) { ?>
 
-    <ol class="breadcrumb">
+	<!-- Template for a link to a thread -->
+	<div hidden>
+		<a id="forum-thread-link-template" href="./?page=<? Request.write_param('page'); ?>&sub=<? Request.write_param('sub'); ?>" class="list-group-item striped">
+			<div class="row">
+				<div class="col-sm-9">
+					<strong data-thread-subject></strong>
+					<p>
+						<? locale.write('label_thread_from'); ?> 
+						<strong data-thread-from></strong> 
+						<? locale.write('label_message_date'); ?> 
+						<span data-thread-date-start></span>
+					</p>
+					<div data-replies hidden>
+						<strong data-message-count></strong>
+						<span data-suffix-reply hidden><? locale.write('suffix_reply_count'); ?>.</span>
+						<span data-suffix-replies hidden><? locale.write('suffix_replies_count'); ?>.</span>
+						<? locale.write('label_thread_latest_reply'); ?>
+						<strong data-last-from></strong>
+						<? locale.write('label_message_date'); ?> 
+						<span data-last-time></span>
+					</div>
+				</div>
+				<div class="col-sm-3">
+					<div class="pull-right" data-stats hidden>
+						<? if (is_user()) { ?>
+							<span data-unread-messages title="<? locale.write('badge_unread_messages'); ?>" class="badge" hidden></span>
+						<? } ?>
+						<span data-upvotes-badge title="<? locale.write('badge_upvotes'); ?>" class="badge upvote-bg" style="display:none;">
+							<span class="glyphicon glyphicon-arrow-up"></span>
+							<span data-upvotes></span>
+						</span>
+						<span data-downvotes-badge title="<? locale.write('badge_downvotes'); ?>" class="badge downvote-bg" style="display:none;">
+							<span class="glyphicon glyphicon-arrow-down"></span>
+							<span data-downvotes></span>
+						</span>
+					</div>
+				</div>
+			</div>
+		</a>
+	</div>
+
+	<!-- Template for a new message input -->
+    <div id="forum-new-message-template" hidden>
+        <input class="form-control" type="text" placeholder="<? locale.write('label_message_to'); ?>" />
+        <br />
+        <input class="form-control" type="text" placeholder="<? locale.write('label_message_subject'); ?>" />
+        <br />
+        <textarea class="form-control" rows="8"></textarea>
+        <br />
+		<input class="btn btn-primary" type="submit" value="<? locale.write('button_submit', 'main') ?>" onclick="postNew('SUB')" />
+    </div>
+
+	<!-- Template for a message reply input -->
+    <div id="forum-message-reply-template" class="reply" hidden>
+		<strong>
+            <? locale.write('label_form_reply'); ?>
+        </strong>
+		<textarea rows="8" class="form-control reply" id="replytext-ID"></textarea>
+        <button id="quote-ID" class="btn" onclick="quotify('ID')">
+            <? locale.write('button_quote_message'); ?>
+        </button>
+		<input id="reply-button-ID" class="btn btn-primary" type="submit" value="Submit" onclick="postReply('SUB', 'ID')" />
+    </div>
+
+	<!-- Template for adding a new poll -->
+    <div id="forum-new-poll-template" hidden>
+        <strong>
+			<p><? locale.write('label_poll_add'); ?></p>
+        </strong>
+        <form id="newpoll-form-template" class="form-horizontal">
+            <div class="clearfix">
+				<strong>
+					<? locale.write('label_poll_add_question'); ?>
+				</strong>
+				<br />
+                <div class="col-sm-10 form-group">
+                    <input id="newpoll-subject-template" class="form-control" type="text" placeholder="<? locale.write('placeholder_required', 'main'); ?>" maxlength="70" />
+                </div>
+            </div>
+            <div id="newpoll-comment-group-template">
+				<strong>
+					<? locale.write('label_poll_add_comments'); ?>
+				</strong>
+				<br />
+			</div>
+            <div class="form-group">
+                <label for="newpoll-answers-template" class="col-sm-2 control-label">
+                <? locale.write('label_poll_add_selection'); ?>
+                </label>
+                <div class="col-sm-10">
+                    <label class="radio-inline">
+                        <input type="radio" name="newpoll-answers-template" value="1" checked /> <? locale.write('label_poll_selection_single'); ?> 
+                    </label>
+                    <label class="radio-inline">
+                        <input type="radio" name="newpoll-answers-template" value="2" /> <? locale.write('label_poll_selection_multiple'); ?>  
+                        <input type="number" name="newpoll-answer-count-template" min="1" max="15" value="1" />
+                    </label>
+                </div>
+            </div>
+            <div class="form-group">
+                <label for="newpoll-results-template" class="col-sm-2 control-label">
+                <? locale.write('label_poll_show_results'); ?>
+                </label>
+                <div class="col-sm-10">
+                    <label class="radio-inline">
+                        <input type="radio" name="newpoll-results-template" value="0" checked /> <? locale.write('label_poll_show_results_voters'); ?> 
+                    </label>
+                    <label class="radio-inline">
+                        <input type="radio" name="newpoll-results-template" value="1" />  <? locale.write('label_poll_show_results_everyone'); ?> 
+                    </label>
+                    <label class="radio-inline">
+                        <input type="radio" name="newpoll-results-template" value="2" /> <? locale.write('label_poll_show_results_me_only_a'); ?> 
+                    </label>
+                    <label class="radio-inline">
+                        <input type="radio" name="newpoll-results-template" value="3" /> <? locale.write('label_poll_show_results_me_only_b'); ?> 
+                    </label>
+                </div>
+            </div>
+            <div id="newpoll-answer-group-template">
+				<strong>
+					<? locale.write('label_poll_add_answers'); ?>
+				</strong>
+				<br />
+			</div>
+            <div id="newpoll-button-template" class="form-group">
+                <div class="col-sm-offset-2 col-sm-10">
+                    <button id="newpoll-submit-template" type="button" class="btn btn-primary" onclick="postNewPoll('SUB')">
+                        <? locale.write('button_submit', 'main'); ?>
+                    </button>
+                    <div class="pull-right">
+                        <button type="button" title="<? locale.write('button_poll_add_comment'); ?>" class="btn btn-success" onclick="addPollField('comment', 'newpoll-comment-group')">
+                            <span class="glyphicon glyphicon-pencil"></span>
+                        </button> 
+                        <button type="button" title="<? locale.write('button_poll_add_answer'); ?>" class="btn btn-success" onclick="addPollField('answer', 'newpoll-answer-group')">
+                            <span class="glyphicon glyphicon-plus"></span>
+                        </button> 
+                    </div>
+                </div>
+            </div>
+        </form>
+    </div>
+
+	<!-- Template for adding a poll field (comment or answer) -->
+	<div id="forum-new-poll-field-container-template" name="forum-new-poll-TYPE" class="form-group" hidden>
+        <div class="col-sm-9">
+            <input id="forum-new-poll-field-TYPE-NUMBER-template" class="form-control" name="forum-new-poll-field-TYPE" type="text" maxlength="70">
+        </div>
+        <div class="col-sm-1">
+            <button type="button" class="btn btn-danger" onclick="$('#forum-new-poll-field-container-template').remove()">
+				<span class="glyphicon glyphicon-remove"></span>
+			</button>
+		</div>
+    </div>
+
+
+	<ol class="breadcrumb">
         <li>
             <a href="./?page=<? Request.write_param('page'); ?>">
                 <? locale.write('title'); ?>
@@ -70,11 +291,7 @@
         </li>
     </ol>
 
-    <?xjs
-        var offset = Request.has_param('offset') ? parseInt(Request.get_param('offset')) : 0;
-        var threads = listThreads(Request.get_param('sub'), offset * settings.page_size, settings.page_size, true) || { total: 0, threads: [] };
-        var sub = msg_area.sub[Request.get_param('sub')];
-    ?>
+    <? var sub = msg_area.sub[Request.get_param('sub')]; ?>
 
     <button class="btn btn-default icon" aria-label="<? locale.write('button_post_message'); ?>" title="<? locale.write('button_post_message'); ?>" onclick="addNew('<? write(sub.code); ?>')">
         <span class="glyphicon glyphicon-pencil"></span>
@@ -95,80 +312,12 @@
         </button>
     </div>
 
-    <div id="forum-thread-replies-template" hidden>
-        <p>
-            <strong data-message-count></strong>
-            <span data-suffix-reply hidden><? locale.write('suffix_reply_count'); ?>.</span>
-            <span data-suffix-replies hidden><? locale.write('suffix_replies_count'); ?>.</span>
-            <? locale.write('label_thread_latest_reply'); ?>
-            <strong data-last-from></strong>
-            <span data-last-time></span>
-        </p>
-    </div>
-
-    <div id="forum-thread-stats-template" hidden>
-        <? if (is_user()) { ?>
-            <span data-unread-messages title="<? locale.write('badge_unread_messages'); ?>" class="badge <? write(sub.scan_cfg&SCAN_CFG_NEW || sub.scan_cfg&SCAN_CFG_YONLY ? 'scanned' : ''); ?>" hidden></span>
-        <? } ?>
-        <span data-upvotes-badge title="<? locale.write('badge_upvotes'); ?>" class="badge upvote-bg" style="display:none;">
-            <span class="glyphicon glyphicon-arrow-up"></span>
-            <span data-upvotes></span>
-        </span>
-        <span data-downvotes-badge title="<? locale.write('badge_downvotes'); ?>" class="badge downvote-bg" style="display:none;">
-            <span class="glyphicon glyphicon-arrow-down"></span>
-            <span data-downvotes></span>
-        </span>
-    </div>
-
-    <div id="forum-list-container" class="list-group">
-        <? threads.threads.forEach(function (e) { ?>
-            <a href="./?page=<? Request.write_param('page'); ?>&sub=<? Request.write_param('sub'); ?>&thread=<? write(e.id); ?>" class="list-group-item striped">
-                <div class="row">
-                    <div id="left-<? write(e.id); ?>" class="col-sm-9">
-                        <strong><? write(e.subject); ?></strong>
-                        <p><? locale.write('label_thread_from'); ?> <strong><? write(e.first.from); ?></strong> <? locale.write('label_message_date'); ?> <? write(system.timestr(e.first.when_written_time)); ?></p>
-                    </div>
-                    <div class="col-sm-3">
-                        <div id="right-<? write(e.id); ?>" class="pull-right"></div>
-                    </div>
-                </div>
-            </a>
-        <? }); ?>
-    </div>
-
-    <? if (threads.total / settings.page_size > 1) { ?>
-        <? var qs = http_request.query_string.replace(/&offset=\d+/g, ''); ?>
-        <div class="btn-group">
-            <? if (offset > 0) { ?>
-                <a href="./?<? write(qs); ?>&offset=0" role="button" class="btn btn-default" title="<? locale.write('button_thread_first_page'); ?>">
-                    <span class="glyphicon glyphicon-fast-backward"></span>
-                </a>
-                <a href="./?<? write(qs); ?>&offset=<? write(Math.max(0, offset - 9)); ?>" role="button" class="btn btn-default" title="<? locale.writef('button_thread_back_pages', Math.min(9, offset)); ?>">
-                    <span class="glyphicon glyphicon-backward"></span>
-                </a>
-                <a href="./?<? write(qs); ?>&offset=<? write(Math.max(0, offset - 1)); ?>" role="button" class="btn btn-default" title="<? locale.write('button_thread_previous_page'); ?>">
-                    <span class="glyphicon glyphicon-step-backward"></span>
-                </a>
-            <? } ?>
-            <a href="./?<? write(qs); ?>&offset=<? write(offset); ?>" role="button" class="btn btn-default" disabled>
-                <? write(offset + 1); ?>
-            </a>
-            <? for (var n = offset + 1; n < threads.total / settings.page_size && n - offset < 9; n++) { ?>
-                <a href="./?<? write(qs); ?>&offset=<? write(n); ?>" role="button" class="btn btn-default"><? write(n + 1); ?></a>
-            <? } ?>
-            <? if (threads.total / settings.page_size > offset + 9) { ?>
-                <a href="./?<? write(qs); ?>&offset=<? write(offset + 1); ?>" role="button" class="btn btn-default" title="<? locale.write('button_thread_next_page'); ?>">
-                    <span class="glyphicon glyphicon-step-forward"></span>
-                </a>
-                <a href="./?<? write(qs); ?>&offset=<? write(offset + 9); ?>" role="button" class="btn btn-default" title="<? locale.writef('button_thread_forward_pages', Math.min(9, Math.floor(threads.total / settings.page_size))); ?>">
-                    <span class="glyphicon glyphicon-forward"></span>
-                </a>
-                <a href="./?<? write(qs); ?>&offset=<? write(Math.floor(threads.total / settings.page_size)); ?>" role="button" class="btn btn-default" title="<? locale.write('button_thread_last_page'); ?>">
-                    <span class="glyphicon glyphicon-fast-forward"></span>
-                </a>
-            <? } ?>
-        </div class="btn-group">
-    <? } ?>
+	<div id="forum-list-container" class="list-group"></div>
+	<div id="forum-load-more-threads" class="text-center" hidden>
+		<button id="load-more-threads" class="btn btn-primary" aria-label="<? locale.write('button_load_more', 'main'); ?>" title="<? locale.write('button_load_more', 'main'); ?>" onclick="listThreads('<? write(sub.code); ?>', <? write(settings.page_size); ?>, true)">
+			<? locale.write('button_load_more', 'main'); ?>
+		</button>
+	</div>
 
     <script type="text/javascript">
 
@@ -177,15 +326,35 @@
             if (data.type != 'threads') return;
             onThreadStats(data.data);
         }, {
-            threads: '<? Request.write_param("sub"); ?>',
-            offset: '<? write(offset); ?>',
-            page_size: '<? write(settings.page_size); ?>',
-        });
+            sub: '<? Request.write_param("sub"); ?>',
+		});
+
+		listThreads('<? write(sub.code); ?>', <? write(settings.page_size); ?>);
 
     </script>
 
+<!-- Viewing a group (list of subs) -->
 <? } else if (Request.has_param('group') && msg_area.grp_list[Request.get_param('group')] !== undefined) { ?>
 
+	<div hidden>
+		<a id="forum-sub-link-template" href="./?page=<? Request.write_param('page'); ?>" class="list-group-item striped">
+			<h4>
+				<strong data-sub-name></strong>
+			</h4>
+			<span title="<? locale.write('badge_unread_messages'); ?>" class="badge ignored" data-unread-unscanned></span>
+			<span title="<? locale.write('badge_unread_messages'); ?>" class="badge scanned" data-unread-scanned></span>
+			<p data-sub-description></p>
+			<span data-newest-message-container hidden>
+				<? locale.write('label_sub_newest_message'); ?>:
+				<strong data-newest-message-subject></strong>
+				<? locale.write('label_thread_from'); ?>
+				<span data-newest-message-from></span>
+				<? locale.write('label_message_date'); ?>
+				<span data-newest-message-date></span>
+			</span>
+		</a>
+	</div>
+
     <ol class="breadcrumb">
         <li>
             <a href="./?page=<? Request.write_param('page'); ?>">
@@ -199,48 +368,35 @@
         </li>
     </ol>
 
-    <div id="forum-list-container" class="list-group">
-        <? var subs = listSubs(Request.get_param('group')); ?>
-        <? subs.forEach(function (e) { ?>
-            <a href="./?page=<? Request.write_param('page'); ?>&sub=<? write(e.code); ?>" class="list-group-item striped">
-                <h4>
-                    <strong>
-                        <? write(e.name); ?>
-                    </strong>
-                </h4>
-                <span title="<? locale.write('badge_unread_messages'); ?>" class="badge <? write(e.scan_cfg&SCAN_CFG_NEW || e.scan_cfg&SCAN_CFG_YONLY ? 'scanned' : 'total'); ?>" id="badge-<? write(e.code); ?>"></span>
-                <p><? write(e.description); ?></p>
-                <span id="newest-msg-<? write(e.code); ?>"></span>
-            </a>
-        <? }); ?>
-    </div>
+	<div id="forum-list-container" class="list-group"></div>
 
-    <? if (is_user()) { ?>
-        <script type="text/javascript">
+	<script type="text/javascript">
+		(async () => await listSubs('<? Request.write_param('group'); ?>'))();
+		getNewestMessagePerSub('<? Request.write_param('group'); ?>');
+		<? if (is_user()) { ?>
             registerEventListener('forum', e => {
                 const data = JSON.parse(e.data);
                 if (data.type != 'subs_unread') return;
                 onSubUnreadCount(data.data);
              }, { subs_unread: '<? Request.write_param("group"); ?>' });
-        </script>
-    <? } ?>
-
-    <script type="text/javascript">
-        (async function getNewestMessagePerSub() {
-            try {
-                const response = await fetch('./api/forum.ssjs?call=get-newest-message-per-sub&group=<? Request.write_param('group'); ?>');
-                const data = await response.json();
-                Object.entries(data).forEach(e => {
-                    document.getElementById(`newest-msg-${e[0]}`).innerHTML = `<? locale.write('label_sub_newest_message'); ?>: ${e[1].subject}, <? locale.write('label_thread_from'); ?> ${e[1].from} <? locale.write('label_message_date'); ?> ${new Date(e[1].when_written_time * 1000)}`;
-                });
-            } catch (err) {
-                console.error('Error fetching latest messages', err);
-            }
-        })();
+		<? } ?>
     </script>
 
+<!-- Viewing the group list -->
 <? } else { ?>
 
+	<div hidden>
+		<a id="forum-group-link-template" href="./?page=<? Request.write_param('page'); ?>" class="list-group-item striped">
+			<h3>
+				<strong data-group-name></strong>
+			</h3>
+			<span title="<? locale.write('badge_unread_messages'); ?>" class="badge ignored" data-unread-unscanned></span>
+			<span title="<? locale.write('badge_unread_messages'); ?>" class="badge scanned" data-unread-scanned></span>
+			<span data-group-description></span>:
+			<span data-group-sub-count></span> <? locale.write('sub_boards'); ?>
+		</a>
+	</div>
+
     <ol class="breadcrumb">
         <li>
             <a href="./?page=<? Request.write_param('page'); ?>">
@@ -249,31 +405,16 @@
         </li>
     </ol>
 
-    <div id="forum-list-container" class="list-group">
-        <? const groups = listGroups(); ?>
-        <? groups.forEach(function (e) { ?>
-            <a href="./?page=<? Request.write_param('page'); ?>&group=<? write(e.index); ?>" class="list-group-item striped">
-                <h3>
-                    <strong>
-                        <? write(e.name); ?>
-                    </strong>
-                </h3>
-                <span title="<? locale.write('badge_unread_messages'); ?>" class="badge ignored" id="badge-ignored-<? write(e.index); ?>"></span>
-                <span title="<? locale.write('badge_unread_messages'); ?>" class="badge scanned" id="badge-scanned-<? write(e.index); ?>"></span>
-                <p>
-                    <? write(e.description); ?>: <? write(e.sub_count); ?> <? locale.write('sub_boards'); ?>
-                </p>
-            </a>
-        <? }); ?>
-    </div>
+	<div id="forum-list-container" class="list-group"></div>
 
     <? if (is_user()) { ?>
         <script type="text/javascript">
+			(async () => await listGroups())();
             registerEventListener('forum', e => {
                 const data = JSON.parse(e.data);
                 if (data.type != 'groups_unread') return;
                 onGroupUnreadCount(data.data);
-            }, { groups_unread: null });
+			}, { groups_unread: null });
         </script>
     <? } ?>