From 865edb70c2eb49ba667d98b0e207b3f30d56ec60 Mon Sep 17 00:00:00 2001
From: Julian Lam <julian@designcreateplay.com>
Date: Mon, 30 Sep 2013 16:28:22 -0400
Subject: [PATCH 01/29] added meta description to topics, closes #362

---
 src/webserver.js | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/webserver.js b/src/webserver.js
index 0b07f23f43..c19cee772c 100644
--- a/src/webserver.js
+++ b/src/webserver.js
@@ -23,7 +23,8 @@ var express = require('express'),
 	feed = require('./feed'),
 	plugins = require('./plugins'),
 	nconf = require('nconf'),
-	winston = require('winston');
+	winston = require('winston'),
+	validator = require('validator');
 
 (function (app) {
 	var templates = null,
@@ -321,6 +322,9 @@ var express = require('express'),
 						metaTags: [{
 							name: "title",
 							content: topicData.topic_name
+						}, {
+							name: "description",
+							content: validator.sanitize(topicData.main_posts[0].content.substr(0, 255)).escape().replace('\n', '')
 						}, {
 							property: 'og:title',
 							content: topicData.topic_name + ' | ' + (meta.config.title || 'NodeBB')

From 249c45dfe27f0d21bb718ce0b9463ab2f2255ef5 Mon Sep 17 00:00:00 2001
From: Julian Lam <julian@designcreateplay.com>
Date: Mon, 30 Sep 2013 16:30:11 -0400
Subject: [PATCH 02/29] 0.0.7

---
 package.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index e0b3bf5aff..ab168e0d0b 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
   "name": "nodebb",
   "license": "GPLv3 or later",
   "description": "NodeBB Forum",
-  "version": "0.0.6",
+  "version": "0.0.7",
   "homepage": "http://www.nodebb.org",
   "repository": {
     "type": "git",
@@ -42,7 +42,7 @@
     "validator": "~1.5.1"
   },
   "optionalDependencies": {
-    "hiredis" : "~0.1.15"
+    "hiredis": "~0.1.15"
   },
   "bugs": {
     "url": "https://github.com/designcreateplay/NodeBB/issues"

From 181220621e0a9ee541ad08a8a9cb9f558a44c467 Mon Sep 17 00:00:00 2001
From: Julian Lam <julian@designcreateplay.com>
Date: Mon, 30 Sep 2013 17:01:39 -0400
Subject: [PATCH 03/29] fixed issue with server crashing on post

---
 src/plugins.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/plugins.js b/src/plugins.js
index 645c90952f..0aff3ce2fb 100644
--- a/src/plugins.js
+++ b/src/plugins.js
@@ -185,7 +185,7 @@ var fs = require('fs'),
 			} else {
 				// Otherwise, this hook contains no methods
 				var returnVal = (Array.isArray(args) ? args[0] : args);
-				if (callback) callback(err, returnVal);
+				if (callback) callback(null, returnVal);
 			}
 		},
 		isActive: function(id, callback) {

From 7a919fbac4b869abd60fd4a97ededa0a46a3e4aa Mon Sep 17 00:00:00 2001
From: Julian Lam <julian@designcreateplay.com>
Date: Mon, 30 Sep 2013 17:05:22 -0400
Subject: [PATCH 04/29] dropping down to 0.0.6 for re-release

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index ab168e0d0b..ef1726477b 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
   "name": "nodebb",
   "license": "GPLv3 or later",
   "description": "NodeBB Forum",
-  "version": "0.0.7",
+  "version": "0.0.6",
   "homepage": "http://www.nodebb.org",
   "repository": {
     "type": "git",

From 48a7c48f7b0739ae9fd926e0cd55dff1eb5b3d86 Mon Sep 17 00:00:00 2001
From: Julian Lam <julian@designcreateplay.com>
Date: Mon, 30 Sep 2013 17:05:28 -0400
Subject: [PATCH 05/29] 0.0.7

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index ef1726477b..ab168e0d0b 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
   "name": "nodebb",
   "license": "GPLv3 or later",
   "description": "NodeBB Forum",
-  "version": "0.0.6",
+  "version": "0.0.7",
   "homepage": "http://www.nodebb.org",
   "repository": {
     "type": "git",

From d7953eb779ef47bdb269a53309d5ec36d4387042 Mon Sep 17 00:00:00 2001
From: Julian Lam <julian@designcreateplay.com>
Date: Mon, 30 Sep 2013 17:22:39 -0400
Subject: [PATCH 06/29] updated screenshots for v0.0.7

---
 README.md | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md
index f1f90f2a98..2d3bab3918 100644
--- a/README.md
+++ b/README.md
@@ -3,13 +3,14 @@
 **NodeBB** is a robust Node.js driven forum built on a redis database. It is powered by web sockets, and is compatible down to IE8.
 
 * [NodeBB Homepage](http://www.nodebb.org/ "NodeBB")
+* [Demo & Meta Discussion](http://try.nodebb.org)
+* [Join us on IRC](https://kiwiirc.com/client/irc.freenode.net/nodebb) - #nodebb on Freenode
 * [Follow on Twitter](http://www.twitter.com/NodeBB/ "NodeBB Twitter")
 * [Like us on Facebook](http://www.facebook.com/NodeBB/ "NodeBB Facebook")
-* [Join us on IRC](https://kiwiirc.com/client/irc.freenode.net/nodebb) - #nodebb on Freenode
 
-![NodeBB Main Category Listing](http://i.imgur.com/ZBrHqLr.png)
+![NodeBB Main Category Listing](http://i.imgur.com/zffCFoh.png)
 
-![NodeBB Topic Page](http://i.imgur.com/YSBA6Vr.png)
+![NodeBB Topic Page](http://i.imgur.com/tcHW08M.png)
 
 ## How can I follow along/contribute?
 

From 59c9bdb3a52b668ec10ed06f0228b7bbd9471d47 Mon Sep 17 00:00:00 2001
From: Julian Lam <julian.lam@gmail.com>
Date: Tue, 1 Oct 2013 11:17:57 -0400
Subject: [PATCH 07/29] updated contributor list

---
 package.json | 17 ++++++++++++++---
 1 file changed, 14 insertions(+), 3 deletions(-)

diff --git a/package.json b/package.json
index e0b3bf5aff..c7bd713f8e 100644
--- a/package.json
+++ b/package.json
@@ -53,15 +53,22 @@
   "contributors": [
     {
       "name": "Andrew Rodrigues",
-      "email": "andrew@designcreateplay.com"
+      "email": "andrew@designcreateplay.com",
+      "url": "https://github.com/psychobunny"
     },
     {
       "name": "Julian Lam",
-      "email": "julian@designcreateplay.com"
+      "email": "julian@designcreateplay.com",
+      "url": "https://github.com/julianlam"
     },
     {
       "name": "Barış Soner Uşaklı",
-      "email": "baris@designcreateplay.com"
+      "email": "baris@designcreateplay.com",
+      "url": "https://github.com/barisusakli"
+    },
+    {
+      "name": "Andrew Darqui",
+      "url": "https://github.com/adarqui"
     },
     {
       "name": "Damian Bushong",
@@ -70,6 +77,10 @@
     {
       "name": "Matt Smith",
       "url": "https://github.com/soimafreak"
+    },
+    {
+      "name": "Quinton Marchi",
+      "url": "https://github.com/iamcardinal"
     }
   ]
 }

From 9613ea901895ee11f76f2b7d01ef2814ed3953fd Mon Sep 17 00:00:00 2001
From: Julian Lam <julian.lam@gmail.com>
Date: Tue, 1 Oct 2013 11:54:00 -0400
Subject: [PATCH 08/29] reverted change where post title was sanitized on
 saving (which didn't seem to work), now sanitizing post title on output

---
 src/postTools.js |  4 ++--
 src/posts.js     |  6 +++---
 src/topics.js    | 15 ++++++++-------
 src/webserver.js |  5 +++--
 4 files changed, 16 insertions(+), 14 deletions(-)

diff --git a/src/postTools.js b/src/postTools.js
index 8f41608391..9f989866cd 100644
--- a/src/postTools.js
+++ b/src/postTools.js
@@ -5,6 +5,7 @@ var RDB = require('./redis.js'),
 	user = require('./user.js'),
 	async = require('async'),
 	nconf = require('nconf'),
+	validator = require('validator'),
 
 	utils = require('../public/src/utils'),
 	plugins = require('./plugins'),
@@ -92,10 +93,9 @@ var RDB = require('./redis.js'),
 			], function(err, results) {
 				io.sockets.in('topic_' + results[0].tid).emit('event:post_edited', {
 					pid: pid,
-					title: title,
+					title: validator.sanitize(title).escape(),
 					isMainPost: results[0].isMainPost,
 					content: results[1]
-
 				});
 			});
 		};
diff --git a/src/posts.js b/src/posts.js
index 957d2790b9..411610ee5e 100644
--- a/src/posts.js
+++ b/src/posts.js
@@ -264,9 +264,9 @@ var RDB = require('./redis.js'),
 						var socketData = {
 							posts: [postData]
 						};
-						io.sockets. in ('topic_' + tid).emit('event:new_post', socketData);
-						io.sockets. in ('recent_posts').emit('event:new_post', socketData);
-						io.sockets. in ('user/' + uid).emit('event:new_post', socketData);
+						io.sockets.in('topic_' + tid).emit('event:new_post', socketData);
+						io.sockets.in('recent_posts').emit('event:new_post', socketData);
+						io.sockets.in('user/' + uid).emit('event:new_post', socketData);
 					});
 
 					callback(null, 'Reply successful');
diff --git a/src/topics.js b/src/topics.js
index 54d303bdbf..30d9b5c9bf 100644
--- a/src/topics.js
+++ b/src/topics.js
@@ -15,15 +15,17 @@ schema = require('./schema.js'),
 	topicSearch = reds.createSearch('nodebbtopicsearch'),
 	validator = require('validator');
 
-
 (function(Topics) {
 
 	Topics.getTopicData = function(tid, callback) {
 		RDB.hgetall('topic:' + tid, function(err, data) {
-			if (err === null)
+			if (err === null) {
+				data.title = validator.sanitize(data.title).escape();
+
 				callback(data);
-			else
+			} else {
 				console.log(err);
+			}
 		});
 	}
 
@@ -658,7 +660,6 @@ schema = require('./schema.js'),
 
 				var slug = tid + '/' + utils.slugify(title);
 				var timestamp = Date.now();
-				title = validator.sanitize(title).escape();
 				RDB.hmset('topic:' + tid, {
 					'tid': tid,
 					'uid': uid,
@@ -698,9 +699,9 @@ schema = require('./schema.js'),
 
 						// Notify any users looking at the category that a new topic has arrived
 						Topics.getTopicForCategoryView(tid, uid, function(topicData) {
-							io.sockets. in ('category_' + category_id).emit('event:new_topic', topicData);
-							io.sockets. in ('recent_posts').emit('event:new_topic', topicData);
-							io.sockets. in ('user/' + uid).emit('event:new_post', {
+							io.sockets.in('category_' + category_id).emit('event:new_topic', topicData);
+							io.sockets.in('recent_posts').emit('event:new_topic', topicData);
+							io.sockets.in('user/' + uid).emit('event:new_post', {
 								posts: postData
 							});
 						});
diff --git a/src/webserver.js b/src/webserver.js
index c19cee772c..a134a4ac58 100644
--- a/src/webserver.js
+++ b/src/webserver.js
@@ -309,7 +309,8 @@ var express = require('express'),
 				},
 				function (topicData, next) {
 					var lastMod = 0,
-						timestamp;
+						timestamp,
+						sanitize = validator.sanitize;
 
 					for (var x = 0, numPosts = topicData.posts.length; x < numPosts; x++) {
 						timestamp = parseInt(topicData.posts[x].timestamp, 10);
@@ -324,7 +325,7 @@ var express = require('express'),
 							content: topicData.topic_name
 						}, {
 							name: "description",
-							content: validator.sanitize(topicData.main_posts[0].content.substr(0, 255)).escape().replace('\n', '')
+							content: sanitize(topicData.main_posts[0].content.substr(0, 255)).escape().replace('\n', '')
 						}, {
 							property: 'og:title',
 							content: topicData.topic_name + ' | ' + (meta.config.title || 'NodeBB')

From 22c73f3c12aaf37ee46e2c1e88d8cae1cc2c0b0d Mon Sep 17 00:00:00 2001
From: psychobunny <rodrigues.andrew@gmail.com>
Date: Tue, 1 Oct 2013 12:07:58 -0400
Subject: [PATCH 09/29] closes #345

---
 public/src/ajaxify.js               | 7 +++++--
 public/templates/admin/settings.tpl | 6 +++++-
 src/routes/api.js                   | 1 +
 3 files changed, 11 insertions(+), 3 deletions(-)

diff --git a/public/src/ajaxify.js b/public/src/ajaxify.js
index 497823e3c7..4c6b83991a 100644
--- a/public/src/ajaxify.js
+++ b/public/src/ajaxify.js
@@ -127,8 +127,11 @@ var ajaxify = {};
 					}
 				} else if (window.location.pathname !== '/outgoing') {
 					// External Link
-					ajaxify.go('outgoing?url=' + encodeURIComponent(this.href));
-					e.preventDefault();
+
+					if (config.useOutgoingLinksPage == true) {
+						ajaxify.go('outgoing?url=' + encodeURIComponent(this.href));
+						e.preventDefault();
+					}
 				}
 			}
 		});
diff --git a/public/templates/admin/settings.tpl b/public/templates/admin/settings.tpl
index 68fbacd632..ecc51cd730 100644
--- a/public/templates/admin/settings.tpl
+++ b/public/templates/admin/settings.tpl
@@ -70,7 +70,11 @@
 		<strong>Post Delay</strong><br /> <input type="text" class="form-control" value="10000" data-field="postDelay"><br />
 		<strong>Minimum Title Length</strong><br /> <input type="text" class="form-control" value="3" data-field="minimumTitleLength"><br />
 		<strong>Minimum Post Length</strong><br /> <input type="text" class="form-control" value="8" data-field="minimumPostLength"><br />
-
+		<div class="checkbox">
+			<label>
+				<input type="checkbox" data-field="useOutgoingLinksPage"> <strong>Use Outgoing Links Warning Page</strong>
+			</label>
+		</div>
 	</div>
 </form>
 
diff --git a/src/routes/api.js b/src/routes/api.js
index 40b19aec91..d201a31c93 100644
--- a/src/routes/api.js
+++ b/src/routes/api.js
@@ -28,6 +28,7 @@ var user = require('./../user.js'),
 				config.minimumUsernameLength = meta.config.minimumUsernameLength;
 				config.maximumUsernameLength = meta.config.maximumUsernameLength;
 				config.minimumPasswordLength = meta.config.minimumPasswordLength;
+				config.useOutgoingLinksPage = meta.config.useOutgoingLinksPage;
 
 				res.json(200, config);
 			});

From 1c32acf7b6209ac19258a65263898ceb10208b52 Mon Sep 17 00:00:00 2001
From: Baris Soner Usakli <barisusakli@gmail.com>
Date: Tue, 1 Oct 2013 14:17:24 -0400
Subject: [PATCH 10/29] removed WITHSCORES from getLatestTopics, how was this
 working at all?

---
 src/topics.js | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/topics.js b/src/topics.js
index 30d9b5c9bf..2e88ebf16c 100644
--- a/src/topics.js
+++ b/src/topics.js
@@ -20,7 +20,8 @@ schema = require('./schema.js'),
 	Topics.getTopicData = function(tid, callback) {
 		RDB.hgetall('topic:' + tid, function(err, data) {
 			if (err === null) {
-				data.title = validator.sanitize(data.title).escape();
+				if(data)
+					data.title = validator.sanitize(data.title).escape();
 
 				callback(data);
 			} else {
@@ -97,7 +98,7 @@ schema = require('./schema.js'),
 
 		var timestamp = Date.now();
 
-		var args = ['topics:recent', '+inf', timestamp - 86400000, 'WITHSCORES', 'LIMIT', start, end - start + 1];
+		var args = ['topics:recent', '+inf', timestamp - 86400000, 'LIMIT', start, end - start + 1];
 
 		RDB.zrevrangebyscore(args, function(err, tids) {
 

From 90b4d688f8ce267bedf459251e88c8c18d5a03c1 Mon Sep 17 00:00:00 2001
From: Minami <cardinal@iamcardinal.com>
Date: Tue, 1 Oct 2013 22:14:16 -0500
Subject: [PATCH 11/29] Testing adding of Meta Tags

---
 public/templates/admin/settings.tpl | 2 ++
 src/install.js                      | 8 ++++----
 src/webserver.js                    | 3 +++
 3 files changed, 9 insertions(+), 4 deletions(-)

diff --git a/public/templates/admin/settings.tpl b/public/templates/admin/settings.tpl
index 68fbacd632..a872497faf 100644
--- a/public/templates/admin/settings.tpl
+++ b/public/templates/admin/settings.tpl
@@ -8,6 +8,8 @@
 		<input class="form-control" type="text" placeholder="Your Community Name" data-field="title" /><br />
 		<label>Site Description</label>
 		<input type="text" class="form-control" placeholder="A short description about your community" data-field="description" /><br />
+		<label>Site Keywords</label>
+		<input type="text" class="form-control" placeholder="Keywords describing your community, comma-seperated" data-field="keywords" /><br />
 		<label>Imgur Client ID</label>
 		<input type="text" class="form-control" placeholder="Imgur ClientID for image uploads" data-field="imgurClientID" /><br />
 		<label>Maximum User Image Size</label>
diff --git a/src/install.js b/src/install.js
index 4f60f6203d..fe0b5169d1 100644
--- a/src/install.js
+++ b/src/install.js
@@ -41,10 +41,10 @@ var async = require('async'),
 			name: 'redis:password',
 			description: 'Password of your Redis database'
 		}, {
-            name: 'bind_address',
-            description: 'IP or Hostname to bind to',
-            'default': '0.0.0.0'
-        }],
+			name: 'bind_address',
+			description: 'IP or Hostname to bind to',
+			'default': '0.0.0.0'
+		}],
 		setup: function (callback) {
 			async.series([
 				function (next) {
diff --git a/src/webserver.js b/src/webserver.js
index ee87e0fa16..0a5374cdf5 100644
--- a/src/webserver.js
+++ b/src/webserver.js
@@ -54,6 +54,9 @@ var express = require('express'),
 		}, {
 			property: 'og:site_name',
 			content: meta.config.title || 'NodeBB'
+		}, {
+			property: 'og:keywords',
+			content: meta.config['keywords'] || ''
 		}],
 			metaString = utils.buildMetaTags(defaultMetaTags.concat(options.metaTags || [])),
 			templateValues = {

From eb022220f4597a0d242e1ae6a991839c47065b64 Mon Sep 17 00:00:00 2001
From: Quinton Marchi <cardinal@iamcardinal.com>
Date: Wed, 2 Oct 2013 14:22:56 -0400
Subject: [PATCH 12/29] Final Edit for keywords

---
 src/webserver.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/webserver.js b/src/webserver.js
index 0a5374cdf5..371e64c53e 100644
--- a/src/webserver.js
+++ b/src/webserver.js
@@ -55,7 +55,7 @@ var express = require('express'),
 			property: 'og:site_name',
 			content: meta.config.title || 'NodeBB'
 		}, {
-			property: 'og:keywords',
+			property: 'keywords',
 			content: meta.config['keywords'] || ''
 		}],
 			metaString = utils.buildMetaTags(defaultMetaTags.concat(options.metaTags || [])),

From d177e71b46fe4b92c1b5699f5a68be5bba9eb174 Mon Sep 17 00:00:00 2001
From: Quinton Marchi <cardinal@iamcardinal.com>
Date: Wed, 2 Oct 2013 14:31:41 -0400
Subject: [PATCH 13/29] Fixes double search, closes #370

Haven't tested it but I can't see any problems.
---
 public/templates/header.tpl | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/public/templates/header.tpl b/public/templates/header.tpl
index c151a5bd58..8eb3bba4d6 100644
--- a/public/templates/header.tpl
+++ b/public/templates/header.tpl
@@ -50,9 +50,6 @@
 					<li class="visible-xs">
 						<a href="/search">[[global:header.search]]</a>
 					</li>
-					<li class="visible-xs">
-						<a href="/search">Search</a>
-					</li>
 					<li>
 						<a href="/"></a>
 					</li>

From 994791add6ea539748af759739629f67ed5e7502 Mon Sep 17 00:00:00 2001
From: Quinton Marchi <cardinal@iamcardinal.com>
Date: Wed, 2 Oct 2013 17:48:27 -0400
Subject: [PATCH 14/29] Touches on postbox

Adds border-radius. Compatible with: Firefox, Webkit-based, and IE9+.
---
 public/themes/vanilla/modules/postWindow.less | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/public/themes/vanilla/modules/postWindow.less b/public/themes/vanilla/modules/postWindow.less
index 5f97d9cbe7..54db9b44a1 100644
--- a/public/themes/vanilla/modules/postWindow.less
+++ b/public/themes/vanilla/modules/postWindow.less
@@ -10,6 +10,9 @@
 		height: 100%;
 		background: rgba(64, 64, 64, 0.6);
 		visibility: visible;
+		-webkit-border-radius: 10px;
+		-moz-border-radius: 10px;
+		border-radius: 10px;
 
 		.btn-toolbar {
 			&.formatting-bar {

From 948949c5710a96ffc2cafe8adcc739d76addee52 Mon Sep 17 00:00:00 2001
From: Julian Lam <julian@designcreateplay.com>
Date: Thu, 3 Oct 2013 11:34:15 -0400
Subject: [PATCH 15/29] closed #375 - now asking socket.io to connect to
 "current page" instead of hardcoded url, removed api_url and "socket" section
 from public config

---
 public/src/app.js                             |  2 +-
 public/templates/admin/testing/categories.tpl | 10 +++++-----
 src/install.js                                |  4 ----
 3 files changed, 6 insertions(+), 10 deletions(-)

diff --git a/public/src/app.js b/public/src/app.js
index 5b9918afed..94108cd895 100644
--- a/public/src/app.js
+++ b/public/src/app.js
@@ -17,7 +17,7 @@ var socket,
 						socket.socket.connect();
 					}, 200);
 				} else {
-					socket = io.connect(config.socket.address);
+					socket = io.connect(RELATIVE_PATH);
 
 					var reconnecting = false,
 						reconnectEl, reconnectTimer;
diff --git a/public/templates/admin/testing/categories.tpl b/public/templates/admin/testing/categories.tpl
index 1b2709d1fb..37a784d550 100644
--- a/public/templates/admin/testing/categories.tpl
+++ b/public/templates/admin/testing/categories.tpl
@@ -9,25 +9,25 @@ jQuery(document).ready(function () {
 
 	QUnit.init();
 	asyncTest( "Loading Categories", function() {
-		
+
 		jQuery.get(RELATIVE_PATH + '/api/home', function(data) {
 			ok( data.categories.length > 0, JSON.stringify(data.categories) );
-			
+
 			start();
-			
+
 			for (var i = 0, ii = data.categories.length; i < ii; i++) {
 				var category = data.categories[i],
 					slug = 'category/' + category.slug;
 
 				asyncTest( "Loading Category '" + category.name + "' located at " + slug, function() {
-					jQuery.get(config.api_url + slug, function(data) {
+					jQuery.get(RELATIVE_PATH + '/api/' + slug, function(data) {
 						ok( data.category_name, JSON.stringify(data) ); //todo: check this against data.categories
 						start();
 					});
 				});
 			}
 		});
-	});	
+	});
 
 	QUnit.start();
 });
diff --git a/src/install.js b/src/install.js
index 84eb798f2c..d84682ebc7 100644
--- a/src/install.js
+++ b/src/install.js
@@ -81,10 +81,6 @@ var async = require('async'),
 							protocol = urlObject.protocol,
 							server_conf = config,
 							client_conf = {
-								socket: {
-									address: protocol + '//' + host + (config.use_port ? ':' + config.port : '')
-								},
-								api_url: protocol + '//' + host + (config.use_port ? ':' + config.port : '') + relative_path + '/api/',
 								relative_path: relative_path
 							};
 

From b49c7b8609f2f01f9f5dbda6a315ab180abf9dc2 Mon Sep 17 00:00:00 2001
From: Julian Lam <julian@designcreateplay.com>
Date: Thu, 3 Oct 2013 11:47:40 -0400
Subject: [PATCH 16/29] added user-scalable=no to header meta tag (in lieu of
 fastclick lib)

closes #376 - reopen if necessary.
---
 src/webserver.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/webserver.js b/src/webserver.js
index 5d76dc3fce..585d6566bc 100644
--- a/src/webserver.js
+++ b/src/webserver.js
@@ -46,7 +46,7 @@ var express = require('express'),
 	app.build_header = function (options, callback) {
 		var defaultMetaTags = [{
 			name: 'viewport',
-			content: 'width=device-width, initial-scale=1.0'
+			content: 'width=device-width, initial-scale=1.0, user-scalable=no'
 		}, {
 			name: 'content-type',
 			content: 'text/html; charset=UTF-8'

From 038e04dee6ec223f72ab73bd54f7464ab45aba01 Mon Sep 17 00:00:00 2001
From: Julian Lam <julian@designcreateplay.com>
Date: Thu, 3 Oct 2013 15:04:25 -0400
Subject: [PATCH 17/29] revamped client side scripts so that they are loaded
 using Require.js instead.

---
 public/src/ajaxify.js                 |    3 +
 public/src/forum/account.js           |  135 +--
 public/src/forum/accountedit.js       |  436 ++++-----
 public/src/forum/accountheader.js     |   43 +-
 public/src/forum/accountsettings.js   |   34 +-
 public/src/forum/admin/categories.js  |  241 ++---
 public/src/forum/admin/footer.js      |  155 +---
 public/src/forum/admin/groups.js      |  352 +++----
 public/src/forum/admin/index.js       |   39 +-
 public/src/forum/admin/plugins.js     |   13 +-
 public/src/forum/admin/settings.js    |   81 ++
 public/src/forum/admin/themes.js      |  179 ++--
 public/src/forum/admin/topics.js      |  254 +++---
 public/src/forum/admin/users.js       |  284 +++---
 public/src/forum/category.js          |  166 ++--
 public/src/forum/favourites.js        |   14 +-
 public/src/forum/followers.js         |   26 +-
 public/src/forum/following.js         |   18 +-
 public/src/forum/login.js             |  116 +--
 public/src/forum/recent.js            |   89 +-
 public/src/forum/register.js          |  277 +++---
 public/src/forum/reset.js             |   78 +-
 public/src/forum/reset_code.js        |  110 +--
 public/src/forum/search.js            |    8 +-
 public/src/forum/topic.js             | 1208 +++++++++++++------------
 public/src/forum/unread.js            |  213 ++---
 public/src/forum/users.js             |   10 +-
 public/templates/account.tpl          |    5 +-
 public/templates/accountedit.tpl      |    6 -
 public/templates/accountsettings.tpl  |    3 -
 public/templates/admin/categories.tpl |    2 -
 public/templates/admin/facebook.tpl   |    9 +-
 public/templates/admin/footer.tpl     |    2 +-
 public/templates/admin/gplus.tpl      |    9 +-
 public/templates/admin/groups.tpl     |    2 -
 public/templates/admin/header.tpl     |    5 +-
 public/templates/admin/index.tpl      |    2 -
 public/templates/admin/motd.tpl       |    9 +-
 public/templates/admin/plugins.tpl    |    2 -
 public/templates/admin/settings.tpl   |    9 +-
 public/templates/admin/themes.tpl     |    8 +-
 public/templates/admin/topics.tpl     |    2 -
 public/templates/admin/twitter.tpl    |    9 +-
 public/templates/admin/users.tpl      |    3 -
 public/templates/category.tpl         |    4 +-
 public/templates/favourites.tpl       |    3 -
 public/templates/followers.tpl        |    3 -
 public/templates/following.tpl        |    3 -
 public/templates/footer.tpl           |    2 +-
 public/templates/header.tpl           |    8 +-
 public/templates/login.tpl            |    2 -
 public/templates/recent.tpl           |    2 -
 public/templates/register.tpl         |    2 -
 public/templates/reset.tpl            |    2 -
 public/templates/reset_code.tpl       |    3 -
 public/templates/search.tpl           |    2 -
 public/templates/topic.tpl            |    4 -
 public/templates/unread.tpl           |    2 -
 public/templates/users.tpl            |    2 -
 src/webserver.js                      |   24 +-
 60 files changed, 2389 insertions(+), 2348 deletions(-)
 create mode 100644 public/src/forum/admin/settings.js

diff --git a/public/src/ajaxify.js b/public/src/ajaxify.js
index 4c6b83991a..12ede65135 100644
--- a/public/src/ajaxify.js
+++ b/public/src/ajaxify.js
@@ -76,6 +76,9 @@ var ajaxify = {};
 			templates.flush();
 			templates.load_template(function () {
 				exec_body_scripts(content);
+				require(['forum/' + tpl_url], function(script) {
+					if (script) script.init();
+				});
 
 				if (callback) {
 					callback();
diff --git a/public/src/forum/account.js b/public/src/forum/account.js
index 45d5813ae6..aea4cf764e 100644
--- a/public/src/forum/account.js
+++ b/public/src/forum/account.js
@@ -1,85 +1,92 @@
-(function() {
-	var yourid = templates.get('yourid'),
-		theirid = templates.get('theirid'),
-		isFollowing = templates.get('isFollowing');
+define(['forum/accountheader'], function(header) {
+	var Account = {};
 
-	$(document).ready(function() {
-		var username = $('.account-username a').html();
-		app.enter_room('user/' + theirid);
+	Account.init = function() {
+		header.init();
 
-		app.addCommasToNumbers();
+		var yourid = templates.get('yourid'),
+			theirid = templates.get('theirid'),
+			isFollowing = templates.get('isFollowing');
 
-		var followBtn = $('#follow-btn');
-		var unfollowBtn = $('#unfollow-btn');
+		$(document).ready(function() {
+			var username = $('.account-username a').html();
+			app.enter_room('user/' + theirid);
 
-		if (yourid !== theirid) {
-			if (isFollowing) {
-				followBtn.hide();
-				unfollowBtn.show();
-			} else {
-				followBtn.show();
-				unfollowBtn.hide();
-			}
-		} else {
-			followBtn.hide();
-			unfollowBtn.hide();
-		}
+			app.addCommasToNumbers();
 
-		followBtn.on('click', function() {
-			socket.emit('api:user.follow', {
-				uid: theirid
-			}, function(success) {
-				if (success) {
+			var followBtn = $('#follow-btn');
+			var unfollowBtn = $('#unfollow-btn');
+
+			if (yourid !== theirid) {
+				if (isFollowing) {
 					followBtn.hide();
 					unfollowBtn.show();
-					app.alertSuccess('You are now following ' + username + '!');
 				} else {
-					app.alertError('There was an error following' + username + '!');
-				}
-			});
-			return false;
-		});
-
-		unfollowBtn.on('click', function() {
-			socket.emit('api:user.unfollow', {
-				uid: theirid
-			}, function(success) {
-				if (success) {
 					followBtn.show();
 					unfollowBtn.hide();
-					app.alertSuccess('You are no longer following ' + username + '!');
-				} else {
-					app.alertError('There was an error unfollowing ' + username + '!');
 				}
+			} else {
+				followBtn.hide();
+				unfollowBtn.hide();
+			}
+
+			followBtn.on('click', function() {
+				socket.emit('api:user.follow', {
+					uid: theirid
+				}, function(success) {
+					if (success) {
+						followBtn.hide();
+						unfollowBtn.show();
+						app.alertSuccess('You are now following ' + username + '!');
+					} else {
+						app.alertError('There was an error following' + username + '!');
+					}
+				});
+				return false;
 			});
-			return false;
-		});
 
-		$('.user-recent-posts .topic-row').on('click', function() {
-			ajaxify.go($(this).attr('topic-url'));
-		});
+			unfollowBtn.on('click', function() {
+				socket.emit('api:user.unfollow', {
+					uid: theirid
+				}, function(success) {
+					if (success) {
+						followBtn.show();
+						unfollowBtn.hide();
+						app.alertSuccess('You are no longer following ' + username + '!');
+					} else {
+						app.alertError('There was an error unfollowing ' + username + '!');
+					}
+				});
+				return false;
+			});
 
-		var onlineStatus = $('.account-online-status');
+			$('.user-recent-posts .topic-row').on('click', function() {
+				ajaxify.go($(this).attr('topic-url'));
+			});
 
-		function handleUserOnline(data) {
-			if (data.online) {
-				onlineStatus.find('span span').text('online');
-				onlineStatus.find('i').attr('class', 'icon-circle');
-			} else {
-				onlineStatus.find('span span').text('offline');
-				onlineStatus.find('i').attr('class', 'icon-circle-blank');
-			}
-		}
+			socket.on('api:user.isOnline', Account.handleUserOnline);
 
-		socket.on('api:user.isOnline', handleUserOnline);
+			socket.emit('api:user.isOnline', theirid, Account.handleUserOnline);
 
-		socket.emit('api:user.isOnline', theirid, handleUserOnline);
+			socket.on('event:new_post', function(data) {
+				var html = templates.prepare(templates['account'].blocks['posts']).parse(data);
+				$('.user-recent-posts').prepend(html);
+			});
 
-		socket.on('event:new_post', function(data) {
-			var html = templates.prepare(templates['account'].blocks['posts']).parse(data);
-			$('.user-recent-posts').prepend(html);
 		});
+	};
 
-	});
+	Account.handleUserOnline = function(data) {
+		var onlineStatus = $('.account-online-status');
+
+		if (data.online) {
+			onlineStatus.find('span span').text('online');
+			onlineStatus.find('i').attr('class', 'icon-circle');
+		} else {
+			onlineStatus.find('span span').text('offline');
+			onlineStatus.find('i').attr('class', 'icon-circle-blank');
+		}
+	};
 
-}());
\ No newline at end of file
+	return Account;
+});
\ No newline at end of file
diff --git a/public/src/forum/accountedit.js b/public/src/forum/accountedit.js
index 25c6018e91..7771562454 100644
--- a/public/src/forum/accountedit.js
+++ b/public/src/forum/accountedit.js
@@ -1,88 +1,258 @@
-var gravatarPicture = templates.get('gravatarpicture');
-var uploadedPicture = templates.get('uploadedpicture');
+define(['forum/accountheader'], function(header) {
+	var AccountEdit = {};
 
-$(document).ready(function() {
+	AccountEdit.init = function() {
+		header.init();
 
+		var gravatarPicture = templates.get('gravatarpicture');
+		var uploadedPicture = templates.get('uploadedpicture');
 
+		$('#uploadForm').submit(function() {
+			AccountEdit.status('uploading the file ...');
 
-	$('#uploadForm').submit(function() {
-		status('uploading the file ...');
+			$('#upload-progress-bar').css('width', '0%');
+			$('#upload-progress-box').show();
+			$('#upload-progress-box').removeClass('hide');
 
-		$('#upload-progress-bar').css('width', '0%');
-		$('#upload-progress-box').show();
-		$('#upload-progress-box').removeClass('hide');
+			if (!$('#userPhotoInput').val()) {
+				AccountEdit.error('select an image to upload!');
+				return false;
+			}
+
+			$(this).find('#imageUploadCsrf').val($('#csrf_token').val());
 
-		if (!$('#userPhotoInput').val()) {
-			error('select an image to upload!');
-			return false;
-		}
 
-		$(this).find('#imageUploadCsrf').val($('#csrf_token').val());
+			$(this).ajaxSubmit({
 
+				error: function(xhr) {
+					AccountEdit.error('Error: ' + xhr.status);
+				},
 
-		$(this).ajaxSubmit({
+				uploadProgress: function(event, position, total, percent) {
+					console.log(percent);
+					$('#upload-progress-bar').css('width', percent + '%');
+				},
 
-			error: function(xhr) {
-				error('Error: ' + xhr.status);
-			},
 
-			uploadProgress: function(event, position, total, percent) {
-				console.log(percent);
-				$('#upload-progress-bar').css('width', percent + '%');
-			},
+				success: function(response) {
+					if (response.error) {
+						AccountEdit.error(response.error);
+						return;
+					}
 
+					var imageUrlOnServer = response.path;
 
-			success: function(response) {
-				if (response.error) {
-					error(response.error);
-					return;
+					$('#user-current-picture').attr('src', imageUrlOnServer);
+					$('#user-uploaded-picture').attr('src', imageUrlOnServer);
+
+					uploadedPicture = imageUrlOnServer;
+
+					setTimeout(function() {
+						AccountEdit.hideAlerts();
+						$('#upload-picture-modal').modal('hide');
+					}, 750);
+
+					socket.emit('api:updateHeader', {
+						fields: ['username', 'picture', 'userslug']
+					});
+					AccountEdit.success('File uploaded successfully!');
 				}
+			});
 
-				var imageUrlOnServer = response.path;
+			return false;
+		});
 
-				$('#user-current-picture').attr('src', imageUrlOnServer);
-				$('#user-uploaded-picture').attr('src', imageUrlOnServer);
+		var selectedImageType = '';
+
+		$('#submitBtn').on('click', function() {
+
+			var userData = {
+				uid: $('#inputUID').val(),
+				email: $('#inputEmail').val(),
+				fullname: $('#inputFullname').val(),
+				website: $('#inputWebsite').val(),
+				birthday: $('#inputBirthday').val(),
+				location: $('#inputLocation').val(),
+				signature: $('#inputSignature').val()
+			};
+
+			socket.emit('api:user.updateProfile', userData, function(err, data) {
+				if (data.success) {
+					app.alertSuccess('Your profile has been updated successfully!');
+					if (data.picture) {
+						$('#user-current-picture').attr('src', data.picture);
+						$('#user_label img').attr('src', data.picture);
+					}
+					if (data.gravatarpicture) {
+						$('#user-gravatar-picture').attr('src', data.gravatarpicture);
+						gravatarPicture = data.gravatarpicture;
+					}
+				} else {
+					app.alertError('There was an error updating your profile! ' + err.error);
+				}
+			});
+			return false;
+		});
+
+		$('#changePictureBtn').on('click', function() {
+			selectedImageType = '';
+			AccountEdit.updateImages();
 
-				uploadedPicture = imageUrlOnServer;
+			$('#change-picture-modal').modal('show');
+			$('#change-picture-modal').removeClass('hide');
 
-				setTimeout(function() {
-					hideAlerts();
-					$('#upload-picture-modal').modal('hide');
-				}, 750);
+			return false;
+		});
+
+		$('#gravatar-box').on('click', function() {
+			$('#gravatar-box .icon-ok').show();
+			$('#uploaded-box .icon-ok').hide();
+			selectedImageType = 'gravatar';
+		});
 
-				socket.emit('api:updateHeader', {
-					fields: ['username', 'picture', 'userslug']
-				});
-				success('File uploaded successfully!');
+		$('#uploaded-box').on('click', function() {
+			$('#gravatar-box .icon-ok').hide();
+			$('#uploaded-box .icon-ok').show();
+			selectedImageType = 'uploaded';
+		});
+
+		$('#savePictureChangesBtn').on('click', function() {
+			$('#change-picture-modal').modal('hide');
+
+			if (selectedImageType) {
+				AccountEdit.changeUserPicture(selectedImageType);
+
+				if (selectedImageType == 'gravatar')
+					$('#user-current-picture').attr('src', gravatarPicture);
+				else if (selectedImageType == 'uploaded')
+					$('#user-current-picture').attr('src', uploadedPicture);
 			}
+
+		});
+
+		$('#upload-picture-modal').on('hide', function() {
+			$('#userPhotoInput').val('');
+		});
+
+		$('#uploadPictureBtn').on('click', function() {
+
+			$('#change-picture-modal').modal('hide');
+			$('#upload-picture-modal').modal('show');
+			$('#upload-picture-modal').removeClass('hide');
+
+			AccountEdit.hideAlerts();
+
+			return false;
+		});
+
+		$('#pictureUploadSubmitBtn').on('click', function() {
+			$('#uploadForm').submit();
 		});
 
-		return false;
-	});
+		(function handlePasswordChange() {
+			var currentPassword = $('#inputCurrentPassword');
+			var password_notify = $('#password-notify');
+			var password_confirm_notify = $('#password-confirm-notify');
+			var password = $('#inputNewPassword');
+			var password_confirm = $('#inputNewPasswordAgain');
+			var passwordvalid = false;
+			var passwordsmatch = false;
+
+
+			function onPasswordChanged() {
+				passwordvalid = utils.isPasswordValid(password.val());
+				if (password.val().length < config.minimumPasswordLength) {
+					password_notify.html('Password too short');
+					password_notify.attr('class', 'alert alert-danger');
+					password_notify.removeClass('hide');
+				} else if (!passwordvalid) {
+					password_notify.html('Invalid password');
+					password_notify.attr('class', 'alert alert-danger');
+					password_notify.removeClass('hide');
+				} else {
+					password_notify.html('OK!');
+					password_notify.attr('class', 'alert alert-success');
+					password_notify.removeClass('hide');
+				}
+
+				onPasswordConfirmChanged();
+			}
+
+			function onPasswordConfirmChanged() {
+				if (password_notify.hasClass('alert-danger') || !password_confirm.val()) {
+					password_confirm_notify.addClass('hide');
+					return;
+				}
+				if (password.val() !== password_confirm.val()) {
+					password_confirm_notify.html('Passwords must match!');
+					password_confirm_notify.attr('class', 'alert alert-danger');
+					password_confirm_notify.removeClass('hide');
+					passwordsmatch = false;
+				} else {
+					password_confirm_notify.html('OK!');
+					password_confirm_notify.attr('class', 'alert alert-success');
+					password_confirm_notify.removeClass('hide');
+					passwordsmatch = true;
+				}
+			}
+
+			password.on('blur', onPasswordChanged);
+			password_confirm.on('blur', onPasswordConfirmChanged);
 
-	function hideAlerts() {
+			$('#changePasswordBtn').on('click', function() {
+
+				if (passwordvalid && passwordsmatch && currentPassword.val()) {
+					socket.emit('api:user.changePassword', {
+						'currentPassword': currentPassword.val(),
+						'newPassword': password.val()
+					}, function(err) {
+
+						currentPassword.val('');
+						password.val('');
+						password_confirm.val('');
+						password_notify.addClass('hide');
+						password_confirm_notify.addClass('hide');
+						passwordsmatch = false;
+						passwordvalid = false;
+
+						if (err) {
+							app.alertError(err.error);
+							return;
+						}
+
+						app.alertSuccess('Your password is updated!');
+
+					});
+				}
+				return false;
+			});
+
+		}());
+	};
+
+	AccountEdit.hideAlerts = function() {
 		$('#alert-status').addClass('hide');
 		$('#alert-success').addClass('hide');
 		$('#alert-error').addClass('hide');
 		$('#upload-progress-box').addClass('hide');
 	}
 
-	function status(message) {
-		hideAlerts();
+	AccountEdit.status = function(message) {
+		AccountEdit.hideAlerts();
 		$('#alert-status').text(message).removeClass('hide');
 	}
 
-	function success(message) {
-		hideAlerts();
+	AccountEdit.success = function(message) {
+		AccountEdit.hideAlerts();
 		$('#alert-success').text(message).removeClass('hide');
 	}
 
-	function error(message) {
-		hideAlerts();
+	AccountEdit.error = function(message) {
+		AccountEdit.hideAlerts();
 		$('#alert-error').text(message).removeClass('hide');
 	}
 
-	function changeUserPicture(type) {
+	AccountEdit.changeUserPicture = function(type) {
 		var userData = {
 			type: type
 		};
@@ -94,40 +264,10 @@ $(document).ready(function() {
 		});
 	}
 
-	var selectedImageType = '';
-
-	$('#submitBtn').on('click', function() {
-
-		var userData = {
-			uid: $('#inputUID').val(),
-			email: $('#inputEmail').val(),
-			fullname: $('#inputFullname').val(),
-			website: $('#inputWebsite').val(),
-			birthday: $('#inputBirthday').val(),
-			location: $('#inputLocation').val(),
-			signature: $('#inputSignature').val()
-		};
-
-		socket.emit('api:user.updateProfile', userData, function(err, data) {
-			if (data.success) {
-				app.alertSuccess('Your profile has been updated successfully!');
-				if (data.picture) {
-					$('#user-current-picture').attr('src', data.picture);
-					$('#user_label img').attr('src', data.picture);
-				}
-				if (data.gravatarpicture) {
-					$('#user-gravatar-picture').attr('src', data.gravatarpicture);
-					gravatarPicture = data.gravatarpicture;
-				}
-			} else {
-				app.alertError('There was an error updating your profile! ' + err.error);
-			}
-		});
-		return false;
-	});
-
-	function updateImages() {
+	AccountEdit.updateImages = function() {
 		var currentPicture = $('#user-current-picture').attr('src');
+		var gravatarPicture = templates.get('gravatarpicture');
+		var uploadedPicture = templates.get('uploadedpicture');
 
 		if (gravatarPicture) {
 			$('#user-gravatar-picture').attr('src', gravatarPicture);
@@ -153,139 +293,5 @@ $(document).ready(function() {
 			$('#uploaded-box .icon-ok').hide();
 	}
 
-
-	$('#changePictureBtn').on('click', function() {
-		selectedImageType = '';
-		updateImages();
-
-		$('#change-picture-modal').modal('show');
-		$('#change-picture-modal').removeClass('hide');
-
-		return false;
-	});
-
-	$('#gravatar-box').on('click', function() {
-		$('#gravatar-box .icon-ok').show();
-		$('#uploaded-box .icon-ok').hide();
-		selectedImageType = 'gravatar';
-	});
-
-	$('#uploaded-box').on('click', function() {
-		$('#gravatar-box .icon-ok').hide();
-		$('#uploaded-box .icon-ok').show();
-		selectedImageType = 'uploaded';
-	});
-
-	$('#savePictureChangesBtn').on('click', function() {
-		$('#change-picture-modal').modal('hide');
-
-		if (selectedImageType) {
-			changeUserPicture(selectedImageType);
-
-			if (selectedImageType == 'gravatar')
-				$('#user-current-picture').attr('src', gravatarPicture);
-			else if (selectedImageType == 'uploaded')
-				$('#user-current-picture').attr('src', uploadedPicture);
-		}
-
-	});
-
-	$('#upload-picture-modal').on('hide', function() {
-		$('#userPhotoInput').val('');
-	});
-
-	$('#uploadPictureBtn').on('click', function() {
-
-		$('#change-picture-modal').modal('hide');
-		$('#upload-picture-modal').modal('show');
-		$('#upload-picture-modal').removeClass('hide');
-
-		hideAlerts();
-
-		return false;
-	});
-
-	$('#pictureUploadSubmitBtn').on('click', function() {
-		$('#uploadForm').submit();
-	});
-
-	(function handlePasswordChange() {
-		var currentPassword = $('#inputCurrentPassword');
-		var password_notify = $('#password-notify');
-		var password_confirm_notify = $('#password-confirm-notify');
-		var password = $('#inputNewPassword');
-		var password_confirm = $('#inputNewPasswordAgain');
-		var passwordvalid = false;
-		var passwordsmatch = false;
-
-
-		function onPasswordChanged() {
-			passwordvalid = utils.isPasswordValid(password.val());
-			if (password.val().length < config.minimumPasswordLength) {
-				password_notify.html('Password too short');
-				password_notify.attr('class', 'alert alert-danger');
-				password_notify.removeClass('hide');
-			} else if (!passwordvalid) {
-				password_notify.html('Invalid password');
-				password_notify.attr('class', 'alert alert-danger');
-				password_notify.removeClass('hide');
-			} else {
-				password_notify.html('OK!');
-				password_notify.attr('class', 'alert alert-success');
-				password_notify.removeClass('hide');
-			}
-
-			onPasswordConfirmChanged();
-		}
-
-		function onPasswordConfirmChanged() {
-			if (password_notify.hasClass('alert-danger') || !password_confirm.val()) {
-				password_confirm_notify.addClass('hide');
-				return;
-			}
-			if (password.val() !== password_confirm.val()) {
-				password_confirm_notify.html('Passwords must match!');
-				password_confirm_notify.attr('class', 'alert alert-danger');
-				password_confirm_notify.removeClass('hide');
-				passwordsmatch = false;
-			} else {
-				password_confirm_notify.html('OK!');
-				password_confirm_notify.attr('class', 'alert alert-success');
-				password_confirm_notify.removeClass('hide');
-				passwordsmatch = true;
-			}
-		}
-
-		password.on('blur', onPasswordChanged);
-		password_confirm.on('blur', onPasswordConfirmChanged);
-
-		$('#changePasswordBtn').on('click', function() {
-
-			if (passwordvalid && passwordsmatch && currentPassword.val()) {
-				socket.emit('api:user.changePassword', {
-					'currentPassword': currentPassword.val(),
-					'newPassword': password.val()
-				}, function(err) {
-
-					currentPassword.val('');
-					password.val('');
-					password_confirm.val('');
-					password_notify.addClass('hide');
-					password_confirm_notify.addClass('hide');
-					passwordsmatch = false;
-					passwordvalid = false;
-
-					if (err) {
-						app.alertError(err.error);
-						return;
-					}
-
-					app.alertSuccess('Your password is updated!');
-
-				});
-			}
-			return false;
-		});
-
-	}());
+	return AccountEdit;
 });
\ No newline at end of file
diff --git a/public/src/forum/accountheader.js b/public/src/forum/accountheader.js
index 15bb2db614..d865a83b1b 100644
--- a/public/src/forum/accountheader.js
+++ b/public/src/forum/accountheader.js
@@ -1,24 +1,11 @@
-(function() {
-	var yourid = templates.get('yourid'),
-		theirid = templates.get('theirid');
+define(function() {
+	var	AccountHeader = {};
 
+	AccountHeader.init = function() {
+		var yourid = templates.get('yourid'),
+			theirid = templates.get('theirid');
 
-	function createMenu() {
-		var userslug = $('.account-username-box').attr('data-userslug');
-		var links = $('<div class="account-sub-links inline-block pull-right">\
-			<span id="settingsLink" class="pull-right"><a href="/user/' + userslug + '/settings">settings</a></span>\
-			<span id="favouritesLink" class="pull-right"><a href="/user/' + userslug + '/favourites">favourites</a></span>\
-			<span class="pull-right"><a href="/user/' + userslug + '/followers">followers</a></span>\
-			<span class="pull-right"><a href="/user/' + userslug + '/following">following</a></span>\
-			<span id="editLink" class="pull-right"><a href="/user/' + userslug + '/edit">edit</a></span>\
-		</div>');
-
-		$('.account-username-box').append(links);
-	}
-
-	$(document).ready(function() {
-
-		createMenu();
+		AccountHeader.createMenu();
 
 		var editLink = $('#editLink');
 		var settingsLink = $('#settingsLink');
@@ -37,6 +24,20 @@
 				return false;
 			}
 		});
-	});
+	}
+
+	AccountHeader.createMenu = function() {
+		var userslug = $('.account-username-box').attr('data-userslug');
+		var links = $('<div class="account-sub-links inline-block pull-right">\
+			<span id="settingsLink" class="pull-right"><a href="/user/' + userslug + '/settings">settings</a></span>\
+			<span id="favouritesLink" class="pull-right"><a href="/user/' + userslug + '/favourites">favourites</a></span>\
+			<span class="pull-right"><a href="/user/' + userslug + '/followers">followers</a></span>\
+			<span class="pull-right"><a href="/user/' + userslug + '/following">following</a></span>\
+			<span id="editLink" class="pull-right"><a href="/user/' + userslug + '/edit">edit</a></span>\
+		</div>');
+
+		$('.account-username-box').append(links);
+	}
 
-}());
\ No newline at end of file
+	return AccountHeader;
+});
\ No newline at end of file
diff --git a/public/src/forum/accountsettings.js b/public/src/forum/accountsettings.js
index d121ecbe85..6f6d6edcbb 100644
--- a/public/src/forum/accountsettings.js
+++ b/public/src/forum/accountsettings.js
@@ -1,19 +1,25 @@
-$(document).ready(function() {
+define(['forum/accountheader'], function(header) {
+	var	AccountSettings = {};
 
-	$('#submitBtn').on('click', function() {
+	AccountSettings.init = function() {
+		header.init();
 
-		var settings = {
-			showemail: $('#showemailCheckBox').is(':checked') ? 1 : 0
-		};
+		$('#submitBtn').on('click', function() {
 
-		socket.emit('api:user.saveSettings', settings, function(success) {
-			if (success) {
-				app.alertSuccess('Settings saved!');
-			} else {
-				app.alertError('There was an error saving settings!');
-			}
+			var settings = {
+				showemail: $('#showemailCheckBox').is(':checked') ? 1 : 0
+			};
+
+			socket.emit('api:user.saveSettings', settings, function(success) {
+				if (success) {
+					app.alertSuccess('Settings saved!');
+				} else {
+					app.alertError('There was an error saving settings!');
+				}
+			});
+			return false;
 		});
-		return false;
-	});
+	};
 
-});
\ No newline at end of file
+	return AccountSettings;
+});
diff --git a/public/src/forum/admin/categories.js b/public/src/forum/admin/categories.js
index cb7ddbd39e..4568e7ceae 100644
--- a/public/src/forum/admin/categories.js
+++ b/public/src/forum/admin/categories.js
@@ -1,139 +1,144 @@
-var modified_categories = {};
+define(function() {
+	var	Categories = {};
 
-function modified(el) {
-	var cid = $(el).parents('li').attr('data-cid');
+	Categories.init = function() {
+		var modified_categories = {};
 
-	modified_categories[cid] = modified_categories[cid] || {};
-	modified_categories[cid][$(el).attr('data-name')] = $(el).val();
-}
+		function modified(el) {
+			var cid = $(el).parents('li').attr('data-cid');
 
-function save() {
-	socket.emit('api:admin.categories.update', modified_categories);
-	modified_categories = {};
-}
-
-function select_icon(el) {
-	var selected = el.attr('class').replace(' icon-2x', '');
-	jQuery('#icons .selected').removeClass('selected');
-	if (selected)
-		jQuery('#icons .' + selected).parent().addClass('selected');
-
-
-	bootbox.confirm('<h2>Select an icon.</h2>' + document.getElementById('icons').innerHTML, function(confirm) {
-		if (confirm) {
-			var iconClass = jQuery('.bootbox .selected').children(':first').attr('class');
-			el.attr('class', iconClass + ' icon-2x');
-			el.val(iconClass);
-
-			modified(el);
+			modified_categories[cid] = modified_categories[cid] || {};
+			modified_categories[cid][$(el).attr('data-name')] = $(el).val();
 		}
-	});
 
-	setTimeout(function() { //bootbox was rewritten for BS3 and I had to add this timeout for the previous code to work. TODO: to look into
-		jQuery('.bootbox .col-md-3').on('click', function() {
-			jQuery('.bootbox .selected').removeClass('selected');
-			jQuery(this).addClass('selected');
-		});
-	}, 500);
-}
-
-
-function update_blockclass(el) {
-	el.parentNode.parentNode.className = 'entry-row ' + el.value;
-}
-
-jQuery('#entry-container').sortable();
-jQuery('.blockclass').each(function() {
-	jQuery(this).val(this.getAttribute('data-value'));
-});
-
-
-//DRY Failure. this needs to go into an ajaxify onready style fn. Currently is copy pasted into every single function so after ACP is off the ground fix asap
-(function() {
-	function showCreateCategoryModal() {
-		$('#new-category-modal').modal();
-	}
-
-	function createNewCategory() {
-		var category = {
-			name: $('#inputName').val(),
-			description: $('#inputDescription').val(),
-			icon: $('#new-category-modal i').attr('value'),
-			blockclass: $('#inputBlockclass').val()
-		};
-
-		socket.emit('api:admin.categories.create', category, function(err, data) {
-			if (!err) {
-				app.alert({
-					alert_id: 'category_created',
-					title: 'Created',
-					message: 'Category successfully created!',
-					type: 'success',
-					timeout: 2000
-				});
+		function save() {
+			socket.emit('api:admin.categories.update', modified_categories);
+			modified_categories = {};
+		}
 
-				var html = templates.prepare(templates['admin/categories'].blocks['categories']).parse({
-					categories: [data]
-				});
-				$('#entry-container').append(html);
+		function select_icon(el) {
+			var selected = el.attr('class').replace(' icon-2x', '');
+			jQuery('#icons .selected').removeClass('selected');
+			if (selected)
+				jQuery('#icons .' + selected).parent().addClass('selected');
 
-				$('#new-category-modal').modal('hide');
-			}
-		});
-	}
 
-	jQuery('document').ready(function() {
-		var url = window.location.href,
-			parts = url.split('/'),
-			active = parts[parts.length - 1];
+			bootbox.confirm('<h2>Select an icon.</h2>' + document.getElementById('icons').innerHTML, function(confirm) {
+				if (confirm) {
+					var iconClass = jQuery('.bootbox .selected').children(':first').attr('class');
+					el.attr('class', iconClass + ' icon-2x');
+					el.val(iconClass);
 
-		jQuery('.nav-pills li').removeClass('active');
-		jQuery('.nav-pills li a').each(function() {
-			if (this.getAttribute('href').match(active)) {
-				jQuery(this.parentNode).addClass('active');
-				return false;
-			}
-		});
+					modified(el);
+				}
+			});
 
-		jQuery('#save').on('click', save);
-		jQuery('#addNew').on('click', showCreateCategoryModal);
-		jQuery('#create-category-btn').on('click', createNewCategory);
+			setTimeout(function() { //bootbox was rewritten for BS3 and I had to add this timeout for the previous code to work. TODO: to look into
+				jQuery('.bootbox .col-md-3').on('click', function() {
+					jQuery('.bootbox .selected').removeClass('selected');
+					jQuery(this).addClass('selected');
+				});
+			}, 500);
+		}
 
-		jQuery('#entry-container').on('click', '.icon', function(ev) {
-			select_icon($(this).find('i'));
-		});
 
-		jQuery('.blockclass').on('change', function(ev) {
-			update_blockclass(ev.target);
-		});
+		function update_blockclass(el) {
+			el.parentNode.parentNode.className = 'entry-row ' + el.value;
+		}
 
-		jQuery('.category_name, .category_description, .blockclass').on('change', function(ev) {
-			modified(ev.target);
+		jQuery('#entry-container').sortable();
+		jQuery('.blockclass').each(function() {
+			jQuery(this).val(this.getAttribute('data-value'));
 		});
 
-		jQuery('.entry-row button').each(function(index, element) {
-			var disabled = $(element).attr('data-disabled');
-			if (disabled == "0" || disabled == "")
-				$(element).html('Disable');
-			else
-				$(element).html('Enable');
 
-		});
+		//DRY Failure. this needs to go into an ajaxify onready style fn. Currently is copy pasted into every single function so after ACP is off the ground fix asap
+		function showCreateCategoryModal() {
+			$('#new-category-modal').modal();
+		}
 
-		jQuery('.entry-row button').on('click', function(ev) {
-			var btn = jQuery(this);
-			var categoryRow = btn.parents('li');
-			var cid = categoryRow.attr('data-cid');
+		function createNewCategory() {
+			var category = {
+				name: $('#inputName').val(),
+				description: $('#inputDescription').val(),
+				icon: $('#new-category-modal i').attr('value'),
+				blockclass: $('#inputBlockclass').val()
+			};
+
+			socket.emit('api:admin.categories.create', category, function(err, data) {
+				if (!err) {
+					app.alert({
+						alert_id: 'category_created',
+						title: 'Created',
+						message: 'Category successfully created!',
+						type: 'success',
+						timeout: 2000
+					});
+
+					var html = templates.prepare(templates['admin/categories'].blocks['categories']).parse({
+						categories: [data]
+					});
+					$('#entry-container').append(html);
+
+					$('#new-category-modal').modal('hide');
+				}
+			});
+		}
 
-			var disabled = btn.html() == "Disable" ? "1" : "0";
-			categoryRow.remove();
-			modified_categories[cid] = modified_categories[cid] || {};
-			modified_categories[cid]['disabled'] = disabled;
+		jQuery('document').ready(function() {
+			var url = window.location.href,
+				parts = url.split('/'),
+				active = parts[parts.length - 1];
+
+			jQuery('.nav-pills li').removeClass('active');
+			jQuery('.nav-pills li a').each(function() {
+				if (this.getAttribute('href').match(active)) {
+					jQuery(this.parentNode).addClass('active');
+					return false;
+				}
+			});
+
+			jQuery('#save').on('click', save);
+			jQuery('#addNew').on('click', showCreateCategoryModal);
+			jQuery('#create-category-btn').on('click', createNewCategory);
+
+			jQuery('#entry-container').on('click', '.icon', function(ev) {
+				select_icon($(this).find('i'));
+			});
+
+			jQuery('.blockclass').on('change', function(ev) {
+				update_blockclass(ev.target);
+			});
+
+			jQuery('.category_name, .category_description, .blockclass').on('change', function(ev) {
+				modified(ev.target);
+			});
+
+			jQuery('.entry-row button').each(function(index, element) {
+				var disabled = $(element).attr('data-disabled');
+				if (disabled == "0" || disabled == "")
+					$(element).html('Disable');
+				else
+					$(element).html('Enable');
+
+			});
+
+			jQuery('.entry-row button').on('click', function(ev) {
+				var btn = jQuery(this);
+				var categoryRow = btn.parents('li');
+				var cid = categoryRow.attr('data-cid');
+
+				var disabled = btn.html() == "Disable" ? "1" : "0";
+				categoryRow.remove();
+				modified_categories[cid] = modified_categories[cid] || {};
+				modified_categories[cid]['disabled'] = disabled;
+
+				save();
+				return false;
+			});
 
-			save();
-			return false;
 		});
+	};
 
-	});
-
-}());
\ No newline at end of file
+	return Categories;
+});
\ No newline at end of file
diff --git a/public/src/forum/admin/footer.js b/public/src/forum/admin/footer.js
index ee9062af9d..22990fcf7b 100644
--- a/public/src/forum/admin/footer.js
+++ b/public/src/forum/admin/footer.js
@@ -1,121 +1,44 @@
-var nodebb_admin = (function(nodebb_admin) {
-
-	nodebb_admin.config = undefined;
-
-	nodebb_admin.prepare = function() {
-		// Come back in 500ms if the config isn't ready yet
-		if (nodebb_admin.config === undefined) {
-			setTimeout(function() {
-				nodebb_admin.prepare();
-			}, 500);
-			return;
-		}
-
-		// Populate the fields on the page from the config
-		var fields = document.querySelectorAll('#content [data-field]'),
-			numFields = fields.length,
-			saveBtn = document.getElementById('save'),
-			x, key, inputType;
-		for (x = 0; x < numFields; x++) {
-			key = fields[x].getAttribute('data-field');
-			inputType = fields[x].getAttribute('type');
-			if (fields[x].nodeName === 'INPUT') {
-				if (nodebb_admin.config[key]) {
-					switch (inputType) {
-						case 'text':
-						case 'textarea':
-						case 'number':
-							fields[x].value = nodebb_admin.config[key];
-							break;
-
-						case 'checkbox':
-							fields[x].checked = nodebb_admin.config[key] === '1' ? true : false;
-							break;
-					}
-				}
-			} else if (fields[x].nodeName === 'TEXTAREA') {
-				if (nodebb_admin.config[key]) fields[x].value = nodebb_admin.config[key];
+jQuery('document').ready(function() {
+	// On menu click, change "active" state
+	var menuEl = document.querySelector('.sidebar-nav'),
+		liEls = menuEl.querySelectorAll('li')
+		parentEl = null;
+
+	menuEl.addEventListener('click', function(e) {
+		parentEl = e.target.parentNode;
+		if (parentEl.nodeName === 'LI') {
+			for (var x = 0, numLis = liEls.length; x < numLis; x++) {
+				if (liEls[x] !== parentEl) jQuery(liEls[x]).removeClass('active');
+				else jQuery(parentEl).addClass('active');
 			}
 		}
+	}, false);
+});
 
-		saveBtn.addEventListener('click', function(e) {
-			var key, value;
-			e.preventDefault();
-
-			for (x = 0; x < numFields; x++) {
-				key = fields[x].getAttribute('data-field');
-				if (fields[x].nodeName === 'INPUT') {
-					inputType = fields[x].getAttribute('type');
-					switch (inputType) {
-						case 'text':
-						case 'number':
-							value = fields[x].value;
-							break;
-
-						case 'checkbox':
-							value = fields[x].checked ? '1' : '0';
-							break;
-					}
-				} else if (fields[x].nodeName === 'TEXTAREA') {
-					value = fields[x].value;
-				}
-
-				socket.emit('api:config.set', {
-					key: key,
-					value: value
-				});
-			}
+socket.once('api:config.get', function(config) {
+	require(['forum/admin/settings'], function(Settings) {
+		Settings.config = config;
+	});
+});
+
+socket.emit('api:config.get');
+
+socket.on('api:config.set', function(data) {
+	if (data.status === 'ok') {
+		app.alert({
+			alert_id: 'config_status',
+			timeout: 2500,
+			title: 'Changes Saved',
+			message: 'Your changes to the NodeBB configuration have been saved.',
+			type: 'success'
+		});
+	} else {
+		app.alert({
+			alert_id: 'config_status',
+			timeout: 2500,
+			title: 'Changes Not Saved',
+			message: 'NodeBB encountered a problem saving your changes',
+			type: 'danger'
 		});
 	}
-
-	nodebb_admin.remove = function(key) {
-		socket.emit('api:config.remove', key);
-	}
-
-
-	jQuery('document').ready(function() {
-		// On menu click, change "active" state
-		var menuEl = document.querySelector('.sidebar-nav'),
-			liEls = menuEl.querySelectorAll('li')
-			parentEl = null;
-
-		menuEl.addEventListener('click', function(e) {
-			parentEl = e.target.parentNode;
-			if (parentEl.nodeName === 'LI') {
-				for (var x = 0, numLis = liEls.length; x < numLis; x++) {
-					if (liEls[x] !== parentEl) jQuery(liEls[x]).removeClass('active');
-					else jQuery(parentEl).addClass('active');
-				}
-			}
-		}, false);
-	});
-
-	socket.once('api:config.get', function(config) {
-		nodebb_admin.config = config;
-	});
-
-	socket.emit('api:config.get');
-
-	socket.on('api:config.set', function(data) {
-		if (data.status === 'ok') {
-			app.alert({
-				alert_id: 'config_status',
-				timeout: 2500,
-				title: 'Changes Saved',
-				message: 'Your changes to the NodeBB configuration have been saved.',
-				type: 'success'
-			});
-		} else {
-			app.alert({
-				alert_id: 'config_status',
-				timeout: 2500,
-				title: 'Changes Not Saved',
-				message: 'NodeBB encountered a problem saving your changes',
-				type: 'danger'
-			});
-		}
-	});
-
-	return nodebb_admin;
-
-}(nodebb_admin || {}));
\ No newline at end of file
+});
\ No newline at end of file
diff --git a/public/src/forum/admin/groups.js b/public/src/forum/admin/groups.js
index c9dda987fe..80737b44b9 100644
--- a/public/src/forum/admin/groups.js
+++ b/public/src/forum/admin/groups.js
@@ -1,194 +1,200 @@
-$(document).ready(function() {
-	var createEl = document.getElementById('create'),
-		createModal = $('#create-modal'),
-		createSubmitBtn = document.getElementById('create-modal-go'),
-		createNameEl = $('#create-group-name'),
-		detailsModal = $('#group-details-modal'),
-		detailsSearch = detailsModal.find('#group-details-search'),
-		searchResults = detailsModal.find('#group-details-search-results'),
-		groupMembersEl = detailsModal.find('ul.current_members'),
-		detailsModalSave = detailsModal.find('.btn-primary'),
-		searchDelay = undefined,
-		listEl = $('#groups-list');
-
-	createEl.addEventListener('click', function() {
-		createModal.modal('show');
-		setTimeout(function() {
-			createNameEl.focus();
-		}, 250);
-	}, false);
-
-	createSubmitBtn.addEventListener('click', function() {
-		var submitObj = {
-			name: createNameEl.val(),
-			description: $('#create-group-desc').val()
-		},
-			errorEl = $('#create-modal-error'),
-			errorText;
-
-		socket.emit('api:groups.create', submitObj, function(err, data) {
-			if (err) {
-				switch (err) {
-					case 'group-exists':
-						errorText = '<strong>Please choose another name</strong><p>There seems to be a group with this name already.</p>';
-						break;
-					case 'name-too-short':
-						errorText = '<strong>Please specify a grou name</strong><p>A group name is required for administrative purposes.</p>';
-						break;
-					default:
-						errorText = '<strong>Uh-Oh</strong><p>There was a problem creating your group. Please try again later!</p>';
-						break;
+define(function() {
+	var	Groups = {};
+
+	Groups.init = function() {
+		var createEl = document.getElementById('create'),
+			createModal = $('#create-modal'),
+			createSubmitBtn = document.getElementById('create-modal-go'),
+			createNameEl = $('#create-group-name'),
+			detailsModal = $('#group-details-modal'),
+			detailsSearch = detailsModal.find('#group-details-search'),
+			searchResults = detailsModal.find('#group-details-search-results'),
+			groupMembersEl = detailsModal.find('ul.current_members'),
+			detailsModalSave = detailsModal.find('.btn-primary'),
+			searchDelay = undefined,
+			listEl = $('#groups-list');
+
+		createEl.addEventListener('click', function() {
+			createModal.modal('show');
+			setTimeout(function() {
+				createNameEl.focus();
+			}, 250);
+		}, false);
+
+		createSubmitBtn.addEventListener('click', function() {
+			var submitObj = {
+				name: createNameEl.val(),
+				description: $('#create-group-desc').val()
+			},
+				errorEl = $('#create-modal-error'),
+				errorText;
+
+			socket.emit('api:groups.create', submitObj, function(err, data) {
+				if (err) {
+					switch (err) {
+						case 'group-exists':
+							errorText = '<strong>Please choose another name</strong><p>There seems to be a group with this name already.</p>';
+							break;
+						case 'name-too-short':
+							errorText = '<strong>Please specify a grou name</strong><p>A group name is required for administrative purposes.</p>';
+							break;
+						default:
+							errorText = '<strong>Uh-Oh</strong><p>There was a problem creating your group. Please try again later!</p>';
+							break;
+					}
+
+					errorEl.html(errorText).removeClass('hide');
+				} else {
+					createModal.modal('hide');
+					errorEl.addClass('hide');
+					createNameEl.val('');
+					ajaxify.go('admin/groups');
 				}
+			});
+		});
 
-				errorEl.html(errorText).removeClass('hide');
-			} else {
-				createModal.modal('hide');
-				errorEl.addClass('hide');
-				createNameEl.val('');
-				ajaxify.go('admin/groups');
+		listEl.on('click', 'button[data-action]', function() {
+			var action = this.getAttribute('data-action'),
+				gid = $(this).parents('li[data-gid]').attr('data-gid');
+
+			switch (action) {
+				case 'delete':
+					bootbox.confirm('Are you sure you wish to delete this group?', function(confirm) {
+						if (confirm) {
+							socket.emit('api:groups.delete', gid, function(err, data) {
+								if (data === 'OK') ajaxify.go('admin/groups');
+							});
+						}
+					});
+					break;
+				case 'members':
+					socket.emit('api:groups.get', gid, function(err, groupObj) {
+						var formEl = detailsModal.find('form'),
+							nameEl = formEl.find('#change-group-name'),
+							descEl = formEl.find('#change-group-desc'),
+							memberIcon = document.createElement('li'),
+							numMembers = groupObj.members.length,
+							membersFrag = document.createDocumentFragment(),
+							memberIconImg, x;
+
+
+						nameEl.val(groupObj.name);
+						descEl.val(groupObj.description);
+
+						// Member list
+						memberIcon.innerHTML = '<img /><span></span>';
+						memberIconImg = memberIcon.querySelector('img');
+						memberIconLabel = memberIcon.querySelector('span');
+						if (numMembers > 0) {
+							for (x = 0; x < numMembers; x++) {
+								memberIconImg.src = groupObj.members[x].picture;
+								memberIconLabel.innerHTML = groupObj.members[x].username;
+								memberIcon.setAttribute('data-uid', groupObj.members[x].uid);
+								membersFrag.appendChild(memberIcon.cloneNode(true));
+							}
+							groupMembersEl.html('');
+							groupMembersEl[0].appendChild(membersFrag);
+						}
+
+						detailsModal.attr('data-gid', groupObj.gid);
+						detailsModal.modal('show');
+					});
+					break;
 			}
 		});
-	});
-
-	listEl.on('click', 'button[data-action]', function() {
-		var action = this.getAttribute('data-action'),
-			gid = $(this).parents('li[data-gid]').attr('data-gid');
-
-		switch (action) {
-			case 'delete':
-				bootbox.confirm('Are you sure you wish to delete this group?', function(confirm) {
-					if (confirm) {
-						socket.emit('api:groups.delete', gid, function(err, data) {
-							if (data === 'OK') ajaxify.go('admin/groups');
-						});
-					}
-				});
-				break;
-			case 'members':
-				socket.emit('api:groups.get', gid, function(err, groupObj) {
-					var formEl = detailsModal.find('form'),
-						nameEl = formEl.find('#change-group-name'),
-						descEl = formEl.find('#change-group-desc'),
-						memberIcon = document.createElement('li'),
-						numMembers = groupObj.members.length,
-						membersFrag = document.createDocumentFragment(),
-						memberIconImg, x;
-
-
-					nameEl.val(groupObj.name);
-					descEl.val(groupObj.description);
-
-					// Member list
-					memberIcon.innerHTML = '<img /><span></span>';
-					memberIconImg = memberIcon.querySelector('img');
-					memberIconLabel = memberIcon.querySelector('span');
-					if (numMembers > 0) {
-						for (x = 0; x < numMembers; x++) {
-							memberIconImg.src = groupObj.members[x].picture;
-							memberIconLabel.innerHTML = groupObj.members[x].username;
-							memberIcon.setAttribute('data-uid', groupObj.members[x].uid);
-							membersFrag.appendChild(memberIcon.cloneNode(true));
+
+		detailsSearch.on('keyup', function() {
+			var searchEl = this;
+
+			if (searchDelay) clearTimeout(searchDelay);
+
+			searchDelay = setTimeout(function() {
+				var searchText = searchEl.value,
+					resultsEl = document.getElementById('group-details-search-results'),
+					foundUser = document.createElement('li'),
+					foundUserImg, foundUserLabel;
+
+				foundUser.innerHTML = '<img /><span></span>';
+				foundUserImg = foundUser.getElementsByTagName('img')[0];
+				foundUserLabel = foundUser.getElementsByTagName('span')[0];
+
+				socket.emit('api:admin.user.search', searchText, function(err, results) {
+					if (!err && results && results.length > 0) {
+						var numResults = results.length,
+							resultsSlug = document.createDocumentFragment(),
+							x;
+						if (numResults > 4) numResults = 4;
+						for (x = 0; x < numResults; x++) {
+							foundUserImg.src = results[x].picture;
+							foundUserLabel.innerHTML = results[x].username;
+							foundUser.setAttribute('title', results[x].username);
+							foundUser.setAttribute('data-uid', results[x].uid);
+							resultsSlug.appendChild(foundUser.cloneNode(true));
 						}
-						groupMembersEl.html('');
-						groupMembersEl[0].appendChild(membersFrag);
-					}
 
-					detailsModal.attr('data-gid', groupObj.gid);
-					detailsModal.modal('show');
+						resultsEl.innerHTML = '';
+						resultsEl.appendChild(resultsSlug);
+					} else resultsEl.innerHTML = '<li>No Users Found</li>';
 				});
-				break;
-		}
-	});
-
-	detailsSearch.on('keyup', function() {
-		var searchEl = this;
-
-		if (searchDelay) clearTimeout(searchDelay);
-
-		searchDelay = setTimeout(function() {
-			var searchText = searchEl.value,
-				resultsEl = document.getElementById('group-details-search-results'),
-				foundUser = document.createElement('li'),
-				foundUserImg, foundUserLabel;
-
-			foundUser.innerHTML = '<img /><span></span>';
-			foundUserImg = foundUser.getElementsByTagName('img')[0];
-			foundUserLabel = foundUser.getElementsByTagName('span')[0];
-
-			socket.emit('api:admin.user.search', searchText, function(err, results) {
-				if (!err && results && results.length > 0) {
-					var numResults = results.length,
-						resultsSlug = document.createDocumentFragment(),
-						x;
-					if (numResults > 4) numResults = 4;
-					for (x = 0; x < numResults; x++) {
-						foundUserImg.src = results[x].picture;
-						foundUserLabel.innerHTML = results[x].username;
-						foundUser.setAttribute('title', results[x].username);
-						foundUser.setAttribute('data-uid', results[x].uid);
-						resultsSlug.appendChild(foundUser.cloneNode(true));
-					}
+			}, 200);
+		});
 
-					resultsEl.innerHTML = '';
-					resultsEl.appendChild(resultsSlug);
-				} else resultsEl.innerHTML = '<li>No Users Found</li>';
-			});
-		}, 200);
-	});
+		searchResults.on('click', 'li[data-uid]', function() {
+			var userLabel = this,
+				uid = parseInt(this.getAttribute('data-uid')),
+				gid = detailsModal.attr('data-gid'),
+				members = [];
 
-	searchResults.on('click', 'li[data-uid]', function() {
-		var userLabel = this,
-			uid = parseInt(this.getAttribute('data-uid')),
-			gid = detailsModal.attr('data-gid'),
-			members = [];
+			groupMembersEl.find('li[data-uid]').each(function() {
+				members.push(parseInt(this.getAttribute('data-uid')));
+			});
 
-		groupMembersEl.find('li[data-uid]').each(function() {
-			members.push(parseInt(this.getAttribute('data-uid')));
+			if (members.indexOf(uid) === -1) {
+				socket.emit('api:groups.join', {
+					gid: gid,
+					uid: uid
+				}, function(err, data) {
+					if (!err) {
+						groupMembersEl.append(userLabel.cloneNode(true));
+					}
+				});
+			}
 		});
 
-		if (members.indexOf(uid) === -1) {
-			socket.emit('api:groups.join', {
+		groupMembersEl.on('click', 'li[data-uid]', function() {
+			var uid = this.getAttribute('data-uid'),
+				gid = detailsModal.attr('data-gid');
+
+			socket.emit('api:groups.leave', {
 				gid: gid,
 				uid: uid
 			}, function(err, data) {
 				if (!err) {
-					groupMembersEl.append(userLabel.cloneNode(true));
+					groupMembersEl.find('li[data-uid="' + uid + '"]').remove();
 				}
 			});
-		}
-	});
-
-	groupMembersEl.on('click', 'li[data-uid]', function() {
-		var uid = this.getAttribute('data-uid'),
-			gid = detailsModal.attr('data-gid');
-
-		socket.emit('api:groups.leave', {
-			gid: gid,
-			uid: uid
-		}, function(err, data) {
-			if (!err) {
-				groupMembersEl.find('li[data-uid="' + uid + '"]').remove();
-			}
 		});
-	});
-
-	detailsModalSave.on('click', function() {
-		var formEl = detailsModal.find('form'),
-			nameEl = formEl.find('#change-group-name'),
-			descEl = formEl.find('#change-group-desc'),
-			gid = detailsModal.attr('data-gid');
-
-		socket.emit('api:groups.update', {
-			gid: gid,
-			values: {
-				name: nameEl.val(),
-				description: descEl.val()
-			}
-		}, function(err) {
-			if (!err) {
-				detailsModal.modal('hide');
-				ajaxify.go('admin/groups');
-			}
+
+		detailsModalSave.on('click', function() {
+			var formEl = detailsModal.find('form'),
+				nameEl = formEl.find('#change-group-name'),
+				descEl = formEl.find('#change-group-desc'),
+				gid = detailsModal.attr('data-gid');
+
+			socket.emit('api:groups.update', {
+				gid: gid,
+				values: {
+					name: nameEl.val(),
+					description: descEl.val()
+				}
+			}, function(err) {
+				if (!err) {
+					detailsModal.modal('hide');
+					ajaxify.go('admin/groups');
+				}
+			});
 		});
-	});
+	};
+
+	return Groups;
 });
\ No newline at end of file
diff --git a/public/src/forum/admin/index.js b/public/src/forum/admin/index.js
index 66133c6ac2..4b7008a541 100644
--- a/public/src/forum/admin/index.js
+++ b/public/src/forum/admin/index.js
@@ -1,25 +1,28 @@
+define(function() {
+	var	Admin = {};
 
-(function() {
+		Admin.init = function() {
+		ajaxify.register_events(['api:get_all_rooms']);
+		socket.on('api:get_all_rooms', function(data) {
 
-	ajaxify.register_events(['api:get_all_rooms']);
-	socket.on('api:get_all_rooms', function(data) {
+			var active_users = document.getElementById('active_users'),
+				total = 0;
+				active_users.innerHTML = '';
 
-		var active_users = document.getElementById('active_users'),
-			total = 0;
-			active_users.innerHTML = '';
-
-		for (var room in data) {
-			if (room !== '') {
-				var count = data[room].length;
-				total += count;
-				active_users.innerHTML = active_users.innerHTML + "<div class='alert alert-success'><strong>" + room + "</strong> " + count + " active user" + (count > 1 ? "s" : "") + "</div>";
+			for (var room in data) {
+				if (room !== '') {
+					var count = data[room].length;
+					total += count;
+					active_users.innerHTML = active_users.innerHTML + "<div class='alert alert-success'><strong>" + room + "</strong> " + count + " active user" + (count > 1 ? "s" : "") + "</div>";
+				}
 			}
-		}
 
-		document.getElementById('connections').innerHTML = total;
-	});
+			document.getElementById('connections').innerHTML = total;
+		});
 
-	app.enter_room('admin');
-	socket.emit('api:get_all_rooms');
+		app.enter_room('admin');
+		socket.emit('api:get_all_rooms');
+	};
 
-}());
\ No newline at end of file
+	return Admin;
+});
diff --git a/public/src/forum/admin/plugins.js b/public/src/forum/admin/plugins.js
index 297fc6eb3c..daa3e04eb2 100644
--- a/public/src/forum/admin/plugins.js
+++ b/public/src/forum/admin/plugins.js
@@ -1,7 +1,5 @@
-var nodebb_admin = nodebb_admin || {};
-
-(function() {
-	var plugins = {
+define(function() {
+	var Plugins = {
 		init: function() {
 			var pluginsList = $('.plugins'),
 				numPlugins = pluginsList[0].querySelectorAll('li').length,
@@ -31,8 +29,5 @@ var nodebb_admin = nodebb_admin || {};
 		}
 	};
 
-	jQuery(document).ready(function() {
-		nodebb_admin.plugins = plugins;
-		nodebb_admin.plugins.init();
-	});
-})();
\ No newline at end of file
+	return Plugins;
+});
\ No newline at end of file
diff --git a/public/src/forum/admin/settings.js b/public/src/forum/admin/settings.js
new file mode 100644
index 0000000000..75f1a78c83
--- /dev/null
+++ b/public/src/forum/admin/settings.js
@@ -0,0 +1,81 @@
+define(function() {
+	var Settings = {};
+
+	Settings.config = {};
+
+	Settings.init = function() {
+		Settings.prepare();
+	};
+
+	Settings.prepare = function() {
+		// Come back in 500ms if the config isn't ready yet
+		if (Settings.config === undefined) {
+			setTimeout(function() {
+				Settings.prepare();
+			}, 500);
+			return;
+		}
+
+		// Populate the fields on the page from the config
+		var fields = document.querySelectorAll('#content [data-field]'),
+			numFields = fields.length,
+			saveBtn = document.getElementById('save'),
+			x, key, inputType;
+		for (x = 0; x < numFields; x++) {
+			key = fields[x].getAttribute('data-field');
+			inputType = fields[x].getAttribute('type');
+			if (fields[x].nodeName === 'INPUT') {
+				if (Settings.config[key]) {
+					switch (inputType) {
+						case 'text':
+						case 'textarea':
+						case 'number':
+							fields[x].value = Settings.config[key];
+							break;
+
+						case 'checkbox':
+							fields[x].checked = Settings.config[key] === '1' ? true : false;
+							break;
+					}
+				}
+			} else if (fields[x].nodeName === 'TEXTAREA') {
+				if (Settings.config[key]) fields[x].value = Settings.config[key];
+			}
+		}
+
+		saveBtn.addEventListener('click', function(e) {
+			var key, value;
+			e.preventDefault();
+
+			for (x = 0; x < numFields; x++) {
+				key = fields[x].getAttribute('data-field');
+				if (fields[x].nodeName === 'INPUT') {
+					inputType = fields[x].getAttribute('type');
+					switch (inputType) {
+						case 'text':
+						case 'number':
+							value = fields[x].value;
+							break;
+
+						case 'checkbox':
+							value = fields[x].checked ? '1' : '0';
+							break;
+					}
+				} else if (fields[x].nodeName === 'TEXTAREA') {
+					value = fields[x].value;
+				}
+
+				socket.emit('api:config.set', {
+					key: key,
+					value: value
+				});
+			}
+		});
+	};
+
+	Settings.remove = function(key) {
+		socket.emit('api:config.remove', key);
+	};
+
+	return Settings;
+});
\ No newline at end of file
diff --git a/public/src/forum/admin/themes.js b/public/src/forum/admin/themes.js
index 05440189cb..1b4f0f8e4d 100644
--- a/public/src/forum/admin/themes.js
+++ b/public/src/forum/admin/themes.js
@@ -1,8 +1,91 @@
-var nodebb_admin = (function(nodebb_admin) {
+define(function() {
+	var Themes = {};
 
-	var themes = {};
+	Themes.init = function() {
+		var scriptEl = document.createElement('script');
+		scriptEl.src = 'http://api.bootswatch.com/3/?callback=bootswatchListener';
+		document.body.appendChild(scriptEl);
 
-	themes.render = function(bootswatch) {
+		var bootstrapThemeContainer = document.querySelector('#bootstrap_themes'),
+			installedThemeContainer = document.querySelector('#installed_themes'),
+			themeEvent = function(e) {
+				if (e.target.hasAttribute('data-action')) {
+					switch (e.target.getAttribute('data-action')) {
+						case 'preview':
+							var cssSrc = $(e.target).parents('li').attr('data-css'),
+								cssEl = document.getElementById('base-theme');
+
+							cssEl.href = cssSrc;
+							break;
+						case 'use':
+							var parentEl = $(e.target).parents('li'),
+								cssSrc = parentEl.attr('data-css'),
+								cssName = parentEl.attr('data-theme');
+							socket.emit('api:config.set', {
+								key: 'theme:id',
+								value: 'bootswatch:' + cssName
+							});
+							socket.emit('api:config.set', {
+								key: 'theme:src',
+								value: cssSrc
+							});
+							break;
+					}
+				}
+			};
+		bootstrapThemeContainer.addEventListener('click', themeEvent);
+		installedThemeContainer.addEventListener('click', themeEvent);
+
+		var revertEl = document.getElementById('revert_theme');
+		revertEl.addEventListener('click', function() {
+			bootbox.confirm('Are you sure you wish to remove the custom theme and restore the NodeBB default theme?', function(confirm) {
+				if (confirm) {
+					require(['forum/admin/settings'], function(Settings) {
+						Settings.remove('theme:id');
+						Settings.remove('theme:src');
+					});
+				}
+			});
+		}, false);
+
+		// Installed Themes
+		socket.emit('api:admin.themes.getInstalled', function(themes) {
+			var instListEl = document.getElementById('installed_themes'),
+				themeFrag = document.createDocumentFragment(),
+				liEl = document.createElement('li');
+
+			if (themes.length > 0) {
+				for (var x = 0, numThemes = themes.length; x < numThemes; x++) {
+					liEl.setAttribute('data-theme', themes[x].id);
+					liEl.setAttribute('data-css', themes[x].src);
+					liEl.innerHTML = '<img src="' + themes[x].screenshot + '" />' +
+						'<div>' +
+						'<div class="pull-right">' +
+						'<button class="btn btn-primary" data-action="use">Use</button> ' +
+						'<button class="btn btn-default" data-action="preview">Preview</button>' +
+						'</div>' +
+						'<h4>' + themes[x].name + '</h4>' +
+						'<p>' +
+						themes[x].description +
+						(themes[x].url ? ' (<a href="' + themes[x].url + '">Homepage</a>)' : '') +
+						'</p>' +
+						'</div>' +
+						'<div class="clear">';
+					themeFrag.appendChild(liEl.cloneNode(true));
+				}
+			} else {
+				// No themes found
+				liEl.className = 'no-themes';
+				liEl.innerHTML = 'No installed themes found';
+				themeFrag.appendChild(liEl);
+			}
+
+			instListEl.innerHTML = '';
+			instListEl.appendChild(themeFrag);
+		});
+	}
+
+	Themes.render = function(bootswatch) {
 		var themeFrag = document.createDocumentFragment(),
 			themeEl = document.createElement('li'),
 			themeContainer = document.querySelector('#bootstrap_themes'),
@@ -28,91 +111,5 @@ var nodebb_admin = (function(nodebb_admin) {
 		themeContainer.appendChild(themeFrag);
 	}
 
-	nodebb_admin.themes = themes;
-
-	return nodebb_admin;
-
-}(nodebb_admin || {}));
-
-
-(function() {
-	var scriptEl = document.createElement('script');
-	scriptEl.src = 'http://api.bootswatch.com/3/?callback=nodebb_admin.themes.render';
-	document.body.appendChild(scriptEl);
-
-	var bootstrapThemeContainer = document.querySelector('#bootstrap_themes'),
-		installedThemeContainer = document.querySelector('#installed_themes'),
-		themeEvent = function(e) {
-			if (e.target.hasAttribute('data-action')) {
-				switch (e.target.getAttribute('data-action')) {
-					case 'preview':
-						var cssSrc = $(e.target).parents('li').attr('data-css'),
-							cssEl = document.getElementById('base-theme');
-
-						cssEl.href = cssSrc;
-						break;
-					case 'use':
-						var parentEl = $(e.target).parents('li'),
-							cssSrc = parentEl.attr('data-css'),
-							cssName = parentEl.attr('data-theme');
-						socket.emit('api:config.set', {
-							key: 'theme:id',
-							value: 'bootswatch:' + cssName
-						});
-						socket.emit('api:config.set', {
-							key: 'theme:src',
-							value: cssSrc
-						});
-						break;
-				}
-			}
-		};
-	bootstrapThemeContainer.addEventListener('click', themeEvent);
-	installedThemeContainer.addEventListener('click', themeEvent);
-
-	var revertEl = document.getElementById('revert_theme');
-	revertEl.addEventListener('click', function() {
-		bootbox.confirm('Are you sure you wish to remove the custom theme and restore the NodeBB default theme?', function(confirm) {
-			if (confirm) {
-				nodebb_admin.remove('theme:id');
-				nodebb_admin.remove('theme:src');
-			}
-		});
-	}, false);
-
-	// Installed Themes
-	socket.emit('api:admin.themes.getInstalled', function(themes) {
-		var instListEl = document.getElementById('installed_themes'),
-			themeFrag = document.createDocumentFragment(),
-			liEl = document.createElement('li');
-
-		if (themes.length > 0) {
-			for (var x = 0, numThemes = themes.length; x < numThemes; x++) {
-				liEl.setAttribute('data-theme', themes[x].id);
-				liEl.setAttribute('data-css', themes[x].src);
-				liEl.innerHTML = '<img src="' + themes[x].screenshot + '" />' +
-					'<div>' +
-					'<div class="pull-right">' +
-					'<button class="btn btn-primary" data-action="use">Use</button> ' +
-					'<button class="btn btn-default" data-action="preview">Preview</button>' +
-					'</div>' +
-					'<h4>' + themes[x].name + '</h4>' +
-					'<p>' +
-					themes[x].description +
-					(themes[x].url ? ' (<a href="' + themes[x].url + '">Homepage</a>)' : '') +
-					'</p>' +
-					'</div>' +
-					'<div class="clear">';
-				themeFrag.appendChild(liEl.cloneNode(true));
-			}
-		} else {
-			// No themes found
-			liEl.className = 'no-themes';
-			liEl.innerHTML = 'No installed themes found';
-			themeFrag.appendChild(liEl);
-		}
-
-		instListEl.innerHTML = '';
-		instListEl.appendChild(themeFrag);
-	});
-})();
\ No newline at end of file
+	return Themes;
+});
\ No newline at end of file
diff --git a/public/src/forum/admin/topics.js b/public/src/forum/admin/topics.js
index 040197df82..8960bed25d 100644
--- a/public/src/forum/admin/topics.js
+++ b/public/src/forum/admin/topics.js
@@ -1,127 +1,133 @@
-$(document).ready(function() {
-	var topicsListEl = document.querySelector('.topics'),
-		loadMoreEl = document.getElementById('topics_loadmore');
-
-	$(topicsListEl).on('click', '[data-action]', function() {
-		var $this = $(this),
-			action = this.getAttribute('data-action'),
-			tid = $this.parents('[data-tid]').attr('data-tid');
-
-		switch (action) {
-			case 'pin':
-				if (!$this.hasClass('active')) socket.emit('api:topic.pin', {
-					tid: tid
+define(function() {
+	var	Topics = {};
+
+	Topics.init = function() {
+		var topicsListEl = document.querySelector('.topics'),
+			loadMoreEl = document.getElementById('topics_loadmore');
+
+		$(topicsListEl).on('click', '[data-action]', function() {
+			var $this = $(this),
+				action = this.getAttribute('data-action'),
+				tid = $this.parents('[data-tid]').attr('data-tid');
+
+			switch (action) {
+				case 'pin':
+					if (!$this.hasClass('active')) socket.emit('api:topic.pin', {
+						tid: tid
+					});
+					else socket.emit('api:topic.unpin', {
+						tid: tid
+					});
+					break;
+				case 'lock':
+					if (!$this.hasClass('active')) socket.emit('api:topic.lock', {
+						tid: tid
+					});
+					else socket.emit('api:topic.unlock', {
+						tid: tid
+					});
+					break;
+				case 'delete':
+					if (!$this.hasClass('active')) socket.emit('api:topic.delete', {
+						tid: tid
+					});
+					else socket.emit('api:topic.restore', {
+						tid: tid
+					});
+					break;
+			}
+		});
+
+		loadMoreEl.addEventListener('click', function() {
+			if (this.className.indexOf('disabled') === -1) {
+				var topics = document.querySelectorAll('.topics li[data-tid]'),
+					lastTid = parseInt(topics[topics.length - 1].getAttribute('data-tid'));
+
+				this.innerHTML = '<i class="icon-refresh icon-spin"></i> Retrieving topics';
+				socket.emit('api:admin.topics.getMore', {
+					limit: 10,
+					after: lastTid
+				}, function(topics) {
+					var btnEl = document.getElementById('topics_loadmore');
+
+					topics = JSON.parse(topics);
+					if (topics.length > 0) {
+						var html = templates.prepare(templates['admin/topics'].blocks['topics']).parse({
+							topics: topics
+						}),
+							topicsListEl = document.querySelector('.topics');
+
+						topicsListEl.innerHTML += html;
+						btnEl.innerHTML = 'Load More Topics';
+					} else {
+						// Exhausted all topics
+						btnEl.className += ' disabled';
+						btnEl.innerHTML = 'No more topics';
+					}
 				});
-				else socket.emit('api:topic.unpin', {
-					tid: tid
-				});
-				break;
-			case 'lock':
-				if (!$this.hasClass('active')) socket.emit('api:topic.lock', {
-					tid: tid
-				});
-				else socket.emit('api:topic.unlock', {
-					tid: tid
-				});
-				break;
-			case 'delete':
-				if (!$this.hasClass('active')) socket.emit('api:topic.delete', {
-					tid: tid
-				});
-				else socket.emit('api:topic.restore', {
-					tid: tid
-				});
-				break;
+			}
+		}, false);
+
+		// Resolve proper button state for all topics
+		var topicEls = topicsListEl.querySelectorAll('li'),
+			numTopics = topicEls.length;
+		for (var x = 0; x < numTopics; x++) {
+			if (topicEls[x].getAttribute('data-pinned') === '1') topicEls[x].querySelector('[data-action="pin"]').className += ' active';
+			if (topicEls[x].getAttribute('data-locked') === '1') topicEls[x].querySelector('[data-action="lock"]').className += ' active';
+			if (topicEls[x].getAttribute('data-deleted') === '1') topicEls[x].querySelector('[data-action="delete"]').className += ' active';
+			topicEls[x].removeAttribute('data-pinned');
+			topicEls[x].removeAttribute('data-locked');
+			topicEls[x].removeAttribute('data-deleted');
 		}
-	});
-
-	loadMoreEl.addEventListener('click', function() {
-		if (this.className.indexOf('disabled') === -1) {
-			var topics = document.querySelectorAll('.topics li[data-tid]'),
-				lastTid = parseInt(topics[topics.length - 1].getAttribute('data-tid'));
-
-			this.innerHTML = '<i class="icon-refresh icon-spin"></i> Retrieving topics';
-			socket.emit('api:admin.topics.getMore', {
-				limit: 10,
-				after: lastTid
-			}, function(topics) {
-				var btnEl = document.getElementById('topics_loadmore');
-
-				topics = JSON.parse(topics);
-				if (topics.length > 0) {
-					var html = templates.prepare(templates['admin/topics'].blocks['topics']).parse({
-						topics: topics
-					}),
-						topicsListEl = document.querySelector('.topics');
-
-					topicsListEl.innerHTML += html;
-					btnEl.innerHTML = 'Load More Topics';
-				} else {
-					// Exhausted all topics
-					btnEl.className += ' disabled';
-					btnEl.innerHTML = 'No more topics';
-				}
-			});
-		}
-	}, false);
-
-	// Resolve proper button state for all topics
-	var topicEls = topicsListEl.querySelectorAll('li'),
-		numTopics = topicEls.length;
-	for (var x = 0; x < numTopics; x++) {
-		if (topicEls[x].getAttribute('data-pinned') === '1') topicEls[x].querySelector('[data-action="pin"]').className += ' active';
-		if (topicEls[x].getAttribute('data-locked') === '1') topicEls[x].querySelector('[data-action="lock"]').className += ' active';
-		if (topicEls[x].getAttribute('data-deleted') === '1') topicEls[x].querySelector('[data-action="delete"]').className += ' active';
-		topicEls[x].removeAttribute('data-pinned');
-		topicEls[x].removeAttribute('data-locked');
-		topicEls[x].removeAttribute('data-deleted');
-	}
-});
-
-socket.on('api:topic.pin', function(response) {
-	if (response.status === 'ok') {
-		var btnEl = document.querySelector('li[data-tid="' + response.tid + '"] button[data-action="pin"]');
-
-		$(btnEl).addClass('active');
-	}
-});
-
-socket.on('api:topic.unpin', function(response) {
-	if (response.status === 'ok') {
-		var btnEl = document.querySelector('li[data-tid="' + response.tid + '"] button[data-action="pin"]');
-
-		$(btnEl).removeClass('active');
-	}
-});
-
-socket.on('api:topic.lock', function(response) {
-	if (response.status === 'ok') {
-		var btnEl = document.querySelector('li[data-tid="' + response.tid + '"] button[data-action="lock"]');
-
-		$(btnEl).addClass('active');
-	}
-});
-
-socket.on('api:topic.unlock', function(response) {
-	if (response.status === 'ok') {
-		var btnEl = document.querySelector('li[data-tid="' + response.tid + '"] button[data-action="lock"]');
-
-		$(btnEl).removeClass('active');
-	}
-});
-
-socket.on('api:topic.delete', function(response) {
-	if (response.status === 'ok') {
-		var btnEl = document.querySelector('li[data-tid="' + response.tid + '"] button[data-action="delete"]');
-
-		$(btnEl).addClass('active');
-	}
-});
-
-socket.on('api:topic.restore', function(response) {
-	if (response.status === 'ok') {
-		var btnEl = document.querySelector('li[data-tid="' + response.tid + '"] button[data-action="delete"]');
-
-		$(btnEl).removeClass('active');
-	}
+
+		socket.on('api:topic.pin', function(response) {
+			if (response.status === 'ok') {
+				var btnEl = document.querySelector('li[data-tid="' + response.tid + '"] button[data-action="pin"]');
+
+				$(btnEl).addClass('active');
+			}
+		});
+
+		socket.on('api:topic.unpin', function(response) {
+			if (response.status === 'ok') {
+				var btnEl = document.querySelector('li[data-tid="' + response.tid + '"] button[data-action="pin"]');
+
+				$(btnEl).removeClass('active');
+			}
+		});
+
+		socket.on('api:topic.lock', function(response) {
+			if (response.status === 'ok') {
+				var btnEl = document.querySelector('li[data-tid="' + response.tid + '"] button[data-action="lock"]');
+
+				$(btnEl).addClass('active');
+			}
+		});
+
+		socket.on('api:topic.unlock', function(response) {
+			if (response.status === 'ok') {
+				var btnEl = document.querySelector('li[data-tid="' + response.tid + '"] button[data-action="lock"]');
+
+				$(btnEl).removeClass('active');
+			}
+		});
+
+		socket.on('api:topic.delete', function(response) {
+			if (response.status === 'ok') {
+				var btnEl = document.querySelector('li[data-tid="' + response.tid + '"] button[data-action="delete"]');
+
+				$(btnEl).addClass('active');
+			}
+		});
+
+		socket.on('api:topic.restore', function(response) {
+			if (response.status === 'ok') {
+				var btnEl = document.querySelector('li[data-tid="' + response.tid + '"] button[data-action="delete"]');
+
+				$(btnEl).removeClass('active');
+			}
+		});
+	};
+
+	return Topics;
 });
\ No newline at end of file
diff --git a/public/src/forum/admin/users.js b/public/src/forum/admin/users.js
index 4f9ba9dc88..6d41fb10b3 100644
--- a/public/src/forum/admin/users.js
+++ b/public/src/forum/admin/users.js
@@ -1,170 +1,174 @@
-(function() {
-
-	var yourid = templates.get('yourid');
-
-	function isUserAdmin(element) {
-		var parent = $(element).parents('.users-box');
-		return (parent.attr('data-admin') !== "0");
-	}
-
-	function isUserBanned(element) {
-		var parent = $(element).parents('.users-box');
-		return (parent.attr('data-banned') !== "" && parent.attr('data-banned') !== "0");
-	}
-
-	function getUID(element) {
-		var parent = $(element).parents('.users-box');
-		return parent.attr('data-uid');
-	}
-
-	function updateUserButtons() {
-		jQuery('.ban-btn').each(function(index, element) {
-			var banBtn = $(element);
-			var uid = getUID(banBtn);
-			if (isUserAdmin(banBtn) || uid === yourid)
-				banBtn.addClass('disabled');
-			else if (isUserBanned(banBtn))
-				banBtn.addClass('btn-warning');
-			else
-				banBtn.removeClass('btn-warning');
+define(function() {
+	var Users = {};
 
-		});
-	}
+	Users.init = function() {
+		var yourid = templates.get('yourid');
 
-	function initUsers() {
+		function isUserAdmin(element) {
+			var parent = $(element).parents('.users-box');
+			return (parent.attr('data-admin') !== "0");
+		}
 
-		updateUserButtons();
+		function isUserBanned(element) {
+			var parent = $(element).parents('.users-box');
+			return (parent.attr('data-banned') !== "" && parent.attr('data-banned') !== "0");
+		}
 
-		$('#users-container').on('click', '.ban-btn', function() {
-			var banBtn = $(this);
-			var isAdmin = isUserAdmin(banBtn);
-			var isBanned = isUserBanned(banBtn);
-			var parent = banBtn.parents('.users-box');
-			var uid = getUID(banBtn);
+		function getUID(element) {
+			var parent = $(element).parents('.users-box');
+			return parent.attr('data-uid');
+		}
 
-			if (!isAdmin) {
-				if (isBanned) {
-					socket.emit('api:admin.user.unbanUser', uid);
+		function updateUserButtons() {
+			jQuery('.ban-btn').each(function(index, element) {
+				var banBtn = $(element);
+				var uid = getUID(banBtn);
+				if (isUserAdmin(banBtn) || uid === yourid)
+					banBtn.addClass('disabled');
+				else if (isUserBanned(banBtn))
+					banBtn.addClass('btn-warning');
+				else
 					banBtn.removeClass('btn-warning');
-					parent.attr('data-banned', 0);
-				} else {
-					bootbox.confirm('Do you really want to ban "' + parent.attr('data-username') + '"?', function(confirm) {
-						if (confirm) {
-							socket.emit('api:admin.user.banUser', uid);
-							banBtn.addClass('btn-warning');
-							parent.attr('data-banned', 1);
-						}
-					});
-				}
-			}
-
-			return false;
-		});
-	}
 
+			});
+		}
 
-	jQuery('document').ready(function() {
+		function initUsers() {
 
-		var timeoutId = 0,
-			loadingMoreUsers = false;
+			updateUserButtons();
 
-		var url = window.location.href,
-			parts = url.split('/'),
-			active = parts[parts.length - 1];
+			$('#users-container').on('click', '.ban-btn', function() {
+				var banBtn = $(this);
+				var isAdmin = isUserAdmin(banBtn);
+				var isBanned = isUserBanned(banBtn);
+				var parent = banBtn.parents('.users-box');
+				var uid = getUID(banBtn);
+
+				if (!isAdmin) {
+					if (isBanned) {
+						socket.emit('api:admin.user.unbanUser', uid);
+						banBtn.removeClass('btn-warning');
+						parent.attr('data-banned', 0);
+					} else {
+						bootbox.confirm('Do you really want to ban "' + parent.attr('data-username') + '"?', function(confirm) {
+							if (confirm) {
+								socket.emit('api:admin.user.banUser', uid);
+								banBtn.addClass('btn-warning');
+								parent.attr('data-banned', 1);
+							}
+						});
+					}
+				}
 
-		jQuery('.nav-pills li').removeClass('active');
-		jQuery('.nav-pills li a').each(function() {
-			if (this.getAttribute('href').match(active)) {
-				jQuery(this.parentNode).addClass('active');
 				return false;
-			}
-		});
+			});
+		}
 
-		jQuery('#search-user').on('keyup', function() {
-			if (timeoutId !== 0) {
-				clearTimeout(timeoutId);
-				timeoutId = 0;
-			}
 
-			timeoutId = setTimeout(function() {
-				var username = $('#search-user').val();
+		jQuery('document').ready(function() {
 
-				jQuery('.icon-spinner').removeClass('none');
-				socket.emit('api:admin.user.search', username);
+			var timeoutId = 0,
+				loadingMoreUsers = false;
 
-			}, 250);
-		});
+			var url = window.location.href,
+				parts = url.split('/'),
+				active = parts[parts.length - 1];
 
-		initUsers();
-
-		socket.removeAllListeners('api:admin.user.search');
-
-		socket.on('api:admin.user.search', function(data) {
-			var html = templates.prepare(templates['admin/users'].blocks['users']).parse({
-				users: data
-			}),
-				userListEl = document.querySelector('.users');
-
-			userListEl.innerHTML = html;
-			jQuery('.icon-spinner').addClass('none');
-
-			if (data && data.length === 0) {
-				$('#user-notfound-notify').html('User not found!')
-					.show()
-					.addClass('label-danger')
-					.removeClass('label-success');
-			} else {
-				$('#user-notfound-notify').html(data.length + ' user' + (data.length > 1 ? 's' : '') + ' found!')
-					.show()
-					.addClass('label-success')
-					.removeClass('label-danger');
-			}
+			jQuery('.nav-pills li').removeClass('active');
+			jQuery('.nav-pills li a').each(function() {
+				if (this.getAttribute('href').match(active)) {
+					jQuery(this.parentNode).addClass('active');
+					return false;
+				}
+			});
+
+			jQuery('#search-user').on('keyup', function() {
+				if (timeoutId !== 0) {
+					clearTimeout(timeoutId);
+					timeoutId = 0;
+				}
+
+				timeoutId = setTimeout(function() {
+					var username = $('#search-user').val();
+
+					jQuery('.icon-spinner').removeClass('none');
+					socket.emit('api:admin.user.search', username);
+
+				}, 250);
+			});
 
 			initUsers();
-		});
 
-		function onUsersLoaded(users) {
-			var html = templates.prepare(templates['admin/users'].blocks['users']).parse({
-				users: users
+			socket.removeAllListeners('api:admin.user.search');
+
+			socket.on('api:admin.user.search', function(data) {
+				var html = templates.prepare(templates['admin/users'].blocks['users']).parse({
+					users: data
+				}),
+					userListEl = document.querySelector('.users');
+
+				userListEl.innerHTML = html;
+				jQuery('.icon-spinner').addClass('none');
+
+				if (data && data.length === 0) {
+					$('#user-notfound-notify').html('User not found!')
+						.show()
+						.addClass('label-danger')
+						.removeClass('label-success');
+				} else {
+					$('#user-notfound-notify').html(data.length + ' user' + (data.length > 1 ? 's' : '') + ' found!')
+						.show()
+						.addClass('label-success')
+						.removeClass('label-danger');
+				}
+
+				initUsers();
 			});
-			$('#users-container').append(html);
-			updateUserButtons();
-		}
 
-		function loadMoreUsers() {
-			var set = '';
-			if (active === 'latest') {
-				set = 'users:joindate';
-			} else if (active === 'sort-posts') {
-				set = 'users:postcount';
-			} else if (active === 'sort-reputation') {
-				set = 'users:reputation';
+			function onUsersLoaded(users) {
+				var html = templates.prepare(templates['admin/users'].blocks['users']).parse({
+					users: users
+				});
+				$('#users-container').append(html);
+				updateUserButtons();
 			}
 
-			if (set) {
-				loadingMoreUsers = true;
-				socket.emit('api:users.loadMore', {
-					set: set,
-					after: $('#users-container').children().length
-				}, function(data) {
-					if (data.users.length) {
-						onUsersLoaded(data.users);
-					}
-					loadingMoreUsers = false;
-				});
+			function loadMoreUsers() {
+				var set = '';
+				if (active === 'latest') {
+					set = 'users:joindate';
+				} else if (active === 'sort-posts') {
+					set = 'users:postcount';
+				} else if (active === 'sort-reputation') {
+					set = 'users:reputation';
+				}
+
+				if (set) {
+					loadingMoreUsers = true;
+					socket.emit('api:users.loadMore', {
+						set: set,
+						after: $('#users-container').children().length
+					}, function(data) {
+						if (data.users.length) {
+							onUsersLoaded(data.users);
+						}
+						loadingMoreUsers = false;
+					});
+				}
 			}
-		}
 
-		$('#load-more-users-btn').on('click', loadMoreUsers);
+			$('#load-more-users-btn').on('click', loadMoreUsers);
 
-		$(window).off('scroll').on('scroll', function() {
-			var bottom = ($(document).height() - $(window).height()) * 0.9;
+			$(window).off('scroll').on('scroll', function() {
+				var bottom = ($(document).height() - $(window).height()) * 0.9;
 
-			if ($(window).scrollTop() > bottom && !loadingMoreUsers) {
-				loadMoreUsers();
-			}
-		});
+				if ($(window).scrollTop() > bottom && !loadingMoreUsers) {
+					loadMoreUsers();
+				}
+			});
 
-	});
+		});
+	};
 
-}());
\ No newline at end of file
+	return Users;
+});
\ No newline at end of file
diff --git a/public/src/forum/category.js b/public/src/forum/category.js
index b2bc60a07f..ab16a32369 100644
--- a/public/src/forum/category.js
+++ b/public/src/forum/category.js
@@ -1,41 +1,86 @@
-(function () {
-	var cid = templates.get('category_id'),
-		room = 'category_' + cid,
-		twitterEl = document.getElementById('twitter-intent'),
-		facebookEl = document.getElementById('facebook-share'),
-		googleEl = document.getElementById('google-share'),
-		twitter_url = templates.get('twitter-intent-url'),
-		facebook_url = templates.get('facebook-share-url'),
-		google_url = templates.get('google-share-url'),
-		loadingMoreTopics = false;
-
-	app.enter_room(room);
-
-	twitterEl.addEventListener('click', function () {
-		window.open(twitter_url, '_blank', 'width=550,height=420,scrollbars=no,status=no');
-		return false;
-	}, false);
-	facebookEl.addEventListener('click', function () {
-		window.open(facebook_url, '_blank', 'width=626,height=436,scrollbars=no,status=no');
-		return false;
-	}, false);
-	googleEl.addEventListener('click', function () {
-		window.open(google_url, '_blank', 'width=500,height=570,scrollbars=no,status=no');
-		return false;
-	}, false);
-
-	var new_post = document.getElementById('new_post');
-	new_post.onclick = function () {
-		require(['composer'], function (cmp) {
-			cmp.push(0, cid);
+define(function () {
+	var	Category = {};
+
+	Category.init = function() {
+		var cid = templates.get('category_id'),
+			room = 'category_' + cid,
+			twitterEl = document.getElementById('twitter-intent'),
+			facebookEl = document.getElementById('facebook-share'),
+			googleEl = document.getElementById('google-share'),
+			twitter_url = templates.get('twitter-intent-url'),
+			facebook_url = templates.get('facebook-share-url'),
+			google_url = templates.get('google-share-url'),
+			loadingMoreTopics = false;
+
+		app.enter_room(room);
+
+		twitterEl.addEventListener('click', function () {
+			window.open(twitter_url, '_blank', 'width=550,height=420,scrollbars=no,status=no');
+			return false;
+		}, false);
+		facebookEl.addEventListener('click', function () {
+			window.open(facebook_url, '_blank', 'width=626,height=436,scrollbars=no,status=no');
+			return false;
+		}, false);
+		googleEl.addEventListener('click', function () {
+			window.open(google_url, '_blank', 'width=500,height=570,scrollbars=no,status=no');
+			return false;
+		}, false);
+
+		var new_post = document.getElementById('new_post');
+		new_post.onclick = function () {
+			require(['composer'], function (cmp) {
+				cmp.push(0, cid);
+			});
+		}
+
+		ajaxify.register_events([
+			'event:new_topic'
+		]);
+
+		socket.on('event:new_topic', Category.onNewTopic);
+
+		socket.emit('api:categories.getRecentReplies', cid);
+		socket.on('api:categories.getRecentReplies', function (posts) {
+			if (!posts || posts.length === 0) {
+				return;
+			}
+
+			var recent_replies = document.getElementById('category_recent_replies');
+
+			recent_replies.innerHTML = '';
+
+			var frag = document.createDocumentFragment(),
+				li = document.createElement('li');
+			for (var i = 0, numPosts = posts.length; i < numPosts; i++) {
+
+				li.setAttribute('data-pid', posts[i].pid);
+
+
+				li.innerHTML = '<a href="/user/' + posts[i].userslug + '"><img title="' + posts[i].username + '" style="width: 48px; height: 48px; /*temporary*/" class="img-rounded" src="' + posts[i].picture + '" class="" /></a>' +
+					'<a href="/topic/' + posts[i].topicSlug + '#' + posts[i].pid + '">' +
+					'<p>' +
+					posts[i].content +
+					'</p>' +
+					'<p class="meta"><strong>' + posts[i].username + '</strong></span> -<span class="timeago" title="' + posts[i].relativeTime + '"></span></p>' +
+					'</a>';
+
+				frag.appendChild(li.cloneNode(true));
+				recent_replies.appendChild(frag);
+			}
+			$('#category_recent_replies span.timeago').timeago();
 		});
-	}
 
-	ajaxify.register_events([
-		'event:new_topic'
-	]);
+		$(window).off('scroll').on('scroll', function (ev) {
+			var bottom = ($(document).height() - $(window).height()) * 0.9;
 
-	function onNewTopic(data) {
+			if ($(window).scrollTop() > bottom && !loadingMoreTopics) {
+				Category.loadMoreTopics(cid);
+			}
+		});
+	};
+
+	Category.onNewTopic = function(data) {
 		var html = templates.prepare(templates['category'].blocks['topics']).parse({
 			topics: [data]
 		}),
@@ -64,40 +109,7 @@
 		$('#topics-container span.timeago').timeago();
 	}
 
-	socket.on('event:new_topic', onNewTopic);
-
-	socket.emit('api:categories.getRecentReplies', cid);
-	socket.on('api:categories.getRecentReplies', function (posts) {
-		if (!posts || posts.length === 0) {
-			return;
-		}
-
-		var recent_replies = document.getElementById('category_recent_replies');
-
-		recent_replies.innerHTML = '';
-
-		var frag = document.createDocumentFragment(),
-			li = document.createElement('li');
-		for (var i = 0, numPosts = posts.length; i < numPosts; i++) {
-
-			li.setAttribute('data-pid', posts[i].pid);
-
-
-			li.innerHTML = '<a href="/user/' + posts[i].userslug + '"><img title="' + posts[i].username + '" style="width: 48px; height: 48px; /*temporary*/" class="img-rounded" src="' + posts[i].picture + '" class="" /></a>' +
-				'<a href="/topic/' + posts[i].topicSlug + '#' + posts[i].pid + '">' +
-				'<p>' +
-				posts[i].content +
-				'</p>' +
-				'<p class="meta"><strong>' + posts[i].username + '</strong></span> -<span class="timeago" title="' + posts[i].relativeTime + '"></span></p>' +
-				'</a>';
-
-			frag.appendChild(li.cloneNode(true));
-			recent_replies.appendChild(frag);
-		}
-		$('#category_recent_replies span.timeago').timeago();
-	});
-
-	function onTopicsLoaded(topics) {
+	Category.onTopicsLoaded = function(topics) {
 
 		var html = templates.prepare(templates['category'].blocks['topics']).parse({
 			topics: topics
@@ -113,26 +125,18 @@
 	}
 
 
-	function loadMoreTopics(cid) {
+	Category.loadMoreTopics = function(cid) {
 		loadingMoreTopics = true;
 		socket.emit('api:category.loadMore', {
 			cid: cid,
 			after: $('#topics-container').children().length
 		}, function (data) {
 			if (data.topics.length) {
-				onTopicsLoaded(data.topics);
+				Category.onTopicsLoaded(data.topics);
 			}
 			loadingMoreTopics = false;
 		});
 	}
 
-	$(window).off('scroll').on('scroll', function (ev) {
-		var bottom = ($(document).height() - $(window).height()) * 0.9;
-
-		if ($(window).scrollTop() > bottom && !loadingMoreTopics) {
-			loadMoreTopics(cid);
-		}
-	});
-
-
-})();
\ No newline at end of file
+	return Category;
+});
\ No newline at end of file
diff --git a/public/src/forum/favourites.js b/public/src/forum/favourites.js
index cff01ab271..20b77b4d41 100644
--- a/public/src/forum/favourites.js
+++ b/public/src/forum/favourites.js
@@ -1,7 +1,13 @@
-(function() {
-	$(document).ready(function() {
+define(['forum/accountheader'], function(header) {
+	var AccountHeader = {};
+
+	AccountHeader.init = function() {
+		header.init();
+
 		$('.user-favourite-posts .topic-row').on('click', function() {
 			ajaxify.go($(this).attr('topic-url'));
 		});
-	});
-}());
\ No newline at end of file
+	};
+
+	return AccountHeader;
+});
\ No newline at end of file
diff --git a/public/src/forum/followers.js b/public/src/forum/followers.js
index 0fcb464f0b..ee955ff615 100644
--- a/public/src/forum/followers.js
+++ b/public/src/forum/followers.js
@@ -1,18 +1,20 @@
-(function() {
+define(['forum/accountheader'], function(header) {
+	var	Followers = {};
 
-	var yourid = templates.get('yourid'),
-		theirid = templates.get('theirid'),
-		followersCount = templates.get('followersCount');
+	Followers.init = function() {
+		header.init();
 
-	$(document).ready(function() {
+		var yourid = templates.get('yourid'),
+			theirid = templates.get('theirid'),
+			followersCount = templates.get('followersCount');
 
-		if (parseInt(followersCount, 10) === 0) {
-			$('#no-followers-notice').removeClass('hide');
-		}
 
-		app.addCommasToNumbers();
-
-	});
+			if (parseInt(followersCount, 10) === 0) {
+				$('#no-followers-notice').removeClass('hide');
+			}
 
+		app.addCommasToNumbers();
+	};
 
-}());
\ No newline at end of file
+	return Followers;
+});
\ No newline at end of file
diff --git a/public/src/forum/following.js b/public/src/forum/following.js
index cd31d25466..7e1c2cde4c 100644
--- a/public/src/forum/following.js
+++ b/public/src/forum/following.js
@@ -1,10 +1,12 @@
-(function() {
+define(['forum/accountheader'], function(header) {
+	var	Following = {};
 
-	var yourid = templates.get('yourid'),
-		theirid = templates.get('theirid'),
-		followingCount = templates.get('followingCount');
+	Following.init = function() {
+		header.init();
 
-	$(document).ready(function() {
+		var yourid = templates.get('yourid'),
+			theirid = templates.get('theirid'),
+			followingCount = templates.get('followingCount');
 
 		if (parseInt(followingCount, 10) === 0) {
 			$('#no-following-notice').removeClass('hide');
@@ -34,7 +36,7 @@
 		}
 
 		app.addCommasToNumbers();
-	});
+	};
 
-
-}());
\ No newline at end of file
+	return Following;
+});
\ No newline at end of file
diff --git a/public/src/forum/login.js b/public/src/forum/login.js
index b99465668a..ed4e0753f6 100644
--- a/public/src/forum/login.js
+++ b/public/src/forum/login.js
@@ -1,60 +1,66 @@
-(function() {
-	// Alternate Logins
-	var altLoginEl = document.querySelector('.alt-logins');
-	altLoginEl.addEventListener('click', function(e) {
-		var target;
-		switch (e.target.nodeName) {
-			case 'LI':
-				target = e.target;
-				break;
-			case 'I':
-				target = e.target.parentNode;
-				break;
-		}
-		if (target) {
-			document.location.href = target.getAttribute('data-url');
-		}
-	});
-
-	$('#login').on('click', function() {
-		var loginData = {
-			'username': $('#username').val(),
-			'password': $('#password').val(),
-			'_csrf': $('#csrf-token').val()
-		};
-
-		$.ajax({
-			type: "POST",
-			url: RELATIVE_PATH + '/login',
-			data: loginData,
-			success: function(data, textStatus, jqXHR) {
-				if (!data.success) {
+define(function() {
+	var	Login = {};
+
+	Login.init = function() {
+		// Alternate Logins
+		var altLoginEl = document.querySelector('.alt-logins');
+		altLoginEl.addEventListener('click', function(e) {
+			var target;
+			switch (e.target.nodeName) {
+				case 'LI':
+					target = e.target;
+					break;
+				case 'I':
+					target = e.target.parentNode;
+					break;
+			}
+			if (target) {
+				document.location.href = target.getAttribute('data-url');
+			}
+		});
+
+		$('#login').on('click', function() {
+			var loginData = {
+				'username': $('#username').val(),
+				'password': $('#password').val(),
+				'_csrf': $('#csrf-token').val()
+			};
+
+			$.ajax({
+				type: "POST",
+				url: RELATIVE_PATH + '/login',
+				data: loginData,
+				success: function(data, textStatus, jqXHR) {
+					if (!data.success) {
+						$('#login-error-notify').show();
+					} else {
+						$('#login-error-notify').hide();
+						if(app.previousUrl.indexOf('/reset/') != -1)
+							window.location.replace(RELATIVE_PATH + "/?loggedin");
+						else
+							window.location.replace(app.previousUrl + "?loggedin");
+
+						app.loadConfig();
+					}
+				},
+				error: function(data, textStatus, jqXHR) {
 					$('#login-error-notify').show();
-				} else {
-					$('#login-error-notify').hide();
-					if(app.previousUrl.indexOf('/reset/') != -1)
-						window.location.replace(RELATIVE_PATH + "/?loggedin");
-					else
-						window.location.replace(app.previousUrl + "?loggedin");
-
-					app.loadConfig();
-				}
-			},
-			error: function(data, textStatus, jqXHR) {
-				$('#login-error-notify').show();
-			},
-			dataType: 'json',
-			async: true,
-			timeout: 2000
+				},
+				dataType: 'json',
+				async: true,
+				timeout: 2000
+			});
+
+			return false;
 		});
 
-		return false;
-	});
+		$('#login-error-notify button').on('click', function() {
+			$('#login-error-notify').hide();
+			return false;
+		});
 
-	$('#login-error-notify button').on('click', function() {
-		$('#login-error-notify').hide();
-		return false;
-	});
+		document.querySelector('#content input').focus();
+	};
 
-	document.querySelector('#content input').focus();
-}());
\ No newline at end of file
+	return Login;
+});
\ No newline at end of file
diff --git a/public/src/forum/recent.js b/public/src/forum/recent.js
index f9b1fe228c..5fed07107e 100644
--- a/public/src/forum/recent.js
+++ b/public/src/forum/recent.js
@@ -1,40 +1,56 @@
-(function() {
-	var loadingMoreTopics = false;
+define(function() {
+	var	Recent = {};
 
-	app.enter_room('recent_posts');
+	Recent.newTopicCount = 0;
+	Recent.newPostCount = 0;
+	Recent.loadingMoreTopics = false;
 
-	ajaxify.register_events([
-		'event:new_topic',
-		'event:new_post'
-	]);
+	Recent.init = function() {
+		app.enter_room('recent_posts');
 
-	var newTopicCount = 0,
-		newPostCount = 0;
+		ajaxify.register_events([
+			'event:new_topic',
+			'event:new_post'
+		]);
 
-	$('#new-topics-alert').on('click', function() {
-		$(this).hide();
-	});
+		$('#new-topics-alert').on('click', function() {
+			$(this).hide();
+		});
+
+		socket.on('event:new_topic', function(data) {
+
+			++Recent.newTopicCount;
+			Recent.updateAlertText();
 
-	socket.on('event:new_topic', function(data) {
+		});
+
+		socket.on('event:new_post', function(data) {
+			++Recent.newPostCount;
+			Recent.updateAlertText();
+		});
 
-		++newTopicCount;
-		updateAlertText();
+		$(window).off('scroll').on('scroll', function() {
+			var bottom = ($(document).height() - $(window).height()) * 0.9;
 
-	});
+			if ($(window).scrollTop() > bottom && !Recent.loadingMoreTopics) {
+				Recent.loadMoreTopics();
+			}
+		});
+	};
 
-	function updateAlertText() {
+	Recent.updateAlertText = function() {
 		var text = '';
 
-		if (newTopicCount > 1)
-			text = 'There are ' + newTopicCount + ' new topics';
-		else if (newTopicCount === 1)
+		if (Recent.newTopicCount > 1)
+			text = 'There are ' + Recent.newTopicCount + ' new topics';
+		else if (Recent.newTopicCount === 1)
 			text = 'There is 1 new topic';
 		else
 			text = 'There are no new topics';
 
-		if (newPostCount > 1)
-			text += ' and ' + newPostCount + ' new posts.';
-		else if (newPostCount === 1)
+		if (Recent.newPostCount > 1)
+			text += ' and ' + Recent.newPostCount + ' new posts.';
+		else if (Recent.newPostCount === 1)
 			text += ' and 1 new post.';
 		else
 			text += ' and no new posts.';
@@ -44,12 +60,7 @@
 		$('#new-topics-alert').html(text).fadeIn('slow');
 	}
 
-	socket.on('event:new_post', function(data) {
-		++newPostCount;
-		updateAlertText();
-	});
-
-	function onTopicsLoaded(topics) {
+	Recent.onTopicsLoaded = function(topics) {
 
 		var html = templates.prepare(templates['recent'].blocks['topics']).parse({
 			topics: topics
@@ -61,25 +72,17 @@
 		container.append(html);
 	}
 
-	function loadMoreTopics() {
-		loadingMoreTopics = true;
+	Recent.loadMoreTopics = function() {
+		Recent.loadingMoreTopics = true;
 		socket.emit('api:topics.loadMoreRecentTopics', {
 			after: $('#topics-container').children().length
 		}, function(data) {
 			if (data.topics && data.topics.length) {
-				onTopicsLoaded(data.topics);
+				Recent.onTopicsLoaded(data.topics);
 			}
-			loadingMoreTopics = false;
+			Recent.loadingMoreTopics = false;
 		});
 	}
 
-	$(window).off('scroll').on('scroll', function() {
-		var bottom = ($(document).height() - $(window).height()) * 0.9;
-
-		if ($(window).scrollTop() > bottom && !loadingMoreTopics) {
-			loadMoreTopics();
-		}
-	});
-
-
-})();
\ No newline at end of file
+	return Recent;
+});
diff --git a/public/src/forum/register.js b/public/src/forum/register.js
index e485f4960f..fc7cf26b2a 100644
--- a/public/src/forum/register.js
+++ b/public/src/forum/register.js
@@ -1,154 +1,159 @@
-(function() {
-	var username = $('#username'),
-		password = $('#password'),
-		password_confirm = $('#password-confirm'),
-		register = $('#register'),
-		emailEl = $('#email'),
-		username_notify = $('#username-notify'),
-		email_notify = $('#email-notify'),
-		password_notify = $('#password-notify'),
-		password_confirm_notify = $('#password-confirm-notify'),
-		validationError = false,
-		successIcon = '<i class="icon icon-ok"></i>';
-
-	$('#referrer').val(app.previousUrl);
-
-	function showError(element, msg) {
-		element.html(msg);
-		element.parent()
-			.removeClass('alert-success')
-			.addClass('alert-danger');
-		element.show();
-		validationError = true;
-	}
-
-	function showSuccess(element, msg) {
-		element.html(msg);
-		element.parent()
-			.removeClass('alert-danger')
-			.addClass('alert-success');
-		element.show();
-	}
-
-	function validateEmail() {
-		if (!emailEl.val()) {
+define(function() {
+	var Register = {};
+
+	Register.init = function() {
+		var username = $('#username'),
+			password = $('#password'),
+			password_confirm = $('#password-confirm'),
+			register = $('#register'),
+			emailEl = $('#email'),
+			username_notify = $('#username-notify'),
+			email_notify = $('#email-notify'),
+			password_notify = $('#password-notify'),
+			password_confirm_notify = $('#password-confirm-notify'),
+			validationError = false,
+			successIcon = '<i class="icon icon-ok"></i>';
+
+		$('#referrer').val(app.previousUrl);
+
+		function showError(element, msg) {
+			element.html(msg);
+			element.parent()
+				.removeClass('alert-success')
+				.addClass('alert-danger');
+			element.show();
 			validationError = true;
-			return;
 		}
 
-		if (!utils.isEmailValid(emailEl.val())) {
-			showError(email_notify, 'Invalid email address.');
-		} else
-			socket.emit('user.email.exists', {
-				email: emailEl.val()
-			});
-	}
-
-	emailEl.on('blur', function() {
-		validateEmail();
-	});
-
-	function validateUsername() {
-		if (!username.val()) {
-			validationError = true;
-			return;
-		}
-
-		if (username.val().length < config.minimumUsernameLength) {
-			showError(username_notify, 'Username too short!');
-		} else if (username.val().length > config.maximumUsernameLength) {
-			showError(username_notify, 'Username too long!');
-		} else if (!utils.isUserNameValid(username.val())) {
-			showError(username_notify, 'Invalid username!');
-		} else {
-			socket.emit('user.exists', {
-				username: username.val()
-			});
-		}
-	}
-
-	username.on('keyup', function() {
-		jQuery('#yourUsername').html(this.value.length > 0 ? this.value : 'username');
-	});
-	username.on('blur', function() {
-		validateUsername();
-	});
-
-	function validatePassword() {
-		if (!password.val()) {
-			validationError = true;
-			return;
-		}
-
-		if (password.val().length < config.minimumPasswordLength) {
-			showError(password_notify, 'Password too short!');
-		} else if (!utils.isPasswordValid(password.val())) {
-			showError(password_notify, 'Invalid password!');
-		} else {
-			showSuccess(password_notify, successIcon);
+		function showSuccess(element, msg) {
+			element.html(msg);
+			element.parent()
+				.removeClass('alert-danger')
+				.addClass('alert-success');
+			element.show();
 		}
 
-		if (password.val() !== password_confirm.val() && password_confirm.val() !== '') {
-			showError(password_confirm_notify, 'Passwords must match!');
+		function validateEmail() {
+			if (!emailEl.val()) {
+				validationError = true;
+				return;
+			}
+
+			if (!utils.isEmailValid(emailEl.val())) {
+				showError(email_notify, 'Invalid email address.');
+			} else
+				socket.emit('user.email.exists', {
+					email: emailEl.val()
+				});
 		}
-	}
-
-	$(password).on('blur', function() {
-		validatePassword();
-	});
 
-	function validatePasswordConfirm() {
-		if (!password.val() || password_notify.hasClass('alert-error')) {
-			return;
+		emailEl.on('blur', function() {
+			validateEmail();
+		});
+
+		function validateUsername() {
+			if (!username.val()) {
+				validationError = true;
+				return;
+			}
+
+			if (username.val().length < config.minimumUsernameLength) {
+				showError(username_notify, 'Username too short!');
+			} else if (username.val().length > config.maximumUsernameLength) {
+				showError(username_notify, 'Username too long!');
+			} else if (!utils.isUserNameValid(username.val())) {
+				showError(username_notify, 'Invalid username!');
+			} else {
+				socket.emit('user.exists', {
+					username: username.val()
+				});
+			}
 		}
 
-		if (password.val() !== password_confirm.val()) {
-			showError(password_confirm_notify, 'Passwords must match!');
-		} else {
-			showSuccess(password_confirm_notify, successIcon);
+		username.on('keyup', function() {
+			jQuery('#yourUsername').html(this.value.length > 0 ? this.value : 'username');
+		});
+		username.on('blur', function() {
+			validateUsername();
+		});
+
+		function validatePassword() {
+			if (!password.val()) {
+				validationError = true;
+				return;
+			}
+
+			if (password.val().length < config.minimumPasswordLength) {
+				showError(password_notify, 'Password too short!');
+			} else if (!utils.isPasswordValid(password.val())) {
+				showError(password_notify, 'Invalid password!');
+			} else {
+				showSuccess(password_notify, successIcon);
+			}
+
+			if (password.val() !== password_confirm.val() && password_confirm.val() !== '') {
+				showError(password_confirm_notify, 'Passwords must match!');
+			}
 		}
-	}
 
-	$(password_confirm).on('blur', function() {
-		validatePasswordConfirm();
-	});
+		$(password).on('blur', function() {
+			validatePassword();
+		});
 
-	ajaxify.register_events(['user.exists', 'user.email.exists']);
+		function validatePasswordConfirm() {
+			if (!password.val() || password_notify.hasClass('alert-error')) {
+				return;
+			}
 
-	socket.on('user.exists', function(data) {
-		if (data.exists === true) {
-			showError(username_notify, 'Username already taken!');
-		} else {
-			showSuccess(username_notify, successIcon);
+			if (password.val() !== password_confirm.val()) {
+				showError(password_confirm_notify, 'Passwords must match!');
+			} else {
+				showSuccess(password_confirm_notify, successIcon);
+			}
 		}
-	});
 
-	socket.on('user.email.exists', function(data) {
-		if (data.exists === true) {
-			showError(email_notify, 'Email address already taken!');
-		} else {
-			showSuccess(email_notify, successIcon);
+		$(password_confirm).on('blur', function() {
+			validatePasswordConfirm();
+		});
+
+		ajaxify.register_events(['user.exists', 'user.email.exists']);
+
+		socket.on('user.exists', function(data) {
+			if (data.exists === true) {
+				showError(username_notify, 'Username already taken!');
+			} else {
+				showSuccess(username_notify, successIcon);
+			}
+		});
+
+		socket.on('user.email.exists', function(data) {
+			if (data.exists === true) {
+				showError(email_notify, 'Email address already taken!');
+			} else {
+				showSuccess(email_notify, successIcon);
+			}
+		});
+
+		// Alternate Logins
+		$('.alt-logins li').on('click', function(e) {
+			document.location.href = $(this).attr('data-url');
+		});
+
+		function validateForm() {
+			validationError = false;
+
+			validateEmail();
+			validateUsername();
+			validatePassword();
+			validatePasswordConfirm();
+
+			return validationError;
 		}
-	});
-
-	// Alternate Logins
-	$('.alt-logins li').on('click', function(e) {
-		document.location.href = $(this).attr('data-url');
-	});
-
-	function validateForm() {
-		validationError = false;
-
-		validateEmail();
-		validateUsername();
-		validatePassword();
-		validatePasswordConfirm();
-
-		return validationError;
-	}
 
-	register.on('click', function(e) {
-		if (validateForm()) e.preventDefault();
-	});
+		register.on('click', function(e) {
+			if (validateForm()) e.preventDefault();
+		});
+	};
 
-}());
\ No newline at end of file
+	return Register;
+});
\ No newline at end of file
diff --git a/public/src/forum/reset.js b/public/src/forum/reset.js
index 366d382356..150b282fe0 100644
--- a/public/src/forum/reset.js
+++ b/public/src/forum/reset.js
@@ -1,41 +1,47 @@
-(function() {
-	var inputEl = document.getElementById('email'),
-		errorEl = document.getElementById('error'),
-		errorTextEl = errorEl.querySelector('p');
+define(function() {
+	var	ResetPassword = {};
 
-	document.getElementById('reset').onclick = function() {
-		if (inputEl.value.length > 0 && inputEl.value.indexOf('@') !== -1) {
-			socket.emit('user:reset.send', {
-				email: inputEl.value
-			});
-		} else {
-			jQuery('#success').hide();
-			jQuery(errorEl).show();
-			errorTextEl.innerHTML = 'Please enter a valid email';
-		}
-	};
+	ResetPassword.init = function() {
+		var inputEl = document.getElementById('email'),
+			errorEl = document.getElementById('error'),
+			errorTextEl = errorEl.querySelector('p');
+
+		document.getElementById('reset').onclick = function() {
+			if (inputEl.value.length > 0 && inputEl.value.indexOf('@') !== -1) {
+				socket.emit('user:reset.send', {
+					email: inputEl.value
+				});
+			} else {
+				jQuery('#success').hide();
+				jQuery(errorEl).show();
+				errorTextEl.innerHTML = 'Please enter a valid email';
+			}
+		};
 
-	ajaxify.register_events(['user.send_reset']);
+		ajaxify.register_events(['user.send_reset']);
 
-	socket.on('user.send_reset', function(data) {
-		var submitEl = document.getElementById('reset');
+		socket.on('user.send_reset', function(data) {
+			var submitEl = document.getElementById('reset');
 
-		if (data.status === 'ok') {
-			jQuery('#error').hide();
-			jQuery('#success').show();
-			jQuery('#success p').html('An email has been dispatched to "' + data.email + '" with instructions on setting a new password.');
-			inputEl.value = '';
-		} else {
-			jQuery('#success').hide();
-			jQuery(errorEl).show();
-			switch (data.message) {
-				case 'invalid-email':
-					errorTextEl.innerHTML = 'The email you put in (<span>' + data.email + '</span>) is not registered with us. Please try again.';
-					break;
-				case 'send-failed':
-					errorTextEl.innerHTML = 'There was a problem sending the reset code. Please try again later.';
-					break;
+			if (data.status === 'ok') {
+				jQuery('#error').hide();
+				jQuery('#success').show();
+				jQuery('#success p').html('An email has been dispatched to "' + data.email + '" with instructions on setting a new password.');
+				inputEl.value = '';
+			} else {
+				jQuery('#success').hide();
+				jQuery(errorEl).show();
+				switch (data.message) {
+					case 'invalid-email':
+						errorTextEl.innerHTML = 'The email you put in (<span>' + data.email + '</span>) is not registered with us. Please try again.';
+						break;
+					case 'send-failed':
+						errorTextEl.innerHTML = 'There was a problem sending the reset code. Please try again later.';
+						break;
+				}
 			}
-		}
-	});
-}());
\ No newline at end of file
+		});
+	};
+
+	return ResetPassword;
+});
\ No newline at end of file
diff --git a/public/src/forum/reset_code.js b/public/src/forum/reset_code.js
index 527b0c7bf6..f36ff00836 100644
--- a/public/src/forum/reset_code.js
+++ b/public/src/forum/reset_code.js
@@ -1,52 +1,58 @@
-(function() {
-	var reset_code = templates.get('reset_code');
-
-	var resetEl = document.getElementById('reset'),
-		password = document.getElementById('password'),
-		repeat = document.getElementById('repeat'),
-		noticeEl = document.getElementById('notice');
-
-	resetEl.addEventListener('click', function() {
-		if (password.value.length < 6) {
-			$('#error').hide();
-			noticeEl.querySelector('strong').innerHTML = 'Invalid Password';
-			noticeEl.querySelector('p').innerHTML = 'The password entered is too short, please pick a different password.';
-			noticeEl.style.display = 'block';
-		} else if (password.value !== repeat.value) {
-			$('#error').hide();
-			noticeEl.querySelector('strong').innerHTML = 'Invalid Password';
-			noticeEl.querySelector('p').innerHTML = 'The two passwords you\'ve entered do not match.';
-			noticeEl.style.display = 'block';
-		} else {
-			socket.emit('user:reset.commit', {
-				code: reset_code,
-				password: password.value
-			});
-		}
-	}, false);
-
-	// Enable the form if the code is valid
-	socket.emit('user:reset.valid', {
-		code: reset_code
-	});
-
-
-	ajaxify.register_events(['user:reset.valid', 'user:reset.commit']);
-	socket.on('user:reset.valid', function(data) {
-		if ( !! data.valid) resetEl.disabled = false;
-		else {
-			var formEl = document.getElementById('reset-form');
-			// Show error message
-			$('#error').show();
-			formEl.parentNode.removeChild(formEl);
-		}
-	})
-
-	socket.on('user:reset.commit', function(data) {
-		if (data.status === 'ok') {
-			$('#error').hide();
-			$('#notice').hide();
-			$('#success').show();
-		}
-	});
-}());
\ No newline at end of file
+define(function() {
+	var	ResetCode = {};
+
+	ResetCode.init = function() {
+		var reset_code = templates.get('reset_code');
+
+		var resetEl = document.getElementById('reset'),
+			password = document.getElementById('password'),
+			repeat = document.getElementById('repeat'),
+			noticeEl = document.getElementById('notice');
+
+		resetEl.addEventListener('click', function() {
+			if (password.value.length < 6) {
+				$('#error').hide();
+				noticeEl.querySelector('strong').innerHTML = 'Invalid Password';
+				noticeEl.querySelector('p').innerHTML = 'The password entered is too short, please pick a different password.';
+				noticeEl.style.display = 'block';
+			} else if (password.value !== repeat.value) {
+				$('#error').hide();
+				noticeEl.querySelector('strong').innerHTML = 'Invalid Password';
+				noticeEl.querySelector('p').innerHTML = 'The two passwords you\'ve entered do not match.';
+				noticeEl.style.display = 'block';
+			} else {
+				socket.emit('user:reset.commit', {
+					code: reset_code,
+					password: password.value
+				});
+			}
+		}, false);
+
+		// Enable the form if the code is valid
+		socket.emit('user:reset.valid', {
+			code: reset_code
+		});
+
+
+		ajaxify.register_events(['user:reset.valid', 'user:reset.commit']);
+		socket.on('user:reset.valid', function(data) {
+			if ( !! data.valid) resetEl.disabled = false;
+			else {
+				var formEl = document.getElementById('reset-form');
+				// Show error message
+				$('#error').show();
+				formEl.parentNode.removeChild(formEl);
+			}
+		})
+
+		socket.on('user:reset.commit', function(data) {
+			if (data.status === 'ok') {
+				$('#error').hide();
+				$('#notice').hide();
+				$('#success').show();
+			}
+		});
+	};
+
+	return ResetCode;
+});
\ No newline at end of file
diff --git a/public/src/forum/search.js b/public/src/forum/search.js
index f7238ef8c3..619f2676e5 100644
--- a/public/src/forum/search.js
+++ b/public/src/forum/search.js
@@ -1,6 +1,7 @@
-(function() {
+define(function() {
+	var	Search = {};
 
-	$(document).ready(function() {
+	Search.init = function() {
 		var searchQuery = $('#topics-container').attr('data-search-query');
 
 		$('.search-result-text').each(function() {
@@ -21,4 +22,5 @@
 		});
 	});
 
-})();
\ No newline at end of file
+	return Search;
+});
\ No newline at end of file
diff --git a/public/src/forum/topic.js b/public/src/forum/topic.js
index 159fb970bc..7e10e1c762 100644
--- a/public/src/forum/topic.js
+++ b/public/src/forum/topic.js
@@ -1,716 +1,722 @@
-(function() {
-	var expose_tools = templates.get('expose_tools'),
-		tid = templates.get('topic_id'),
-		postListEl = document.getElementById('post-container'),
-		editBtns = document.querySelectorAll('#post-container .post-buttons .edit, #post-container .post-buttons .edit i'),
-		thread_state = {
-			locked: templates.get('locked'),
-			deleted: templates.get('deleted'),
-			pinned: templates.get('pinned')
-		},
-		topic_name = templates.get('topic_name');
+define(function() {
+	var	Topic = {};
 
+	Topic.init = function() {
+		var expose_tools = templates.get('expose_tools'),
+			tid = templates.get('topic_id'),
+			postListEl = document.getElementById('post-container'),
+			editBtns = document.querySelectorAll('#post-container .post-buttons .edit, #post-container .post-buttons .edit i'),
+			thread_state = {
+				locked: templates.get('locked'),
+				deleted: templates.get('deleted'),
+				pinned: templates.get('pinned')
+			},
+			topic_name = templates.get('topic_name');
 
-	jQuery('document').ready(function() {
 
-		app.addCommasToNumbers();
+		jQuery('document').ready(function() {
 
-		var room = 'topic_' + tid,
-			adminTools = document.getElementById('thread-tools');
+			app.addCommasToNumbers();
 
-		app.enter_room(room);
+			var room = 'topic_' + tid,
+				adminTools = document.getElementById('thread-tools');
 
-		// Resetting thread state
-		if (thread_state.locked === '1') set_locked_state(true);
-		if (thread_state.deleted === '1') set_delete_state(true);
-		if (thread_state.pinned === '1') set_pinned_state(true);
+			app.enter_room(room);
 
-		if (expose_tools === '1') {
-			var moveThreadModal = $('#move_thread_modal');
+			// Resetting thread state
+			if (thread_state.locked === '1') set_locked_state(true);
+			if (thread_state.deleted === '1') set_delete_state(true);
+			if (thread_state.pinned === '1') set_pinned_state(true);
 
-			adminTools.style.visibility = 'inherit';
+			if (expose_tools === '1') {
+				var moveThreadModal = $('#move_thread_modal');
 
-			// Add events to the thread tools
-			$('#delete_thread').on('click', function(e) {
-				if (thread_state.deleted !== '1') {
-					bootbox.confirm('Are you sure you want to delete this thread?', function(confirm) {
-						if (confirm) socket.emit('api:topic.delete', {
+				adminTools.style.visibility = 'inherit';
+
+				// Add events to the thread tools
+				$('#delete_thread').on('click', function(e) {
+					if (thread_state.deleted !== '1') {
+						bootbox.confirm('Are you sure you want to delete this thread?', function(confirm) {
+							if (confirm) socket.emit('api:topic.delete', {
+								tid: tid
+							});
+						});
+					} else {
+						bootbox.confirm('Are you sure you want to restore this thread?', function(confirm) {
+							if (confirm) socket.emit('api:topic.restore', {
+								tid: tid
+							});
+						});
+					}
+					return false;
+				});
+
+				$('#lock_thread').on('click', function(e) {
+					if (thread_state.locked !== '1') {
+						socket.emit('api:topic.lock', {
 							tid: tid
 						});
-					});
-				} else {
-					bootbox.confirm('Are you sure you want to restore this thread?', function(confirm) {
-						if (confirm) socket.emit('api:topic.restore', {
+					} else {
+						socket.emit('api:topic.unlock', {
 							tid: tid
 						});
-					});
-				}
-				return false;
-			});
-
-			$('#lock_thread').on('click', function(e) {
-				if (thread_state.locked !== '1') {
-					socket.emit('api:topic.lock', {
-						tid: tid
-					});
-				} else {
-					socket.emit('api:topic.unlock', {
-						tid: tid
-					});
-				}
-				return false;
-			});
+					}
+					return false;
+				});
 
-			$('#pin_thread').on('click', function(e) {
-				if (thread_state.pinned !== '1') {
-					socket.emit('api:topic.pin', {
-						tid: tid
-					});
-				} else {
-					socket.emit('api:topic.unpin', {
-						tid: tid
-					});
-				}
-				return false;
-			});
+				$('#pin_thread').on('click', function(e) {
+					if (thread_state.pinned !== '1') {
+						socket.emit('api:topic.pin', {
+							tid: tid
+						});
+					} else {
+						socket.emit('api:topic.unpin', {
+							tid: tid
+						});
+					}
+					return false;
+				});
 
-			$('#move_thread').on('click', function(e) {
-				moveThreadModal.modal('show');
-				return false;
-			});
+				$('#move_thread').on('click', function(e) {
+					moveThreadModal.modal('show');
+					return false;
+				});
 
-			moveThreadModal.on('shown.bs.modal', function() {
-
-				var loadingEl = document.getElementById('categories-loading');
-				if (loadingEl) {
-					socket.once('api:categories.get', function(data) {
-						// Render categories
-						var categoriesFrag = document.createDocumentFragment(),
-							categoryEl = document.createElement('li'),
-							numCategories = data.categories.length,
-							modalBody = moveThreadModal.find('.modal-body'),
-							categoriesEl = modalBody[0].getElementsByTagName('ul')[0],
-							confirmDiv = document.getElementById('move-confirm'),
-							confirmCat = confirmDiv.getElementsByTagName('span')[0],
-							commitEl = document.getElementById('move_thread_commit'),
-							cancelEl = document.getElementById('move_thread_cancel'),
-							x, info, targetCid, targetCatLabel;
-
-						categoriesEl.className = 'category-list';
-						for (x = 0; x < numCategories; x++) {
-							info = data.categories[x];
-							categoryEl.className = info.blockclass;
-							categoryEl.innerHTML = '<i class="' + info.icon + '"></i> ' + info.name;
-							categoryEl.setAttribute('data-cid', info.cid);
-							categoriesFrag.appendChild(categoryEl.cloneNode(true));
-						}
-						categoriesEl.appendChild(categoriesFrag);
-						modalBody[0].removeChild(loadingEl);
-
-						categoriesEl.addEventListener('click', function(e) {
-							if (e.target.nodeName === 'LI') {
-								confirmCat.innerHTML = e.target.innerHTML;
-								confirmDiv.style.display = 'block';
-								targetCid = e.target.getAttribute('data-cid');
-								targetCatLabel = e.target.innerHTML;
-								commitEl.disabled = false;
-							}
-						}, false);
-
-						commitEl.addEventListener('click', function() {
-							if (!commitEl.disabled && targetCid) {
-								commitEl.disabled = true;
-								$(cancelEl).fadeOut(250);
-								$(moveThreadModal).find('.modal-header button').fadeOut(250);
-								commitEl.innerHTML = 'Moving <i class="icon-spin icon-refresh"></i>';
-
-								socket.once('api:topic.move', function(data) {
-									moveThreadModal.modal('hide');
-									if (data.status === 'ok') {
-										app.alert({
-											'alert_id': 'thread_move',
-											type: 'success',
-											title: 'Topic Successfully Moved',
-											message: 'This topic has been successfully moved to ' + targetCatLabel,
-											timeout: 5000
-										});
-									} else {
-										app.alert({
-											'alert_id': 'thread_move',
-											type: 'danger',
-											title: 'Unable to Move Topic',
-											message: 'This topic could not be moved to ' + targetCatLabel + '.<br />Please try again later',
-											timeout: 5000
-										});
-									}
-								});
-								socket.emit('api:topic.move', {
-									tid: tid,
-									cid: targetCid
-								});
+				moveThreadModal.on('shown.bs.modal', function() {
+
+					var loadingEl = document.getElementById('categories-loading');
+					if (loadingEl) {
+						socket.once('api:categories.get', function(data) {
+							// Render categories
+							var categoriesFrag = document.createDocumentFragment(),
+								categoryEl = document.createElement('li'),
+								numCategories = data.categories.length,
+								modalBody = moveThreadModal.find('.modal-body'),
+								categoriesEl = modalBody[0].getElementsByTagName('ul')[0],
+								confirmDiv = document.getElementById('move-confirm'),
+								confirmCat = confirmDiv.getElementsByTagName('span')[0],
+								commitEl = document.getElementById('move_thread_commit'),
+								cancelEl = document.getElementById('move_thread_cancel'),
+								x, info, targetCid, targetCatLabel;
+
+							categoriesEl.className = 'category-list';
+							for (x = 0; x < numCategories; x++) {
+								info = data.categories[x];
+								categoryEl.className = info.blockclass;
+								categoryEl.innerHTML = '<i class="' + info.icon + '"></i> ' + info.name;
+								categoryEl.setAttribute('data-cid', info.cid);
+								categoriesFrag.appendChild(categoryEl.cloneNode(true));
 							}
+							categoriesEl.appendChild(categoriesFrag);
+							modalBody[0].removeChild(loadingEl);
+
+							categoriesEl.addEventListener('click', function(e) {
+								if (e.target.nodeName === 'LI') {
+									confirmCat.innerHTML = e.target.innerHTML;
+									confirmDiv.style.display = 'block';
+									targetCid = e.target.getAttribute('data-cid');
+									targetCatLabel = e.target.innerHTML;
+									commitEl.disabled = false;
+								}
+							}, false);
+
+							commitEl.addEventListener('click', function() {
+								if (!commitEl.disabled && targetCid) {
+									commitEl.disabled = true;
+									$(cancelEl).fadeOut(250);
+									$(moveThreadModal).find('.modal-header button').fadeOut(250);
+									commitEl.innerHTML = 'Moving <i class="icon-spin icon-refresh"></i>';
+
+									socket.once('api:topic.move', function(data) {
+										moveThreadModal.modal('hide');
+										if (data.status === 'ok') {
+											app.alert({
+												'alert_id': 'thread_move',
+												type: 'success',
+												title: 'Topic Successfully Moved',
+												message: 'This topic has been successfully moved to ' + targetCatLabel,
+												timeout: 5000
+											});
+										} else {
+											app.alert({
+												'alert_id': 'thread_move',
+												type: 'danger',
+												title: 'Unable to Move Topic',
+												message: 'This topic could not be moved to ' + targetCatLabel + '.<br />Please try again later',
+												timeout: 5000
+											});
+										}
+									});
+									socket.emit('api:topic.move', {
+										tid: tid,
+										cid: targetCid
+									});
+								}
+							});
 						});
+						socket.emit('api:categories.get');
+					}
+				});
+			}
+
+			// Fix delete state for this thread's posts
+			var postEls = document.querySelectorAll('#post-container li[data-deleted]');
+			for (var x = 0, numPosts = postEls.length; x < numPosts; x++) {
+				if (postEls[x].getAttribute('data-deleted') === '1') toggle_post_delete_state(postEls[x].getAttribute('data-pid'));
+				postEls[x].removeAttribute('data-deleted');
+			}
+
+			// Follow Thread State
+			var followEl = $('.main-post .follow'),
+				set_follow_state = function(state, quiet) {
+					if (state && !followEl.hasClass('btn-success')) {
+						followEl.addClass('btn-success');
+						followEl[0].title = 'You are currently receiving updates to this topic';
+						if (!quiet) {
+							app.alert({
+								alert_id: 'topic_follow',
+								timeout: 2500,
+								title: 'Following Topic',
+								message: 'You will now be receiving notifications when somebody posts to this topic.',
+								type: 'success'
+							});
+						}
+					} else if (!state && followEl.hasClass('btn-success')) {
+						followEl.removeClass('btn-success');
+						followEl[0].title = 'Be notified of new replies in this topic';
+						if (!quiet) {
+							app.alert({
+								alert_id: 'topic_follow',
+								timeout: 2500,
+								title: 'Not Following Topic',
+								message: 'You will no longer receive notifications from this topic.',
+								type: 'success'
+							});
+						}
+					}
+				};
+			socket.on('api:topic.followCheck', function(state) {
+				set_follow_state(state, true);
+			});
+			socket.on('api:topic.follow', function(data) {
+				if (data.status && data.status === 'ok') set_follow_state(data.follow);
+				else {
+					app.alert({
+						type: 'danger',
+						alert_id: 'topic_follow',
+						title: 'Please Log In',
+						message: 'Please register or log in in order to subscribe to this topic',
+						timeout: 5000
 					});
-					socket.emit('api:categories.get');
 				}
 			});
-		}
 
-		// Fix delete state for this thread's posts
-		var postEls = document.querySelectorAll('#post-container li[data-deleted]');
-		for (var x = 0, numPosts = postEls.length; x < numPosts; x++) {
-			if (postEls[x].getAttribute('data-deleted') === '1') toggle_post_delete_state(postEls[x].getAttribute('data-pid'));
-			postEls[x].removeAttribute('data-deleted');
-		}
+			socket.emit('api:topic.followCheck', tid);
+			if (followEl[0]) {
+				followEl[0].addEventListener('click', function() {
+					socket.emit('api:topic.follow', tid);
+				}, false);
+			}
 
-		// Follow Thread State
-		var followEl = $('.main-post .follow'),
-			set_follow_state = function(state, quiet) {
-				if (state && !followEl.hasClass('btn-success')) {
-					followEl.addClass('btn-success');
-					followEl[0].title = 'You are currently receiving updates to this topic';
-					if (!quiet) {
-						app.alert({
-							alert_id: 'topic_follow',
-							timeout: 2500,
-							title: 'Following Topic',
-							message: 'You will now be receiving notifications when somebody posts to this topic.',
-							type: 'success'
-						});
-					}
-				} else if (!state && followEl.hasClass('btn-success')) {
-					followEl.removeClass('btn-success');
-					followEl[0].title = 'Be notified of new replies in this topic';
-					if (!quiet) {
-						app.alert({
-							alert_id: 'topic_follow',
-							timeout: 2500,
-							title: 'Not Following Topic',
-							message: 'You will no longer receive notifications from this topic.',
-							type: 'success'
-						});
-					}
-				}
-			};
-		socket.on('api:topic.followCheck', function(state) {
-			set_follow_state(state, true);
-		});
-		socket.on('api:topic.follow', function(data) {
-			if (data.status && data.status === 'ok') set_follow_state(data.follow);
-			else {
-				app.alert({
-					type: 'danger',
-					alert_id: 'topic_follow',
-					title: 'Please Log In',
-					message: 'Please register or log in in order to subscribe to this topic',
-					timeout: 5000
-				});
+			enableInfiniteLoading();
+
+			var bookmark = localStorage.getItem('topic:' + tid + ':bookmark');
+
+			if(bookmark) {
+				app.scrollToPost(parseInt(bookmark, 10));
 			}
+
+			$('#post-container').on('click', '.deleted', function(ev) {
+				$(this).toggleClass('deleted-expanded');
+			});
 		});
 
-		socket.emit('api:topic.followCheck', tid);
-		if (followEl[0]) {
-			followEl[0].addEventListener('click', function() {
-				socket.emit('api:topic.follow', tid);
-			}, false);
+		function enableInfiniteLoading() {
+			$(window).off('scroll').on('scroll', function() {
+				var bottom = ($(document).height() - $(window).height()) * 0.9;
+
+				if ($(window).scrollTop() > bottom && !app.infiniteLoaderActive && $('#post-container').children().length) {
+					app.loadMorePosts(tid);
+				}
+			});
 		}
 
-		enableInfiniteLoading();
+		var reply_fn = function() {
+			var selectionText = '',
+				selection = window.getSelection() || document.getSelection();
 
-		var bookmark = localStorage.getItem('topic:' + tid + ':bookmark');
+			if ($(selection.baseNode).parents('.post-content').length > 0) {
+				var snippet = selection.toString();
+				if (snippet.length > 0) selectionText = '> ' + snippet.replace(/\n/g, '\n> ');
+			}
 
-		if(bookmark) {
-			app.scrollToPost(parseInt(bookmark, 10));
-		}
+			if (thread_state.locked !== '1') {
+				require(['composer'], function(cmp) {
+					cmp.push(tid, null, null, selectionText.length > 0 ? selectionText + '\n\n' : '');
+				});
+			}
+		};
+		$('#post-container').on('click', '.post_reply', reply_fn);
+		$('#post_reply').on('click', reply_fn);
 
-		$('#post-container').on('click', '.deleted', function(ev) {
-			$(this).toggleClass('deleted-expanded');
+		$('#post-container').on('click', '.quote', function() {
+			if (thread_state.locked !== '1') {
+				var pid = $(this).parents('li').attr('data-pid');
+
+				socket.once('api:posts.getRawPost', function(data) {
+
+					quoted = '> ' + data.post.replace(/\n/g, '\n> ') + '\n\n';
+					require(['composer'], function(cmp) {
+						cmp.push(tid, null, null, quoted);
+					});
+				});
+				socket.emit('api:posts.getRawPost', {
+					pid: pid
+				});
+			}
 		});
-	});
 
-	function enableInfiniteLoading() {
-		$(window).off('scroll').on('scroll', function() {
-			var bottom = ($(document).height() - $(window).height()) * 0.9;
+		$('#post-container').on('click', '.favourite', function() {
+			var pid = $(this).parents('li').attr('data-pid');
+			var uid = $(this).parents('li').attr('data-uid');
 
-			if ($(window).scrollTop() > bottom && !app.infiniteLoaderActive && $('#post-container').children().length) {
-				app.loadMorePosts(tid);
+			var element = $(this).find('i');
+			if (element.attr('class') == 'icon-star-empty') {
+				socket.emit('api:posts.favourite', {
+					pid: pid,
+					room_id: app.current_room
+				});
+			} else {
+				socket.emit('api:posts.unfavourite', {
+					pid: pid,
+					room_id: app.current_room
+				});
 			}
 		});
-	}
 
-	var reply_fn = function() {
-		var selectionText = '',
-			selection = window.getSelection() || document.getSelection();
+		$('#post-container').on('click', '.link', function() {
+			var pid = $(this).parents('li').attr('data-pid');
+			$('#post_' + pid + '_link').val(window.location.href + "#" + pid).stop(true, false).fadeIn().select();
+			$('#post_' + pid + '_link').off('blur').on('blur', function() {
+				$(this).fadeOut();
+			});
+		});
 
-		if ($(selection.baseNode).parents('.post-content').length > 0) {
-			var snippet = selection.toString();
-			if (snippet.length > 0) selectionText = '> ' + snippet.replace(/\n/g, '\n> ');
-		}
+		$('#post-container').delegate('.edit', 'click', function(e) {
+			var pid = $(this).parents('li').attr('data-pid'),
+				main = $(this).parents('.main-post');
 
-		if (thread_state.locked !== '1') {
 			require(['composer'], function(cmp) {
-				cmp.push(tid, null, null, selectionText.length > 0 ? selectionText + '\n\n' : '');
+				cmp.push(null, null, pid);
 			});
-		}
-	};
-	$('#post-container').on('click', '.post_reply', reply_fn);
-	$('#post_reply').on('click', reply_fn);
+		});
 
-	$('#post-container').on('click', '.quote', function() {
-		if (thread_state.locked !== '1') {
-			var pid = $(this).parents('li').attr('data-pid');
+		$('#post-container').delegate('.delete', 'click', function(e) {
+			var pid = $(this).parents('li').attr('data-pid'),
+				postEl = $(document.querySelector('#post-container li[data-pid="' + pid + '"]')),
+				deleteAction = !postEl.hasClass('deleted') ? true : false,
+				confirmDel = confirm((deleteAction ? 'Delete' : 'Restore') + ' this post?');
+
+			if (confirmDel) {
+				deleteAction ?
+					socket.emit('api:posts.delete', {
+						pid: pid
+					}) :
+					socket.emit('api:posts.restore', {
+						pid: pid
+					});
+			}
+		});
 
-			socket.once('api:posts.getRawPost', function(data) {
+		$('#post-container').on('click', '.chat', function(e) {
+			var username = $(this).parents('li.row').attr('data-username');
+			var touid = $(this).parents('li.row').attr('data-uid');
 
-				quoted = '> ' + data.post.replace(/\n/g, '\n> ') + '\n\n';
-				require(['composer'], function(cmp) {
-					cmp.push(tid, null, null, quoted);
-				});
-			});
-			socket.emit('api:posts.getRawPost', {
-				pid: pid
-			});
-		}
-	});
+			if (username === app.username || !app.username)
+				return;
 
-	$('#post-container').on('click', '.favourite', function() {
-		var pid = $(this).parents('li').attr('data-pid');
-		var uid = $(this).parents('li').attr('data-uid');
+			app.openChat(username, touid);
+		});
 
-		var element = $(this).find('i');
-		if (element.attr('class') == 'icon-star-empty') {
-			socket.emit('api:posts.favourite', {
-				pid: pid,
-				room_id: app.current_room
-			});
-		} else {
-			socket.emit('api:posts.unfavourite', {
-				pid: pid,
-				room_id: app.current_room
-			});
-		}
-	});
+		ajaxify.register_events([
+			'event:rep_up', 'event:rep_down', 'event:new_post', 'api:get_users_in_room',
+			'event:topic_deleted', 'event:topic_restored', 'event:topic:locked',
+			'event:topic_unlocked', 'event:topic_pinned', 'event:topic_unpinned',
+			'event:topic_moved', 'event:post_edited', 'event:post_deleted', 'event:post_restored',
+			'api:posts.favourite'
+		]);
 
-	$('#post-container').on('click', '.link', function() {
-		var pid = $(this).parents('li').attr('data-pid');
-		$('#post_' + pid + '_link').val(window.location.href + "#" + pid).stop(true, false).fadeIn().select();
-		$('#post_' + pid + '_link').off('blur').on('blur', function() {
-			$(this).fadeOut();
+
+		socket.on('api:get_users_in_room', function(data) {
+			var activeEl = $('#thread_active_users');
+			if (activeEl.length)
+				activeEl.html(data);
+
+			app.populate_online_users();
 		});
-	});
 
-	$('#post-container').delegate('.edit', 'click', function(e) {
-		var pid = $(this).parents('li').attr('data-pid'),
-			main = $(this).parents('.main-post');
+		socket.on('event:rep_up', function(data) {
+			adjust_rep(1, data.pid, data.uid);
+		});
 
-		require(['composer'], function(cmp) {
-			cmp.push(null, null, pid);
+		socket.on('event:rep_down', function(data) {
+			adjust_rep(-1, data.pid, data.uid);
 		});
-	});
 
-	$('#post-container').delegate('.delete', 'click', function(e) {
-		var pid = $(this).parents('li').attr('data-pid'),
-			postEl = $(document.querySelector('#post-container li[data-pid="' + pid + '"]')),
-			deleteAction = !postEl.hasClass('deleted') ? true : false,
-			confirmDel = confirm((deleteAction ? 'Delete' : 'Restore') + ' this post?');
+		socket.on('event:new_post', app.createNewPosts);
 
-		if (confirmDel) {
-			deleteAction ?
-				socket.emit('api:posts.delete', {
-					pid: pid
-				}) :
-				socket.emit('api:posts.restore', {
-					pid: pid
-				});
-		}
-	});
+		socket.on('event:topic_deleted', function(data) {
+			if (data.tid === tid && data.status === 'ok') {
+				set_locked_state(true);
+				set_delete_state(true);
+			}
+		});
 
-	$('#post-container').on('click', '.chat', function(e) {
-		var username = $(this).parents('li.row').attr('data-username');
-		var touid = $(this).parents('li.row').attr('data-uid');
+		socket.on('event:topic_restored', function(data) {
+			if (data.tid === tid && data.status === 'ok') {
+				set_locked_state(false);
+				set_delete_state(false);
+			}
+		});
 
-		if (username === app.username || !app.username)
-			return;
+		socket.on('event:topic_locked', function(data) {
+			if (data.tid === tid && data.status === 'ok') {
+				set_locked_state(true, 1);
+			}
+		});
 
-		app.openChat(username, touid);
-	});
+		socket.on('event:topic_unlocked', function(data) {
+			if (data.tid === tid && data.status === 'ok') {
+				set_locked_state(false, 1);
+			}
+		});
 
-	ajaxify.register_events([
-		'event:rep_up', 'event:rep_down', 'event:new_post', 'api:get_users_in_room',
-		'event:topic_deleted', 'event:topic_restored', 'event:topic:locked',
-		'event:topic_unlocked', 'event:topic_pinned', 'event:topic_unpinned',
-		'event:topic_moved', 'event:post_edited', 'event:post_deleted', 'event:post_restored',
-		'api:posts.favourite'
-	]);
+		socket.on('event:topic_pinned', function(data) {
+			if (data.tid === tid && data.status === 'ok') {
+				set_pinned_state(true, 1);
+			}
+		});
 
+		socket.on('event:topic_unpinned', function(data) {
+			if (data.tid === tid && data.status === 'ok') {
+				set_pinned_state(false, 1);
+			}
+		});
 
-	socket.on('api:get_users_in_room', function(data) {
-		var activeEl = $('#thread_active_users');
-		if (activeEl.length)
-			activeEl.html(data);
+		socket.on('event:topic_moved', function(data) {
+			if (data && data.tid > 0) ajaxify.go('topic/' + data.tid);
+		});
 
-		app.populate_online_users();
-	});
+		socket.on('event:post_edited', function(data) {
+			var editedPostEl = document.getElementById('content_' + data.pid);
 
-	socket.on('event:rep_up', function(data) {
-		adjust_rep(1, data.pid, data.uid);
-	});
+			var editedPostTitle = $('#topic_title_' + data.pid);
 
-	socket.on('event:rep_down', function(data) {
-		adjust_rep(-1, data.pid, data.uid);
-	});
+			if (editedPostTitle.length > 0) {
+				editedPostTitle.fadeOut(250, function() {
+					editedPostTitle.html(data.title);
+					editedPostTitle.fadeIn(250);
+				});
+			}
 
-	socket.on('event:new_post', app.createNewPosts);
+			$(editedPostEl).fadeOut(250, function() {
+				this.innerHTML = data.content;
+				$(this).fadeIn(250);
+			});
 
-	socket.on('event:topic_deleted', function(data) {
-		if (data.tid === tid && data.status === 'ok') {
-			set_locked_state(true);
-			set_delete_state(true);
-		}
-	});
+		});
 
-	socket.on('event:topic_restored', function(data) {
-		if (data.tid === tid && data.status === 'ok') {
-			set_locked_state(false);
-			set_delete_state(false);
-		}
-	});
+		socket.on('api:posts.favourite', function(data) {
+			if (data.status === 'ok' && data.pid) {
+				var favEl = document.querySelector('.post_rep_' + data.pid).nextSibling;
+				if (favEl) {
+					favEl.className = 'icon-star';
+					$(favEl).parent().addClass('btn-warning');
+				}
+			}
+		});
 
-	socket.on('event:topic_locked', function(data) {
-		if (data.tid === tid && data.status === 'ok') {
-			set_locked_state(true, 1);
-		}
-	});
+		socket.on('api:posts.unfavourite', function(data) {
+			if (data.status === 'ok' && data.pid) {
+				var favEl = document.querySelector('.post_rep_' + data.pid).nextSibling;
+				if (favEl) {
+					favEl.className = 'icon-star-empty';
+					$(favEl).parent().removeClass('btn-warning');
+				}
+			}
+		});
 
-	socket.on('event:topic_unlocked', function(data) {
-		if (data.tid === tid && data.status === 'ok') {
-			set_locked_state(false, 1);
-		}
-	});
+		socket.on('event:post_deleted', function(data) {
+			if (data.pid) toggle_post_delete_state(data.pid, true);
+		});
 
-	socket.on('event:topic_pinned', function(data) {
-		if (data.tid === tid && data.status === 'ok') {
-			set_pinned_state(true, 1);
-		}
-	});
+		socket.on('event:post_restored', function(data) {
+			if (data.pid) toggle_post_delete_state(data.pid, true);
+		});
 
-	socket.on('event:topic_unpinned', function(data) {
-		if (data.tid === tid && data.status === 'ok') {
-			set_pinned_state(false, 1);
-		}
-	});
+		socket.on('api:post.privileges', function(privileges) {
+			if (privileges.editable) toggle_mod_tools(privileges.pid, true);
+		});
 
-	socket.on('event:topic_moved', function(data) {
-		if (data && data.tid > 0) ajaxify.go('topic/' + data.tid);
-	});
+		function adjust_rep(value, pid, uid) {
+			var post_rep = jQuery('.post_rep_' + pid),
+				user_rep = jQuery('.user_rep_' + uid);
 
-	socket.on('event:post_edited', function(data) {
-		var editedPostEl = document.getElementById('content_' + data.pid);
+			var ptotal = parseInt(post_rep.html(), 10),
+				utotal = parseInt(user_rep.html(), 10);
 
-		var editedPostTitle = $('#topic_title_' + data.pid);
+			ptotal += value;
+			utotal += value;
 
-		if (editedPostTitle.length > 0) {
-			editedPostTitle.fadeOut(250, function() {
-				editedPostTitle.html(data.title);
-				editedPostTitle.fadeIn(250);
-			});
+			post_rep.html(ptotal + ' ');
+			user_rep.html(utotal + ' ');
 		}
 
-		$(editedPostEl).fadeOut(250, function() {
-			this.innerHTML = data.content;
-			$(this).fadeIn(250);
-		});
+		function set_locked_state(locked, alert) {
+			var threadReplyBtn = document.getElementById('post_reply'),
+				postReplyBtns = document.querySelectorAll('#post-container .post_reply'),
+				quoteBtns = document.querySelectorAll('#post-container .quote'),
+				editBtns = document.querySelectorAll('#post-container .edit'),
+				deleteBtns = document.querySelectorAll('#post-container .delete'),
+				numPosts = document.querySelectorAll('#post_container li[data-pid]').length,
+				lockThreadEl = document.getElementById('lock_thread'),
+				x;
+
+			if (locked === true) {
+				lockThreadEl.innerHTML = '<i class="icon-unlock"></i> Unlock Thread';
+				threadReplyBtn.disabled = true;
+				threadReplyBtn.innerHTML = 'Locked <i class="icon-lock"></i>';
+				for (x = 0; x < numPosts; x++) {
+					postReplyBtns[x].innerHTML = 'Locked <i class="icon-lock"></i>';
+					quoteBtns[x].style.display = 'none';
+					editBtns[x].style.display = 'none';
+					deleteBtns[x].style.display = 'none';
+				}
 
-	});
+				if (alert) {
+					app.alert({
+						'alert_id': 'thread_lock',
+						type: 'success',
+						title: 'Thread Locked',
+						message: 'Thread has been successfully locked',
+						timeout: 5000
+					});
+				}
+
+				thread_state.locked = '1';
+			} else {
+				lockThreadEl.innerHTML = '<i class="icon-lock"></i> Lock Thread';
+				threadReplyBtn.disabled = false;
+				threadReplyBtn.innerHTML = 'Reply';
+				for (x = 0; x < numPosts; x++) {
+					postReplyBtns[x].innerHTML = 'Reply <i class="icon-reply"></i>';
+					quoteBtns[x].style.display = 'inline-block';
+					editBtns[x].style.display = 'inline-block';
+					deleteBtns[x].style.display = 'inline-block';
+				}
+
+				if (alert) {
+					app.alert({
+						'alert_id': 'thread_lock',
+						type: 'success',
+						title: 'Thread Unlocked',
+						message: 'Thread has been successfully unlocked',
+						timeout: 5000
+					});
+				}
 
-	socket.on('api:posts.favourite', function(data) {
-		if (data.status === 'ok' && data.pid) {
-			var favEl = document.querySelector('.post_rep_' + data.pid).nextSibling;
-			if (favEl) {
-				favEl.className = 'icon-star';
-				$(favEl).parent().addClass('btn-warning');
+				thread_state.locked = '0';
 			}
 		}
-	});
-
-	socket.on('api:posts.unfavourite', function(data) {
-		if (data.status === 'ok' && data.pid) {
-			var favEl = document.querySelector('.post_rep_' + data.pid).nextSibling;
-			if (favEl) {
-				favEl.className = 'icon-star-empty';
-				$(favEl).parent().removeClass('btn-warning');
+
+		function set_delete_state(deleted) {
+			var deleteThreadEl = document.getElementById('delete_thread'),
+				deleteTextEl = deleteThreadEl.getElementsByTagName('span')[0],
+				threadEl = $('#post-container'),
+				deleteNotice = document.getElementById('thread-deleted') || document.createElement('div');
+
+			if (deleted) {
+				deleteTextEl.innerHTML = '<i class="icon-comment"></i> Restore Thread';
+				threadEl.addClass('deleted');
+
+				// Spawn a 'deleted' notice at the top of the page
+				deleteNotice.setAttribute('id', 'thread-deleted');
+				deleteNotice.className = 'alert alert-warning';
+				deleteNotice.innerHTML = 'This thread has been deleted. Only users with thread management privileges can see it.';
+				threadEl.before(deleteNotice);
+
+				thread_state.deleted = '1';
+			} else {
+				deleteTextEl.innerHTML = '<i class="icon-trash"></i> Delete Thread';
+				threadEl.removeClass('deleted');
+				deleteNotice.parentNode.removeChild(deleteNotice);
+
+				thread_state.deleted = '0';
 			}
 		}
-	});
-
-	socket.on('event:post_deleted', function(data) {
-		if (data.pid) toggle_post_delete_state(data.pid, true);
-	});
-
-	socket.on('event:post_restored', function(data) {
-		if (data.pid) toggle_post_delete_state(data.pid, true);
-	});
-
-	socket.on('api:post.privileges', function(privileges) {
-		if (privileges.editable) toggle_mod_tools(privileges.pid, true);
-	});
-
-	function adjust_rep(value, pid, uid) {
-		var post_rep = jQuery('.post_rep_' + pid),
-			user_rep = jQuery('.user_rep_' + uid);
-
-		var ptotal = parseInt(post_rep.html(), 10),
-			utotal = parseInt(user_rep.html(), 10);
-
-		ptotal += value;
-		utotal += value;
-
-		post_rep.html(ptotal + ' ');
-		user_rep.html(utotal + ' ');
-	}
-
-	function set_locked_state(locked, alert) {
-		var threadReplyBtn = document.getElementById('post_reply'),
-			postReplyBtns = document.querySelectorAll('#post-container .post_reply'),
-			quoteBtns = document.querySelectorAll('#post-container .quote'),
-			editBtns = document.querySelectorAll('#post-container .edit'),
-			deleteBtns = document.querySelectorAll('#post-container .delete'),
-			numPosts = document.querySelectorAll('#post_container li[data-pid]').length,
-			lockThreadEl = document.getElementById('lock_thread'),
-			x;
-
-		if (locked === true) {
-			lockThreadEl.innerHTML = '<i class="icon-unlock"></i> Unlock Thread';
-			threadReplyBtn.disabled = true;
-			threadReplyBtn.innerHTML = 'Locked <i class="icon-lock"></i>';
-			for (x = 0; x < numPosts; x++) {
-				postReplyBtns[x].innerHTML = 'Locked <i class="icon-lock"></i>';
-				quoteBtns[x].style.display = 'none';
-				editBtns[x].style.display = 'none';
-				deleteBtns[x].style.display = 'none';
-			}
 
-			if (alert) {
-				app.alert({
-					'alert_id': 'thread_lock',
-					type: 'success',
-					title: 'Thread Locked',
-					message: 'Thread has been successfully locked',
-					timeout: 5000
-				});
-			}
+		function set_pinned_state(pinned, alert) {
+			var pinEl = document.getElementById('pin_thread');
+
+			if (pinned) {
+				pinEl.innerHTML = '<i class="icon-pushpin"></i> Unpin Thread';
+				if (alert) {
+					app.alert({
+						'alert_id': 'thread_pin',
+						type: 'success',
+						title: 'Thread Pinned',
+						message: 'Thread has been successfully pinned',
+						timeout: 5000
+					});
+				}
 
-			thread_state.locked = '1';
-		} else {
-			lockThreadEl.innerHTML = '<i class="icon-lock"></i> Lock Thread';
-			threadReplyBtn.disabled = false;
-			threadReplyBtn.innerHTML = 'Reply';
-			for (x = 0; x < numPosts; x++) {
-				postReplyBtns[x].innerHTML = 'Reply <i class="icon-reply"></i>';
-				quoteBtns[x].style.display = 'inline-block';
-				editBtns[x].style.display = 'inline-block';
-				deleteBtns[x].style.display = 'inline-block';
-			}
+				thread_state.pinned = '1';
+			} else {
+				pinEl.innerHTML = '<i class="icon-pushpin"></i> Pin Thread';
+				if (alert) {
+					app.alert({
+						'alert_id': 'thread_pin',
+						type: 'success',
+						title: 'Thread Unpinned',
+						message: 'Thread has been successfully unpinned',
+						timeout: 5000
+					});
+				}
 
-			if (alert) {
-				app.alert({
-					'alert_id': 'thread_lock',
-					type: 'success',
-					title: 'Thread Unlocked',
-					message: 'Thread has been successfully unlocked',
-					timeout: 5000
-				});
+				thread_state.pinned = '0';
 			}
-
-			thread_state.locked = '0';
 		}
-	}
-
-	function set_delete_state(deleted) {
-		var deleteThreadEl = document.getElementById('delete_thread'),
-			deleteTextEl = deleteThreadEl.getElementsByTagName('span')[0],
-			threadEl = $('#post-container'),
-			deleteNotice = document.getElementById('thread-deleted') || document.createElement('div');
-
-		if (deleted) {
-			deleteTextEl.innerHTML = '<i class="icon-comment"></i> Restore Thread';
-			threadEl.addClass('deleted');
-
-			// Spawn a 'deleted' notice at the top of the page
-			deleteNotice.setAttribute('id', 'thread-deleted');
-			deleteNotice.className = 'alert alert-warning';
-			deleteNotice.innerHTML = 'This thread has been deleted. Only users with thread management privileges can see it.';
-			threadEl.before(deleteNotice);
-
-			thread_state.deleted = '1';
-		} else {
-			deleteTextEl.innerHTML = '<i class="icon-trash"></i> Delete Thread';
-			threadEl.removeClass('deleted');
-			deleteNotice.parentNode.removeChild(deleteNotice);
-
-			thread_state.deleted = '0';
-		}
-	}
-
-	function set_pinned_state(pinned, alert) {
-		var pinEl = document.getElementById('pin_thread');
-
-		if (pinned) {
-			pinEl.innerHTML = '<i class="icon-pushpin"></i> Unpin Thread';
-			if (alert) {
-				app.alert({
-					'alert_id': 'thread_pin',
-					type: 'success',
-					title: 'Thread Pinned',
-					message: 'Thread has been successfully pinned',
-					timeout: 5000
-				});
-			}
-
-			thread_state.pinned = '1';
-		} else {
-			pinEl.innerHTML = '<i class="icon-pushpin"></i> Pin Thread';
-			if (alert) {
-				app.alert({
-					'alert_id': 'thread_pin',
-					type: 'success',
-					title: 'Thread Unpinned',
-					message: 'Thread has been successfully unpinned',
-					timeout: 5000
-				});
-			}
 
-			thread_state.pinned = '0';
-		}
-	}
+		function toggle_post_delete_state(pid) {
+			var postEl = $(document.querySelector('#post-container li[data-pid="' + pid + '"]'));
 
-	function toggle_post_delete_state(pid) {
-		var postEl = $(document.querySelector('#post-container li[data-pid="' + pid + '"]'));
+			if (postEl[0]) {
+				quoteEl = $(postEl[0].querySelector('.quote')),
+				favEl = $(postEl[0].querySelector('.favourite')),
+				replyEl = $(postEl[0].querySelector('.post_reply'));
 
-		if (postEl[0]) {
-			quoteEl = $(postEl[0].querySelector('.quote')),
-			favEl = $(postEl[0].querySelector('.favourite')),
-			replyEl = $(postEl[0].querySelector('.post_reply'));
+				socket.once('api:post.privileges', function(privileges) {
+					if (privileges.editable) {
+						if (!postEl.hasClass('deleted')) {
+							toggle_post_tools(pid, false);
+						} else {
+							toggle_post_tools(pid, true);
+						}
+					}
 
-			socket.once('api:post.privileges', function(privileges) {
-				if (privileges.editable) {
-					if (!postEl.hasClass('deleted')) {
-						toggle_post_tools(pid, false);
+					if (privileges.view_deleted) {
+						postEl.toggleClass('deleted');
 					} else {
-						toggle_post_tools(pid, true);
+						postEl.toggleClass('none');
 					}
-				}
-
-				if (privileges.view_deleted) {
-					postEl.toggleClass('deleted');
-				} else {
-					postEl.toggleClass('none');
-				}
-			});
-			socket.emit('api:post.privileges', pid);
+				});
+				socket.emit('api:post.privileges', pid);
+			}
 		}
-	}
-
-	function toggle_post_tools(pid, state) {
-		var postEl = $(document.querySelector('#post-container li[data-pid="' + pid + '"]')),
-			quoteEl = $(postEl[0].querySelector('.quote')),
-			favEl = $(postEl[0].querySelector('.favourite')),
-			replyEl = $(postEl[0].querySelector('.post_reply'));
-
-		if (state) {
-			quoteEl.removeClass('none');
-			favEl.removeClass('none');
-			replyEl.removeClass('none');
-		} else {
-			quoteEl.addClass('none');
-			favEl.addClass('none');
-			replyEl.addClass('none');
+
+		function toggle_post_tools(pid, state) {
+			var postEl = $(document.querySelector('#post-container li[data-pid="' + pid + '"]')),
+				quoteEl = $(postEl[0].querySelector('.quote')),
+				favEl = $(postEl[0].querySelector('.favourite')),
+				replyEl = $(postEl[0].querySelector('.post_reply'));
+
+			if (state) {
+				quoteEl.removeClass('none');
+				favEl.removeClass('none');
+				replyEl.removeClass('none');
+			} else {
+				quoteEl.addClass('none');
+				favEl.addClass('none');
+				replyEl.addClass('none');
+			}
 		}
-	}
-
-	function toggle_mod_tools(pid, state) {
-		var postEl = $(document.querySelector('#post-container li[data-pid="' + pid + '"]')),
-			editEl = postEl.find('.edit'),
-			deleteEl = postEl.find('.delete');
-
-		if (state) {
-			editEl.removeClass('none');
-			deleteEl.removeClass('none');
-		} else {
-			editEl.addClass('none');
-			deleteEl.addClass('none');
+
+		function toggle_mod_tools(pid, state) {
+			var postEl = $(document.querySelector('#post-container li[data-pid="' + pid + '"]')),
+				editEl = postEl.find('.edit'),
+				deleteEl = postEl.find('.delete');
+
+			if (state) {
+				editEl.removeClass('none');
+				deleteEl.removeClass('none');
+			} else {
+				editEl.addClass('none');
+				deleteEl.addClass('none');
+			}
 		}
-	}
 
 
 
-	var postAuthorImage, mobileAuthorOverlay, pagination;
-	var postcount = templates.get('postcount');
+		var postAuthorImage, mobileAuthorOverlay, pagination;
+		var postcount = templates.get('postcount');
 
-	function updateHeader() {
-		if (pagination == null) {
-			jQuery('.pagination-block i:first').on('click', function() {
-				app.scrollToTop();
-			});
-			jQuery('.pagination-block i:last').on('click', function() {
-				app.scrollToBottom();
-			});
-		}
+		function updateHeader() {
+			if (pagination == null) {
+				jQuery('.pagination-block i:first').on('click', function() {
+					app.scrollToTop();
+				});
+				jQuery('.pagination-block i:last').on('click', function() {
+					app.scrollToBottom();
+				});
+			}
 
-		jQuery('.mobile-author-overlay').css('bottom', '0px');
-		postAuthorImage = postAuthorImage || document.getElementById('mobile-author-image');
-		mobileAuthorOverlay = mobileAuthorOverlay || document.getElementById('mobile-author-overlay');
-		pagination = pagination || document.getElementById('pagination');
+			jQuery('.mobile-author-overlay').css('bottom', '0px');
+			postAuthorImage = postAuthorImage || document.getElementById('mobile-author-image');
+			mobileAuthorOverlay = mobileAuthorOverlay || document.getElementById('mobile-author-overlay');
+			pagination = pagination || document.getElementById('pagination');
 
-		pagination.parentNode.style.display = 'block';
+			pagination.parentNode.style.display = 'block';
 
-		var windowHeight = jQuery(window).height();
-		var scrollTop = jQuery(window).scrollTop();
-		var scrollBottom = scrollTop + windowHeight;
+			var windowHeight = jQuery(window).height();
+			var scrollTop = jQuery(window).scrollTop();
+			var scrollBottom = scrollTop + windowHeight;
 
-		if (scrollTop < 50 && postcount > 1) {
-			localStorage.removeItem("topic:" + tid + ":bookmark");
-			postAuthorImage.src = (jQuery('.main-post .avatar img').attr('src'));
-			mobileAuthorOverlay.innerHTML = 'Posted by ' + jQuery('.main-post').attr('data-username') + ', ' + jQuery('.main-post').find('.relativeTimeAgo').html();
-			pagination.innerHTML = '0 out of ' + postcount;
-			return;
-		}
+			if (scrollTop < 50 && postcount > 1) {
+				localStorage.removeItem("topic:" + tid + ":bookmark");
+				postAuthorImage.src = (jQuery('.main-post .avatar img').attr('src'));
+				mobileAuthorOverlay.innerHTML = 'Posted by ' + jQuery('.main-post').attr('data-username') + ', ' + jQuery('.main-post').find('.relativeTimeAgo').html();
+				pagination.innerHTML = '0 out of ' + postcount;
+				return;
+			}
 
 
-		var count = 0, smallestNonNegative = 0;
+			var count = 0, smallestNonNegative = 0;
 
-		jQuery('.sub-posts').each(function() {
-			count++;
-			this.postnumber = count;
+			jQuery('.sub-posts').each(function() {
+				count++;
+				this.postnumber = count;
 
 
-			var el = jQuery(this);
-			var elTop = el.offset().top;
-			var height = Math.floor(el.height());
-			var elBottom = elTop + (height < 300 ? height : 300);
+				var el = jQuery(this);
+				var elTop = el.offset().top;
+				var height = Math.floor(el.height());
+				var elBottom = elTop + (height < 300 ? height : 300);
 
-			var inView = ((elBottom >= scrollTop) && (elTop <= scrollBottom) && (elBottom <= scrollBottom) && (elTop >= scrollTop));
+				var inView = ((elBottom >= scrollTop) && (elTop <= scrollBottom) && (elBottom <= scrollBottom) && (elTop >= scrollTop));
 
 
-			if (inView) {
-				if(elTop - scrollTop > smallestNonNegative) {
-					localStorage.setItem("topic:" + tid + ":bookmark", el.attr('data-pid'));
-					smallestNonNegative = Number.MAX_VALUE;
+				if (inView) {
+					if(elTop - scrollTop > smallestNonNegative) {
+						localStorage.setItem("topic:" + tid + ":bookmark", el.attr('data-pid'));
+						smallestNonNegative = Number.MAX_VALUE;
+					}
+
+					pagination.innerHTML = this.postnumber + ' out of ' + postcount;
+					postAuthorImage.src = (jQuery(this).find('.profile-image-block img').attr('src'));
+					mobileAuthorOverlay.innerHTML = 'Posted by ' + jQuery(this).attr('data-username') + ', ' + jQuery(this).find('.relativeTimeAgo').html();
 				}
+			});
 
-				pagination.innerHTML = this.postnumber + ' out of ' + postcount;
-				postAuthorImage.src = (jQuery(this).find('.profile-image-block img').attr('src'));
-				mobileAuthorOverlay.innerHTML = 'Posted by ' + jQuery(this).attr('data-username') + ', ' + jQuery(this).find('.relativeTimeAgo').html();
-			}
-		});
+			setTimeout(function() {
+				if (scrollTop + windowHeight == jQuery(document).height()) {
+					pagination.innerHTML = postcount + ' out of ' + postcount;
+				}
+			}, 100);
+		}
 
-		setTimeout(function() {
-			if (scrollTop + windowHeight == jQuery(document).height()) {
-				pagination.innerHTML = postcount + ' out of ' + postcount;
-			}
-		}, 100);
-	}
+		window.onscroll = updateHeader;
+		window.onload = updateHeader;
+	};
 
-	window.onscroll = updateHeader;
-	window.onload = updateHeader;
-})();
+	return Topic;
+});
\ No newline at end of file
diff --git a/public/src/forum/unread.js b/public/src/forum/unread.js
index b4b78b67a1..329cfff3ff 100644
--- a/public/src/forum/unread.js
+++ b/public/src/forum/unread.js
@@ -1,114 +1,119 @@
-(function() {
-	var loadingMoreTopics = false;
-
-	app.enter_room('recent_posts');
-
-	ajaxify.register_events([
-		'event:new_topic',
-		'event:new_post'
-	]);
-
-	var newTopicCount = 0,
-		newPostCount = 0;
-
-	$('#new-topics-alert').on('click', function() {
-		$(this).hide();
-	});
-
-	socket.on('event:new_topic', function(data) {
-
-		++newTopicCount;
-		updateAlertText();
-
-	});
-
-	function updateAlertText() {
-		var text = '';
-
-		if (newTopicCount > 1)
-			text = 'There are ' + newTopicCount + ' new topics';
-		else if (newTopicCount === 1)
-			text = 'There is 1 new topic';
-		else
-			text = 'There are no new topics';
-
-		if (newPostCount > 1)
-			text += ' and ' + newPostCount + ' new posts.';
-		else if (newPostCount === 1)
-			text += ' and 1 new post.';
-		else
-			text += ' and no new posts.';
-
-		text += ' Click here to reload.';
-
-		$('#new-topics-alert').html(text).fadeIn('slow');
-	}
-
-	socket.on('event:new_post', function(data) {
-		++newPostCount;
-		updateAlertText();
-	});
-
-	$('#mark-allread-btn').on('click', function() {
-		var btn = $(this);
-		socket.emit('api:topics.markAllRead', {}, function(success) {
-			if (success) {
-				btn.remove();
-				$('#topics-container').empty();
-				$('#category-no-topics').removeClass('hidden');
-				app.alertSuccess('All topics marked as read!');
-				$('#numUnreadBadge')
-					.removeClass('badge-important')
-					.addClass('badge-inverse')
-					.html('0');
-			} else {
-				app.alertError('There was an error marking topics read!');
-			}
+define(function() {
+	var	Unread = {};
+
+	Unread.init = function() {
+		var loadingMoreTopics = false;
+
+		app.enter_room('recent_posts');
+
+		ajaxify.register_events([
+			'event:new_topic',
+			'event:new_post'
+		]);
+
+		var newTopicCount = 0,
+			newPostCount = 0;
+
+		$('#new-topics-alert').on('click', function() {
+			$(this).hide();
 		});
-	});
-
-	function onTopicsLoaded(topics) {
-
-		var html = templates.prepare(templates['unread'].blocks['topics']).parse({
-			topics: topics
-		}),
-			container = $('#topics-container');
-
-		$('#category-no-topics').remove();
-
-		container.append(html);
-	}
-
-	function loadMoreTopics() {
-		loadingMoreTopics = true;
-		socket.emit('api:topics.loadMoreUnreadTopics', {
-			after: parseInt($('#topics-container').attr('data-next-start'), 10)
-		}, function(data) {
-			if (data.topics && data.topics.length) {
-				onTopicsLoaded(data.topics);
-				$('#topics-container').attr('data-next-start', data.nextStart);
-			} else {
-				$('#load-more-btn').hide();
-			}
 
-			loadingMoreTopics = false;
+		socket.on('event:new_topic', function(data) {
+
+			++newTopicCount;
+			updateAlertText();
+
 		});
-	}
 
-	$(window).off('scroll').on('scroll', function() {
-		var bottom = ($(document).height() - $(window).height()) * 0.9;
+		function updateAlertText() {
+			var text = '';
 
-		if ($(window).scrollTop() > bottom && !loadingMoreTopics) {
-			loadMoreTopics();
+			if (newTopicCount > 1)
+				text = 'There are ' + newTopicCount + ' new topics';
+			else if (newTopicCount === 1)
+				text = 'There is 1 new topic';
+			else
+				text = 'There are no new topics';
+
+			if (newPostCount > 1)
+				text += ' and ' + newPostCount + ' new posts.';
+			else if (newPostCount === 1)
+				text += ' and 1 new post.';
+			else
+				text += ' and no new posts.';
+
+			text += ' Click here to reload.';
+
+			$('#new-topics-alert').html(text).fadeIn('slow');
+		}
+
+		socket.on('event:new_post', function(data) {
+			++newPostCount;
+			updateAlertText();
+		});
+
+		$('#mark-allread-btn').on('click', function() {
+			var btn = $(this);
+			socket.emit('api:topics.markAllRead', {}, function(success) {
+				if (success) {
+					btn.remove();
+					$('#topics-container').empty();
+					$('#category-no-topics').removeClass('hidden');
+					app.alertSuccess('All topics marked as read!');
+					$('#numUnreadBadge')
+						.removeClass('badge-important')
+						.addClass('badge-inverse')
+						.html('0');
+				} else {
+					app.alertError('There was an error marking topics read!');
+				}
+			});
+		});
+
+		function onTopicsLoaded(topics) {
+
+			var html = templates.prepare(templates['unread'].blocks['topics']).parse({
+				topics: topics
+			}),
+				container = $('#topics-container');
+
+			$('#category-no-topics').remove();
+
+			container.append(html);
 		}
-	});
 
+		function loadMoreTopics() {
+			loadingMoreTopics = true;
+			socket.emit('api:topics.loadMoreUnreadTopics', {
+				after: parseInt($('#topics-container').attr('data-next-start'), 10)
+			}, function(data) {
+				if (data.topics && data.topics.length) {
+					onTopicsLoaded(data.topics);
+					$('#topics-container').attr('data-next-start', data.nextStart);
+				} else {
+					$('#load-more-btn').hide();
+				}
+
+				loadingMoreTopics = false;
+			});
+		}
 
-	if ($("body").height() <= $(window).height() && $('#topics-container').children().length >= 20)
-		$('#load-more-btn').show();
+		$(window).off('scroll').on('scroll', function() {
+			var bottom = ($(document).height() - $(window).height()) * 0.9;
 
-	$('#load-more-btn').on('click', function() {
-		loadMoreTopics();
-	});
+			if ($(window).scrollTop() > bottom && !loadingMoreTopics) {
+				loadMoreTopics();
+			}
+		});
+
+
+		if ($("body").height() <= $(window).height() && $('#topics-container').children().length >= 20)
+			$('#load-more-btn').show();
+
+		$('#load-more-btn').on('click', function() {
+			loadMoreTopics();
+		});
+	};
 
-})();
\ No newline at end of file
+	return Unread;
+});
\ No newline at end of file
diff --git a/public/src/forum/users.js b/public/src/forum/users.js
index a5d29adaa7..c845a7c4cb 100644
--- a/public/src/forum/users.js
+++ b/public/src/forum/users.js
@@ -1,6 +1,7 @@
-(function() {
+define(function() {
+	var	Users = {};
 
-	$(document).ready(function() {
+	Users.init = function() {
 		var timeoutId = 0;
 		var loadingMoreUsers = false;
 
@@ -131,6 +132,7 @@
 				loadMoreUsers();
 			}
 		});
-	});
+	};
 
-}());
\ No newline at end of file
+	return Users;
+});
\ No newline at end of file
diff --git a/public/templates/account.tpl b/public/templates/account.tpl
index 929771940b..5c7678f10c 100644
--- a/public/templates/account.tpl
+++ b/public/templates/account.tpl
@@ -97,7 +97,4 @@
 
 <input type="hidden" template-variable="yourid" value="{yourid}" />
 <input type="hidden" template-variable="theirid" value="{theirid}" />
-<input type="hidden" template-type="boolean" template-variable="isFollowing" value="{isFollowing}" />
-
-<script type="text/javascript" src="{relative_path}/src/forum/account.js"></script>
-<script type="text/javascript" src="{relative_path}/src/forum/accountheader.js"></script>
\ No newline at end of file
+<input type="hidden" template-type="boolean" template-variable="isFollowing" value="{isFollowing}" />
\ No newline at end of file
diff --git a/public/templates/accountedit.tpl b/public/templates/accountedit.tpl
index 36c0238e48..1e53ab8920 100644
--- a/public/templates/accountedit.tpl
+++ b/public/templates/accountedit.tpl
@@ -174,9 +174,3 @@
 
 	</div>
 </div>
-
-<input type="hidden" template-variable="gravatarpicture" value="{gravatarpicture}" />
-<input type="hidden" template-variable="uploadedpicture" value="{uploadedpicture}" />
-
-<script type="text/javascript" src="{relative_path}/src/forum/accountheader.js"></script>
-<script type="text/javascript" src="{relative_path}/src/forum/accountedit.js"></script>
diff --git a/public/templates/accountsettings.tpl b/public/templates/accountsettings.tpl
index cb200cb79e..f5ae423ac6 100644
--- a/public/templates/accountsettings.tpl
+++ b/public/templates/accountsettings.tpl
@@ -26,6 +26,3 @@
 		<a id="submitBtn" href="#" class="btn btn-primary">Save changes</a>
 	</div>
 </div>
-
-<script type="text/javascript" src="{relative_path}/src/forum/accountheader.js"></script>
-<script type="text/javascript" src="{relative_path}/src/forum/accountsettings.js"></script>
\ No newline at end of file
diff --git a/public/templates/admin/categories.tpl b/public/templates/admin/categories.tpl
index ea6f02589e..b636606750 100644
--- a/public/templates/admin/categories.tpl
+++ b/public/templates/admin/categories.tpl
@@ -101,5 +101,3 @@
   <div class="col-md-3"><i class="icon-adn"></i></div><div class="col-md-3"><i class="icon-android"></i></div><div class="col-md-3"><i class="icon-apple"></i></div><div class="col-md-3"><i class="icon-bitbucket"></i></div><div class="col-md-3"><i class="icon-bitbucket-sign"></i></div><div class="col-md-3"><i class="icon-bitcoin"></i></div><div class="col-md-3"><i class="icon-btc"></i></div><div class="col-md-3"><i class="icon-css3"></i></div><div class="col-md-3"><i class="icon-dribbble"></i></div><div class="col-md-3"><i class="icon-dropbox"></i></div><div class="col-md-3"><i class="icon-facebook"></i></div><div class="col-md-3"><i class="icon-facebook-sign"></i></div><div class="col-md-3"><i class="icon-flickr"></i></div><div class="col-md-3"><i class="icon-foursquare"></i></div><div class="col-md-3"><i class="icon-github"></i></div><div class="col-md-3"><i class="icon-github-alt"></i></div><div class="col-md-3"><i class="icon-github-sign"></i></div><div class="col-md-3"><i class="icon-gittip"></i></div><div class="col-md-3"><i class="icon-google-plus"></i></div><div class="col-md-3"><i class="icon-google-plus-sign"></i></div><div class="col-md-3"><i class="icon-html5"></i></div><div class="col-md-3"><i class="icon-instagram"></i></div><div class="col-md-3"><i class="icon-linkedin"></i></div><div class="col-md-3"><i class="icon-linkedin-sign"></i></div><div class="col-md-3"><i class="icon-linux"></i></div><div class="col-md-3"><i class="icon-maxcdn"></i></div><div class="col-md-3"><i class="icon-pinterest"></i></div><div class="col-md-3"><i class="icon-pinterest-sign"></i></div><div class="col-md-3"><i class="icon-renren"></i></div><div class="col-md-3"><i class="icon-skype"></i></div><div class="col-md-3"><i class="icon-stackexchange"></i></div><div class="col-md-3"><i class="icon-trello"></i></div><div class="col-md-3"><i class="icon-tumblr"></i></div><div class="col-md-3"><i class="icon-tumblr-sign"></i></div><div class="col-md-3"><i class="icon-twitter"></i></div><div class="col-md-3"><i class="icon-twitter-sign"></i></div><div class="col-md-3"><i class="icon-vk"></i></div><div class="col-md-3"><i class="icon-weibo"></i></div><div class="col-md-3"><i class="icon-windows"></i></div><div class="col-md-3"><i class="icon-xing"></i></div><div class="col-md-3"><i class="icon-xing-sign"></i></div><div class="col-md-3"><i class="icon-youtube"></i></div><div class="col-md-3"><i class="icon-youtube-play"></i></div><div class="col-md-3"><i class="icon-youtube-sign"></i></div>
   <div class="col-md-3"><i class="icon-ambulance"></i></div><div class="col-md-3"><i class="icon-h-sign"></i></div><div class="col-md-3"><i class="icon-hospital"></i></div><div class="col-md-3"><i class="icon-medkit"></i></div><div class="col-md-3"><i class="icon-plus-sign-alt"></i></div><div class="col-md-3"><i class="icon-stethoscope"></i></div><div class="col-md-3"><i class="icon-user-md"></i></div>
 </div></div></div>
-
-<script type="text/javascript" src="{relative_path}/src/forum/admin/categories.js"></script>
\ No newline at end of file
diff --git a/public/templates/admin/facebook.tpl b/public/templates/admin/facebook.tpl
index 92118c573e..14c6202cda 100644
--- a/public/templates/admin/facebook.tpl
+++ b/public/templates/admin/facebook.tpl
@@ -17,10 +17,7 @@
 <button class="btn btn-lg btn-primary" id="save">Save</button>
 
 <script>
-	var	loadDelay = setInterval(function() {
-		if (nodebb_admin) {
-			nodebb_admin.prepare();
-			clearInterval(loadDelay);
-		}
-	}, 500);
+	require(['forum/admin/settings'], function(Settings) {
+		Settings.prepare();
+	});
 </script>
\ No newline at end of file
diff --git a/public/templates/admin/footer.tpl b/public/templates/admin/footer.tpl
index f5392a6499..fb69cce64e 100644
--- a/public/templates/admin/footer.tpl
+++ b/public/templates/admin/footer.tpl
@@ -9,7 +9,7 @@
 	</div>
 
 <script type="text/javascript">
-	$.getScript(RELATIVE_PATH + '/src/forum/admin/footer.js');
+	require(['forum/admin/footer']);
 </script>
 
 </body>
diff --git a/public/templates/admin/gplus.tpl b/public/templates/admin/gplus.tpl
index c1a691b99f..81cf11852c 100644
--- a/public/templates/admin/gplus.tpl
+++ b/public/templates/admin/gplus.tpl
@@ -17,10 +17,7 @@
 <button class="btn btn-lg btn-primary" id="save">Save</button>
 
 <script>
-	var	loadDelay = setInterval(function() {
-		if (nodebb_admin) {
-			nodebb_admin.prepare();
-			clearInterval(loadDelay);
-		}
-	}, 500);
+	require(['forum/admin/settings'], function(Settings) {
+		Settings.prepare();
+	});
 </script>
\ No newline at end of file
diff --git a/public/templates/admin/groups.tpl b/public/templates/admin/groups.tpl
index 2637bfedc2..5b53c2c65f 100644
--- a/public/templates/admin/groups.tpl
+++ b/public/templates/admin/groups.tpl
@@ -96,5 +96,3 @@
 		</div>
 	</div>
 </div>
-
-<script type="text/javascript" src="{relative_path}/src/forum/admin/groups.js"></script>
\ No newline at end of file
diff --git a/public/templates/admin/header.tpl b/public/templates/admin/header.tpl
index 72469a018a..aefd3dc6b6 100644
--- a/public/templates/admin/header.tpl
+++ b/public/templates/admin/header.tpl
@@ -25,7 +25,10 @@
 	<script>
 		require.config({
 			baseUrl: "{relative_path}/src/modules",
-			waitSeconds: 3
+			waitSeconds: 3,
+			paths: {
+				"forum": '../forum'
+			}
 		});
 	</script>
 	<link rel="stylesheet" type="text/css" href="http://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css">
diff --git a/public/templates/admin/index.tpl b/public/templates/admin/index.tpl
index a3b5987831..bd921f59a6 100644
--- a/public/templates/admin/index.tpl
+++ b/public/templates/admin/index.tpl
@@ -18,5 +18,3 @@
 
 	</p>
 </div>
-
-<script type="text/javascript" src="{relative_path}/src/forum/admin/index.js"></script>
\ No newline at end of file
diff --git a/public/templates/admin/motd.tpl b/public/templates/admin/motd.tpl
index 17f6bce16e..6c1725099f 100644
--- a/public/templates/admin/motd.tpl
+++ b/public/templates/admin/motd.tpl
@@ -24,10 +24,7 @@
 <button class="btn btn-lg btn-primary" id="save" checked>Save</button>
 
 <script>
-	var	loadDelay = setInterval(function() {
-		if (nodebb_admin) {
-			nodebb_admin.prepare();
-			clearInterval(loadDelay);
-		}
-	}, 500);
+	require(['forum/admin/settings'], function(Settings) {
+		Settings.prepare();
+	});
 </script>
\ No newline at end of file
diff --git a/public/templates/admin/plugins.tpl b/public/templates/admin/plugins.tpl
index 8c4943c32c..074d53f0d9 100644
--- a/public/templates/admin/plugins.tpl
+++ b/public/templates/admin/plugins.tpl
@@ -21,5 +21,3 @@
 		Full documentation regarding plugin authoring can be found in the <a target="_blank" href="https://github.com/designcreateplay/NodeBB/wiki/Writing-Plugins-for-NodeBB">NodeBB Wiki</a>.
 	</p>
 </div>
-
-<script type="text/javascript" src="{relative_path}/src/forum/admin/plugins.js"></script>
\ No newline at end of file
diff --git a/public/templates/admin/settings.tpl b/public/templates/admin/settings.tpl
index b01713c9a6..7aa4fefb30 100644
--- a/public/templates/admin/settings.tpl
+++ b/public/templates/admin/settings.tpl
@@ -83,10 +83,7 @@
 <button class="btn btn-lg btn-primary" id="save">Save</button>
 
 <script>
-	var	loadDelay = setInterval(function() {
-		if (nodebb_admin) {
-			nodebb_admin.prepare();
-			clearInterval(loadDelay);
-		}
-	}, 500);
+	require(['forum/admin/settings'], function(Settings) {
+		Settings.prepare();
+	});
 </script>
\ No newline at end of file
diff --git a/public/templates/admin/themes.tpl b/public/templates/admin/themes.tpl
index fb4aaefe6a..0aa9b18116 100644
--- a/public/templates/admin/themes.tpl
+++ b/public/templates/admin/themes.tpl
@@ -23,4 +23,10 @@
 	<button class="btn btn-warning" id="revert_theme">Revert</button> This will remove any custom theme applied to your NodeBB, and restore the base theme.
 </p>
 
-<script type="text/javascript" src="{relative_path}/src/forum/admin/themes.js"></script>
\ No newline at end of file
+<script>
+	var bootswatchListener = function(data) {
+		require(['forum/admin/themes'], function(t) {
+			t.render(data);
+		});
+	}
+</script>
\ No newline at end of file
diff --git a/public/templates/admin/topics.tpl b/public/templates/admin/topics.tpl
index ee6a058f4e..1521f400ac 100644
--- a/public/templates/admin/topics.tpl
+++ b/public/templates/admin/topics.tpl
@@ -22,5 +22,3 @@
 <div class="text-center">
 	<button id="topics_loadmore" class="btn btn-primary btn-lg">Load More Topics</button>
 </div>
-
-<script type="text/javascript" src="{relative_path}/src/forum/admin/topics.js"></script>
\ No newline at end of file
diff --git a/public/templates/admin/twitter.tpl b/public/templates/admin/twitter.tpl
index 77876e066b..fb826a37bc 100644
--- a/public/templates/admin/twitter.tpl
+++ b/public/templates/admin/twitter.tpl
@@ -17,10 +17,7 @@
 <button class="btn btn-lg btn-primary" id="save">Save</button>
 
 <script>
-	var	loadDelay = setInterval(function() {
-		if (nodebb_admin) {
-			nodebb_admin.prepare();
-			clearInterval(loadDelay);
-		}
-	}, 500);
+	require(['forum/admin/settings'], function(Settings) {
+		Settings.prepare();
+	});
 </script>
diff --git a/public/templates/admin/users.tpl b/public/templates/admin/users.tpl
index 929a95c97c..0a3bde904e 100644
--- a/public/templates/admin/users.tpl
+++ b/public/templates/admin/users.tpl
@@ -42,6 +42,3 @@
 	<button id="load-more-users-btn" class="btn btn-primary">Load More</button>
 </div>
 <input type="hidden" template-variable="yourid" value="{yourid}" />
-
-
-<script type="text/javascript" src="{relative_path}/src/forum/admin/users.js"></script>
diff --git a/public/templates/category.tpl b/public/templates/category.tpl
index 35293467c4..b1894b4b9a 100644
--- a/public/templates/category.tpl
+++ b/public/templates/category.tpl
@@ -96,6 +96,4 @@
 <input type="hidden" template-variable="category_id" value="{category_id}" />
 <input type="hidden" template-variable="twitter-intent-url" value="{twitter-intent-url}" />
 <input type="hidden" template-variable="facebook-share-url" value="{facebook-share-url}" />
-<input type="hidden" template-variable="google-share-url" value="{google-share-url}" />
-
-<script type="text/javascript" src="{relative_path}/src/forum/category.js"></script>
\ No newline at end of file
+<input type="hidden" template-variable="google-share-url" value="{google-share-url}" />
\ No newline at end of file
diff --git a/public/templates/favourites.tpl b/public/templates/favourites.tpl
index 1d124e4710..ddeccd1984 100644
--- a/public/templates/favourites.tpl
+++ b/public/templates/favourites.tpl
@@ -22,6 +22,3 @@
 		</div>
 	</div>
 </div>
-
-<script type="text/javascript" src="{relative_path}/src/forum/accountheader.js"></script>
-<script type="text/javascript" src="{relative_path}/src/forum/favourites.js"></script>
diff --git a/public/templates/followers.tpl b/public/templates/followers.tpl
index 5093d40713..9e118eb1cd 100644
--- a/public/templates/followers.tpl
+++ b/public/templates/followers.tpl
@@ -34,6 +34,3 @@
 <input type="hidden" template-variable="yourid" value="{yourid}" />
 <input type="hidden" template-variable="theirid" value="{theirid}" />
 <input type="hidden" template-variable="followersCount" value="{followersCount}" />
-
-<script type="text/javascript" src="{relative_path}/src/forum/followers.js"></script>
-<script type="text/javascript" src="{relative_path}/src/forum/accountheader.js"></script>
\ No newline at end of file
diff --git a/public/templates/following.tpl b/public/templates/following.tpl
index 8215d9be31..9dda27d9d4 100644
--- a/public/templates/following.tpl
+++ b/public/templates/following.tpl
@@ -35,6 +35,3 @@
 <input type="hidden" template-variable="yourid" value="{yourid}" />
 <input type="hidden" template-variable="theirid" value="{theirid}" />
 <input type="hidden" template-variable="followingCount" value="{followingCount}" />
-
-<script type="text/javascript" src="{relative_path}/src/forum/following.js"></script>
-<script type="text/javascript" src="{relative_path}/src/forum/accountheader.js"></script>
diff --git a/public/templates/footer.tpl b/public/templates/footer.tpl
index 38b4614a79..27d8a74458 100644
--- a/public/templates/footer.tpl
+++ b/public/templates/footer.tpl
@@ -53,7 +53,7 @@
 	</footer>
 
 	<script>
-		$.getScript(RELATIVE_PATH + '/src/forum/footer.js');
+		require(['forum/footer']);
 	</script>
 
 </body>
diff --git a/public/templates/header.tpl b/public/templates/header.tpl
index c151a5bd58..3d55265365 100644
--- a/public/templates/header.tpl
+++ b/public/templates/header.tpl
@@ -15,8 +15,14 @@
 	<script>
 		require.config({
 			baseUrl: "{relative_path}/src/modules",
-			waitSeconds: 3
+			waitSeconds: 3,
+			paths: {
+				"forum": '../forum'
+			}
 		});
+		requirejs.onError = function(err) {
+			console.log(err);
+		}
 	</script>
 
 	<link rel="stylesheet" type="text/css" href="{relative_path}/css/nodebb.css" />
diff --git a/public/templates/login.tpl b/public/templates/login.tpl
index 11d0b86566..10d9d08948 100644
--- a/public/templates/login.tpl
+++ b/public/templates/login.tpl
@@ -60,5 +60,3 @@
 		</div>
 	</div>
 </div>
-
-<script type="text/javascript" src="{relative_path}/src/forum/login.js"></script>
\ No newline at end of file
diff --git a/public/templates/recent.tpl b/public/templates/recent.tpl
index 70c25a978f..8cf6127874 100644
--- a/public/templates/recent.tpl
+++ b/public/templates/recent.tpl
@@ -49,5 +49,3 @@
 		</ul>
 	</div>
 </div>
-
-<script type="text/javascript" src="{relative_path}/src/forum/recent.js"></script>
diff --git a/public/templates/register.tpl b/public/templates/register.tpl
index 7184f0d3a4..06a9f980e8 100644
--- a/public/templates/register.tpl
+++ b/public/templates/register.tpl
@@ -80,5 +80,3 @@
 		</div>
 	</div>
 </div>
-
-<script type="text/javascript" src="{relative_path}/src/forum/register.js"></script>
\ No newline at end of file
diff --git a/public/templates/reset.tpl b/public/templates/reset.tpl
index 5dadf7ce32..3ac48f2752 100644
--- a/public/templates/reset.tpl
+++ b/public/templates/reset.tpl
@@ -26,5 +26,3 @@
 		<button class="btn btn-primary btn-block btn-lg" id="reset" type="submit">Reset Password</button>
 	</form>
 </div>
-
-<script type="text/javascript" src="{relative_path}/src/forum/reset.js"></script>
\ No newline at end of file
diff --git a/public/templates/reset_code.tpl b/public/templates/reset_code.tpl
index dc8a4aab07..0f145d4f48 100644
--- a/public/templates/reset_code.tpl
+++ b/public/templates/reset_code.tpl
@@ -34,6 +34,3 @@
 	</form>
 </div>
 <input type="hidden" template-variable="reset_code" value="{reset_code}" />
-
-
-<script type="text/javascript" src="{relative_path}/src/forum/reset_code.js"></script>
\ No newline at end of file
diff --git a/public/templates/search.tpl b/public/templates/search.tpl
index 30e8995925..87c57c3ca1 100644
--- a/public/templates/search.tpl
+++ b/public/templates/search.tpl
@@ -53,5 +53,3 @@
 		</ul>
 	</div>
 </div>
-
-<script type="text/javascript" src="{relative_path}/src/forum/search.js"></script>
diff --git a/public/templates/topic.tpl b/public/templates/topic.tpl
index a568e61cc0..42473a4274 100644
--- a/public/templates/topic.tpl
+++ b/public/templates/topic.tpl
@@ -199,7 +199,3 @@
 <input type="hidden" template-variable="pinned" value="{pinned}" />
 <input type="hidden" template-variable="topic_name" value="{topic_name}" />
 <input type="hidden" template-variable="postcount" value="{postcount}" />
-
-
-
-<script type="text/javascript" src="{relative_path}/src/forum/topic.js"></script>
\ No newline at end of file
diff --git a/public/templates/unread.tpl b/public/templates/unread.tpl
index d960ded415..6d1ec7dcca 100644
--- a/public/templates/unread.tpl
+++ b/public/templates/unread.tpl
@@ -54,5 +54,3 @@
 		</div>
 	</div>
 </div>
-
-<script type="text/javascript" src="{relative_path}/src/forum/unread.js"></script>
\ No newline at end of file
diff --git a/public/templates/users.tpl b/public/templates/users.tpl
index fdd7abcafc..d68b7cd397 100644
--- a/public/templates/users.tpl
+++ b/public/templates/users.tpl
@@ -45,5 +45,3 @@
 		<button id="load-more-users-btn" class="btn btn-primary">[[users:load_more]]</button>
 	</div>
 </div>
-
-<script type="text/javascript" src="{relative_path}/src/forum/users.js"></script>
\ No newline at end of file
diff --git a/src/webserver.js b/src/webserver.js
index 585d6566bc..a09ec60258 100644
--- a/src/webserver.js
+++ b/src/webserver.js
@@ -135,22 +135,22 @@ var express = require('express'),
 				app.use(function (req, res, next) {
 					res.status(404);
 
-					// respond with html page
-					if (req.accepts('html')) {
+					if (path.dirname(req.url) === '/src/forum') {
+						// Handle missing client-side scripts
+						res.type('text/javascript').send(200, '');
+					} else if (req.accepts('html')) {
+						// respond with html page
+						winston.warn('Route requested but not found: ' + req.url);
 						res.redirect(nconf.get('relative_path') + '/404');
-						return;
-					}
-
-					// respond with json
-					if (req.accepts('json')) {
-						res.send({
+					} else if (req.accepts('json')) {
+						// respond with json
+						res.json({
 							error: 'Not found'
 						});
-						return;
+					} else {
+						// default to plain-text. send()
+						res.type('txt').send('Not found');
 					}
-
-					// default to plain-text. send()
-					res.type('txt').send('Not found');
 				});
 
 				app.use(function (err, req, res, next) {

From 64117ab6135f5ec7e7a22d5f2f3b6885affc8c8a Mon Sep 17 00:00:00 2001
From: Julian Lam <julian@designcreateplay.com>
Date: Thu, 3 Oct 2013 15:07:30 -0400
Subject: [PATCH 18/29] removing deprecated bootstrap css file from admin
 header

---
 public/templates/admin/header.tpl | 1 -
 1 file changed, 1 deletion(-)

diff --git a/public/templates/admin/header.tpl b/public/templates/admin/header.tpl
index aefd3dc6b6..dc3004c3a2 100644
--- a/public/templates/admin/header.tpl
+++ b/public/templates/admin/header.tpl
@@ -7,7 +7,6 @@
 		var RELATIVE_PATH = "{relative_path}";
 	</script>
 	<link id="base-theme" href="{relative_path}/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen">
-	<link href="{relative_path}/vendor/bootstrap/css/bootstrap-responsive.min.css" rel="stylesheet" media="screen">
 	<link rel="stylesheet" href="{relative_path}/vendor/fontawesome/css/font-awesome.min.css">
 	<script type="text/javascript" src="http://code.jquery.com/jquery.js"></script>
 	<script type="text/javascript" src="{relative_path}/vendor/bootstrap/js/bootstrap.min.js"></script>

From 0e18ec022c4a8f31112e9709dbf82c33b306f13d Mon Sep 17 00:00:00 2001
From: Julian Lam <julian@designcreateplay.com>
Date: Thu, 3 Oct 2013 15:51:48 -0400
Subject: [PATCH 19/29] not running init() if there is no init method in each
 template script

---
 public/src/ajaxify.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/public/src/ajaxify.js b/public/src/ajaxify.js
index 12ede65135..7b8cf215fb 100644
--- a/public/src/ajaxify.js
+++ b/public/src/ajaxify.js
@@ -77,7 +77,7 @@ var ajaxify = {};
 			templates.load_template(function () {
 				exec_body_scripts(content);
 				require(['forum/' + tpl_url], function(script) {
-					if (script) script.init();
+					if (script && script.init) script.init();
 				});
 
 				if (callback) {

From 46f03de9f65af561a76986aaef30a357c551685d Mon Sep 17 00:00:00 2001
From: Peter Peterson <peter@artfire.com>
Date: Thu, 3 Oct 2013 13:00:30 -0700
Subject: [PATCH 20/29] Added ability to set redis db to use, defaults to db0

---
 src/install.js | 10 ++++++++--
 src/redis.js   | 11 +++++++++++
 2 files changed, 19 insertions(+), 2 deletions(-)

diff --git a/src/install.js b/src/install.js
index d84682ebc7..eb292e319c 100644
--- a/src/install.js
+++ b/src/install.js
@@ -42,6 +42,10 @@ var async = require('async'),
 		}, {
 			name: 'redis:password',
 			description: 'Password of your Redis database'
+		}, {
+			name: "redis:database",
+			description: "Which database to use (0..n)",
+			'default': 0
 		}, {
 			name: 'bind_address',
 			description: 'IP or Hostname to bind to',
@@ -64,12 +68,14 @@ var async = require('async'),
 						config.redis = {
 							host: config['redis:host'],
 							port: config['redis:port'],
-							password: config['redis:password']
+							password: config['redis:password'],
+							database: config['redis:database']
 						};
 						delete config['redis:host'];
 						delete config['redis:port'];
 						delete config['redis:password'];
-
+						delete config['redis:database'];
+						
 						// Add hardcoded values
 						config.bcrypt_rounds = 12;
 						config.upload_path = '/public/uploads';
diff --git a/src/redis.js b/src/redis.js
index c66c720788..b5852f92b3 100644
--- a/src/redis.js
+++ b/src/redis.js
@@ -17,6 +17,17 @@
 		RedisDB.exports.auth(nconf.get('redis:password'));
 	}
 
+	if( (db = nconf.get('redis:database')) ){
+		RedisDB.exports.select(db, function(error){
+			if(error !== null){
+				winston.err(error);
+				if (global.env !== 'production') {
+					throw new Error(error);
+				}
+			}
+		}); 	
+	}
+
 	RedisDB.exports.handle = function(error) {
 		if (error !== null) {
 			winston.err(error);

From 585e07bc793539ea107ee3180ca20d35107b455d Mon Sep 17 00:00:00 2001
From: Julian Lam <julian.lam@gmail.com>
Date: Thu, 3 Oct 2013 20:35:16 -0400
Subject: [PATCH 21/29] closed #368 - notifications now no longer need scores

---
 src/notifications.js | 33 +++++++++++++--------------------
 src/threadTools.js   |  2 +-
 src/user.js          | 22 +++-------------------
 src/websockets.js    |  2 +-
 4 files changed, 18 insertions(+), 41 deletions(-)

diff --git a/src/notifications.js b/src/notifications.js
index c2fb26cd26..d2e1477aac 100644
--- a/src/notifications.js
+++ b/src/notifications.js
@@ -15,29 +15,22 @@ var RDB = require('./redis.js'),
 				});
 			});
 		},
-		create: function(text, score, path, uniqueId, callback) {
-			/*
-			 * Score guide:
-			 * 0	Low priority messages (probably unused)
-			 * 5	Normal messages
-			 * 10	High priority messages
-			 *
+		create: function(text, path, uniqueId, callback) {
+			/**
 			 * uniqueId is used solely to override stale nids.
 			 * 		If a new nid is pushed to a user and an existing nid in the user's
 			 *		(un)read list contains the same uniqueId, it will be removed, and
 			 *		the new one put in its place.
 			 */
 			RDB.incr('notifications:next_nid', function(err, nid) {
-				RDB.hmset(
-					'notifications:' + nid,
-					'text', text || '',
-					'score', score || 5,
-					'path', path || null,
-					'datetime', Date.now(),
-					'uniqueId', uniqueId || utils.generateUUID(),
-					function(err, status) {
-						if (status === 'OK') callback(nid);
-					});
+				RDB.hmset('notifications:' + nid, {
+					text: text || '',
+					path: path || null,
+					datetime: Date.now(),
+					uniqueId: uniqueId || utils.generateUUID()
+				}, function(err, status) {
+					if (!err) callback(nid);
+				});
 			});
 		},
 		push: function(nid, uids, callback) {
@@ -51,8 +44,8 @@ var RDB = require('./redis.js'),
 					if (parseInt(uids[x]) > 0) {
 						(function(uid) {
 							notifications.remove_by_uniqueId(notif_data.uniqueId, uid, function() {
-								RDB.zadd('uid:' + uid + ':notifications:unread', notif_data.score, nid);
-								global.io.sockets. in ('uid_' + uid).emit('event:new_notification');
+								RDB.zadd('uid:' + uid + ':notifications:unread', notif_data.datetime, nid);
+								global.io.sockets.in('uid_' + uid).emit('event:new_notification');
 								if (callback) callback(true);
 							});
 						})(uids[x]);
@@ -98,7 +91,7 @@ var RDB = require('./redis.js'),
 			if (parseInt(uid) > 0) {
 				notifications.get(nid, function(notif_data) {
 					RDB.zrem('uid:' + uid + ':notifications:unread', nid);
-					RDB.zadd('uid:' + uid + ':notifications:read', notif_data.score, nid);
+					RDB.zadd('uid:' + uid + ':notifications:read', notif_data.datetime, nid);
 					if (callback) callback();
 				});
 			}
diff --git a/src/threadTools.js b/src/threadTools.js
index 23b641f0d2..bd0c1a126c 100644
--- a/src/threadTools.js
+++ b/src/threadTools.js
@@ -276,7 +276,7 @@ var RDB = require('./redis.js'),
 				topics.getTopicField(tid, 'title', function(err, title) {
 					topics.getTeaser(tid, function(err, teaser) {
 						if (!err) {
-							notifications.create('<strong>' + teaser.username + '</strong> has posted a reply to: "<strong>' + title + '</strong>"', null, nconf.get('relative_path') + '/topic/' + tid, 'topic:' + tid, function(nid) {
+							notifications.create('<strong>' + teaser.username + '</strong> has posted a reply to: "<strong>' + title + '</strong>"', nconf.get('relative_path') + '/topic/' + tid, 'topic:' + tid, function(nid) {
 								next(null, nid);
 							});
 						} else next(err);
diff --git a/src/user.js b/src/user.js
index bbe6779303..4c6ccfc811 100644
--- a/src/user.js
+++ b/src/user.js
@@ -578,7 +578,7 @@ var utils = require('./../public/src/utils.js'),
 				topics.getTopicField(tid, 'slug', function(err, slug) {
 					var message = '<strong>' + username + '</strong> made a new post';
 
-					notifications.create(message, 5, nconf.get('relative_path') + '/topic/' + slug + '#' + pid, 'topic:' + tid, function(nid) {
+					notifications.create(message, nconf.get('relative_path') + '/topic/' + slug + '#' + pid, 'topic:' + tid, function(nid) {
 						notifications.push(nid, followers);
 					});
 				});
@@ -888,7 +888,7 @@ var utils = require('./../public/src/utils.js'),
 
 			async.parallel({
 				unread: function(next) {
-					RDB.zrevrangebyscore('uid:' + uid + ':notifications:unread', 10, 0, function(err, nids) {
+					RDB.zrevrange('uid:' + uid + ':notifications:unread', 0, 10, function(err, nids) {
 						// @todo handle err
 						var unread = [];
 
@@ -910,7 +910,7 @@ var utils = require('./../public/src/utils.js'),
 					});
 				},
 				read: function(next) {
-					RDB.zrevrangebyscore('uid:' + uid + ':notifications:read', 10, 0, function(err, nids) {
+					RDB.zrevrange('uid:' + uid + ':notifications:read', 0, 10, function(err, nids) {
 						// @todo handle err
 						var read = [];
 
@@ -932,22 +932,6 @@ var utils = require('./../public/src/utils.js'),
 					});
 				}
 			}, function(err, notifications) {
-				// While maintaining score sorting, sort by time
-				var readCount = notifications.read.length,
-					unreadCount = notifications.unread.length;
-
-				notifications.read.sort(function(a, b) {
-					if (a.score === b.score) {
-						return (a.datetime - b.datetime) > 0 ? -1 : 1;
-					}
-				});
-
-				notifications.unread.sort(function(a, b) {
-					if (a.score === b.score) {
-						return (a.datetime - b.datetime) > 0 ? -1 : 1;
-					}
-				});
-
 				// Limit the number of notifications to `maxNotifs`, prioritising unread notifications
 				if (notifications.read.length + notifications.unread.length > maxNotifs) {
 					notifications.read.length = maxNotifs - notifications.unread.length;
diff --git a/src/websockets.js b/src/websockets.js
index 27cdb0ef8a..90784ad847 100644
--- a/src/websockets.js
+++ b/src/websockets.js
@@ -580,7 +580,7 @@ module.exports.init = function(io) {
 					notifText = 'New message from <strong>' + username + '</strong>';
 
 				if (!isUserOnline(touid)) {
-					notifications.create(notifText, 5, 'javascript:app.openChat(&apos;' + username + '&apos;, ' + uid + ');', 'notification_' + uid + '_' + touid, function(nid) {
+					notifications.create(notifText, 'javascript:app.openChat(&apos;' + username + '&apos;, ' + uid + ');', 'notification_' + uid + '_' + touid, function(nid) {
 						notifications.push(nid, [touid], function(success) {
 
 						});

From 763bd775c436000462d6fdaa598af1f8d7277b4e Mon Sep 17 00:00:00 2001
From: Julian Lam <julian.lam@gmail.com>
Date: Thu, 3 Oct 2013 21:18:59 -0400
Subject: [PATCH 22/29] closed #380

---
 src/threadTools.js | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/threadTools.js b/src/threadTools.js
index bd0c1a126c..6f7b9bbaf7 100644
--- a/src/threadTools.js
+++ b/src/threadTools.js
@@ -265,14 +265,15 @@ var RDB = require('./redis.js'),
 
 	ThreadTools.get_followers = function(tid, callback) {
 		RDB.smembers('tid:' + tid + ':followers', function(err, followers) {
-			callback(err, followers);
+			callback(err, followers.map(function(follower) {
+				return parseInt(follower, 10);
+			}));
 		});
 	}
 
 	ThreadTools.notify_followers = function(tid, exceptUid) {
 		async.parallel([
 			function(next) {
-
 				topics.getTopicField(tid, 'title', function(err, title) {
 					topics.getTeaser(tid, function(err, teaser) {
 						if (!err) {
@@ -282,11 +283,10 @@ var RDB = require('./redis.js'),
 						} else next(err);
 					});
 				});
-
-
 			},
 			function(next) {
 				ThreadTools.get_followers(tid, function(err, followers) {
+					exceptUid = parseInt(exceptUid, 10);
 					if (followers.indexOf(exceptUid) !== -1) followers.splice(followers.indexOf(exceptUid), 1);
 					next(null, followers);
 				});

From 4d6881fa65f440a4c30c6881c4d8ae2671879007 Mon Sep 17 00:00:00 2001
From: Julian Lam <julian.lam@gmail.com>
Date: Thu, 3 Oct 2013 22:36:00 -0400
Subject: [PATCH 23/29] reset update for 0.0.7, and added new schema update for
 notifications

---
 src/upgrade.js | 213 +++++++------------------------------------------
 1 file changed, 31 insertions(+), 182 deletions(-)

diff --git a/src/upgrade.js b/src/upgrade.js
index 36dfb076c4..b060c532c5 100644
--- a/src/upgrade.js
+++ b/src/upgrade.js
@@ -1,193 +1,42 @@
 var RDB = require('./redis.js'),
 	async = require('async'),
 	winston = require('winston'),
-	user = require('./user'),
-	Groups = require('./groups');
-
-
-function upgradeCategory(cid, callback) {
-	RDB.type('categories:' + cid + ':tid', function(err, type) {
-		if (type === 'set') {
-			RDB.smembers('categories:' + cid + ':tid', function(err, tids) {
-
-				function moveTopic(tid, callback) {
-					RDB.hget('topic:' + tid, 'timestamp', function(err, timestamp) {
-						if (err)
-							return callback(err);
-
-						RDB.zadd('temp_categories:' + cid + ':tid', timestamp, tid);
-						callback(null);
+	notifications = require('./notifications')
+	Upgrade = {};
+
+Upgrade.upgrade = function() {
+	winston.info('Beginning Redis database schema update');
+
+	async.series([
+		function(next) {
+			RDB.hget('notifications:1', 'score', function(err, score) {
+				if (score) {
+					winston.info('[2013/10/03] Updating Notifications');
+					RDB.keys('uid:*:notifications:*', function(err, keys) {
+						async.each(keys, function(key, next) {
+							RDB.zrange(key, 0, -1, function(err, nids) {
+								async.each(nids, function(nid, next) {
+									notifications.get(nid, function(notif_data) {
+										RDB.zadd(key, notif_data.datetime, nid, next);
+									});
+								}, next);
+							});
+						}, next);
 					});
+				} else {
+					winston.info('[2013/10/03] Updates to Notifications skipped.');
 				}
-
-				async.each(tids, moveTopic, function(err) {
-					if (!err) {
-						RDB.rename('temp_categories:' + cid + ':tid', 'categories:' + cid + ':tid');
-						callback(null);
-					} else
-						callback(err);
-				});
-
 			});
-		} else {
-			winston.info('category already upgraded ' + cid);
-			callback(null);
 		}
-	});
-}
-
-function upgradeUser(uid, callback) {
-	user.getUserFields(uid, ['joindate', 'postcount', 'reputation'], function(err, userData) {
-		if (err)
-			return callback(err);
-
-		async.parallel([
-			function(next) {
-				if (userData.joindate)
-					RDB.zadd('users:joindate', userData.joindate, uid, next);
-				else
-					next(null);
-			},
-			function(next) {
-				if (userData.postcount)
-					RDB.zadd('users:postcount', userData.postcount, uid, next);
-				else
-					next(null);
-			},
-			function(next) {
-				if (userData.reputation)
-					RDB.zadd('users:reputation', userData.reputation, uid, next);
-				else
-					next(null);
-			}
-		], function(err, result) {
-			callback(err);
-		});
-	});
-}
-
-function upgradeUserHash(uid, callback) {
-	user.getUserFields(uid, ['username', 'userslug', 'email'], function(err, userData) {
-		if (err)
-			return callback(err);
-
-		async.parallel([
-			function(next) {
-				if (userData.username)
-					RDB.hset('username:uid', userData.username, uid, next);
-				else
-					next(null);
-			},
-			function(next) {
-				if (userData.userslug)
-					RDB.hset('userslug:uid', userData.userslug, uid, next);
-				else
-					next(null);
-			},
-			function(next) {
-				if (userData.email)
-					RDB.hset('email:uid', userData.email, uid, next);
-				else
-					next(null);
-			}
-
-		], function(err, result) {
-			callback(err);
-		});
-	});
-}
-
-function upgradeAdmins(callback) {
-	Groups.getGidFromName('Administrators', function(err, gid) {
-		if (!err && !gid) {
-			winston.info('Upgrading Administrators');
-
-			async.parallel([
-				function(next) {
-					RDB.smembers("administrators", next);
-				},
-				function(next) {
-					Groups.create('Administrators', 'Forum Administrators', next);
-				}
-			], function(err, results) {
-				var gid = results[1].gid;
-
-				async.each(results[0], function(uid, next) {
-					Groups.join(gid, uid, next);
-				}, callback);
-			});
+		// Add new schema updates here
+	], function(err) {
+		if (!err) {
+			winston.info('Redis schema update complete!');
+			process.exit();
 		} else {
-			winston.info('Administrators group OK')
-			callback();
+			winston.error('Errors were encountered while updating the NodeBB schema: ' + err.message);
 		}
 	});
-}
-
-exports.upgrade = function() {
-
-	winston.info('upgrading nodebb now');
-
-	var schema = [
-		function upgradeCategories(next) {
-			winston.info('upgrading categories');
-
-			RDB.lrange('categories:cid', 0, -1, function(err, cids) {
-
-				async.each(cids, upgradeCategory, function(err) {
-					if (!err) {
-						winston.info('upgraded categories');
-						next(null, null);
-					} else {
-						next(err, null);
-					}
-				});
-			});
-		},
-
-		function upgradeUsers(next) {
-			winston.info('upgrading users');
-
-			RDB.lrange('userlist', 0, -1, function(err, uids) {
-
-				async.each(uids, upgradeUser, function(err) {
-					if (!err) {
-						winston.info('upgraded users');
-						next(null, null);
-					} else {
-						next(err, null);
-					}
-				});
+};
 
-			});
-		},
-
-		function upgradeUserHashes(next) {
-			winston.info('upgrading user hashes');
-			RDB.zrange('users:joindate', 0, -1, function(err, uids) {
-				if (err)
-					return next(err);
-
-				async.each(uids, upgradeUserHash, function(err) {
-					if (!err) {
-						winston.info('upgraded user hashes');
-						next(null, null);
-					} else {
-						next(err, null);
-					}
-				});
-			});
-		},
-
-		upgradeAdmins
-	];
-
-	async.series(schema, function(err, results) {
-		if (!err)
-			winston.info('upgrade complete');
-		else
-			winston.err(err);
-
-		process.exit();
-
-	});
-}
\ No newline at end of file
+module.exports = Upgrade;
\ No newline at end of file

From 12af2a7ff62833eac010f0788c64fc6e62d01f8c Mon Sep 17 00:00:00 2001
From: Julian Lam <julian.lam@gmail.com>
Date: Thu, 3 Oct 2013 23:10:17 -0400
Subject: [PATCH 24/29] install script calling proper redis db

---
 app.js | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/app.js b/app.js
index a081cd4fa3..750e128288 100644
--- a/app.js
+++ b/app.js
@@ -109,6 +109,9 @@
 
 	} else if (nconf.get('upgrade')) {
 		meta = require('./src/meta.js');
+		nconf.file({
+			file: __dirname + '/config.json'
+		});
 
 		meta.configs.init(function () {
 			require('./src/upgrade').upgrade();

From 0414ec7f837656e3c0ed1698cac2f6c0c316face Mon Sep 17 00:00:00 2001
From: Julian Lam <julian.lam@gmail.com>
Date: Thu, 3 Oct 2013 23:21:29 -0400
Subject: [PATCH 25/29] removing testbed code from repo (why was it even
 checked in?!!)

---
 src/routes/testbed.js | 75 -------------------------------------------
 src/webserver.js      |  2 --
 2 files changed, 77 deletions(-)
 delete mode 100644 src/routes/testbed.js

diff --git a/src/routes/testbed.js b/src/routes/testbed.js
deleted file mode 100644
index df9dc2c9c2..0000000000
--- a/src/routes/testbed.js
+++ /dev/null
@@ -1,75 +0,0 @@
-(function(TestBed) {
-	TestBed.create_routes = function(app) {
-
-		app.get('/bench/forloop', function(req, res) {
-			var benchData = {};
-
-			var myArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
-
-			function f(x) {
-				return x;
-			}
-
-			var runCount = req.query.runs ? req.query.runs : 1000000;
-
-			function withCaching() {
-				var time = process.hrtime();
-
-				for (var n = 0; n < runCount; ++n) {
-					for (var i = 0, len = myArray.length; i < len; ++i) {
-						f(myArray[i]);
-					}
-				}
-
-				var diff = process.hrtime(time);
-				diff = diff[0] + diff[1] / 1e9;
-				return diff;
-			}
-
-			function withoutCaching() {
-				var time = process.hrtime();
-
-				for (var n = 0; n < runCount; ++n) {
-					for (var i = 0; i < myArray.length; ++i) {
-						f(myArray[i]);
-					}
-				}
-
-				var diff = process.hrtime(time);
-				diff = diff[0] + diff[1] / 1e9;
-				return diff;
-			}
-
-			function withForeach() {
-				var time = process.hrtime();
-
-				for (var n = 0; n < runCount; ++n) {
-					myArray.forEach(function(index) {
-
-					});
-				}
-
-				var diff = process.hrtime(time);
-				diff = diff[0] + diff[1] / 1e9;
-				return diff;
-			}
-
-			benchData['runs'] = runCount;
-
-			benchData['withCaching'] = withCaching();
-			benchData['withoutCaching'] = withoutCaching();
-			benchData['withForeach'] = withForeach();
-
-			res.json(benchData);
-
-
-		});
-
-
-
-
-
-	};
-
-
-}(exports));
\ No newline at end of file
diff --git a/src/webserver.js b/src/webserver.js
index a09ec60258..e1d3bfa591 100644
--- a/src/webserver.js
+++ b/src/webserver.js
@@ -17,7 +17,6 @@ var express = require('express'),
 	admin = require('./routes/admin.js'),
 	userRoute = require('./routes/user.js'),
 	apiRoute = require('./routes/api.js'),
-	testBed = require('./routes/testbed.js'),
 	auth = require('./routes/authentication.js'),
 	meta = require('./meta.js'),
 	feed = require('./feed'),
@@ -201,7 +200,6 @@ var express = require('express'),
 		auth.create_routes(app);
 		admin.create_routes(app);
 		userRoute.create_routes(app);
-		testBed.create_routes(app);
 		apiRoute.create_routes(app);
 
 

From 07d07020f0a8c6b87337dc5216781a4e220559b4 Mon Sep 17 00:00:00 2001
From: Julian Lam <julian.lam@gmail.com>
Date: Fri, 4 Oct 2013 10:06:17 -0400
Subject: [PATCH 26/29] requiring latest nodebb plugin versions

---
 package.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index e9244ea8db..e96d196792 100644
--- a/package.json
+++ b/package.json
@@ -34,8 +34,8 @@
     "request": "~2.25.0",
     "reds": "~0.2.4",
     "winston": "~0.7.2",
-    "nodebb-plugin-mentions": "~0.1.0",
-    "nodebb-plugin-markdown": "~0.1.0",
+    "nodebb-plugin-mentions": "~0.1.9",
+    "nodebb-plugin-markdown": "~0.1.2",
     "rss": "~0.2.0",
     "prompt": "~0.2.11",
     "uglify-js": "~2.4.0",

From be8d9be832b4b82e098a319100ba0675c689ad96 Mon Sep 17 00:00:00 2001
From: Julian Lam <julian.lam@gmail.com>
Date: Fri, 4 Oct 2013 10:27:32 -0400
Subject: [PATCH 27/29] flushed out upgrade path for notifications

---
 src/upgrade.js | 50 +++++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 41 insertions(+), 9 deletions(-)

diff --git a/src/upgrade.js b/src/upgrade.js
index b060c532c5..153c7c0399 100644
--- a/src/upgrade.js
+++ b/src/upgrade.js
@@ -11,20 +11,52 @@ Upgrade.upgrade = function() {
 		function(next) {
 			RDB.hget('notifications:1', 'score', function(err, score) {
 				if (score) {
-					winston.info('[2013/10/03] Updating Notifications');
-					RDB.keys('uid:*:notifications:*', function(err, keys) {
-						async.each(keys, function(key, next) {
-							RDB.zrange(key, 0, -1, function(err, nids) {
-								async.each(nids, function(nid, next) {
-									notifications.get(nid, function(notif_data) {
-										RDB.zadd(key, notif_data.datetime, nid, next);
+					async.series([
+						function(next) {
+							RDB.keys('uid:*:notifications:flag', function(err, keys) {
+								if (keys.length > 0) {
+									winston.info('[2013/10/03] Removing deprecated Notification Flags');
+									async.each(keys, function(key, next) {
+										RDB.del(key, next);
+									}, next);
+								} else {
+									winston.info('[2013/10/03] No Notification Flags found. Good.');
+									next();
+								}
+							});
+						},
+						function(next) {
+							winston.info('[2013/10/03] Updating Notifications');
+							RDB.keys('uid:*:notifications:*', function(err, keys) {
+								async.each(keys, function(key, next) {
+									RDB.zrange(key, 0, -1, function(err, nids) {
+										async.each(nids, function(nid, next) {
+											notifications.get(nid, function(notif_data) {
+												RDB.zadd(key, notif_data.datetime, nid, next);
+											});
+										}, next);
 									});
 								}, next);
 							});
-						}, next);
-					});
+						},
+						function(next) {
+							RDB.keys('notifications:*', function(err, keys) {
+								if (keys.length > 0) {
+									winston.info('[2013/10/03] Removing Notification Scores');
+									async.each(keys, function(key, next) {
+										if (key === 'notifications:next_nid') return next();
+										RDB.hdel(key, 'score', next);
+									}, next);
+								} else {
+									winston.info('[2013/10/03] No Notification Scores found. Good.');
+									next();
+								}
+							});
+						}
+					], next);
 				} else {
 					winston.info('[2013/10/03] Updates to Notifications skipped.');
+					next();
 				}
 			});
 		}

From 01e04d60a99eecb1b92130250b34814972608f52 Mon Sep 17 00:00:00 2001
From: Julian Lam <julian.lam@gmail.com>
Date: Fri, 4 Oct 2013 10:34:08 -0400
Subject: [PATCH 28/29] fixing redis db in upgrade path

---
 app.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app.js b/app.js
index 750e128288..492faf9609 100644
--- a/app.js
+++ b/app.js
@@ -108,10 +108,10 @@
 		});
 
 	} else if (nconf.get('upgrade')) {
-		meta = require('./src/meta.js');
 		nconf.file({
 			file: __dirname + '/config.json'
 		});
+		meta = require('./src/meta.js');
 
 		meta.configs.init(function () {
 			require('./src/upgrade').upgrade();

From 57465eb2775c596825313e6767f9f01a3b131691 Mon Sep 17 00:00:00 2001
From: Julian Lam <julian.lam@gmail.com>
Date: Fri, 4 Oct 2013 10:37:32 -0400
Subject: [PATCH 29/29] misrouting now only shows warning when in debug mode

---
 src/webserver.js | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/webserver.js b/src/webserver.js
index e1d3bfa591..0759ae3e37 100644
--- a/src/webserver.js
+++ b/src/webserver.js
@@ -139,10 +139,11 @@ var express = require('express'),
 						res.type('text/javascript').send(200, '');
 					} else if (req.accepts('html')) {
 						// respond with html page
-						winston.warn('Route requested but not found: ' + req.url);
+						if (process.env.NODE_ENV === 'development') winston.warn('Route requested but not found: ' + req.url);
 						res.redirect(nconf.get('relative_path') + '/404');
 					} else if (req.accepts('json')) {
 						// respond with json
+						if (process.env.NODE_ENV === 'development') winston.warn('Route requested but not found: ' + req.url);
 						res.json({
 							error: 'Not found'
 						});