diff --git a/bcrypt.js b/bcrypt.js
index 1445999a8a..ae81d09eb7 100644
--- a/bcrypt.js
+++ b/bcrypt.js
@@ -9,11 +9,10 @@ process.on('message', function(msg) {
 	if (msg.type === 'hash') {
 		hashPassword(msg.password, msg.rounds);
 	} else if (msg.type === 'compare') {
-		compare(msg.password, msg.hash);
+		bcrypt.compare(msg.password, msg.hash, done);
 	}
 });
 
-
 function hashPassword(password, rounds) {
 	async.waterfall([
 		function(next) {
@@ -22,23 +21,14 @@ function hashPassword(password, rounds) {
 		function(salt, next) {
 			bcrypt.hash(password, salt, next);
 		}
-	], function(err, hash) {
-		if (err) {
-			process.send({err: err.message});
-			return process.disconnect();
-		}
-		process.send({result: hash});
-		process.disconnect();
-	});
+	], done);
 }
 
-function compare(password, hash) {
-	bcrypt.compare(password, hash, function(err, res) {
-		if (err) {
-			process.send({err: err.message});
-			return process.disconnect();
-		}
-		process.send({result: res});
-		process.disconnect();
- 	});
+function done(err, result) {
+	if (err) {
+		process.send({err: err.message});
+		return process.disconnect();
+	}
+	process.send({result: result});
+	process.disconnect();
 }
\ No newline at end of file
diff --git a/package.json b/package.json
index 24bc90bef9..7e03c304f3 100644
--- a/package.json
+++ b/package.json
@@ -39,14 +39,14 @@
     "mmmagic": "^0.3.13",
     "morgan": "^1.3.2",
     "nconf": "~0.7.1",
-    "nodebb-plugin-dbsearch": "^0.1.0",
+    "nodebb-plugin-dbsearch": "^0.2.1",
     "nodebb-plugin-emoji-extended": "^0.4.1-4",
     "nodebb-plugin-markdown": "^1.0.0",
-    "nodebb-plugin-mentions": "^0.10.0",
+    "nodebb-plugin-mentions": "^0.11.0",
     "nodebb-plugin-soundpack-default": "~0.1.1",
     "nodebb-plugin-spam-be-gone": "^0.4.0",
-    "nodebb-theme-lavender": "^1.0.6",
-    "nodebb-theme-vanilla": "^1.0.28",
+    "nodebb-theme-lavender": "^1.0.22",
+    "nodebb-theme-vanilla": "^1.0.65",
     "nodebb-widget-essentials": "~0.2.12",
     "nodebb-rewards-essentials": "^0.0.1",
     "npm": "^2.1.4",
@@ -64,7 +64,7 @@
     "socket.io-redis": "^0.1.3",
     "socketio-wildcard": "~0.1.1",
     "string": "^3.0.0",
-    "templates.js": "^0.1.28",
+    "templates.js": "^0.1.30",
     "uglify-js": "git+https://github.com/julianlam/UglifyJS2.git",
     "underscore": "~1.7.0",
     "validator": "^3.30.0",
diff --git a/public/language/en_GB/modules.json b/public/language/en_GB/modules.json
index 00a2ba412d..12eeb0f660 100644
--- a/public/language/en_GB/modules.json
+++ b/public/language/en_GB/modules.json
@@ -18,5 +18,6 @@
 
 	"composer.user_said_in": "%1 said in %2:",
 	"composer.user_said": "%1 said:",
-	"composer.discard": "Are you sure you wish to discard this post?"
+	"composer.discard": "Are you sure you wish to discard this post?",
+	"composer.submit_and_lock": "Submit and Lock"
 }
\ No newline at end of file
diff --git a/public/language/it/email.json b/public/language/it/email.json
index be6567c1c2..e351a25036 100644
--- a/public/language/it/email.json
+++ b/public/language/it/email.json
@@ -11,7 +11,7 @@
     "reset.cta": "Clicca qui per resettare la tua password",
     "reset.notify.subject": "Possword modificata con successo.",
     "reset.notify.text1": "Ti informiamo che in data %1, la password è stata cambiata con successo.",
-    "reset.notify.text2": "If you did not authorise this, please notify an administrator immediately.",
+    "reset.notify.text2": "Se non hai autorizzato questo, per favore notifica immediatamente un amministratore.",
     "digest.notifications": "Hai una notifica non letta da %1:",
     "digest.latest_topics": "Ultimi argomenti su %1",
     "digest.cta": "Clicca qui per visitare %1",
@@ -21,7 +21,7 @@
     "notif.chat.cta": "Clicca qui per continuare la conversazione",
     "notif.chat.unsub.info": "Questa notifica di chat ti è stata inviata perché l'hai scelta nelle impostazioni.",
     "notif.post.cta": "Clicca qui per leggere il topic completo.",
-    "notif.post.unsub.info": "This post notification was sent to you due to your subscription settings.",
+    "notif.post.unsub.info": "Questo post ti è stato notificato in base alle tue impostazioni di sottoscrizione.",
     "test.text1": "Questa è una email di test per verificare che il servizio di invio email è configurato correttamente sul tuo NodeBB.",
     "unsub.cta": "Clicca qui per modificare queste impostazioni",
     "closing": "Grazie!"
diff --git a/public/language/it/error.json b/public/language/it/error.json
index 777c76856c..cbcc20dedc 100644
--- a/public/language/it/error.json
+++ b/public/language/it/error.json
@@ -19,8 +19,8 @@
     "email-taken": "Email già esistente",
     "email-not-confirmed": "La tua Email deve essere ancora confermata, per favore clicca qui per confermare la tua Email.",
     "email-not-confirmed-chat": "Non potrai chattare finchè non avrai confermato la tua email",
-    "no-email-to-confirm": "This forum requires email confirmation, please click here to enter an email",
-    "email-confirm-failed": "We could not confirm your email, please try again later.",
+    "no-email-to-confirm": "Questo forum richiede la conferma dell'indirizzo email, per favore clicca qui per inserirne uno",
+    "email-confirm-failed": "Non possiamo confermare la tua email, per favore prova ancora più tardi.",
     "username-too-short": "Nome utente troppo corto",
     "username-too-long": "Nome utente troppo lungo",
     "user-banned": "Utente bannato",
@@ -77,5 +77,5 @@
     "registration-error": "Errore nella registrazione",
     "parse-error": "Qualcosa è andato storto durante l'analisi della risposta proveniente dal server",
     "wrong-login-type-email": "Please use your email to login",
-    "wrong-login-type-username": "Please use your username to login"
+    "wrong-login-type-username": "Per favore usa il tuo nome utente per accedere"
 }
\ No newline at end of file
diff --git a/public/language/it/notifications.json b/public/language/it/notifications.json
index a52aabb969..c3f067b0cf 100644
--- a/public/language/it/notifications.json
+++ b/public/language/it/notifications.json
@@ -2,7 +2,7 @@
     "title": "Notifiche",
     "no_notifs": "Non hai nuove notifiche",
     "see_all": "Vedi tutte le Notifiche",
-    "mark_all_read": "Mark all notifications read",
+    "mark_all_read": "Segna tutte le notifiche come già lette",
     "back_to_home": "Indietro a %1",
     "outgoing_link": "Link in uscita",
     "outgoing_link_message": "Stai lasciando %1.",
diff --git a/public/language/it/pages.json b/public/language/it/pages.json
index 72d2a3e042..3d3ed9865a 100644
--- a/public/language/it/pages.json
+++ b/public/language/it/pages.json
@@ -11,7 +11,7 @@
     "user.followers": "Persone che seguono %1",
     "user.posts": "Post creati da %1",
     "user.topics": "Discussioni create da %1",
-    "user.groups": "%1's Groups",
+    "user.groups": "%1's Gruppi",
     "user.favourites": "Post Favoriti da %1",
     "user.settings": "Impostazioni Utente",
     "maintenance.text": "%1 è attualmente in manutenzione. Per favore ritorna più tardi.",
diff --git a/public/language/lt/category.json b/public/language/lt/category.json
index 149a102553..3694a767a7 100644
--- a/public/language/lt/category.json
+++ b/public/language/lt/category.json
@@ -1,9 +1,9 @@
 {
     "new_topic_button": "Nauja tema",
-    "guest-login-post": "Log in to post",
+    "guest-login-post": "Prisijungti įrašų paskelbimui",
     "no_topics": "<strong>Šioje kategorijoje temų nėra.</strong><br/>Kodėl gi jums nesukūrus naujos?",
     "browsing": "naršo",
     "no_replies": "Niekas dar neatsakė",
     "share_this_category": "Pasidalinti šią kategoriją",
-    "ignore": "Ignore"
+    "ignore": "Nepaisyti"
 }
\ No newline at end of file
diff --git a/public/language/lt/email.json b/public/language/lt/email.json
index f290435e75..af0968b55e 100644
--- a/public/language/lt/email.json
+++ b/public/language/lt/email.json
@@ -1,28 +1,28 @@
 {
     "password-reset-requested": "Password Reset Requested - %1!",
-    "welcome-to": "Welcome to %1",
-    "greeting_no_name": "Hello",
-    "greeting_with_name": "Hello %1",
+    "welcome-to": "Sveiki atvykę į %1",
+    "greeting_no_name": "Sveiki",
+    "greeting_with_name": "Sveiki %1",
     "welcome.text1": "Thank you for registering with %1!",
     "welcome.text2": "To fully activate your account, we need to verify that you own the email address you registered with.",
-    "welcome.cta": "Click here to confirm your email address",
+    "welcome.cta": "El. adreso patvirtinimui spauskite čia",
     "reset.text1": "We received a request to reset your password, possibly because you have forgotten it. If this is not the case, please ignore this email.",
     "reset.text2": "To continue with the password reset, please click on the following link:",
-    "reset.cta": "Click here to reset your password",
-    "reset.notify.subject": "Password successfully changed",
+    "reset.cta": "Slaptažodžio atstatymui spauskite čia",
+    "reset.notify.subject": "Slaptažodis sėkmingai pakeistas",
     "reset.notify.text1": "We are notifying you that on %1, your password was changed successfully.",
     "reset.notify.text2": "If you did not authorise this, please notify an administrator immediately.",
-    "digest.notifications": "You have unread notifications from %1:",
+    "digest.notifications": "Turite neskaitytų pranešimų nuo %1:",
     "digest.latest_topics": "Latest topics from %1",
-    "digest.cta": "Click here to visit %1",
+    "digest.cta": "Kad aplankyti %1, spauskite čia",
     "digest.unsub.info": "This digest was sent to you due to your subscription settings.",
     "digest.no_topics": "There have been no active topics in the past %1",
     "notif.chat.subject": "New chat message received from %1",
-    "notif.chat.cta": "Click here to continue the conversation",
+    "notif.chat.cta": "Pokalbio pratęsimui spauskite čia",
     "notif.chat.unsub.info": "This chat notification was sent to you due to your subscription settings.",
-    "notif.post.cta": "Click here to read the full topic",
+    "notif.post.cta": "Spauskite čia norėdami skaityti visą temą",
     "notif.post.unsub.info": "This post notification was sent to you due to your subscription settings.",
     "test.text1": "This is a test email to verify that the emailer is set up correctly for your NodeBB.",
-    "unsub.cta": "Click here to alter those settings",
-    "closing": "Thanks!"
+    "unsub.cta": "Spauskite čia norėdami pakeisti šiuos nustatymus",
+    "closing": "Ačiū!"
 }
\ No newline at end of file
diff --git a/public/language/lt/error.json b/public/language/lt/error.json
index 295af44ce4..08bf3c80a4 100644
--- a/public/language/lt/error.json
+++ b/public/language/lt/error.json
@@ -12,26 +12,26 @@
     "invalid-title": "Neteisingas pavadinimas!",
     "invalid-user-data": "Klaidingi vartotojo duomenys",
     "invalid-password": "Klaidingas slaptažodis",
-    "invalid-username-or-password": "Please specify both a username and password",
-    "invalid-search-term": "Invalid search term",
+    "invalid-username-or-password": "Prašome nurodyti tiek vartotojo vardą, tiek ir slaptažodį",
+    "invalid-search-term": "Neteisingas paieškos terminas",
     "invalid-pagination-value": "Klaidinga puslapiavimo reikšmė",
     "username-taken": "Vartotojo vardas jau užimtas",
     "email-taken": "El. pašto adresas jau užimtas",
-    "email-not-confirmed": "Your email has not been confirmed yet, please click here to confirm your email.",
-    "email-not-confirmed-chat": "You are unable to chat until your email is confirmed",
-    "no-email-to-confirm": "This forum requires email confirmation, please click here to enter an email",
-    "email-confirm-failed": "We could not confirm your email, please try again later.",
+    "email-not-confirmed": "Jūsų el. paštas nepatvirtintas, prašome paspausti čia norint jį patvirtinti.",
+    "email-not-confirmed-chat": "Jūs negalite kalbėtis, kol el. adresas nepatvirtintas",
+    "no-email-to-confirm": "Šis forumas reikalauja patvirtinimo el. paštu prašome spausti čia el. adreso įrašymui",
+    "email-confirm-failed": "Negalime patvirtinti jūsų el. adreso, prašom bandyti vėliau.",
     "username-too-short": "Slapyvardis per trumpas",
-    "username-too-long": "Username too long",
+    "username-too-long": "Vartotojo vardas per ilgas",
     "user-banned": "Vartotojas užblokuotas",
-    "user-too-new": "Sorry, you are required to wait %1 seconds before making your first post",
-    "no-category": "Category does not exist",
-    "no-topic": "Topic does not exist",
-    "no-post": "Post does not exist",
-    "no-group": "Group does not exist",
-    "no-user": "User does not exist",
+    "user-too-new": "Atsiprašome, bet jums reikia laukti 1% sekundes prieš patalpinant savo pirmąjį įrašą",
+    "no-category": "Tokios kategorijos nėra",
+    "no-topic": "Tokios temos nėra",
+    "no-post": "Tokio įrašo nėra",
+    "no-group": "Grupė neegzistuoja",
+    "no-user": "Tokio vartotojo nėra",
     "no-teaser": "Teaser does not exist",
-    "no-privileges": "You do not have enough privileges for this action.",
+    "no-privileges": "Šiam veiksmui jūs neturite pakankamų privilegijų.",
     "no-emailers-configured": "No email plugins were loaded, so a test email could not be sent",
     "category-disabled": "Kategorija išjungta",
     "topic-locked": "Tema užrakinta",
@@ -50,32 +50,32 @@
     "already-favourited": "You have already favourited this post",
     "already-unfavourited": "You have already unfavourited this post",
     "cant-ban-other-admins": "Jūs negalite užblokuoti kitų administratorių!",
-    "invalid-image-type": "Invalid image type. Allowed types are: %1",
-    "invalid-image-extension": "Invalid image extension",
-    "invalid-file-type": "Invalid file type. Allowed types are: %1",
+    "invalid-image-type": "Neteisingas vaizdo tipas. Leidžiami tipai :%1",
+    "invalid-image-extension": "Neteisingas vaizdo plėtinys",
+    "invalid-file-type": "Neteisingas failo tipas. Leidžiami tipai: %1",
     "group-name-too-short": "Grupės pavadinimas per trumpas",
     "group-already-exists": "Tokia grupė jau egzistuoja",
     "group-name-change-not-allowed": "Grupės pavadinimas keitimas neleidžiamas",
-    "group-already-member": "You are already part of this group",
+    "group-already-member": "Jūs jau priklausote šiai grupei",
     "group-needs-owner": "This group requires at least one owner",
-    "post-already-deleted": "This post has already been deleted",
-    "post-already-restored": "This post has already been restored",
-    "topic-already-deleted": "This topic has already been deleted",
-    "topic-already-restored": "This topic has already been restored",
+    "post-already-deleted": "Šis įrašas jau buvo ištrintas",
+    "post-already-restored": "Šis įrašas jau atstatytas",
+    "topic-already-deleted": "Ši tema jau ištrinta",
+    "topic-already-restored": "Ši tema jau atkurta",
     "topic-thumbnails-are-disabled": "Temos paveikslėliai neleidžiami.",
     "invalid-file": "Klaidingas failas",
     "uploads-are-disabled": "Įkėlimai neleidžiami",
     "signature-too-long": "Sorry, your signature cannot be longer than %1 characters.",
     "cant-chat-with-yourself": "Jūs negalite susirašinėti su savimi!",
     "chat-restricted": "This user has restricted their chat messages. They must follow you before you can chat with them",
-    "too-many-messages": "You have sent too many messages, please wait awhile.",
-    "reputation-system-disabled": "Reputation system is disabled.",
+    "too-many-messages": "Išsiuntėte per daug pranešimų, kurį laiką prašome palaukti.",
+    "reputation-system-disabled": "Reputacijos sistema išjungta.",
     "downvoting-disabled": "Downvoting is disabled",
     "not-enough-reputation-to-downvote": "Jūs neturite pakankamai reputacijos balsuoti prieš šį pranešimą",
     "not-enough-reputation-to-flag": "You do not have enough reputation to flag this post",
     "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading.",
-    "registration-error": "Registration Error",
+    "registration-error": "Registracijos klaida",
     "parse-error": "Something went wrong while parsing server response",
-    "wrong-login-type-email": "Please use your email to login",
-    "wrong-login-type-username": "Please use your username to login"
+    "wrong-login-type-email": "Prisijungimui prašom naudoti jūsų el. adresą",
+    "wrong-login-type-username": "Prisijungimui prašome naudoti vartotojo vardą"
 }
\ No newline at end of file
diff --git a/public/language/lt/global.json b/public/language/lt/global.json
index c4bf81cf4b..8301db2346 100644
--- a/public/language/lt/global.json
+++ b/public/language/lt/global.json
@@ -27,7 +27,7 @@
     "header.tags": "Žymos",
     "header.popular": "Populiarūs",
     "header.users": "Vartotojai",
-    "header.groups": "Groups",
+    "header.groups": "Grupės",
     "header.chats": "Susirašinėjimai",
     "header.notifications": "Pranešimai",
     "header.search": "Ieškoti",
@@ -74,8 +74,8 @@
     "guests": "Svečiai",
     "updated.title": "Forumas atnaujintas",
     "updated.message": "Forumas buvo atnaujintas iki naujausios versijos. Paspauskite čia norėdami perkrauti puslapį.",
-    "privacy": "Privacy",
-    "follow": "Follow",
-    "unfollow": "Unfollow",
-    "delete_all": "Delete All"
+    "privacy": "Privatumas",
+    "follow": "Sekti",
+    "unfollow": "Nebesekti",
+    "delete_all": "Viską ištrinti"
 }
\ No newline at end of file
diff --git a/public/language/lt/groups.json b/public/language/lt/groups.json
index d2314fdc29..076154cf92 100644
--- a/public/language/lt/groups.json
+++ b/public/language/lt/groups.json
@@ -1,34 +1,34 @@
 {
-    "groups": "Groups",
-    "view_group": "View Group",
-    "owner": "Group Owner",
-    "new_group": "Create New Group",
+    "groups": "Grupės",
+    "view_group": "Grupės peržiūra",
+    "owner": "Grupės savininkas",
+    "new_group": "Kurti naują grupę",
     "no_groups_found": "There are no groups to see",
-    "pending.accept": "Accept",
-    "pending.reject": "Reject",
-    "cover-instructions": "Drag and Drop a photo, drag to position, and hit <strong>Save</strong>",
-    "cover-change": "Change",
-    "cover-save": "Save",
-    "cover-saving": "Saving",
-    "details.title": "Group Details",
-    "details.members": "Member List",
-    "details.pending": "Pending Members",
-    "details.has_no_posts": "This group's members have not made any posts.",
-    "details.latest_posts": "Latest Posts",
-    "details.private": "Private",
+    "pending.accept": "Priimti",
+    "pending.reject": "Atmesti",
+    "cover-instructions": "Vilkite čia nuotrauką, perkelkite į reikalingą poziciją ir spauskite <strong>Save</strong>",
+    "cover-change": "Keisti",
+    "cover-save": "Saugoti",
+    "cover-saving": "Išsaugoma",
+    "details.title": "Grupės detalės",
+    "details.members": "Narių sąrašas",
+    "details.pending": "Laukiantys nariai",
+    "details.has_no_posts": "Šios grupės nariai neatliko jokių įrašų.",
+    "details.latest_posts": "Vėliausi įrašai",
+    "details.private": "Asmeniška",
     "details.grant": "Grant/Rescind Ownership",
     "details.kick": "Kick",
     "details.owner_options": "Group Administration",
-    "details.group_name": "Group Name",
-    "details.description": "Description",
+    "details.group_name": "Grupės pavadinimas",
+    "details.description": "Aprašymas",
     "details.badge_preview": "Badge Preview",
-    "details.change_icon": "Change Icon",
-    "details.change_colour": "Change Colour",
+    "details.change_icon": "Pakeisti paveikslėlį",
+    "details.change_colour": "Pakeisti spalvą",
     "details.badge_text": "Badge Text",
     "details.userTitleEnabled": "Show Badge",
     "details.private_help": "If enabled, joining of groups requires approval from a group owner",
-    "details.hidden": "Hidden",
+    "details.hidden": "Paslėptas",
     "details.hidden_help": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually",
-    "event.updated": "Group details have been updated",
-    "event.deleted": "The group \"%1\" has been deleted"
+    "event.updated": "Grupės informacija atnaujinta",
+    "event.deleted": "Grupė \"%1\" pašalinta"
 }
\ No newline at end of file
diff --git a/public/language/lt/login.json b/public/language/lt/login.json
index ea6dab6e24..dd18d2949c 100644
--- a/public/language/lt/login.json
+++ b/public/language/lt/login.json
@@ -1,7 +1,7 @@
 {
-    "username-email": "Username / Email",
-    "username": "Username",
-    "email": "Email",
+    "username-email": "Vartotojo vardas / El. paštas",
+    "username": "Vartotojo vardas",
+    "email": "El. paštas",
     "remember_me": "Prisiminti?",
     "forgot_password": "Užmiršote slaptažodį?",
     "alternative_logins": "Alternatyvūs prisijungimo būdai",
diff --git a/public/language/lt/modules.json b/public/language/lt/modules.json
index aefb40aed0..d7e012a412 100644
--- a/public/language/lt/modules.json
+++ b/public/language/lt/modules.json
@@ -12,9 +12,9 @@
     "chat.message-history": "Žinučių istorija",
     "chat.pop-out": "Iššokančio lango pokalbiai",
     "chat.maximize": "Padininti",
-    "chat.seven_days": "7 Days",
-    "chat.thirty_days": "30 Days",
-    "chat.three_months": "3 Months",
+    "chat.seven_days": "7 dienos",
+    "chat.thirty_days": "30 dienų",
+    "chat.three_months": "3 mėnesiai",
     "composer.user_said_in": "%1 parašė į %2:",
     "composer.user_said": "%1 parašė:",
     "composer.discard": "Ar tikrai norite sunaikinti šį pranešimą?"
diff --git a/public/language/lt/notifications.json b/public/language/lt/notifications.json
index 0a38999bff..e7deae258e 100644
--- a/public/language/lt/notifications.json
+++ b/public/language/lt/notifications.json
@@ -2,12 +2,12 @@
     "title": "Pranešimai",
     "no_notifs": "Jūs neturite naujų pranešimų",
     "see_all": "Peržiūrėti visus pranešimus",
-    "mark_all_read": "Mark all notifications read",
+    "mark_all_read": "Žymėti visus perspėjimus kaip skaitytus",
     "back_to_home": "Atgal į %1",
     "outgoing_link": "Išeinanti nuoroda",
     "outgoing_link_message": "You are now leaving %1.",
     "continue_to": "Continue to %1",
-    "return_to": "Return to %1",
+    "return_to": "Grįžti į %1",
     "new_notification": "Naujas pranešimas",
     "you_have_unread_notifications": "Jūs turite neperskaitytų pranešimų.",
     "new_message_from": "Nauja žinutė nuo <strong>%1</strong>",
@@ -17,7 +17,7 @@
     "favourited_your_post_in": "<strong>%1</strong> has favourited your post in <strong>%2</strong>.",
     "user_flagged_post_in": "<strong>%1</strong> flagged a post in <strong>%2</strong>",
     "user_posted_to": "<strong>%1</strong> parašė atsaką <strong>%2</strong>",
-    "user_posted_topic": "<strong>%1</strong> has posted a new topic: <strong>%2</strong>",
+    "user_posted_topic": "<strong>%1</strong> paskelbė naują temą: <strong>%2</strong>",
     "user_mentioned_you_in": "<strong>%1</strong> paminėjo Jus <strong>%2</strong>",
     "user_started_following_you": "<strong>%1</strong> started following you.",
     "email-confirmed": "El. paštas patvirtintas",
diff --git a/public/language/lt/pages.json b/public/language/lt/pages.json
index 338cd346f5..b317d51487 100644
--- a/public/language/lt/pages.json
+++ b/public/language/lt/pages.json
@@ -11,9 +11,9 @@
     "user.followers": "Žmonės, kurie seka %1",
     "user.posts": "Pranešimai, kuriuos parašė %1",
     "user.topics": "Temos, kurias sukūrė %1",
-    "user.groups": "%1's Groups",
+    "user.groups": "%1's grupės",
     "user.favourites": "Vartotojo %1 mėgstami pranešimai",
     "user.settings": "Vartotojo nustatymai",
     "maintenance.text": "%1 is currently undergoing maintenance. Please come back another time.",
-    "maintenance.messageIntro": "Additionally, the administrator has left this message:"
+    "maintenance.messageIntro": "Be to, administratorius paliko šį pranešimą:"
 }
\ No newline at end of file
diff --git a/public/language/lt/recent.json b/public/language/lt/recent.json
index ebb6269a19..bebb9353fa 100644
--- a/public/language/lt/recent.json
+++ b/public/language/lt/recent.json
@@ -4,16 +4,16 @@
     "week": "Savaitė",
     "month": "Mėnesis",
     "year": "Metai",
-    "alltime": "All Time",
+    "alltime": "Per visą laiką",
     "no_recent_topics": "Paskutinių temų nėra",
-    "no_popular_topics": "There are no popular topics.",
-    "there-is-a-new-topic": "There is a new topic.",
-    "there-is-a-new-topic-and-a-new-post": "There is a new topic and a new post.",
-    "there-is-a-new-topic-and-new-posts": "There is a new topic and %1 new posts.",
-    "there-are-new-topics": "There are %1 new topics.",
-    "there-are-new-topics-and-a-new-post": "There are %1 new topics and a new post.",
-    "there-are-new-topics-and-new-posts": "There are %1 new topics and %2 new posts.",
-    "there-is-a-new-post": "There is a new post.",
-    "there-are-new-posts": "There are %1 new posts.",
-    "click-here-to-reload": "Click here to reload."
+    "no_popular_topics": "Populiarių temų nėra.",
+    "there-is-a-new-topic": "Yra nauja tema.",
+    "there-is-a-new-topic-and-a-new-post": "Yra nauja tema ir naujas įrašas.",
+    "there-is-a-new-topic-and-new-posts": "Yra nauja tema ir %1 nauji įrašai.",
+    "there-are-new-topics": "Yra %1 naujos temos.",
+    "there-are-new-topics-and-a-new-post": "Yra %1 naujos temos ir naujas įrašas.",
+    "there-are-new-topics-and-new-posts": "Yra %1 naujos temos ir %2 nauji įrašai.",
+    "there-is-a-new-post": "Yra naujas įrašas.",
+    "there-are-new-posts": "Yra %1 naujas pranešimas.",
+    "click-here-to-reload": "Spauskite čia norėdami perkrauti."
 }
\ No newline at end of file
diff --git a/public/language/lt/reset_password.json b/public/language/lt/reset_password.json
index 104f75e6e3..b95f3800be 100644
--- a/public/language/lt/reset_password.json
+++ b/public/language/lt/reset_password.json
@@ -11,6 +11,6 @@
     "enter_email_address": "Įrašykite el. pašto adresą",
     "password_reset_sent": "Slaptažodžio atstatymas išsiųstas",
     "invalid_email": "Klaidingas arba neegzistuojantis el. pašto adresas!",
-    "password_too_short": "The password entered is too short, please pick a different password.",
-    "passwords_do_not_match": "The two passwords you've entered do not match."
+    "password_too_short": "Įvestas slaptažodis yra per trumpas, prašome pasirinkti kitą slaptažodį.",
+    "passwords_do_not_match": "Du slaptažodžiai, kuriuos įvedėte, nesutampa."
 }
\ No newline at end of file
diff --git a/public/language/lt/search.json b/public/language/lt/search.json
index 9dad8b6eab..b31715a6b4 100644
--- a/public/language/lt/search.json
+++ b/public/language/lt/search.json
@@ -1,40 +1,40 @@
 {
     "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)",
-    "no-matches": "No matches found",
+    "no-matches": "Atitikmenų nerasta",
     "in": "In",
     "by": "By",
-    "titles": "Titles",
-    "titles-posts": "Titles and Posts",
-    "posted-by": "Posted by",
-    "in-categories": "In Categories",
+    "titles": "Antraštės",
+    "titles-posts": "Antraštės ir įrašai",
+    "posted-by": "Parašė",
+    "in-categories": "Kategorijose",
     "search-child-categories": "Search child categories",
     "reply-count": "Reply Count",
-    "at-least": "At least",
-    "at-most": "At most",
-    "post-time": "Post time",
-    "newer-than": "Newer than",
-    "older-than": "Older than",
-    "any-date": "Any date",
-    "yesterday": "Yesterday",
-    "one-week": "One week",
-    "two-weeks": "Two weeks",
-    "one-month": "One month",
-    "three-months": "Three months",
-    "six-months": "Six months",
-    "one-year": "One year",
-    "sort-by": "Sort by",
+    "at-least": "Mažiausiai",
+    "at-most": "Daugiausia",
+    "post-time": "Įrašo laikas",
+    "newer-than": "Naujesni kaip",
+    "older-than": "Senesni kaip",
+    "any-date": "Bet kokia data",
+    "yesterday": "Vakar",
+    "one-week": "Viena savaitė",
+    "two-weeks": "Dvi savaitės",
+    "one-month": "Mėnuo",
+    "three-months": "Trys mėnesiai",
+    "six-months": "Šeši mėnesiai",
+    "one-year": "Vieneri metai",
+    "sort-by": "Rūšiuoti pagal",
     "last-reply-time": "Last reply time",
-    "topic-title": "Topic title",
-    "number-of-replies": "Number of replies",
-    "number-of-views": "Number of views",
-    "topic-start-date": "Topic start date",
-    "username": "Username",
-    "category": "Category",
-    "descending": "In descending order",
-    "ascending": "In ascending order",
-    "save-preferences": "Save preferences",
-    "clear-preferences": "Clear preferences",
-    "search-preferences-saved": "Search preferences saved",
-    "search-preferences-cleared": "Search preferences cleared",
-    "show-results-as": "Show results as"
+    "topic-title": "Temos pavadinimas",
+    "number-of-replies": "Atsakymų skaičius",
+    "number-of-views": "Peržiūrų skaičius",
+    "topic-start-date": "Temos pradžia",
+    "username": "Vartotojo vardas",
+    "category": "Kategorija",
+    "descending": "Mažėjančia tvarka",
+    "ascending": "Didėjančia tvarka",
+    "save-preferences": "Išsaugoti nustatymus",
+    "clear-preferences": "Išvalyti nustatymus",
+    "search-preferences-saved": "Paieškos nustatymai išsaugoti",
+    "search-preferences-cleared": "Paieškos nuostatos išvalytos",
+    "show-results-as": "Rodyti rezultatus kaip"
 }
\ No newline at end of file
diff --git a/public/language/lt/topic.json b/public/language/lt/topic.json
index 5138bc2766..e79c869a1b 100644
--- a/public/language/lt/topic.json
+++ b/public/language/lt/topic.json
@@ -12,7 +12,7 @@
     "notify_me": "Gauti pranešimus apie naujus atsakymus šioje temoje",
     "quote": "Cituoti",
     "reply": "Atsakyti",
-    "guest-login-reply": "Log in to reply",
+    "guest-login-reply": "Norėdami atsakyti, prisijunkite",
     "edit": "Redaguoti",
     "delete": "Ištrinti",
     "purge": "Išvalyti",
@@ -34,11 +34,11 @@
     "login_to_subscribe": "Norėdami prenumeruoti šią temą, prašome prisiregistruoti arba prisijungti.",
     "markAsUnreadForAll.success": "Tema visiems vartotojams pažymėta kaip neskaityta.",
     "watch": "Žiūrėti",
-    "unwatch": "Unwatch",
+    "unwatch": "Nebesekti",
     "watch.title": "Gauti pranešimą apie naujus įrašus šioje temoje",
-    "unwatch.title": "Stop watching this topic",
+    "unwatch.title": "Baigti šios temos stebėjimą",
     "share_this_post": "Dalintis šiuo įrašu",
-    "thread_tools.title": "Topic Tools",
+    "thread_tools.title": "Temos priemonės",
     "thread_tools.markAsUnreadForAll": "Pažymėti kaip neskaitytą",
     "thread_tools.pin": "Prisegti temą",
     "thread_tools.unpin": "Atsegti temą",
@@ -48,11 +48,11 @@
     "thread_tools.move_all": "Perkelti visus",
     "thread_tools.fork": "Išskaidyti temą",
     "thread_tools.delete": "Ištrinti temą",
-    "thread_tools.delete_confirm": "Are you sure you want to delete this topic?",
+    "thread_tools.delete_confirm": "Ar jūs tikrai norite ištrinti šią temą?",
     "thread_tools.restore": "Atkurti temą",
-    "thread_tools.restore_confirm": "Are you sure you want to restore this topic?",
+    "thread_tools.restore_confirm": "Ar jūs tikrai norite atkurti šią temą?",
     "thread_tools.purge": "Išvalyti temą",
-    "thread_tools.purge_confirm": "Are you sure you want to purge this topic?",
+    "thread_tools.purge_confirm": "Ar tikrai norite išvalyti šią temą?",
     "topic_move_success": "Ši tema buvo sėkmingai perkelta į %1",
     "post_delete_confirm": "Ar jūs tikrai norite ištrinti šį įrašą?",
     "post_restore_confirm": "Ar jūs tikrai norite atkurti šį įrašą?",
@@ -75,7 +75,7 @@
     "fork_no_pids": "Nepasirinktas joks įrašas!",
     "fork_success": "Successfully forked topic! Click here to go to the forked topic.",
     "composer.title_placeholder": "Įrašykite temos pavadinimą...",
-    "composer.handle_placeholder": "Name",
+    "composer.handle_placeholder": "Vardas ir pavardė",
     "composer.discard": "Atšaukti",
     "composer.submit": "Patvirtinti",
     "composer.replying_to": "Atsakymas %1",
@@ -90,10 +90,10 @@
     "more_users_and_guests": "dar %1 vartotojai(-ų) ir %2 svečiai(-ių)",
     "more_users": "dar %1 vartotojai(-ų)",
     "more_guests": "dar %1 svečiai(-ių)",
-    "users_and_others": "%1 and %2 others",
+    "users_and_others": "%1 ir kiti %2",
     "sort_by": "Rūšiuoti pagal",
     "oldest_to_newest": "Nuo seniausių iki naujausių",
     "newest_to_oldest": "Nuo naujausių iki seniausių",
     "most_votes": "Daugiausiai balsų",
-    "most_posts": "Most posts"
+    "most_posts": "Daugiausia įrašų"
 }
\ No newline at end of file
diff --git a/public/language/lt/user.json b/public/language/lt/user.json
index 7ec5d24423..2928033fc2 100644
--- a/public/language/lt/user.json
+++ b/public/language/lt/user.json
@@ -2,12 +2,12 @@
     "banned": "Užblokuotas",
     "offline": "Atsijungęs",
     "username": "Vartotojo vardas",
-    "joindate": "Join Date",
-    "postcount": "Post Count",
+    "joindate": "Prisijungimo data",
+    "postcount": "Įrašų kiekis",
     "email": "El. paštas",
     "confirm_email": "Patvirtinti el. paštą",
-    "delete_account": "Delete Account",
-    "delete_account_confirm": "Are you sure you want to delete your account? <br /><strong>This action is irreversible and you will not be able to recover any of your data</strong><br /><br />Enter your username to confirm that you wish to destroy this account.",
+    "delete_account": "Ištrinti paskyrą",
+    "delete_account_confirm": "Ar tikrai norite ištrinti savo paskyrą? <br /> <strong> Šis veiksmas yra negrįžtamas, ir jūs negalėsite susigrąžinti jokių duomenų </ strong> <br /> <br /> Įveskite savo vardą, kad patvirtintumėte, jog norite panaikinti šią paskyrą.",
     "fullname": "Vardas ir pavardė",
     "website": "Tinklalapis",
     "location": "Vieta",
@@ -32,7 +32,7 @@
     "edit": "Redaguoti",
     "uploaded_picture": "Įkeltas paveikslėlis",
     "upload_new_picture": "Įkelti naują paveikslėlį",
-    "upload_new_picture_from_url": "Upload New Picture From URL",
+    "upload_new_picture_from_url": "Įkelti naują paveikslėlį iš URL",
     "current_password": "Dabartinis slaptažodis",
     "change_password": "Pakeisti slaptažodį",
     "change_password_error": "Negalimas slaptažodis!",
@@ -50,21 +50,21 @@
     "max": "maks.",
     "settings": "Nustatymai",
     "show_email": "Rodyti mano el. paštą viešai",
-    "show_fullname": "Show My Full Name",
-    "restrict_chats": "Only allow chat messages from users I follow",
+    "show_fullname": "Rodyti mano vardą ir pavardę",
+    "restrict_chats": "Gauti pokalbių žinutes tik iš tų narių, kuriuos seku",
     "digest_label": "Prenumeruoti įvykių santrauką",
     "digest_description": "Gauti naujienas apie naujus pranešimus ir temas į el. paštą pagal nustatytą grafiką",
     "digest_off": "Išjungta",
     "digest_daily": "Kas dieną",
     "digest_weekly": "Kas savaitę",
     "digest_monthly": "Kas mėnesį",
-    "send_chat_notifications": "Send an email if a new chat message arrives and I am not online",
-    "send_post_notifications": "Send an email when replies are made to topics I am subscribed to",
+    "send_chat_notifications": "Jeigu gaunama nauja pokalbių žinutė ir aš neprisijungęs, siųsti el. laišką",
+    "send_post_notifications": "Atsiųsti el. laišką kai parašomi atsakymai į mano prenumeruojamas temas",
     "has_no_follower": "Šis vartotojas neturi jokių sekėjų :(",
     "follows_no_one": "Šis vartotojas nieko neseka :(",
     "has_no_posts": "Šis vartotojas dar neparašė nė vieno pranešimo.",
     "has_no_topics": "Šis vartotojas dar nesukūrė nė vienos temos.",
-    "has_no_watched_topics": "This user didn't watch any topics yet.",
+    "has_no_watched_topics": "Šis vartotojas dar neseka jokių temų",
     "email_hidden": "El. paštas paslėptas",
     "hidden": "paslėptas",
     "paginate_description": "Temose ir kategorijose naudoti numeraciją puslapiams vietoje begalinės slinkties.",
@@ -75,6 +75,6 @@
     "open_links_in_new_tab": "Atidaryti nuorodas naujame skirtuke?",
     "enable_topic_searching": "Enable In-Topic Searching",
     "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen.",
-    "follow_topics_you_reply_to": "Follow topics that you reply to.",
-    "follow_topics_you_create": "Follow topics you create."
+    "follow_topics_you_reply_to": "Sekite temas, į kurias jūs atsakėte.",
+    "follow_topics_you_create": "Sekite jūsų sukurtas temas."
 }
\ No newline at end of file
diff --git a/public/language/lt/users.json b/public/language/lt/users.json
index 043ca55bcc..f4f4360022 100644
--- a/public/language/lt/users.json
+++ b/public/language/lt/users.json
@@ -5,8 +5,8 @@
     "search": "Ieškoti",
     "enter_username": "Įrašykite vartotojo vardą paieškai",
     "load_more": "Įkelti daugiau",
-    "users-found-search-took": "%1 user(s) found! Search took %2 seconds.",
-    "filter-by": "Filter By",
+    "users-found-search-took": "Rasta %1 vartotojas(-ai)! Paieška užtruko %2 sekundes.",
+    "filter-by": "Filtruoti pagal",
     "online-only": "Online only",
-    "picture-only": "Picture only"
+    "picture-only": "Tik paveikslėlis"
 }
\ No newline at end of file
diff --git a/public/language/nb/category.json b/public/language/nb/category.json
index 75f38d0df0..f8afafe14a 100644
--- a/public/language/nb/category.json
+++ b/public/language/nb/category.json
@@ -1,6 +1,6 @@
 {
     "new_topic_button": "Nytt emne",
-    "guest-login-post": "Log in to post",
+    "guest-login-post": "Logg inn til innlegg",
     "no_topics": "<strong>Det er ingen emner i denne kategorien</strong><br />Hvorfor ikke lage ett?",
     "browsing": "leser",
     "no_replies": "Ingen har svart",
diff --git a/public/language/nb/email.json b/public/language/nb/email.json
index 65e1accaee..37b5df85d0 100644
--- a/public/language/nb/email.json
+++ b/public/language/nb/email.json
@@ -9,9 +9,9 @@
     "reset.text1": "Vi har blir bedt om å tilbakestille passordet ditt, muligens fordi du har glemt det. Hvis dette ikke stemmer kan du ignorere denne eposten.",
     "reset.text2": "For å fortsette med tilbakestillingen, vennligst klikk på følgende lenke:",
     "reset.cta": "Klikk her for å tilbakestille passordet ditt",
-    "reset.notify.subject": "Password successfully changed",
-    "reset.notify.text1": "We are notifying you that on %1, your password was changed successfully.",
-    "reset.notify.text2": "If you did not authorise this, please notify an administrator immediately.",
+    "reset.notify.subject": "Passordet ble endret",
+    "reset.notify.text1": "Vi gir deg beskjed om at du endret passordet ditt den %1.",
+    "reset.notify.text2": "Hvis du ikke godkjenner dette, vennligst gi beskjed til en administrator omgående.",
     "digest.notifications": "Du har uleste varsler fra %1:",
     "digest.latest_topics": "Siste emner fra %1",
     "digest.cta": "Klikk her for å besøke %1",
diff --git a/public/language/nb/error.json b/public/language/nb/error.json
index 2fe73f83a4..2b47e8c8ca 100644
--- a/public/language/nb/error.json
+++ b/public/language/nb/error.json
@@ -19,8 +19,8 @@
     "email-taken": "E-post opptatt",
     "email-not-confirmed": "E-posten din har ikke blitt bekreftet enda, vennligst klikk for å bekrefte din e-post.",
     "email-not-confirmed-chat": "Du kan ikke chatte før e-posten din har blitt bekreftet",
-    "no-email-to-confirm": "This forum requires email confirmation, please click here to enter an email",
-    "email-confirm-failed": "We could not confirm your email, please try again later.",
+    "no-email-to-confirm": "Dette forumet krever at e-postbekreftelse, vennligst klikk her for å skrive inn en e-post",
+    "email-confirm-failed": "Vi kunne ikke godkjenne e-posten din, vennligst prøv igjen senere.",
     "username-too-short": "Brukernavnet er for kort",
     "username-too-long": "Brukernavnet er for langt",
     "user-banned": "Bruker utestengt",
@@ -35,7 +35,7 @@
     "no-emailers-configured": "Ingen e-post-tillegg er lastet, så ingen test e-post kunne bli sendt",
     "category-disabled": "Kategori deaktivert",
     "topic-locked": "Emne låst",
-    "post-edit-duration-expired": "You are only allowed to edit posts for %1 seconds after posting",
+    "post-edit-duration-expired": "Du har bare lov til å endre innlegg i %1 sekunder etter at det ble skrevet",
     "still-uploading": "Vennligst vent til opplastingene blir fullført.",
     "content-too-short": "Vennligst skriv et lengere innlegg. Innlegg må inneholde minst %1 tegn.",
     "content-too-long": "Please enter a shorter post. Posts can't be longer than %1 characters.",
diff --git a/public/language/nb/groups.json b/public/language/nb/groups.json
index e837ac8961..6ff3a9b71f 100644
--- a/public/language/nb/groups.json
+++ b/public/language/nb/groups.json
@@ -4,8 +4,8 @@
     "owner": "Gruppe-eier",
     "new_group": "Opprett ny gruppe",
     "no_groups_found": "Det er ingen grupper å se",
-    "pending.accept": "Accept",
-    "pending.reject": "Reject",
+    "pending.accept": "Aksepter",
+    "pending.reject": "Avslå",
     "cover-instructions": "Dra og slipp et bilde, dra til posisjon, og trykk <strong>Lagre</strong>",
     "cover-change": "Endre",
     "cover-save": "Lagre",
@@ -15,20 +15,20 @@
     "details.pending": "Ventende meldemmer",
     "details.has_no_posts": "Medlemmene i denne gruppen har ikke skrevet noen innlegg.",
     "details.latest_posts": "Seneste innlegg",
-    "details.private": "Private",
-    "details.grant": "Grant/Rescind Ownership",
-    "details.kick": "Kick",
+    "details.private": "Privat",
+    "details.grant": "Gi/Opphev Eierskap",
+    "details.kick": "Kast ut",
     "details.owner_options": "Gruppeadministrasjon",
-    "details.group_name": "Group Name",
-    "details.description": "Description",
-    "details.badge_preview": "Badge Preview",
-    "details.change_icon": "Change Icon",
-    "details.change_colour": "Change Colour",
-    "details.badge_text": "Badge Text",
-    "details.userTitleEnabled": "Show Badge",
-    "details.private_help": "If enabled, joining of groups requires approval from a group owner",
-    "details.hidden": "Hidden",
-    "details.hidden_help": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually",
+    "details.group_name": "Gruppenavn",
+    "details.description": "Beskrivelse",
+    "details.badge_preview": "Forhåndsvisning av skilt",
+    "details.change_icon": "Endre ikon",
+    "details.change_colour": "Endre farge",
+    "details.badge_text": "Skilt-tekst",
+    "details.userTitleEnabled": "Vis skilt",
+    "details.private_help": "Hvis aktivert, vil medlemskap i grupper kreve godkjennelse fra en gruppe-eier",
+    "details.hidden": "Skjult",
+    "details.hidden_help": "vis aktivert, vil denne gruppen ikke bli funnet i gruppelista, og brukere må inviteres manuellt",
     "event.updated": "Gruppedetaljer har blitt oppgradert",
     "event.deleted": "Gruppen \"%1\" har blitt slettet"
 }
\ No newline at end of file
diff --git a/public/language/nb/login.json b/public/language/nb/login.json
index 2b51b10c6a..785a7defc4 100644
--- a/public/language/nb/login.json
+++ b/public/language/nb/login.json
@@ -1,7 +1,7 @@
 {
-    "username-email": "Username / Email",
-    "username": "Username",
-    "email": "Email",
+    "username-email": "Brukernavn / E-post",
+    "username": "Brukernavn",
+    "email": "E-post",
     "remember_me": "Husk meg?",
     "forgot_password": "Glemt passord?",
     "alternative_logins": "Alternativ innlogging",
diff --git a/public/language/nb/search.json b/public/language/nb/search.json
index 74fdc462a6..2f06a88eb5 100644
--- a/public/language/nb/search.json
+++ b/public/language/nb/search.json
@@ -3,38 +3,38 @@
     "no-matches": "Ingen matcher funnet",
     "in": "I",
     "by": "Av",
-    "titles": "Titles",
-    "titles-posts": "Titles and Posts",
+    "titles": "Titler",
+    "titles-posts": "Titler og innlegg",
     "posted-by": "Skapt av",
-    "in-categories": "In Categories",
-    "search-child-categories": "Search child categories",
-    "reply-count": "Reply Count",
-    "at-least": "At least",
-    "at-most": "At most",
-    "post-time": "Post time",
-    "newer-than": "Newer than",
-    "older-than": "Older than",
-    "any-date": "Any date",
-    "yesterday": "Yesterday",
-    "one-week": "One week",
-    "two-weeks": "Two weeks",
-    "one-month": "One month",
-    "three-months": "Three months",
-    "six-months": "Six months",
-    "one-year": "One year",
-    "sort-by": "Sort by",
-    "last-reply-time": "Last reply time",
-    "topic-title": "Topic title",
-    "number-of-replies": "Number of replies",
-    "number-of-views": "Number of views",
-    "topic-start-date": "Topic start date",
-    "username": "Username",
-    "category": "Category",
+    "in-categories": "I kategorier",
+    "search-child-categories": "Søk underkategorier",
+    "reply-count": "Mengde svar",
+    "at-least": "Minst",
+    "at-most": "Maks",
+    "post-time": "Innlegg-tid",
+    "newer-than": "Nyere enn",
+    "older-than": "Eldre en",
+    "any-date": "Alle datoer",
+    "yesterday": "I går",
+    "one-week": "En uke",
+    "two-weeks": "To uker",
+    "one-month": "En måned ",
+    "three-months": "Tre måneder",
+    "six-months": "Seks måneder",
+    "one-year": "Ett år",
+    "sort-by": "Sorter etter",
+    "last-reply-time": "Sise svartid",
+    "topic-title": "Emne-tittel",
+    "number-of-replies": "Antall svar",
+    "number-of-views": "Antall visninger",
+    "topic-start-date": "Starttid for emne",
+    "username": "Brukernavn",
+    "category": "Kategori",
     "descending": "In descending order",
     "ascending": "In ascending order",
-    "save-preferences": "Save preferences",
-    "clear-preferences": "Clear preferences",
-    "search-preferences-saved": "Search preferences saved",
-    "search-preferences-cleared": "Search preferences cleared",
-    "show-results-as": "Show results as"
+    "save-preferences": "Lagre innstillinger",
+    "clear-preferences": "Tøm innstillinnger",
+    "search-preferences-saved": "Søkeinnstillinger lagret",
+    "search-preferences-cleared": "Søkeinnstillinger tømt",
+    "show-results-as": "Vis resultateter som"
 }
\ No newline at end of file
diff --git a/public/language/th/category.json b/public/language/th/category.json
index 387458a0dd..e5d6b1e0fc 100644
--- a/public/language/th/category.json
+++ b/public/language/th/category.json
@@ -1,9 +1,9 @@
 {
     "new_topic_button": "กระทู้",
-    "guest-login-post": "Log in to post",
+    "guest-login-post": "เข้าสู่ระบบเพื่อโพส",
     "no_topics": "<strong>ยังไม่มีกระทู้ในหมวดนี้</strong><br />โพสต์กระทู้แรก?",
     "browsing": "เรียกดู",
     "no_replies": "ยังไม่มีใครตอบ",
-    "share_this_category": "Share this category",
-    "ignore": "Ignore"
+    "share_this_category": "แชร์ Category นี้",
+    "ignore": "ไม่ต้องสนใจอีก"
 }
\ No newline at end of file
diff --git a/public/language/th/error.json b/public/language/th/error.json
index 064bd1b530..e265b9cb24 100644
--- a/public/language/th/error.json
+++ b/public/language/th/error.json
@@ -3,37 +3,37 @@
     "not-logged-in": "คุณยังไม่ได้ลงชื่อเข้าระบบ",
     "account-locked": "บัญชีของคุณถูกระงับการใช้งานชั่วคราว",
     "search-requires-login": "ต้องลงทะเบียนบัญชีผู้ใช้สำหรับการค้นหา! โปรดลงชื่อเข้าระบบ หรือ ลงทะเบียน!",
-    "invalid-cid": "Invalid Category ID",
-    "invalid-tid": "Invalid Topic ID",
-    "invalid-pid": "Invalid Post ID",
-    "invalid-uid": "Invalid User ID",
-    "invalid-username": "Invalid Username",
-    "invalid-email": "Invalid Email",
-    "invalid-title": "Invalid title!",
-    "invalid-user-data": "Invalid User Data",
-    "invalid-password": "Invalid Password",
-    "invalid-username-or-password": "Please specify both a username and password",
-    "invalid-search-term": "Invalid search term",
+    "invalid-cid": "Category ID ไม่ถูกต้อง",
+    "invalid-tid": "Topic ID ไม่ถูกต้อง",
+    "invalid-pid": "Post ID ไม่ถูกต้อง",
+    "invalid-uid": "User ID ไม่ถูกต้อง",
+    "invalid-username": "ชื่อผู้ใช้ไม่ถูกต้อง",
+    "invalid-email": "อีเมลไม่ถูกต้อง",
+    "invalid-title": "คำนำหน้าชื่อไม่ถูกต้อง",
+    "invalid-user-data": "User Data ไม่ถูกต้อง",
+    "invalid-password": "รหัสผ่านไม่ถูกต้อง",
+    "invalid-username-or-password": "กรุณาระบุชื่อผู้ใช้และรหัสผ่าน",
+    "invalid-search-term": "ข้อความค้นหาไม่ถูกต้อง",
     "invalid-pagination-value": "Invalid pagination value",
     "username-taken": "ชื่อผู้ใช้นี้มีการใช้แล้ว",
     "email-taken": "อีเมลนี้มีการใช้แล้ว",
     "email-not-confirmed": "ยังไม่มีการยืนยันอีเมลของคุณ, โปรดกดยืนยันอีเมลของคุณตรงนี้",
-    "email-not-confirmed-chat": "You are unable to chat until your email is confirmed",
-    "no-email-to-confirm": "This forum requires email confirmation, please click here to enter an email",
-    "email-confirm-failed": "We could not confirm your email, please try again later.",
+    "email-not-confirmed-chat": "คุณไม่สามารถแชทได้ จนกว่าจะได้รับการยืนยันอีเมล",
+    "no-email-to-confirm": "Forum นี้ต้องการการยืนยันอีเมล กรุณากดที่นี่เพื่อระบุอีเมล",
+    "email-confirm-failed": "เราไม่สามารถยืนยันอีเมลของคุณ ณ ขณะนี้ กรุณาลองใหม่อีกครั้งภายหลัง",
     "username-too-short": "ชื่อบัญชีผู้ใช้ สั้นเกินไป",
     "username-too-long": "ชื่อบัญชีผู้ใช้ ยาวเกินไป",
     "user-banned": "User banned",
     "user-too-new": "Sorry, you are required to wait %1 seconds before making your first post",
-    "no-category": "Category does not exist",
-    "no-topic": "Topic does not exist",
-    "no-post": "Post does not exist",
-    "no-group": "Group does not exist",
-    "no-user": "User does not exist",
+    "no-category": "ยังไม่มี Category นี้",
+    "no-topic": "ยังไม่มี Topic นี้",
+    "no-post": "ยังไม่มี Post นี้",
+    "no-group": "ยังไม่มี Group นี้",
+    "no-user": "ยังไม่มีผู้ใช้งานนี้",
     "no-teaser": "Teaser does not exist",
-    "no-privileges": "You do not have enough privileges for this action.",
+    "no-privileges": "คุณมีสิทธิ์ไม่เพียงพอที่จะทำรายการนี้",
     "no-emailers-configured": "No email plugins were loaded, so a test email could not be sent",
-    "category-disabled": "Category disabled",
+    "category-disabled": "Category นี้ถูกปิดการใช้งานแล้ว",
     "topic-locked": "Topic Locked",
     "post-edit-duration-expired": "You are only allowed to edit posts for %1 seconds after posting",
     "still-uploading": "Please wait for uploads to complete.",
diff --git a/public/language/th/global.json b/public/language/th/global.json
index af4ff3bfd0..d81b58b8eb 100644
--- a/public/language/th/global.json
+++ b/public/language/th/global.json
@@ -14,20 +14,20 @@
     "please_log_in": "กรุณาเข้าสู่ระบบ",
     "logout": "ออกจากระบบ",
     "posting_restriction_info": "คุณต้องต้องเป็นสมาชิกเพื่อทำการโพสต์ คลิกที่นี่เพื่อเข้าสู่ระบบ",
-    "welcome_back": "Welcome Back",
+    "welcome_back": "ยินดีต้อนรับ",
     "you_have_successfully_logged_in": "คุณได้เข้าสู่ระบบแล้ว",
     "save_changes": "บันทึกการเปลี่ยนแปลง",
     "close": "ปิด",
     "pagination": "ให้เลขหน้า",
-    "pagination.out_of": "%1 out of %2",
+    "pagination.out_of": "%1 จาก %2",
     "pagination.enter_index": "Enter index",
     "header.admin": "ผู้ดูแลระบบ",
     "header.recent": "ล่าสุด",
     "header.unread": "ไม่ได้อ่าน",
-    "header.tags": "Tags",
+    "header.tags": "Tag",
     "header.popular": "ฮิต",
     "header.users": "ผู้ใช้",
-    "header.groups": "Groups",
+    "header.groups": "Group",
     "header.chats": "สนทนา",
     "header.notifications": "แจ้งเตือน",
     "header.search": "ค้นหา",
diff --git a/public/language/th/groups.json b/public/language/th/groups.json
index d2314fdc29..be94efdf0b 100644
--- a/public/language/th/groups.json
+++ b/public/language/th/groups.json
@@ -1,34 +1,34 @@
 {
-    "groups": "Groups",
-    "view_group": "View Group",
-    "owner": "Group Owner",
-    "new_group": "Create New Group",
-    "no_groups_found": "There are no groups to see",
-    "pending.accept": "Accept",
-    "pending.reject": "Reject",
-    "cover-instructions": "Drag and Drop a photo, drag to position, and hit <strong>Save</strong>",
-    "cover-change": "Change",
-    "cover-save": "Save",
-    "cover-saving": "Saving",
-    "details.title": "Group Details",
-    "details.members": "Member List",
-    "details.pending": "Pending Members",
-    "details.has_no_posts": "This group's members have not made any posts.",
-    "details.latest_posts": "Latest Posts",
-    "details.private": "Private",
+    "groups": "Group",
+    "view_group": "ดู Group",
+    "owner": "เจ้าของ Group",
+    "new_group": "สร้าง Group ใหม่",
+    "no_groups_found": "ยังไม่มี Group",
+    "pending.accept": "ยอมรับ",
+    "pending.reject": "ไม่ยอมรับ",
+    "cover-instructions": "ลากรูปภาพไปวางยังตำแหน่งที่ต้องการแล้วกดที่ปุ่ม <strong>บันทึก</strong>",
+    "cover-change": "ปรับปรุง",
+    "cover-save": "บันทึก",
+    "cover-saving": "กำลังบันทึก",
+    "details.title": "ข้อมูล Group",
+    "details.members": "รายชื่อสมาชิก",
+    "details.pending": "สมาชิกที่กำลังรอการตอบรับ",
+    "details.has_no_posts": "Group นี้ยังไม่มีโพสจากสมาชิก",
+    "details.latest_posts": "โพสล่าสุด",
+    "details.private": "ส่วนตัว",
     "details.grant": "Grant/Rescind Ownership",
-    "details.kick": "Kick",
-    "details.owner_options": "Group Administration",
-    "details.group_name": "Group Name",
-    "details.description": "Description",
+    "details.kick": "เตะออก",
+    "details.owner_options": "การจัดการ Group",
+    "details.group_name": "ชื่อ Group",
+    "details.description": "คำอธิบาย",
     "details.badge_preview": "Badge Preview",
-    "details.change_icon": "Change Icon",
-    "details.change_colour": "Change Colour",
+    "details.change_icon": "เปลี่ยนไอคอน",
+    "details.change_colour": "เปลี่ยนสี",
     "details.badge_text": "Badge Text",
-    "details.userTitleEnabled": "Show Badge",
+    "details.userTitleEnabled": "แสดง Badge",
     "details.private_help": "If enabled, joining of groups requires approval from a group owner",
-    "details.hidden": "Hidden",
+    "details.hidden": "ซ่อน",
     "details.hidden_help": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually",
-    "event.updated": "Group details have been updated",
+    "event.updated": "ข้อมูล Group ได้รับการบันทึกแล้ว",
     "event.deleted": "The group \"%1\" has been deleted"
 }
\ No newline at end of file
diff --git a/public/language/th/login.json b/public/language/th/login.json
index f0aff3c796..5eba336a39 100644
--- a/public/language/th/login.json
+++ b/public/language/th/login.json
@@ -1,7 +1,7 @@
 {
-    "username-email": "Username / Email",
-    "username": "Username",
-    "email": "Email",
+    "username-email": "ชื่อผู้ใช้ / อีเมล",
+    "username": "ชื่อผู้ใช้",
+    "email": "อีเมล",
     "remember_me": "จำไว้ในระบบ?",
     "forgot_password": "ลืมรหัสผ่าน?",
     "alternative_logins": "เข้าสู่ระบบโดยทางอื่น",
diff --git a/public/language/th/pages.json b/public/language/th/pages.json
index 44f8b127e1..4b4998d3bd 100644
--- a/public/language/th/pages.json
+++ b/public/language/th/pages.json
@@ -5,15 +5,15 @@
     "recent": "กระทู้ล่าสุด",
     "users": "ผู้ใช้ที่ลงทะเบียน",
     "notifications": "แจ้งเตือน",
-    "tags": "Topics tagged under \"%1\"",
+    "tags": "หัวข้อที่ถูก Tag อยู่ภายใต้ \"%1\"",
     "user.edit": "แก้ไข \"%1\"",
     "user.following": "ผู้ใช้ที่ %1 ติดตาม",
     "user.followers": "ผู้ใช้ที่ติดตาม %1",
     "user.posts": "กระทู้โดย %1",
-    "user.topics": "Topics created by %1",
-    "user.groups": "%1's Groups",
+    "user.topics": "หัวข้อที่ถูกสร้างโดย %1",
+    "user.groups": "กลุ่มของ %1",
     "user.favourites": "กระทู้ที่ %1 ชอบ",
     "user.settings": "ตั้งค่าผู้ใช้",
-    "maintenance.text": "%1 is currently undergoing maintenance. Please come back another time.",
-    "maintenance.messageIntro": "Additionally, the administrator has left this message:"
+    "maintenance.text": "%1 กำลังอยู่ระหว่างการปิดปรับปรุงชั่วคราว กรุณาลองใหม่อีกครั้งในภายหลัง",
+    "maintenance.messageIntro": "ผู้ดูแลระบบได้ฝากข้อความต่อไปนี้เอาไว้"
 }
\ No newline at end of file
diff --git a/public/language/th/reset_password.json b/public/language/th/reset_password.json
index 499f2f7136..1b66d7dd33 100644
--- a/public/language/th/reset_password.json
+++ b/public/language/th/reset_password.json
@@ -11,6 +11,6 @@
     "enter_email_address": "ใส่อีเมล์",
     "password_reset_sent": "รหัสรีเซ็ตถูกส่งออกไปแล้ว",
     "invalid_email": "อีเมล์ไม่ถูกต้อง / อีเมล์ไม่มีอยู่!",
-    "password_too_short": "The password entered is too short, please pick a different password.",
-    "passwords_do_not_match": "The two passwords you've entered do not match."
+    "password_too_short": "รหัสผ่านที่คุณกำหนดยังสั้นเกินไป กรุณากำหนดรหัสผ่านของคุณใหม่",
+    "passwords_do_not_match": "รหัสผ่านทั้ง 2 ที่ใส่ไม่ตรงกัน"
 }
\ No newline at end of file
diff --git a/public/language/th/tags.json b/public/language/th/tags.json
index d2d0ff6fe0..bdb45d5eef 100644
--- a/public/language/th/tags.json
+++ b/public/language/th/tags.json
@@ -1,7 +1,7 @@
 {
     "no_tag_topics": "ไม่มีหัวข้อสนทนาที่เกี่ยวข้องกับป้ายคำศัพท์นี้",
     "tags": "ป้ายคำศัพท์",
-    "enter_tags_here": "Enter tags here. %1-%2 characters. Press enter after each tag.",
+    "enter_tags_here": "ใส่ Tags ตรงนี้ได้ทั้งหมด %1-%2 ตัวอักษร และกรุณากดปุ่ม Enter ทุกครั้งเมื่อต้องการเพิ่ม Tag ใหม่",
     "enter_tags_here_short": "ใส่ป้ายคำศัพท์ ...",
     "no_tags": "ยังไม่มีป้ายคำศัพท์"
 }
\ No newline at end of file
diff --git a/public/language/th/topic.json b/public/language/th/topic.json
index 652ebe5db5..ea6a1a8998 100644
--- a/public/language/th/topic.json
+++ b/public/language/th/topic.json
@@ -4,15 +4,15 @@
     "topic_id_placeholder": "Enter topic ID",
     "no_topics_found": "ไม่พบกระทู้",
     "no_posts_found": "ไม่พบโพส",
-    "post_is_deleted": "This post is deleted!",
+    "post_is_deleted": "ลบ Post นี้เรียบร้อยแล้ว!",
     "profile": "รายละเอียด",
-    "posted_by": "Posted by %1",
-    "posted_by_guest": "Posted by Guest",
+    "posted_by": "โพสโดย %1",
+    "posted_by_guest": "โพสโดย Guest",
     "chat": "แชท",
     "notify_me": "แจ้งเตือนเมื่อการตอบใหม่ในกระทู้นี้",
     "quote": "คำอ้างอิง",
     "reply": "ตอบ",
-    "guest-login-reply": "Log in to reply",
+    "guest-login-reply": "เข้าสู่ระบบเพื่อตอบกลับ",
     "edit": "แก้ไข",
     "delete": "ลบ",
     "purge": "Purge",
@@ -26,31 +26,31 @@
     "locked": "Locked",
     "bookmark_instructions": "คลิกที่นี่เพื่อกลับคืนสู่ฐานะสุดท้าย หรือ คลิกปิดเพื่อยกเลิก",
     "flag_title": "ปักธงโพสต์นี้เพื่อดำเนินการ",
-    "flag_confirm": "Are you sure you want to flag this post?",
+    "flag_confirm": "มั่นใจแล้วหรือไม่ที่จะ Flag Post นี้?",
     "flag_success": "This post has been flagged for moderation.",
-    "deleted_message": "This topic has been deleted. Only users with topic management privileges can see it.",
+    "deleted_message": "Topic นี้ถูกลบไปแล้ว เฉพาะผู้ใช้งานที่มีสิทธิ์ในการจัดการ Topic เท่านั้นที่จะมีสิทธิ์ในการเข้าชม",
     "following_topic.message": "คุณจะได้รับการแจ้งเตือนเมื่อมีคนโพสต์ในกระทู้นี้",
     "not_following_topic.message": "คุณจะไม่รับการแจ้งเตือนจากกระทู้นี้",
     "login_to_subscribe": "กรุณาลงทะเบียนหรือเข้าสู่ระบบเพื่อที่จะติดตามกระทู้นี้",
     "markAsUnreadForAll.success": "ทำเครื่องหมายว่ายังไม่ได้อ่านทั้งหมด",
     "watch": "ติดตาม",
-    "unwatch": "Unwatch",
-    "watch.title": "Be notified of new replies in this topic",
-    "unwatch.title": "Stop watching this topic",
+    "unwatch": "ยังไม่ได้ติดตาม",
+    "watch.title": "ให้แจ้งเตือนเมื่อมีการตอบกลับ Topic นี้",
+    "unwatch.title": "ยกเลิกการติดตาม Topic นี้",
     "share_this_post": "แชร์โพสต์นี้",
-    "thread_tools.title": "Topic Tools",
+    "thread_tools.title": "เครื่องมือช่วยจัดการ Topic",
     "thread_tools.markAsUnreadForAll": "ทำหมายว่ายังไม่ได้อ่าน",
     "thread_tools.pin": "ปักหมุดกระทู้",
     "thread_tools.unpin": "เลิกปักหมุดกระทู้",
     "thread_tools.lock": "ล็อคกระทู้",
     "thread_tools.unlock": "ปลดล็อคกระทู้",
     "thread_tools.move": "ย้ายกระทู้",
-    "thread_tools.move_all": "Move All",
+    "thread_tools.move_all": "ย้ายทั้งหมด",
     "thread_tools.fork": "แยกกระทู้",
     "thread_tools.delete": "ลบกระทู้",
-    "thread_tools.delete_confirm": "Are you sure you want to delete this topic?",
+    "thread_tools.delete_confirm": "มั่นใจแล้วหรือไม่ที่จะลบ Topic นี้?",
     "thread_tools.restore": "กู้กระทู้",
-    "thread_tools.restore_confirm": "Are you sure you want to restore this topic?",
+    "thread_tools.restore_confirm": "มั่นใจแล้วหรือไม่ที่จะกู้คืน Topic นี้?",
     "thread_tools.purge": "Purge Topic",
     "thread_tools.purge_confirm": "Are you sure you want to purge this topic?",
     "topic_move_success": "This topic has been successfully moved to %1",
diff --git a/public/language/th/unread.json b/public/language/th/unread.json
index 37451a65ba..2595955dc7 100644
--- a/public/language/th/unread.json
+++ b/public/language/th/unread.json
@@ -2,8 +2,8 @@
     "title": "ไม่ได้อ่าน",
     "no_unread_topics": "ไม่มีกระทู้ที่ยังไม่ได้อ่านเป็น",
     "load_more": "โหลดเพิ่มเติม",
-    "mark_as_read": "Mark as Read",
-    "selected": "Selected",
-    "all": "All",
-    "topics_marked_as_read.success": "Topics marked as read!"
+    "mark_as_read": "ทำเครื่องหมายว่าอ่านแล้ว",
+    "selected": "เลือก",
+    "all": "ทั้งหมด",
+    "topics_marked_as_read.success": "Topic ถูกทำเครื่องหมายว่าอ่านแล้วเรียบร้อย"
 }
\ No newline at end of file
diff --git a/public/language/th/user.json b/public/language/th/user.json
index aaf5a74424..0e8de83da7 100644
--- a/public/language/th/user.json
+++ b/public/language/th/user.json
@@ -2,8 +2,8 @@
     "banned": "เเบน",
     "offline": "ออฟไลน์",
     "username": "ชื่อผู้ใช้",
-    "joindate": "Join Date",
-    "postcount": "Post Count",
+    "joindate": "วันที่เข้าร่วม",
+    "postcount": "จำนวนโพส",
     "email": "อีเมล์",
     "confirm_email": "ยืนยันอีเมล",
     "delete_account": "ลบบัญชี",
@@ -18,7 +18,7 @@
     "profile_views": "ดูข้อมูลส่วนตัว",
     "reputation": "ชื่อเสียง",
     "favourites": "ชอบ",
-    "watched": "Watched",
+    "watched": "ดูแล้ว",
     "followers": "คนติดตาม",
     "following": "ติดตาม",
     "signature": "ลายเซ็น",
@@ -59,12 +59,12 @@
     "digest_weekly": "รายสัปดาห์",
     "digest_monthly": "รายเดือน",
     "send_chat_notifications": "ส่งอีเมลเมื่อมีข้อความใหม่เข้ามาขณะที่ฉันไม่ได้ออนไลน์",
-    "send_post_notifications": "Send an email when replies are made to topics I am subscribed to",
+    "send_post_notifications": "ส่งอีเมลให้ฉันเมื่อมีการตอบกลับในหัวข้อที่ฉันเคยบอกรับเป็นสมาชิกไว้",
     "has_no_follower": "ผู้ใช้รายนี้ไม่มีใครติดตาม :(",
     "follows_no_one": "ผู้ใช้รายนี้ไม่ติดตามใคร :(",
     "has_no_posts": "ผู้ใช้รายนี้ไม่ได้โพสต์อะไรเลย",
     "has_no_topics": "สมาชิกรายนี้ยังไม่ได้มีการโพสต์ข้อความ",
-    "has_no_watched_topics": "This user didn't watch any topics yet.",
+    "has_no_watched_topics": "ผู้ใช้นี้ยังไม่เคยเข้าชมในหัวข้อใดๆ",
     "email_hidden": "ซ่อนอีเมล์",
     "hidden": "ซ่อน",
     "paginate_description": "ให้เลขหน้ากระทู้และโพสต์แทนการใช้สกรอลล์ที่ไม่สิ้นสุด",
@@ -73,8 +73,8 @@
     "notification_sounds": "เตือนด้วยเสียงเมื่อมีข้อความแจ้งเตือน",
     "browsing": "เปิดดูการตั้งค่า",
     "open_links_in_new_tab": "เปิดลิงค์ในแท็บใหม่",
-    "enable_topic_searching": "Enable In-Topic Searching",
-    "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen.",
+    "enable_topic_searching": "เปิดใช้การค้นหาแบบ In-Topic",
+    "topic_search_help": "เมื่อการค้นหาแบบ In-Topic ถูกเปิดใช้งาน การค้นหาแบบ In-Topic จะทำงานแทนการค้นหาในรูปแบบเดิม ซึ่งช่วยให้คุณสามารถทำการค้นหาจาก Topic ทั้งหมด เพิ่มเติมจากที่คุณกำลังเห็นอยู่บนหน้าจอ",
     "follow_topics_you_reply_to": "ติดตามกระทู้ที่คุณตอบ",
     "follow_topics_you_create": "ติดตามกระทู้ที่คุณตั้ง"
 }
\ No newline at end of file
diff --git a/public/language/zh_CN/groups.json b/public/language/zh_CN/groups.json
index 39eb395078..e2fc6cdf7f 100644
--- a/public/language/zh_CN/groups.json
+++ b/public/language/zh_CN/groups.json
@@ -4,8 +4,8 @@
     "owner": "用户组组长",
     "new_group": "创建新用户组",
     "no_groups_found": "还没有用户组",
-    "pending.accept": "Accept",
-    "pending.reject": "Reject",
+    "pending.accept": "接受",
+    "pending.reject": "取消",
     "cover-instructions": "拖放照片,拖动位置,然后点击 <strong>保存</strong>",
     "cover-change": "变更",
     "cover-save": "保存",
@@ -15,19 +15,19 @@
     "details.pending": "预备成员",
     "details.has_no_posts": "此用户组的会员尚未发表任何帖子。",
     "details.latest_posts": "最新帖子",
-    "details.private": "Private",
+    "details.private": "私有",
     "details.grant": "授予/取消所有权",
     "details.kick": "踢",
     "details.owner_options": "用户组管理",
-    "details.group_name": "Group Name",
+    "details.group_name": "用户组名",
     "details.description": "Description",
     "details.badge_preview": "Badge Preview",
-    "details.change_icon": "Change Icon",
+    "details.change_icon": "更改图标",
     "details.change_colour": "Change Colour",
     "details.badge_text": "Badge Text",
     "details.userTitleEnabled": "Show Badge",
     "details.private_help": "If enabled, joining of groups requires approval from a group owner",
-    "details.hidden": "Hidden",
+    "details.hidden": "隐藏",
     "details.hidden_help": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually",
     "event.updated": "用户组信息已更新",
     "event.deleted": "用户组 \"%1\" 已被删除"
diff --git a/public/language/zh_CN/login.json b/public/language/zh_CN/login.json
index 3e8d54af50..b6d1c9926e 100644
--- a/public/language/zh_CN/login.json
+++ b/public/language/zh_CN/login.json
@@ -1,7 +1,7 @@
 {
     "username-email": "Username / Email",
-    "username": "Username",
-    "email": "Email",
+    "username": "用户名",
+    "email": "邮件",
     "remember_me": "记住我?",
     "forgot_password": "忘记密码?",
     "alternative_logins": "使用合作网站帐号登录",
diff --git a/public/language/zh_CN/notifications.json b/public/language/zh_CN/notifications.json
index f8a88ca3fe..f20ee0f1e4 100644
--- a/public/language/zh_CN/notifications.json
+++ b/public/language/zh_CN/notifications.json
@@ -2,7 +2,7 @@
     "title": "通知",
     "no_notifs": "您没有新的通知",
     "see_all": "查看全部通知",
-    "mark_all_read": "Mark all notifications read",
+    "mark_all_read": "标记全部为已读",
     "back_to_home": "返回 %1",
     "outgoing_link": "站外链接",
     "outgoing_link_message": "您正在离开 %1。",
diff --git a/public/src/ajaxify.js b/public/src/ajaxify.js
index 0161d8a6ae..f5cca44ccf 100644
--- a/public/src/ajaxify.js
+++ b/public/src/ajaxify.js
@@ -25,6 +25,8 @@ $(document).ready(function() {
 	ajaxify.go = function (url, callback, quiet) {
 		if (ajaxify.handleACPRedirect(url)) {
 			return true;
+		} else if (ajaxify.handleNonAPIRoutes(url)) {
+			return true;
 		}
 
 		app.enterRoom('');
@@ -39,8 +41,6 @@ $(document).ready(function() {
 
 		$('#footer, #content').removeClass('hide').addClass('ajaxifying');
 
-		var	startTime = (new Date()).getTime();
-
 		ajaxify.variables.flush();
 		ajaxify.loadData(url, function(err, data) {
 			if (err) {
@@ -51,7 +51,7 @@ $(document).ready(function() {
 
 			translator.load(config.defaultLang, data.template.name);
 
-			renderTemplate(url, data.template.name, data, startTime, callback);
+			renderTemplate(url, data.template.name, data, callback);
 
 			require(['search'], function(search) {
 				search.topicDOM.end();
@@ -64,12 +64,21 @@ $(document).ready(function() {
 	ajaxify.handleACPRedirect = function(url) {
 		// If ajaxifying into an admin route from regular site, do a cold load.
 		url = ajaxify.removeRelativePath(url.replace(/\/$/, ''));
-		if (url.indexOf('admin') === 0 && window.location.pathname.indexOf('/admin') !== 0) {
+		if (url.indexOf('admin') === 0 && window.location.pathname.indexOf(RELATIVE_PATH + '/admin') !== 0) {
 			window.open(RELATIVE_PATH + '/' + url, '_blank');
 			return true;
 		}
 		return false;
-	}
+	};
+
+	ajaxify.handleNonAPIRoutes = function(url) {
+		url = ajaxify.removeRelativePath(url.replace(/\/$/, ''));
+		if (url.indexOf('uploads') === 0) {
+			window.open(RELATIVE_PATH + '/' + url, '_blank');
+			return true;
+		}
+		return false;
+	};
 
 	ajaxify.start = function(url, quiet, search) {
 		url = ajaxify.removeRelativePath(url.replace(/\/$/, ''));
@@ -78,6 +87,10 @@ $(document).ready(function() {
 
 		$(window).trigger('action:ajaxify.start', {url: url});
 
+		if (!window.location.pathname.match(/\/(403|404)$/g)) {
+			app.previousUrl = window.location.href;
+		}
+
 		ajaxify.currentPage = url;
 
 		if (window.history && window.history.pushState) {
@@ -114,25 +127,22 @@ $(document).ready(function() {
 		}
 	}
 
-	function renderTemplate(url, tpl_url, data, startTime, callback) {
-		var animationDuration = parseFloat($('#content').css('transition-duration')) || 0.2;
+	function renderTemplate(url, tpl_url, data, callback) {
 		$(window).trigger('action:ajaxify.loadingTemplates', {});
 
 		templates.parse(tpl_url, data, function(template) {
 			translator.translate(template, function(translatedTemplate) {
-				setTimeout(function() {
-					$('#content').html(translatedTemplate);
+				$('#content').html(translatedTemplate);
 
-					ajaxify.end(url, tpl_url);
+				ajaxify.end(url, tpl_url);
 
-					if (typeof callback === 'function') {
-						callback();
-					}
+				if (typeof callback === 'function') {
+					callback();
+				}
 
-					$('#content, #footer').removeClass('ajaxifying');
+				$('#content, #footer').removeClass('ajaxifying');
 
-					app.refreshTitle(url);
-				}, animationDuration * 1000 - ((new Date()).getTime() - startTime));
+				app.refreshTitle(url);
 			});
 		});
 	}
@@ -243,10 +253,6 @@ $(document).ready(function() {
 				return e.preventDefault();
 			}
 
-			if (!window.location.pathname.match(/\/(403|404)$/g)) {
-				app.previousUrl = window.location.href;
-			}
-
 			if (!e.ctrlKey && !e.shiftKey && !e.metaKey && e.which === 1) {
 				if (this.host === '' || this.host === window.location.host) {
 					// Internal link
diff --git a/public/src/app.js b/public/src/app.js
index fdfe7856b3..8974d2316a 100644
--- a/public/src/app.js
+++ b/public/src/app.js
@@ -234,7 +234,7 @@ app.cacheBuster = null;
 	app.processPage = function () {
 		highlightNavigationLink();
 
-		$('span.timeago').timeago();
+		$('.timeago').timeago();
 
 		utils.makeNumbersHumanReadable($('.human-readable-number'));
 
diff --git a/public/src/client/account/favourites.js b/public/src/client/account/favourites.js
index 6a08a814a5..f73e1d4f20 100644
--- a/public/src/client/account/favourites.js
+++ b/public/src/client/account/favourites.js
@@ -34,7 +34,7 @@ define('forum/account/favourites', ['forum/account/header', 'forum/infinitescrol
 		infinitescroll.parseAndTranslate('account/favourites', 'posts', {posts: posts}, function(html) {
 			$('.user-favourite-posts').append(html);
 			html.find('img').addClass('img-responsive');
-			html.find('span.timeago').timeago();
+			html.find('.timeago').timeago();
 			app.createUserTooltips();
 			utils.makeNumbersHumanReadable(html.find('.human-readable-number'));
 			callback();
diff --git a/public/src/client/account/posts.js b/public/src/client/account/posts.js
index 70410e0819..c4759b9016 100644
--- a/public/src/client/account/posts.js
+++ b/public/src/client/account/posts.js
@@ -35,7 +35,7 @@ define('forum/account/posts', ['forum/account/header', 'forum/infinitescroll'],
 		infinitescroll.parseAndTranslate('account/posts', 'posts', {posts: posts}, function(html) {
 			$('.user-favourite-posts').append(html);
 			html.find('img').addClass('img-responsive');
-			html.find('span.timeago').timeago();
+			html.find('.timeago').timeago();
 			app.createUserTooltips();
 			utils.makeNumbersHumanReadable(html.find('.human-readable-number'));
 			callback();
diff --git a/public/src/client/account/profile.js b/public/src/client/account/profile.js
index 89a555a702..5bbf48fe85 100644
--- a/public/src/client/account/profile.js
+++ b/public/src/client/account/profile.js
@@ -116,7 +116,7 @@ define('forum/account/profile', ['forum/account/header', 'forum/infinitescroll']
 		infinitescroll.parseAndTranslate('account/profile', 'posts', {posts: posts}, function(html) {
 
 			$('.user-recent-posts .loading-indicator').before(html);
-			html.find('span.timeago').timeago();
+			html.find('.timeago').timeago();
 
 			callback();
 		});
diff --git a/public/src/client/account/topics.js b/public/src/client/account/topics.js
index c556ef2891..3d3923ceef 100644
--- a/public/src/client/account/topics.js
+++ b/public/src/client/account/topics.js
@@ -32,8 +32,8 @@ define('forum/account/topics', ['forum/account/header', 'forum/infinitescroll'],
 
 	function onTopicsLoaded(topics, callback) {
 		infinitescroll.parseAndTranslate('account/topics', 'topics', {topics: topics}, function(html) {
-			$('#topics-container').append(html);
-			html.find('span.timeago').timeago();
+			$('[component="category"]').append(html);
+			html.find('.timeago').timeago();
 			app.createUserTooltips();
 			utils.makeNumbersHumanReadable(html.find('.human-readable-number'));
 			$(window).trigger('action:topics.loaded');
diff --git a/public/src/client/account/watched.js b/public/src/client/account/watched.js
index 9a502b1ee8..c9d4bed788 100644
--- a/public/src/client/account/watched.js
+++ b/public/src/client/account/watched.js
@@ -30,8 +30,8 @@ define('forum/account/watched', ['forum/account/header', 'forum/infinitescroll']
 
 	function onTopicsLoaded(topics, callback) {
 		infinitescroll.parseAndTranslate('account/watched', 'topics', {topics: topics}, function(html) {
-			$('#topics-container').append(html);
-			html.find('span.timeago').timeago();
+			$('[component="category"]').append(html);
+			html.find('.timeago').timeago();
 			app.createUserTooltips();
 			utils.makeNumbersHumanReadable(html.find('.human-readable-number'));
 			$(window).trigger('action:topics.loaded');
diff --git a/public/src/client/categories.js b/public/src/client/categories.js
index d3cf1cae43..e3882ff831 100644
--- a/public/src/client/categories.js
+++ b/public/src/client/categories.js
@@ -29,7 +29,7 @@ define('forum/categories', function() {
 	};
 
 	function renderNewPost(cid, post) {
-		var category = $('.category-item[data-cid="' + cid + '"]');
+		var category = components.get('category/topic', 'cid', cid);
 		if (!category.length) {
 			return;
 		}
@@ -67,7 +67,7 @@ define('forum/categories', function() {
 			translator.translate(html, function(translatedHTML) {
 				translatedHTML = $(translatedHTML);
 				translatedHTML.find('img').addClass('img-responsive');
-				translatedHTML.find('span.timeago').timeago();
+				translatedHTML.find('.timeago').timeago();
 				callback(translatedHTML);
 			});
 		});
diff --git a/public/src/client/category.js b/public/src/client/category.js
index 3b6a0e69b9..1e6c0e94c4 100644
--- a/public/src/client/category.js
+++ b/public/src/client/category.js
@@ -20,12 +20,6 @@ define('forum/category', [
 		}
 	});
 
-	$(window).on('action:composer.topics.post', function(ev, data) {
-		localStorage.removeItem('category:' + data.data.cid + ':bookmark');
-		localStorage.removeItem('category:' + data.data.cid + ':bookmark:clicked');
-		ajaxify.go('topic/' + data.data.slug);
-	});
-
 	function removeListeners() {
 		socket.removeListener('event:new_topic', Category.onNewTopic);
 		categoryTools.removeListeners();
@@ -52,12 +46,12 @@ define('forum/category', [
 		enableInfiniteLoadingOrPagination();
 
 		if (!config.usePagination) {
-			navigator.init('#topics-container > .category-item', ajaxify.variables.get('topic_count'), Category.toTop, Category.toBottom, Category.navigatorCallback);
+			navigator.init('[component="category/topic"]', ajaxify.variables.get('topic_count'), Category.toTop, Category.toBottom, Category.navigatorCallback);
 		}
 
-		$('#topics-container').on('click', '.topic-title', function() {
+		$('[component="category"]').on('click', '[component="post/header"]', function() {
 			var clickedIndex = $(this).parents('[data-index]').attr('data-index');
-			$('#topics-container li.category-item').each(function(index, el) {
+			$('[component="category/topic"]').each(function(index, el) {
 				if ($(el).offset().top - $(window).scrollTop() > 0) {
 					localStorage.setItem('category:' + cid + ':bookmark', $(el).attr('data-index'));
 					localStorage.setItem('category:' + cid + ':bookmark:clicked', clickedIndex);
@@ -95,8 +89,8 @@ define('forum/category', [
 		});
 	};
 
-	Category.navigatorCallback = function(element, elementCount) {
-		return parseInt(element.attr('data-index'), 10) + 1;
+	Category.navigatorCallback = function(topIndex, bottomIndex, elementCount) {
+		return bottomIndex;
 	};
 
 	$(window).on('action:popstate', function(ev, data) {
@@ -135,7 +129,7 @@ define('forum/category', [
 					bookmarkIndex = 0;
 				}
 
-				$('#topics-container').empty();
+				$('[component="category"]').empty();
 
 				loadTopicsAfter(bookmarkIndex, function() {
 					Category.scrollToTopic(bookmarkIndex, clickedIndex, 0);
@@ -145,7 +139,7 @@ define('forum/category', [
 	});
 
 	Category.highlightTopic = function(topicIndex) {
-		var highlight = $('#topics-container [data-index="' + topicIndex + '"]');
+		var highlight = components.get('category/topic', 'index', topicIndex);
 		if (highlight.length && !highlight.hasClass('highlight')) {
 			highlight.addClass('highlight');
 			setTimeout(function() {
@@ -163,7 +157,7 @@ define('forum/category', [
 			offset = 0;
 		}
 
-		var scrollTo = $('#topics-container [data-index="' + bookmarkIndex + '"]');
+		var scrollTo = components.get('category/topic', 'index', bookmarkIndex);
 		var	cid = ajaxify.variables.get('category_id');
 		if (scrollTo.length && cid) {
 			$('html, body').animate({
@@ -198,11 +192,12 @@ define('forum/category', [
 		}, function(html) {
 			translator.translate(html, function(translatedHTML) {
 				var topic = $(translatedHTML),
-					container = $('#topics-container'),
-					topics = $('#topics-container').children('.category-item'),
+					container = $('[component="category"]'),
+					topics = $('[component="category/topic"]'),
 					numTopics = topics.length;
 
-				$('#topics-container, .category-sidebar').removeClass('hidden');
+				$('[component="category"]').removeClass('hidden');
+				$('.category-sidebar').removeClass('hidden');
 
 				var noTopicsWarning = $('#category-no-topics');
 				if (noTopicsWarning.length) {
@@ -228,7 +223,7 @@ define('forum/category', [
 
 				topic.hide().fadeIn('slow');
 
-				topic.find('span.timeago').timeago();
+				topic.find('.timeago').timeago();
 				app.createUserTooltips();
 				updateTopicCount();
 
@@ -253,7 +248,7 @@ define('forum/category', [
 
 		function removeAlreadyAddedTopics(topics) {
 			return topics.filter(function(topic) {
-				return $('#topics-container li[data-tid="' + topic.tid +'"]').length === 0;
+				return components.get('category/topic', 'tid', topic.tid).length === 0;
 			});
 		}
 
@@ -261,16 +256,20 @@ define('forum/category', [
 			before = null;
 
 		function findInsertionPoint() {
-			if (!$('#topics-container .category-item[data-tid]').length) {
+			var topics = components.get('category/topic');
+
+			if (!topics.length) {
 				return;
 			}
-			var last = $('#topics-container .category-item[data-tid]').last();
-			var lastIndex = last.attr('data-index');
-			var firstIndex = data.topics[data.topics.length - 1].index;
+
+			var last = topics.last(),
+				lastIndex = last.attr('data-index'),
+				firstIndex = data.topics[data.topics.length - 1].index;
+
 			if (firstIndex > lastIndex) {
 				after = last;
 			} else {
-				before = $('#topics-container .category-item[data-tid]').first();
+				before = topics.first();
 			}
 		}
 
@@ -283,10 +282,12 @@ define('forum/category', [
 
 		templates.parse('category', 'topics', data, function(html) {
 			translator.translate(html, function(translatedHTML) {
-				var container = $('#topics-container'),
+				var container = $('[component="category"]'),
 					html = $(translatedHTML);
 
-				$('#topics-container, .category-sidebar').removeClass('hidden');
+				$('[component="category"]').removeClass('hidden');
+				$('.category-sidebar').removeClass('hidden');
+
 				$('#category-no-topics').remove();
 
 				if(config.usePagination) {
@@ -304,7 +305,7 @@ define('forum/category', [
 				if (typeof callback === 'function') {
 					callback();
 				}
-				html.find('span.timeago').timeago();
+				html.find('.timeago').timeago();
 				app.createUserTooltips();
 				utils.makeNumbersHumanReadable(html.find('.human-readable-number'));
 			});
@@ -312,11 +313,11 @@ define('forum/category', [
 	};
 
 	Category.loadMoreTopics = function(direction) {
-		if (!$('#topics-container').length || !$('#topics-container').children().length) {
+		if (!$('[component="category"]').length || !$('[component="category"]').children().length) {
 			return;
 		}
 
-		infinitescroll.calculateAfter(direction, '#topics-container .category-item[data-tid]', config.topicsPerPage, false, function(after, offset, el) {
+		infinitescroll.calculateAfter(direction, components.get('category/topic'), config.topicsPerPage, false, function(after, offset, el) {
 			loadTopicsAfter(after, function() {
 				if (direction < 0 && el) {
 					Category.scrollToTopic(el.attr('data-index'), null, 0, offset);
@@ -326,7 +327,7 @@ define('forum/category', [
 	};
 
 	function loadTopicsAfter(after, callback) {
-		if(!utils.isNumber(after) || (after === 0 && $('#topics-container li.category-item[data-index="0"]').length)) {
+		if(!utils.isNumber(after) || (after === 0 && components.get('category/topic', 'index', 0).length)) {
 			return;
 		}
 
@@ -342,7 +343,7 @@ define('forum/category', [
 					done();
 					callback();
 				});
-				$('#topics-container').attr('data-nextstart', data.nextStart);
+				$('[component="category"]').attr('data-nextstart', data.nextStart);
 			} else {
 				done();
 			}
diff --git a/public/src/client/categoryTools.js b/public/src/client/categoryTools.js
index 0e9bae608b..e711bce436 100644
--- a/public/src/client/categoryTools.js
+++ b/public/src/client/categoryTools.js
@@ -173,26 +173,26 @@ define('forum/categoryTools', ['forum/topic/move', 'topicSelect'], function(move
 	}
 
 	function getTopicEl(tid) {
-		return $('#topics-container li[data-tid="' + tid + '"]');
+		return components.get('category/topic', 'tid', tid);
 	}
 
 	function setDeleteState(data) {
 		var topic = getTopicEl(data.tid);
 		topic.toggleClass('deleted', data.isDeleted);
-		topic.find('.fa-lock').toggleClass('hide', !data.isDeleted);
+		topic.find('[component="topic/locked"]').toggleClass('hide', !data.isDeleted);
 	}
 
 	function setPinnedState(data) {
 		var topic = getTopicEl(data.tid);
 		topic.toggleClass('pinned', data.isPinned);
-		topic.find('.fa-thumb-tack').toggleClass('hide', !data.isPinned);
+		topic.find('[component="topic/pinned"]').toggleClass('hide', !data.isPinned);
 		ajaxify.refresh();
 	}
 
 	function setLockedState(data) {
 		var topic = getTopicEl(data.tid);
 		topic.toggleClass('locked', data.isLocked);
-		topic.find('.fa-lock').toggleClass('hide', !data.isLocked);
+		topic.find('[component="topic/locked"]').toggleClass('hide', !data.isLocked);
 	}
 
 	function onTopicMoved(data) {
diff --git a/public/src/client/chats.js b/public/src/client/chats.js
index fe491b49c3..9ea8beea57 100644
--- a/public/src/client/chats.js
+++ b/public/src/client/chats.js
@@ -20,10 +20,8 @@ define('forum/chats', ['string', 'sounds', 'forum/infinitescroll'], function(S,
 		Chats.addEventListeners();
 		Chats.setActive();
 
-		$(window).on('action:ajaxify.end', function() {
-			Chats.resizeMainWindow();
-			Chats.scrollToBottom(containerEl);
-		});
+		Chats.resizeMainWindow();
+		Chats.scrollToBottom($('.expanded-chat ul'));
 
 		Chats.initialised = true;
 	};
@@ -124,7 +122,7 @@ define('forum/chats', ['string', 'sounds', 'forum/infinitescroll'], function(S,
 	function onMessagesParsed(html) {
 		var newMessage = $(html);
 		newMessage.insertBefore($('.user-typing'));
-		newMessage.find('span.timeago').timeago();
+		newMessage.find('.timeago').timeago();
 		newMessage.find('img:not(".chat-user-image")').addClass('img-responsive');
 		Chats.scrollToBottom($('.expanded-chat .chat-content'));
 	}
diff --git a/public/src/client/notifications.js b/public/src/client/notifications.js
index fb778ec18f..4e7244f83f 100644
--- a/public/src/client/notifications.js
+++ b/public/src/client/notifications.js
@@ -16,7 +16,7 @@ define('forum/notifications', function() {
 			});
 		});
 
-		$('span.timeago').timeago();
+		$('.timeago').timeago();
 
 		$('.notifications .delete').on('click', function() {
 			socket.emit('notifications.markAllRead', function(err) {
diff --git a/public/src/client/recent.js b/public/src/client/recent.js
index 05e936a577..30d635ea0d 100644
--- a/public/src/client/recent.js
+++ b/public/src/client/recent.js
@@ -94,17 +94,17 @@ define('forum/recent', ['forum/infinitescroll', 'composer'], function(infinitesc
 	};
 
 	Recent.loadMoreTopics = function(direction) {
-		if(direction < 0 || !$('#topics-container').length) {
+		if(direction < 0 || !$('[component="category"]').length) {
 			return;
 		}
 
 		infinitescroll.loadMore('topics.loadMoreFromSet', {
-			after: $('#topics-container').attr('data-nextstart'),
+			after: $('[component="category"]').attr('data-nextstart'),
 			set: 'topics:recent'
 		}, function(data, done) {
 			if (data.topics && data.topics.length) {
 				Recent.onTopicsLoaded('recent', data.topics, false, done);
-				$('#topics-container').attr('data-nextstart', data.nextStart);
+				$('[component="category"]').attr('data-nextstart', data.nextStart);
 			} else {
 				done();
 			}
@@ -114,7 +114,7 @@ define('forum/recent', ['forum/infinitescroll', 'composer'], function(infinitesc
 	Recent.onTopicsLoaded = function(templateName, topics, showSelect, callback) {
 
 		topics = topics.filter(function(topic) {
-			return !$('#topics-container li[data-tid=' + topic.tid + ']').length;
+			return !components.get('category/topic', 'tid', topic.tid).length;
 		});
 
 		if (!topics.length) {
@@ -124,8 +124,8 @@ define('forum/recent', ['forum/infinitescroll', 'composer'], function(infinitesc
 		infinitescroll.parseAndTranslate(templateName, 'topics', {topics: topics, showSelect: showSelect}, function(html) {
 			$('#category-no-topics').remove();
 
-			$('#topics-container').append(html);
-			html.find('span.timeago').timeago();
+			$('[component="category"]').append(html);
+			html.find('.timeago').timeago();
 			app.createUserTooltips();
 			utils.makeNumbersHumanReadable(html.find('.human-readable-number'));
 			$(window).trigger('action:topics.loaded');
diff --git a/public/src/client/reset.js b/public/src/client/reset.js
index 7e272c3284..568d0739a1 100644
--- a/public/src/client/reset.js
+++ b/public/src/client/reset.js
@@ -24,6 +24,7 @@ define('forum/reset', function() {
 				successEl.addClass('hide').hide();
 				errorEl.removeClass('hide').show();
 			}
+			return false;
 		});
 	};
 
diff --git a/public/src/client/reset_code.js b/public/src/client/reset_code.js
index c0f46dc356..846feffb42 100644
--- a/public/src/client/reset_code.js
+++ b/public/src/client/reset_code.js
@@ -31,22 +31,8 @@ define('forum/reset_code', function() {
 					window.location.href = RELATIVE_PATH + '/login';
 				});
 			}
+			return false;
 		});
-
-		// socket.emit('user.reset.valid', reset_code, function(err, valid) {
-		// 	if(err) {
-		// 		return app.alertError(err.message);
-		// 	}
-
-		// 	if (valid) {
-		// 		resetEl.prop('disabled', false);
-		// 	} else {
-		// 		var formEl = $('#reset-form');
-		// 		// Show error message
-		// 		$('#error').show();
-		// 		formEl.remove();
-		// 	}
-		// });
 	};
 
 	return ResetCode;
diff --git a/public/src/client/tag.js b/public/src/client/tag.js
index 3565c7e151..7328934e2d 100644
--- a/public/src/client/tag.js
+++ b/public/src/client/tag.js
@@ -8,7 +8,7 @@ define('forum/tag', ['forum/recent', 'forum/infinitescroll'], function(recent, i
 	Tag.init = function() {
 		app.enterRoom('tags');
 
-		if ($('body').height() <= $(window).height() && $('#topics-container').children().length >= 20) {
+		if ($('body').height() <= $(window).height() && $('[component="category"]').children().length >= 20) {
 			$('#load-more-btn').show();
 		}
 
@@ -19,17 +19,17 @@ define('forum/tag', ['forum/recent', 'forum/infinitescroll'], function(recent, i
 		infinitescroll.init(loadMoreTopics);
 
 		function loadMoreTopics(direction) {
-			if(direction < 0 || !$('#topics-container').length) {
+			if(direction < 0 || !$('[component="category"]').length) {
 				return;
 			}
 
 			infinitescroll.loadMore('topics.loadMoreFromSet', {
 				set: 'tag:' + ajaxify.variables.get('tag') + ':topics',
-				after: $('#topics-container').attr('data-nextstart')
+				after: $('[component="category"]').attr('data-nextstart')
 			}, function(data, done) {
 				if (data.topics && data.topics.length) {
 					recent.onTopicsLoaded('tag', data.topics, false, done);
-					$('#topics-container').attr('data-nextstart', data.nextStart);
+					$('[component="category"]').attr('data-nextstart', data.nextStart);
 				} else {
 					done();
 					$('#load-more-btn').hide();
diff --git a/public/src/client/topic.js b/public/src/client/topic.js
index 9acd1a4d73..cbf4074d4f 100644
--- a/public/src/client/topic.js
+++ b/public/src/client/topic.js
@@ -1,7 +1,7 @@
 'use strict';
 
 
-/* globals define, app, templates, translator, socket, bootbox, config, ajaxify, RELATIVE_PATH, utils */
+/* globals define, app, components, templates, translator, socket, bootbox, config, ajaxify, RELATIVE_PATH, utils */
 
 define('forum/topic', [
 	'forum/pagination',
@@ -20,7 +20,7 @@ define('forum/topic', [
 	$(window).on('action:ajaxify.start', function(ev, data) {
 		if (ajaxify.currentPage !== data.url) {
 			navigator.hide();
-			$('.header-topic-title').find('span').text('').hide();
+			components.get('navbar/title').find('span').text('').hide();
 			app.removeAlert('bookmark');
 
 			events.removeListeners();
@@ -53,7 +53,7 @@ define('forum/topic', [
 
 		handleBookmark(tid);
 
-		navigator.init('.posts > .post-row', ajaxify.variables.get('postcount'), Topic.toTop, Topic.toBottom, Topic.navigatorCallback, Topic.calculateIndex);
+		navigator.init(components.get('post'), ajaxify.variables.get('postcount'), Topic.toTop, Topic.toBottom, Topic.navigatorCallback, Topic.calculateIndex);
 
 		$(window).on('scroll', updateTopicTitle);
 
@@ -112,7 +112,7 @@ define('forum/topic', [
 	}
 
 	function addBlockQuoteHandler() {
-		$('#post-container').on('click', 'blockquote .toggle', function() {
+		components.get('topic').on('click', 'blockquote .toggle', function() {
 			var blockQuote = $(this).parent('blockquote');
 			var toggle = $(this);
 			blockQuote.toggleClass('uncollapsed');
@@ -124,7 +124,7 @@ define('forum/topic', [
 
 	function enableInfiniteLoadingOrPagination() {
 		if(!config.usePagination) {
-			infinitescroll.init(posts.loadMorePosts, $('#post-container .post-row[data-index="0"]').height());
+			infinitescroll.init(posts.loadMorePosts, components.get('post', 'index', 0).height());
 		} else {
 			navigator.hide();
 
@@ -135,9 +135,9 @@ define('forum/topic', [
 
 	function updateTopicTitle() {
 		if($(window).scrollTop() > 50) {
-			$('.header-topic-title').find('span').text(ajaxify.variables.get('topic_name')).show();
+			components.get('navbar/title').find('span').text(ajaxify.variables.get('topic_name')).show();
 		} else {
-			$('.header-topic-title').find('span').text('').hide();
+			components.get('navbar/title').find('span').text('').hide();
 		}
 	}
 
@@ -148,18 +148,18 @@ define('forum/topic', [
 		return index;
 	};
 
-	Topic.navigatorCallback = function(element, elementCount) {
+	Topic.navigatorCallback = function(topPostIndex, bottomPostIndex, elementCount) {
 		var path = ajaxify.removeRelativePath(window.location.pathname.slice(1));
 		if (!path.startsWith('topic')) {
 			return 1;
 		}
-		var postIndex = parseInt(element.attr('data-index'), 10);
-		var index = postIndex + 1;
+		var postIndex = topPostIndex;
+		var index = bottomPostIndex;
 		if (config.topicPostSort !== 'oldest_to_newest') {
-			if (postIndex === 0) {
+			if (bottomPostIndex === 0) {
 				index = 1;
 			} else  {
-				index = Math.max(elementCount - postIndex + 1, 1);
+				index = Math.max(elementCount - bottomPostIndex + 2, 1);
 			}
 		}
 
@@ -175,8 +175,8 @@ define('forum/topic', [
 			var topicId = parts[1],
 				slug = parts[2];
 			var newUrl = 'topic/' + topicId + '/' + (slug ? slug : '');
-			if (postIndex > 0) {
-				newUrl += '/' + (postIndex + 1);
+			if (postIndex > 1) {
+				newUrl += '/' + postIndex;
 			}
 
 			if (newUrl !== currentUrl) {
diff --git a/public/src/client/topic/browsing.js b/public/src/client/topic/browsing.js
index 935439fa88..7c14fab158 100644
--- a/public/src/client/topic/browsing.js
+++ b/public/src/client/topic/browsing.js
@@ -10,7 +10,7 @@ define('forum/topic/browsing', function() {
 
 	Browsing.onUpdateUsersInRoom = function(data) {
 		if (data && data.room.indexOf('topic_' + ajaxify.variables.get('topic_id')) !== -1) {
-			$('.browsing-users').toggleClass('hidden', !data.users.length);
+			$('[component="topic/browsing/list"]').parent().toggleClass('hidden', !data.users.length);
 			for(var i=0; i<data.users.length; ++i) {
 				addUserIcon(data.users[i]);	
 			}
@@ -20,7 +20,7 @@ define('forum/topic/browsing', function() {
 	};
 
 	Browsing.onUserEnter = function(data) {
-		var activeEl = $('.thread_active_users');
+		var activeEl = $('[component="topic/browsing/list"]');
 		var user = activeEl.find('a[data-uid="' + data.uid + '"]');
 		if (!user.length && activeEl.first().children().length < 10) {
 			addUserIcon(data);
@@ -35,7 +35,7 @@ define('forum/topic/browsing', function() {
 		if (app.user.uid === parseInt(uid, 10)) {
 			return;
 		}
-		var user = $('.thread_active_users').find('a[data-uid="' + uid + '"]');
+		var user = $('[component="topic/browsing/list"]').find('a[data-uid="' + uid + '"]');
 		if (user.length) {
 			var count = Math.max(0, parseInt(user.attr('data-count'), 10) - 1);
 			user.attr('data-count', count);
@@ -63,7 +63,7 @@ define('forum/topic/browsing', function() {
 	}
 
 	function updateBrowsingUsers(data) {
-		var activeEl = $('.thread_active_users');
+		var activeEl = $('[component="topic/browsing/list"]');
 		var user = activeEl.find('a[data-uid="'+ data.uid + '"]');
 		if (user.length && data.status === 'offline') {
 			user.parent().remove();
@@ -74,7 +74,7 @@ define('forum/topic/browsing', function() {
 		if (!user.userslug) {
 			return;
 		}
-		var activeEl = $('.thread_active_users');
+		var activeEl = $('[component="topic/browsing/list"]');
 		var userEl = createUserIcon(user.uid, user.picture, user.userslug, user.username);
 		var isSelf = parseInt(user.uid, 10) === parseInt(app.user.uid, 10);
 		if (isSelf) {
@@ -89,7 +89,7 @@ define('forum/topic/browsing', function() {
 	}
 
 	function createUserIcon(uid, picture, userslug, username) {
-		if(!$('.thread_active_users').find('[data-uid="' + uid + '"]').length) {
+		if(!$('[component="topic/browsing/list"]').find('[data-uid="' + uid + '"]').length) {
 			return $('<div class="inline-block"><a data-uid="' + uid + '" data-count="1" href="' + config.relative_path + '/user/' + userslug + '"><img title="' + username + '" src="'+ picture +'"/></a></div>');
 		}
 	}
@@ -99,11 +99,11 @@ define('forum/topic/browsing', function() {
 		if (!count || count < 0) {
 			count = 0;
 		}
-		$('.user-count').text(count).parent().toggleClass('hidden', count === 0);
+		$('[component="topic/browsing/count"]').text(count).parent().toggleClass('hidden', count === 0);
 	}
 
 	function increaseUserCount(incr) {
-		updateUserCount(parseInt($('.user-count').first().text(), 10) + incr);
+		updateUserCount(parseInt($('[component="topic/browsing/count"]').first().text(), 10) + incr);
 	}
 
 	return Browsing;
diff --git a/public/src/client/topic/events.js b/public/src/client/topic/events.js
index c90dd149b5..c3ecf6d29a 100644
--- a/public/src/client/topic/events.js
+++ b/public/src/client/topic/events.js
@@ -1,7 +1,7 @@
 
 'use strict';
 
-/* globals app, ajaxify, define, socket, translator, templates */
+/* globals app, ajaxify, components, define, socket, translator, templates */
 
 define('forum/topic/events', [
 	'forum/topic/browsing',
@@ -69,7 +69,7 @@ define('forum/topic/events', [
 	};
 
 	function updatePostVotesAndUserReputation(data) {
-		var votes = $('[data-pid="' + data.post.pid + '"] .votes'),
+		var votes = components.get('post/vote-count', data.post.pid),
 			reputationElements = $('.reputation[data-uid="' + data.post.uid + '"]');
 
 		votes.html(data.post.votes).attr('data-votes', data.post.votes);
@@ -96,12 +96,12 @@ define('forum/topic/events', [
 	}
 
 	function onPostEdited(data) {
-		var editedPostEl = $('#content_' + data.pid),
-			editedPostTitle = $('#topic_title_' + data.pid);
+		var editedPostEl = components.get('post/content', data.pid),
+			editedPostHeader = components.get('post/header', data.pid);
 
-		if (editedPostTitle.length) {
-			editedPostTitle.fadeOut(250, function() {
-				editedPostTitle.html(data.title).fadeIn(250);
+		if (editedPostHeader.length) {
+			editedPostHeader.fadeOut(250, function() {
+				editedPostHeader.html(data.title).fadeIn(250);
 			});
 		}
 
@@ -139,14 +139,15 @@ define('forum/topic/events', [
 	}
 
 	function onPostPurged(pid) {
-		$('#post-container [data-pid="' + pid + '"]').fadeOut(500, function() {
+		components.get('post', 'pid', pid).fadeOut(500, function() {
 			$(this).remove();
 		});
+
 		postTools.updatePostCount();
 	}
 
 	function togglePostDeleteState(data) {
-		var postEl = $('#post-container [data-pid="' + data.pid + '"]');
+		var postEl = components.get('post', 'pid', data.pid);
 
 		if (!postEl.length) {
 			return;
@@ -158,9 +159,9 @@ define('forum/topic/events', [
 
 		if (!app.user.isAdmin && parseInt(data.uid, 10) !== parseInt(app.user.uid, 10)) {
 			if (isDeleted) {
-				postEl.find('.post-content').translateHtml('[[topic:post_is_deleted]]');
+				postEl.find('[component="post/content"]').translateHtml('[[topic:post_is_deleted]]');
 			} else {
-				postEl.find('.post-content').html(data.content);
+				postEl.find('[component="post/content"]').html(data.content);
 			}
 		}
 	}
diff --git a/public/src/client/topic/fork.js b/public/src/client/topic/fork.js
index 216cc63f9f..91ec8a2be9 100644
--- a/public/src/client/topic/fork.js
+++ b/public/src/client/topic/fork.js
@@ -1,6 +1,6 @@
 'use strict';
 
-/* globals define, app, ajaxify, translator, socket */
+/* globals define, app, ajaxify, components, translator, socket */
 
 define('forum/topic/fork', function() {
 
@@ -10,7 +10,7 @@ define('forum/topic/fork', function() {
 		pids = [];
 
 	Fork.init = function() {
-		$('.fork_thread').on('click', onForkThreadClicked);
+		components.get('topic/fork').on('click', onForkThreadClicked);
 	};
 
 	function disableClicks() {
@@ -18,11 +18,11 @@ define('forum/topic/fork', function() {
 	}
 
 	function disableClicksOnPosts() {
-		$('.post-row').on('click', 'button,a', disableClicks);
+		components.get('post').on('click', 'button,a', disableClicks);
 	}
 
 	function enableClicksOnPosts() {
-		$('.post-row').off('click', 'button,a', disableClicks);
+		components.get('post').off('click', 'button,a', disableClicks);
 	}
 
 	function onForkThreadClicked() {
@@ -35,7 +35,7 @@ define('forum/topic/fork', function() {
 
 		forkModal.find('.close,#fork_thread_cancel').on('click', closeForkModal);
 		forkModal.find('#fork-title').on('change', checkForkButtonEnable);
-		$('#post-container').on('click', '[data-pid]', function() {
+		components.get('topic').on('click', '[data-pid]', function() {
 			togglePostSelection($(this));
 		});
 
@@ -58,7 +58,7 @@ define('forum/topic/fork', function() {
 			pids: pids
 		}, function(err, newTopic) {
 			function fadeOutAndRemove(pid) {
-				$('#post-container [data-pid="' + pid + '"]').fadeOut(500, function() {
+				components.get('post', 'pid', pid).fadeOut(500, function() {
 					$(this).remove();
 				});
 			}
@@ -125,10 +125,11 @@ define('forum/topic/fork', function() {
 
 	function closeForkModal() {
 		for(var i=0; i<pids.length; ++i) {
-			$('#post-container [data-pid="' + pids[i] + '"]').css('opacity', 1);
+			components.get('post', 'pid', pids[i]).css('opacity', 1);
 		}
+
 		forkModal.addClass('hide');
-		$('#post-container').off('click', '[data-pid]');
+		components.get('topic').off('click', '[data-pid]');
 		enableClicksOnPosts();
 	}
 
diff --git a/public/src/client/topic/postTools.js b/public/src/client/topic/postTools.js
index 5e6df47e8b..4e561b036c 100644
--- a/public/src/client/topic/postTools.js
+++ b/public/src/client/topic/postTools.js
@@ -1,6 +1,6 @@
 'use strict';
 
-/* globals define, app, utils, templates, translator, ajaxify, socket, bootbox */
+/* globals define, app, ajaxify, bootbox, components, socket, templates, translator, utils */
 
 define('forum/topic/postTools', ['composer', 'share', 'navigator'], function(composer, share, navigator) {
 
@@ -18,12 +18,14 @@ define('forum/topic/postTools', ['composer', 'share', 'navigator'], function(com
 	};
 
 	PostTools.toggle = function(pid, isDeleted) {
-		var postEl = $('#post-container li[data-pid="' + pid + '"]');
+		var postEl = components.get('post', 'pid', pid);
 
-		postEl.find('.quote, .favourite, .post_reply, .chat, .flag').toggleClass('hidden', isDeleted);
-		postEl.find('.purge').toggleClass('hidden', !isDeleted);
-		postEl.find('.delete .i').toggleClass('fa-trash-o', !isDeleted).toggleClass('fa-history', isDeleted);
-		postEl.find('.delete span').translateHtml(isDeleted ? ' [[topic:restore]]' : ' [[topic:delete]]');
+		postEl.find('[component="post/quote"], [component="post/favourite"], [component="post/reply"], [component="post/flag"], [component="user/chat"]')
+			.toggleClass('hidden', isDeleted);
+
+		postEl.find('[component="post/purge"]').toggleClass('hidden', !isDeleted);
+		postEl.find('[component="post/delete"] .i').toggleClass('fa-trash-o', !isDeleted).toggleClass('fa-history', isDeleted);
+		postEl.find('[component="post/delete"] span').translateHtml(isDeleted ? ' [[topic:restore]]' : ' [[topic:delete]]');
 	};
 
 	PostTools.updatePostCount = function() {
@@ -38,13 +40,13 @@ define('forum/topic/postTools', ['composer', 'share', 'navigator'], function(com
 	};
 
 	function addVoteHandler() {
-		$('#post-container').on('mouseenter', '.post-row .votes', function() {
+		components.get('topic').on('mouseenter', '[data-pid] .votes', function() {
 			loadDataAndCreateTooltip($(this));
 		});
 	}
 
 	function loadDataAndCreateTooltip(el) {
-		var pid = el.parents('.post-row').attr('data-pid');
+		var pid = el.parents('[data-pid]').attr('data-pid');
 		socket.emit('posts.getUpvoters', [pid], function(err, data) {
 			if (!err && data.length) {
 				createTooltip(el, data[0]);
@@ -70,57 +72,67 @@ define('forum/topic/postTools', ['composer', 'share', 'navigator'], function(com
 	}
 
 	function addPostHandlers(tid, threadState) {
-		$('.topic').on('click', '.post_reply', function() {
-			if (!threadState.locked) {
-				onReplyClicked($(this), tid, topicName);
+		function canPost() {
+			return !threadState.locked || app.user.isAdmin;
+		}
+
+		var postContainer = components.get('topic');
+
+		postContainer.on('click', '[component="post/quote"]', function() {
+			if (canPost()) {
+				onQuoteClicked($(this), tid, topicName);
 			}
 		});
 
-		var postContainer = $('#post-container');
+		postContainer.on('click', '[component="post/reply"]', function() {
+			if (canPost()) {
+				onReplyClicked($(this), tid, topicName);
+			}
+		});
 
-		postContainer.on('click', '.quote', function() {
-			if (!threadState.locked) {
-				onQuoteClicked($(this), tid, topicName);
+		components.get('topic/reply').on('click', function() {
+			if (canPost()) {
+				onReplyClicked($(this), tid, topicName);
 			}
 		});
 
-		postContainer.on('click', '.favourite', function() {
+		postContainer.on('click', '[component="post/favourite"]', function() {
 			favouritePost($(this), getData($(this), 'data-pid'));
 		});
 
-		postContainer.on('click', '.upvote', function() {
+		postContainer.on('click', '[component="post/upvote"]', function() {
 			return toggleVote($(this), '.upvoted', 'posts.upvote');
 		});
 
-		postContainer.on('click', '.downvote', function() {
+		postContainer.on('click', '[component="post/downvote"]', function() {
 			return toggleVote($(this), '.downvoted', 'posts.downvote');
 		});
 
-		postContainer.on('click', '.votes', function() {
+		postContainer.on('click', '[component="post/vote-count"]', function() {
 			showVotes(getData($(this), 'data-pid'));
 		});
 
-		postContainer.on('click', '.flag', function() {
+		postContainer.on('click', '[component="post/flag"]', function() {
 			flagPost(getData($(this), 'data-pid'));
 		});
 
-		postContainer.on('click', '.edit', function(e) {
+		postContainer.on('click', '[component="post/edit"]', function(e) {
 			composer.editPost(getData($(this), 'data-pid'));
 		});
 
-		postContainer.on('click', '.delete', function(e) {
+		postContainer.on('click', '[component="post/delete"]', function(e) {
 			deletePost($(this), tid);
 		});
 
-		postContainer.on('click', '.purge', function(e) {
+		postContainer.on('click', '[component="post/purge"]', function(e) {
 			purgePost($(this), tid);
 		});
 
-		postContainer.on('click', '.move', function(e) {
+		postContainer.on('click', '[component="post/move"]', function(e) {
 			openMovePostModal($(this));
 		});
 
-		postContainer.on('click', '.chat', function(e) {
+		postContainer.on('click', '[component="user/chat"]', function(e) {
 			openChat($(this));
 		});
 	}
@@ -129,7 +141,7 @@ define('forum/topic/postTools', ['composer', 'share', 'navigator'], function(com
 		var selectionText = '',
 			selection = window.getSelection ? window.getSelection() : document.selection.createRange();
 
-		if ($(selection.baseNode).parents('.post-content').length > 0) {
+		if ($(selection.baseNode).parents('[component="post/content"]').length > 0) {
 			var snippet = selection.toString();
 			if (snippet.length) {
 				selectionText = '> ' + snippet.replace(/\n/g, '\n> ') + '\n\n';
@@ -185,7 +197,7 @@ define('forum/topic/postTools', ['composer', 'share', 'navigator'], function(com
 	}
 
 	function toggleVote(button, className, method) {
-		var post = button.parents('.post-row'),
+		var post = button.parents('[data-pid]'),
 			currentState = post.find(className).length;
 
 		socket.emit(currentState ? 'posts.unvote' : method , {
@@ -222,7 +234,7 @@ define('forum/topic/postTools', ['composer', 'share', 'navigator'], function(com
 	}
 
 	function getData(button, data) {
-		return button.parents('.post-row').attr(data);
+		return button.parents('[data-pid]').attr(data);
 	}
 
 	function getUserName(button) {
@@ -241,7 +253,7 @@ define('forum/topic/postTools', ['composer', 'share', 'navigator'], function(com
 
 	function deletePost(button, tid) {
 		var pid = getData(button, 'data-pid'),
-			postEl = $('#post-container li[data-pid="' + pid + '"]'),
+			postEl = components.get('post', 'pid', pid),
 			action = !postEl.hasClass('deleted') ? 'delete' : 'restore';
 
 		postAction(action, pid, tid);
@@ -290,7 +302,7 @@ define('forum/topic/postTools', ['composer', 'share', 'navigator'], function(com
 		});
 
 		moveBtn.on('click', function() {
-			movePost(button.parents('.post-row'), getData(button, 'data-pid'), topicId.val());
+			movePost(button.parents('[data-pid]'), getData(button, 'data-pid'), topicId.val());
 		});
 	}
 
@@ -338,7 +350,7 @@ define('forum/topic/postTools', ['composer', 'share', 'navigator'], function(com
 	}
 
 	function openChat(button) {
-		var post = button.parents('li.post-row');
+		var post = button.parents('data-pid');
 
 		app.openChat(post.attr('data-username'), post.attr('data-uid'));
 		button.parents('.btn-group').find('.dropdown-toggle').click();
diff --git a/public/src/client/topic/posts.js b/public/src/client/topic/posts.js
index 8731121de1..c2ea3940ff 100644
--- a/public/src/client/topic/posts.js
+++ b/public/src/client/topic/posts.js
@@ -1,6 +1,6 @@
 'use strict';
 
-/* globals config, app, ajaxify, define, socket, utils */
+/* globals config, app, ajaxify, components, define, socket, utils */
 
 define('forum/topic/posts', [
 	'forum/pagination',
@@ -22,11 +22,11 @@ define('forum/topic/posts', [
 		}
 
 		for (var i=0; i<data.posts.length; ++i) {
-			var postcount = $('.user_postcount_' + data.posts[i].uid);
+			var postcount = components.get('user/postcount', data.posts[i].uid);
 			postcount.html(parseInt(postcount.html(), 10) + 1);
 		}
 
-		createNewPosts(data, '.post-row[data-index!="0"]', function(html) {
+		createNewPosts(data, components.get('post').not('[data-index=0]'), function(html) {
 			if (html) {
 				html.addClass('new');
 			}
@@ -43,7 +43,7 @@ define('forum/topic/posts', [
 		pagination.pageCount = Math.max(1, Math.ceil((posts[0].topic.postcount - 1) / config.postsPerPage));
 
 		if (pagination.currentPage === pagination.pageCount) {
-			createNewPosts(data, '.post-row[data-index!="0"]', scrollToPost);
+			createNewPosts(data, components.get('post').not('[data-index=0]'), scrollToPost);
 		} else if (parseInt(posts[0].uid, 10) === parseInt(app.user.uid, 10)) {
 			pagination.loadPage(pagination.pageCount, scrollToPost);
 		}
@@ -57,7 +57,7 @@ define('forum/topic/posts', [
 
 		function removeAlreadyAddedPosts() {
 			data.posts = data.posts.filter(function(post) {
-				return $('#post-container [data-pid="' + post.pid +'"]').length === 0;
+				return components.get('post', 'pid', post.pid).length === 0;
 			});
 		}
 
@@ -116,7 +116,7 @@ define('forum/topic/posts', [
 				// Save document height and position for future reference (about 5 lines down)
 				var height = $(document).height(),
 					scrollTop = $(document).scrollTop(),
-					originalPostEl = $('.post-row[data-index="0"]');
+					originalPostEl = components.get('post', 'index', 0);
 
 				// Insert the new post
 				html.insertBefore(before);
@@ -127,7 +127,7 @@ define('forum/topic/posts', [
 					$(document).scrollTop(scrollTop + ($(document).height() - height));
 				}
 			} else {
-				$('#post-container').append(html);
+				components.get('topic').append(html);
 			}
 
 			html.hide().fadeIn('slow');
@@ -164,34 +164,35 @@ define('forum/topic/posts', [
 	}
 
 	function toggleModTools(pid, privileges) {
-		var postEl = $('.post-row[data-pid="' + pid + '"]');
+		var postEl = components.get('post', 'pid', pid),
+			isSelfPost = parseInt(postEl.attr('data-uid'), 10) === parseInt(app.user.uid, 10);
 
 		if (!privileges.editable) {
-			postEl.find('.edit, .delete, .purge').remove();
+			postEl.find('[component="post/edit"], [component="post/delete"], [component="post/purge"]').remove();
 		}
+
 		if (!privileges.move) {
-			postEl.find('.move').remove();
+			postEl.find('[component="post/move"]').remove();
 		}
-		postEl.find('.reply, .quote').toggleClass('hidden', !$('.post_reply').length);
-		var isSelfPost = parseInt(postEl.attr('data-uid'), 10) === parseInt(app.user.uid, 10);
-		postEl.find('.chat, .flag').toggleClass('hidden', isSelfPost || !app.user.uid);
+
+		postEl.find('[component="user/chat"], [component="post/flag"]').toggleClass('hidden', isSelfPost || !app.user.uid);
 	}
 
 	Posts.loadMorePosts = function(direction) {
-		if (!$('#post-container').length || navigator.scrollActive) {
+		if (!components.get('topic').length || navigator.scrollActive) {
 			return;
 		}
 
 		var reverse = config.topicPostSort === 'newest_to_oldest' || config.topicPostSort === 'most_votes';
 
-		infinitescroll.calculateAfter(direction, '#post-container .post-row[data-index!="0"]:not(.new)', config.postsPerPage, reverse, function(after, offset, el) {
+		infinitescroll.calculateAfter(direction, components.get('topic').find('[data-index][data-index!="0"]:not(.new)'), config.postsPerPage, reverse, function(after, offset, el) {
 			loadPostsAfter(after);
 		});
 	};
 
 	function loadPostsAfter(after) {
 		var tid = ajaxify.variables.get('topic_id');
-		if (!utils.isNumber(tid) || !utils.isNumber(after) || (after === 0 && $('#post-container .post-row[data-index="1"]').length)) {
+		if (!utils.isNumber(tid) || !utils.isNumber(after) || (after === 0 && components.get('post', 'index', 1).length)) {
 			return;
 		}
 
@@ -208,7 +209,7 @@ define('forum/topic/posts', [
 			indicatorEl.fadeOut();
 
 			if (data && data.posts && data.posts.length) {
-				createNewPosts(data, '.post-row[data-index!="0"]:not(.new)', done);
+				createNewPosts(data, components.get('post').not('[data-index=0]').not('.new'), done);
 			} else {
 				if (app.user.uid) {
 					socket.emit('topics.markAsRead', [tid]);
@@ -224,27 +225,27 @@ define('forum/topic/posts', [
 		app.replaceSelfLinks(element.find('a'));
 		utils.addCommasToNumbers(element.find('.formatted-number'));
 		utils.makeNumbersHumanReadable(element.find('.human-readable-number'));
-		element.find('span.timeago').timeago();
-		element.find('.post-content img:not(.emoji)').addClass('img-responsive').each(function() {
+		element.find('.timeago').timeago();
+		element.find('[component="post/content"] img:not(.emoji)').addClass('img-responsive').each(function() {
 			var $this = $(this);
 			if (!$this.parent().is('a')) {
 				$this.wrap('<a href="' + $this.attr('src') + '" target="_blank">');
 			}
 		});
 		postTools.updatePostCount();
-		addBlockquoteEllipses(element.find('.post-content > blockquote'));
+		addBlockquoteEllipses(element.find('[component="post/content"] > blockquote'));
 		hidePostToolsForDeletedPosts(element);
 		showBottomPostBar();
 	};
 
 	function showBottomPostBar() {
-		if($('#post-container .post-row').length > 1 || !$('#post-container [data-index="0"]').length) {
+		if(components.get('post').length > 1 || !components.get('post', 'index', 0).length) {
 			$('.bottom-post-bar').removeClass('hide');
 		}
 	}
 
 	function hidePostToolsForDeletedPosts(element) {
-		element.find('.post-row.deleted').each(function() {
+		element.find('[data-pid].deleted').each(function() {
 			postTools.toggle($(this).attr('data-pid'), true);
 		});
 	}
diff --git a/public/src/client/topic/threadTools.js b/public/src/client/topic/threadTools.js
index c3923e2720..0d10cd844c 100644
--- a/public/src/client/topic/threadTools.js
+++ b/public/src/client/topic/threadTools.js
@@ -1,6 +1,6 @@
 'use strict';
 
-/* globals define, app, translator, ajaxify, socket, bootbox */
+/* globals define, app, components, translator, ajaxify, socket, bootbox */
 
 define('forum/topic/threadTools', ['forum/topic/fork', 'forum/topic/move'], function(fork, move) {
 
@@ -21,27 +21,27 @@ define('forum/topic/threadTools', ['forum/topic/fork', 'forum/topic/move'], func
 			ThreadTools.setPinnedState({tid: tid, isPinned: true});
 		}
 
-		$('.delete_thread').on('click', function() {
+		components.get('topic/delete').on('click', function() {
 			topicCommand(threadState.deleted ? 'restore' : 'delete', tid);
 			return false;
 		});
 
-		$('.purge_thread').on('click', function() {
+		components.get('topic/purge').on('click', function() {
 			topicCommand('purge', tid);
 			return false;
 		});
 
-		$('.lock_thread').on('click', function() {
+		components.get('topic/lock').on('click', function() {
 			socket.emit(threadState.locked ? 'topics.unlock' : 'topics.lock', {tids: [tid], cid: ajaxify.variables.get('category_id')});
 			return false;
 		});
 
-		$('.pin_thread').on('click', function() {
+		components.get('topic/pin').on('click', function() {
 			socket.emit(threadState.pinned ? 'topics.unpin' : 'topics.pin', {tids: [tid], cid: ajaxify.variables.get('category_id')});
 			return false;
 		});
 
-		$('.markAsUnreadForAll').on('click', function() {
+		components.get('topic/mark-unread-for-all').on('click', function() {
 			var btn = $(this);
 			socket.emit('topics.markAsUnreadForAll', [tid], function(err) {
 				if(err) {
@@ -53,14 +53,14 @@ define('forum/topic/threadTools', ['forum/topic/fork', 'forum/topic/move'], func
 			return false;
 		});
 
-		$('.move_thread').on('click', function(e) {
+		components.get('topic/move').on('click', function(e) {
 			move.init([tid], ajaxify.variables.get('category_id'));
 			return false;
 		});
 
 		fork.init();
 
-		$('.posts').on('click', '.follow', function() {
+		components.get('topic').on('click', '[component="topic/follow"]', function() {
 			socket.emit('topics.toggleFollow', tid, function(err, state) {
 				if(err) {
 					return app.alert({
@@ -97,35 +97,36 @@ define('forum/topic/threadTools', ['forum/topic/fork', 'forum/topic/move'], func
 	}
 
 	ThreadTools.setLockedState = function(data) {
-		var threadEl = $('#post-container');
+		var threadEl = components.get('topic');
 		if (parseInt(data.tid, 10) === parseInt(threadEl.attr('data-tid'), 10)) {
 			var isLocked = data.isLocked && !app.user.isAdmin;
 
-			$('.lock_thread').translateHtml('<i class="fa fa-fw fa-' + (data.isLocked ? 'un': '') + 'lock"></i> [[topic:thread_tools.' + (data.isLocked ? 'un': '') + 'lock]]');
+			components.get('topic/lock').translateHtml('<i class="fa fa-fw fa-' + (data.isLocked ? 'un': '') + 'lock"></i> [[topic:thread_tools.' + (data.isLocked ? 'un': '') + 'lock]]');
 
 			translator.translate(isLocked ? '[[topic:locked]]' : '[[topic:reply]]', function(translated) {
 				var className = isLocked ? 'fa-lock' : 'fa-reply';
-				threadEl.find('.post_reply').html('<i class="fa ' + className + '"></i> ' + translated);
-				$('.topic-main-buttons .post_reply').attr('disabled', isLocked).html(isLocked ? '<i class="fa fa-lock"></i> ' + translated : translated);
+				threadEl.find('[component="post/reply"]').html('<i class="fa ' + className + '"></i> ' + translated).attr('disabled', isLocked);
+				$('[component="topic/reply"]').attr('disabled', isLocked).html(isLocked ? '<i class="fa fa-lock"></i> ' + translated : translated);
 			});
 
-			threadEl.find('.quote, .edit, .delete').toggleClass('hidden', isLocked);
-			$('.topic-title i.fa-lock').toggleClass('hide', !data.isLocked);
+			threadEl.find('[component="post/quote"], [component="post/edit"], [component="post/delete"]').toggleClass('hidden', isLocked);
+			$('[component="post/header"] i.fa-lock').toggleClass('hide', !data.isLocked);
 			ThreadTools.threadState.locked = data.isLocked;
 		}
 	};
 
 	ThreadTools.setDeleteState = function(data) {
-		var threadEl = $('#post-container');
+		var threadEl = components.get('topic');
 		if (parseInt(data.tid, 10) !== parseInt(threadEl.attr('data-tid'), 10)) {
 			return;
 		}
 
-		$('.delete_thread span').translateHtml('<i class="fa fa-fw ' + (data.isDelete ? 'fa-history' : 'fa-trash-o') + '"></i> [[topic:thread_tools.' + (data.isDelete ? 'restore' : 'delete') + ']]');
+		components.get('topic/delete').translateHtml('<i class="fa fa-fw ' + (data.isDelete ? 'fa-history' : 'fa-trash-o') + '"></i> [[topic:thread_tools.' + (data.isDelete ? 'restore' : 'delete') + ']]');
 
 		threadEl.toggleClass('deleted', data.isDelete);
 		ThreadTools.threadState.deleted = data.isDelete;
-		$('.purge_thread').toggleClass('hidden', !data.isDelete);
+
+		components.get('topic/purge').toggleClass('hidden', !data.isDelete);
 
 		if (data.isDelete) {
 			translator.translate('[[topic:deleted_message]]', function(translated) {
@@ -137,13 +138,13 @@ define('forum/topic/threadTools', ['forum/topic/fork', 'forum/topic/move'], func
 	};
 
 	ThreadTools.setPinnedState = function(data) {
-		var threadEl = $('#post-container');
+		var threadEl = components.get('topic');
 		if (parseInt(data.tid, 10) === parseInt(threadEl.attr('data-tid'), 10)) {
 			translator.translate('<i class="fa fa-fw fa-thumb-tack"></i> [[topic:thread_tools.' + (data.isPinned ? 'unpin' : 'pin') + ']]', function(translated) {
-				$('.pin_thread').html(translated);
+				components.get('topic/pin').html(translated);
 				ThreadTools.threadState.pinned = data.isPinned;
 			});
-			$('.topic-title i.fa-thumb-tack').toggleClass('hide', !data.isPinned);
+			$('[component="post/header"] i.fa-thumb-tack').toggleClass('hide', !data.isPinned);
 		}
 	};
 
@@ -152,7 +153,7 @@ define('forum/topic/threadTools', ['forum/topic/fork', 'forum/topic/move'], func
 		var iconClass = state ? 'fa fa-eye-slash' : 'fa fa-eye';
 		var text = state ? '[[topic:unwatch]]' : '[[topic:watch]]';
 
-		var followEl = $('.posts .follow');
+		var followEl = components.get('topic/follow');
 
 		translator.translate(title, function(titleTranslated) {
 			followEl.attr('title', titleTranslated).find('i').attr('class', iconClass);
diff --git a/public/src/client/unread.js b/public/src/client/unread.js
index 7e08037f5a..c8c1c96de9 100644
--- a/public/src/client/unread.js
+++ b/public/src/client/unread.js
@@ -42,7 +42,7 @@ define('forum/unread', ['forum/recent', 'topicSelect', 'forum/infinitescroll'],
 
 				app.alertSuccess('[[unread:topics_marked_as_read.success]]');
 
-				$('#topics-container').empty();
+				$('[component="category"]').empty();
 				$('#category-no-topics').removeClass('hidden');
 				$('.markread').addClass('hidden');
 			});
@@ -51,7 +51,7 @@ define('forum/unread', ['forum/recent', 'topicSelect', 'forum/infinitescroll'],
 		$('.markread').on('click', '.category', function() {
 			function getCategoryTids(cid) {
 				var tids = [];
-				$('#topics-container .category-item[data-cid="' + cid + '"]').each(function() {
+				components.get('category/topic', 'cid', cid).each(function() {
 					tids.push($(this).attr('data-tid'));
 				});
 				return tids;
@@ -72,7 +72,7 @@ define('forum/unread', ['forum/recent', 'topicSelect', 'forum/infinitescroll'],
 
 		topicSelect.init();
 
-		if ($("body").height() <= $(window).height() && $('#topics-container').children().length >= 20) {
+		if ($("body").height() <= $(window).height() && $('[component="category"]').children().length >= 20) {
 			$('#load-more-btn').show();
 		}
 
@@ -83,16 +83,16 @@ define('forum/unread', ['forum/recent', 'topicSelect', 'forum/infinitescroll'],
 		infinitescroll.init(loadMoreTopics);
 
 		function loadMoreTopics(direction) {
-			if(direction < 0 || !$('#topics-container').length) {
+			if(direction < 0 || !$('[component="category"]').length) {
 				return;
 			}
 
 			infinitescroll.loadMore('topics.loadMoreUnreadTopics', {
-				after: $('#topics-container').attr('data-nextstart')
+				after: $('[component="category"]').attr('data-nextstart')
 			}, function(data, done) {
 				if (data.topics && data.topics.length) {
 					recent.onTopicsLoaded('unread', data.topics, true, done);
-					$('#topics-container').attr('data-nextstart', data.nextStart);
+					$('[component="category"]').attr('data-nextstart', data.nextStart);
 				} else {
 					done();
 					$('#load-more-btn').hide();
@@ -106,7 +106,7 @@ define('forum/unread', ['forum/recent', 'topicSelect', 'forum/infinitescroll'],
 
 		app.alertSuccess('[[unread:topics_marked_as_read.success]]');
 
-		if (!$('#topics-container').children().length) {
+		if (!$('[component="category"]').children().length) {
 			$('#category-no-topics').removeClass('hidden');
 			$('.markread').addClass('hidden');
 		}
@@ -114,7 +114,7 @@ define('forum/unread', ['forum/recent', 'topicSelect', 'forum/infinitescroll'],
 
 	function removeTids(tids) {
 		for(var i=0; i<tids.length; ++i) {
-			$('#topics-container .category-item[data-tid="' + tids[i] + '"]').remove();
+			components.get('category/topic', 'tid', tids[i]).remove();
 		}
 	}
 
diff --git a/public/src/components.js b/public/src/components.js
new file mode 100644
index 0000000000..bef3c95117
--- /dev/null
+++ b/public/src/components.js
@@ -0,0 +1,47 @@
+"use strict";
+
+var components = components || {};
+
+(function() {
+	components.core = {
+		'post': function(name, value) {
+			return $('[data-' + name + '="' + value + '"]');
+		},
+		'post/content': function(pid) {
+			return components.core.post('pid', pid).find('[component="post/content"]');
+		},
+		'post/header': function(pid) {
+			return components.core.post('pid', pid).find('[component="post/header"]');
+		},
+		'post/anchor': function(index) {
+			return components.core.post('index', index).find('[component="post/anchor"]');
+		},
+		'post/vote-count': function(pid) {
+			return components.core.post('pid', pid).find('[component="post/vote-count"]');
+		},
+		'post/favourite-count': function(pid) {
+			return components.core.post('pid', pid).find('[component="post/favourite-count"]');
+		},
+
+		'user/postcount': function(uid) {
+			return $('[component="user/postcount"][data-uid="' + uid + '"]');
+		},
+		'user/reputation': function(uid) {
+			return $('[component="user/reputation"][data-uid="' + uid + '"]');
+		},
+
+		'category/topic': function(name, value) {
+			return $('[data-' + name + '="' + value + '"]');
+		}
+	};
+
+	components.get = function() {
+		var args = Array.prototype.slice.call(arguments, 1);
+
+		if (components.core[arguments[0]] && args.length) {
+			return components.core[arguments[0]].apply(this, args);
+		} else {
+			return $('[component="' + arguments[0] + '"]');
+		}
+	};
+}());
\ No newline at end of file
diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js
index 5d60f2d411..760f19ca20 100644
--- a/public/src/modules/chat.js
+++ b/public/src/modules/chat.js
@@ -406,7 +406,7 @@ define('chat', ['taskbar', 'string', 'sounds', 'forum/chats'], function(taskbar,
 		Chats.parseMessage(data, function(html) {
 			var message = $(html);
 			message.find('img:not(".chat-user-image")').addClass('img-responsive');
-			message.find('span.timeago').timeago();
+			message.find('.timeago').timeago();
 			message.insertBefore(typingNotif);
 			Chats.scrollToBottom(chatContent);
 
diff --git a/public/src/modules/composer.js b/public/src/modules/composer.js
index 71706704cb..33fa2ac208 100644
--- a/public/src/modules/composer.js
+++ b/public/src/modules/composer.js
@@ -43,6 +43,12 @@ define('composer', [
 		}
 	});
 
+	$(window).on('action:composer.topics.post', function(ev, data) {
+		localStorage.removeItem('category:' + data.data.cid + ':bookmark');
+		localStorage.removeItem('category:' + data.data.cid + ':bookmark:clicked');
+		ajaxify.go('topic/' + data.data.slug);
+	});
+
 	// Query server for formatting options
 	socket.emit('modules.composer.getFormattingOptions', function(err, options) {
 		composer.formatting = options;
@@ -131,12 +137,18 @@ define('composer', [
 	};
 
 	composer.newTopic = function(cid) {
-		push({
-			cid: cid,
-			title: '',
-			body: '',
-			modified: false,
-			isMain: true
+		socket.emit('categories.isModerator', cid, function(err, isMod) {
+			if (err) {
+				return app.alertError(err.message);
+			}
+			push({
+				cid: cid,
+				title: '',
+				body: '',
+				modified: false,
+				isMain: true,
+				isMod: isMod
+			});
 		});
 	};
 
@@ -166,14 +178,20 @@ define('composer', [
 	};
 
 	composer.newReply = function(tid, pid, title, text) {
-		translator.translate(text, config.defaultLang, function(translated) {
-			push({
-				tid: tid,
-				toPid: pid,
-				title: title,
-				body: translated,
-				modified: false,
-				isMain: false
+		socket.emit('topics.isModerator', tid, function(err, isMod) {
+			if (err) {
+				return app.alertError(err.message);
+			}
+			translator.translate(text, config.defaultLang, function(translated) {
+				push({
+					tid: tid,
+					toPid: pid,
+					title: title,
+					body: translated,
+					modified: false,
+					isMain: false,
+					isMod: isMod
+				});
 			});
 		});
 	};
@@ -247,11 +265,13 @@ define('composer', [
 	}
 
 	function createNewComposer(post_uuid) {
-		var allowTopicsThumbnail = config.allowTopicsThumbnail && composer.posts[post_uuid].isMain && (config.hasImageUploadPlugin || config.allowFileUploads),
-			isTopic = composer.posts[post_uuid] ? !!composer.posts[post_uuid].cid : false,
-			isMain = composer.posts[post_uuid] ? !!composer.posts[post_uuid].isMain : false,
-			isEditing = composer.posts[post_uuid] ? !!composer.posts[post_uuid].pid : false,
-			isGuestPost = composer.posts[post_uuid] ? parseInt(composer.posts[post_uuid].uid, 10) === 0 : null;
+		var postData = composer.posts[post_uuid];
+
+		var allowTopicsThumbnail = config.allowTopicsThumbnail && postData.isMain && (config.hasImageUploadPlugin || config.allowFileUploads),
+			isTopic = postData ? !!postData.cid : false,
+			isMain = postData ? !!postData.isMain : false,
+			isEditing = postData ? !!postData.pid : false,
+			isGuestPost = postData ? parseInt(postData.uid, 10) === 0 : false;
 
 		composer.bsEnvironment = utils.findBootstrapEnvironment();
 
@@ -262,9 +282,11 @@ define('composer', [
 			minimumTagLength: config.minimumTagLength,
 			maximumTagLength: config.maximumTagLength,
 			isTopic: isTopic,
-			showHandleInput: (app.user.uid === 0 || (isEditing && isGuestPost && app.user.isAdmin)) && config.allowGuestHandles,
-			handle: composer.posts[post_uuid] ? composer.posts[post_uuid].handle || '' : undefined,
-			formatting: composer.formatting
+			isEditing: isEditing,
+			showHandleInput:  config.allowGuestHandles && (app.user.uid === 0 || (isEditing && isGuestPost && app.user.isAdmin)),
+			handle: postData ? postData.handle || '' : undefined,
+			formatting: composer.formatting,
+			isAdminOrMod: app.user.isAdmin || postData.isMod
 		};
 
 		parseAndTranslate('composer', data, function(composerTemplate) {
@@ -278,7 +300,6 @@ define('composer', [
 			$(document.body).append(composerTemplate);
 
 			var postContainer = $(composerTemplate[0]),
-				postData = composer.posts[post_uuid],
 				bodyEl = postContainer.find('textarea'),
 				draft = drafts.getDraft(postData.save_id);
 
@@ -309,6 +330,11 @@ define('composer', [
 				post(post_uuid);
 			});
 
+			postContainer.on('click', '[data-action="post-lock"]', function() {
+				$(this).attr('disabled', true);
+				post(post_uuid, {lock: true});
+			});
+
 			postContainer.on('click', '[data-action="discard"]', function() {
 				if (!composer.posts[post_uuid].modified) {
 					discard(post_uuid);
@@ -438,7 +464,7 @@ define('composer', [
 		}
 	}
 
-	function post(post_uuid) {
+	function post(post_uuid, options) {
 		var postData = composer.posts[post_uuid],
 			postContainer = $('#cmp-uuid-' + post_uuid),
 			handleEl = postContainer.find('.handle'),
@@ -446,6 +472,8 @@ define('composer', [
 			bodyEl = postContainer.find('textarea'),
 			thumbEl = postContainer.find('input#topic-thumb-url');
 
+		options = options || {};
+
 		titleEl.val(titleEl.val().trim());
 		bodyEl.val(bodyEl.val().trim());
 		if (thumbEl.length) {
@@ -471,28 +499,27 @@ define('composer', [
 		var composerData = {}, action;
 
 		if (parseInt(postData.cid, 10) > 0) {
+			action = 'topics.post';
 			composerData = {
 				handle: handleEl ? handleEl.val() : undefined,
 				title: titleEl.val(),
 				content: bodyEl.val(),
 				topic_thumb: thumbEl.val() || '',
 				category_id: postData.cid,
-				tags: tags.getTags(post_uuid)
+				tags: tags.getTags(post_uuid),
+				lock: options.lock || false
 			};
-
-			action = 'topics.post';
-			socket.emit(action, composerData, done);
 		} else if (parseInt(postData.tid, 10) > 0) {
+			action = 'posts.reply';
 			composerData = {
 				tid: postData.tid,
 				handle: handleEl ? handleEl.val() : undefined,
 				content: bodyEl.val(),
-				toPid: postData.toPid
+				toPid: postData.toPid,
+				lock: options.lock || false
 			};
-
-			action = 'posts.reply';
-			socket.emit(action, composerData, done);
 		} else if (parseInt(postData.pid, 10) > 0) {
+			action = 'posts.edit';
 			composerData = {
 				pid: postData.pid,
 				handle: handleEl ? handleEl.val() : undefined,
@@ -501,12 +528,9 @@ define('composer', [
 				topic_thumb: thumbEl.val() || '',
 				tags: tags.getTags(post_uuid)
 			};
-
-			action = 'posts.edit';
-			socket.emit(action, composerData, done);
 		}
 
-		function done(err, data) {
+		socket.emit(action, composerData, function (err, data) {
 			$('[data-action="post"]').removeAttr('disabled');
 			if (err) {
 				if (err.message === '[[error:email-not-confirmed]]') {
@@ -520,7 +544,7 @@ define('composer', [
 			drafts.removeDraft(postData.save_id);
 
 			$(window).trigger('action:composer.' + action, {composerData: composerData, data: data});
-		}
+		});
 	}
 
 	function discard(post_uuid) {
diff --git a/public/src/modules/helpers.js b/public/src/modules/helpers.js
index 9f643d8736..5361099476 100644
--- a/public/src/modules/helpers.js
+++ b/public/src/modules/helpers.js
@@ -43,6 +43,46 @@
 		return JSON.stringify(obj).replace(/&/gm,"&amp;").replace(/</gm,"&lt;").replace(/>/gm,"&gt;").replace(/"/g, '&quot;');
 	};
 
+	helpers.generateCategoryBackground = function(category) {
+		var style = [];
+
+		if (category.backgroundImage) {
+			style.push('background-image: url(' + category.backgroundImage + ')');
+		}
+
+		if (category.bgColor) {
+			style.push('background-color: ' + category.bgColor + ';');
+		}
+
+		if (category.color) {
+			style.push('color: ' + category.color + ';');
+		}
+
+		return style.join(' ');
+	};
+
+	helpers.generateTopicClass = function(topic) {
+		var style = [];
+
+		if (topic.locked) {
+			style.push('locked');
+		}
+
+		if (topic.pinned) {
+			style.push('pinned');
+		}
+
+		if (topic.deleted) {
+			style.push('deleted');
+		}
+
+		if (topic.unread) {
+			style.push('unread');
+		}
+
+		return style.join(' ');
+	};
+
 	// Groups helpers
 	helpers.membershipBtn = function(groupObj) {
 		if (groupObj.isMember) {
diff --git a/public/src/modules/navigator.js b/public/src/modules/navigator.js
index e7f78c980f..697046bd28 100644
--- a/public/src/modules/navigator.js
+++ b/public/src/modules/navigator.js
@@ -1,7 +1,7 @@
 
 'use strict';
 
-/* globals app, define, ajaxify, utils, translator, config */
+/* globals app, components, define, ajaxify, utils, translator, config */
 
 
 define('navigator', ['forum/pagination'], function(pagination) {
@@ -90,18 +90,26 @@ define('navigator', ['forum/pagination'], function(pagination) {
 	navigator.update = function() {
 		toggle(!!count);
 
-		$($(navigator.selector).get().reverse()).each(function() {
+		var topIndex = 0;
+		var bottomIndex = 0;
+		$(navigator.selector).each(function() {
 			var el = $(this);
 
 			if (elementInView(el)) {
-				if (typeof navigator.callback === 'function') {
-					index = navigator.callback(el, count);
-					navigator.updateTextAndProgressBar();
+				if (!topIndex) {
+					topIndex = parseInt(el.attr('data-index'), 10) + 1;
+				} else {
+					bottomIndex = parseInt(el.attr('data-index'), 10) + 1;
 				}
-
+			} else if (topIndex && bottomIndex) {
 				return false;
 			}
 		});
+
+		if (typeof navigator.callback === 'function' && topIndex && bottomIndex) {
+			index = navigator.callback(topIndex, bottomIndex, count);
+			navigator.updateTextAndProgressBar();
+		}
 	};
 
 	navigator.updateTextAndProgressBar = function() {
@@ -153,7 +161,7 @@ define('navigator', ['forum/pagination'], function(pagination) {
 	}
 
 	navigator.scrollToPost = function(postIndex, highlight, duration, offset) {
-		if (!utils.isNumber(postIndex) || !$('#post-container').length) {
+		if (!utils.isNumber(postIndex) || !components.get('topic').length) {
 			return;
 		}
 
@@ -161,7 +169,7 @@ define('navigator', ['forum/pagination'], function(pagination) {
 		duration = duration !== undefined ? duration : 400;
 		navigator.scrollActive = true;
 
-		if($('#post_anchor_' + postIndex).length) {
+		if(components.get('post/anchor', postIndex).length) {
 			return scrollToPid(postIndex, highlight, duration, offset);
 		}
 
@@ -188,7 +196,7 @@ define('navigator', ['forum/pagination'], function(pagination) {
 	};
 
 	function scrollToPid(postIndex, highlight, duration, offset) {
-		var scrollTo = $('#post_anchor_' + postIndex);
+		var scrollTo = components.get('post/anchor', postIndex);
 
 		if (!scrollTo) {
 			navigator.scrollActive = false;
@@ -222,7 +230,7 @@ define('navigator', ['forum/pagination'], function(pagination) {
 			}
 		}
 
-		if ($('#post-container').length) {
+		if (components.get('topic').length) {
 			animateScroll();
 		}
 	}
diff --git a/public/src/modules/share.js b/public/src/modules/share.js
index 3f26ebb9f8..6665a30b8c 100644
--- a/public/src/modules/share.js
+++ b/public/src/modules/share.js
@@ -50,7 +50,7 @@ define('share', function() {
 
 	function getPostUrl(clickedElement) {
 		var parts = window.location.pathname.split('/');
-		var postIndex = parseInt(clickedElement.parents('.post-row').attr('data-index'), 10);
+		var postIndex = parseInt(clickedElement.parents('[data-index]').attr('data-index'), 10);
 		return '/' + parts[1] + '/' + parts[2] + (parts[3] ? '/' + parts[3] : '') + (postIndex ? '/' + (postIndex + 1) : '');
 	}
 
diff --git a/public/src/modules/topicSelect.js b/public/src/modules/topicSelect.js
index 75f32f80be..3e3ae58d46 100644
--- a/public/src/modules/topicSelect.js
+++ b/public/src/modules/topicSelect.js
@@ -9,7 +9,7 @@ define('topicSelect', function() {
 	var topicsContainer;
 
 	TopicSelect.init = function(onSelect) {
-		topicsContainer = $('#topics-container');
+		topicsContainer = $('[component="category"]');
 		topicsContainer.on('selectstart', function() {
 			return false;
 		});
@@ -18,7 +18,7 @@ define('topicSelect', function() {
 			var select = $(this);
 
 			if (ev.shiftKey) {
-				selectRange($(this).parents('.category-item').attr('data-tid'));
+				selectRange($(this).parents('[component="category/topic"]').attr('data-tid'));
 				lastSelected = select;
 				return false;
 			}
@@ -35,32 +35,32 @@ define('topicSelect', function() {
 	function toggleSelect(select, isSelected) {
 		select.toggleClass('fa-check-square-o', isSelected);
 		select.toggleClass('fa-square-o', !isSelected);
-		select.parents('.category-item').toggleClass('selected', isSelected);
+		select.parents('[component="category/topic"]').toggleClass('selected', isSelected);
 	}
 
 	TopicSelect.getSelectedTids = function() {
 		var tids = [];
-		topicsContainer.find('.category-item.selected').each(function() {
+		topicsContainer.find('[component="category/topic"].selected').each(function() {
 			tids.push($(this).attr('data-tid'));
 		});
 		return tids;
 	};
 
 	TopicSelect.unselectAll = function() {
-		topicsContainer.find('.category-item.selected').removeClass('selected');
+		topicsContainer.find('[component="category/topic"].selected').removeClass('selected');
 		topicsContainer.find('.select').toggleClass('fa-check-square-o', false).toggleClass('fa-square-o', true);
 	};
 
 	function selectRange(clickedTid) {
 
 		if(!lastSelected) {
-			lastSelected = $('.category-item[data-tid]').first().find('.select');
+			lastSelected = $('[component="category/topic"]').first().find('.select');
 		}
 
-		var isClickedSelected = $('.category-item[data-tid="' + clickedTid + '"]').hasClass('selected');
+		var isClickedSelected = components.get('category/topic', 'tid', clickedTid).hasClass('selected');
 
 		var clickedIndex = getIndex(clickedTid);
-		var lastIndex = getIndex(lastSelected.parents('.category-item[data-tid]').attr('data-tid'));
+		var lastIndex = getIndex(lastSelected.parents('[component="category/topic"]').attr('data-tid'));
 		selectIndexRange(clickedIndex, lastIndex, !isClickedSelected);
 	}
 
@@ -72,13 +72,13 @@ define('topicSelect', function() {
 		}
 
 		for(var i=start; i<=end; ++i) {
-			var topic = $('.category-item[data-tid]').eq(i);
+			var topic = $('[component="category/topic"]').eq(i);
 			toggleSelect(topic.find('.select'), isSelected);
 		}
 	}
 
 	function getIndex(tid) {
-		return $('.category-item[data-tid="' + tid + '"]').index('#topics-container .category-item');
+		return components.get('category/topic', 'tid', tid).index('[component="category/topic"]');
 	}
 
 	return TopicSelect;
diff --git a/public/src/translator.js b/public/src/translator.js
index a6f4027faf..d1b85e300c 100644
--- a/public/src/translator.js
+++ b/public/src/translator.js
@@ -79,7 +79,7 @@
 			}
 
 			$.getScript(RELATIVE_PATH + '/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '.js').success(function() {
-				$('span.timeago').timeago();
+				$('.timeago').timeago();
 			}).fail(function() {
 				$.getScript(RELATIVE_PATH + '/vendor/jquery/timeago/locales/jquery.timeago.en.js');
 			});
diff --git a/public/src/widgets.js b/public/src/widgets.js
index 990a42c6f0..e83353ec1b 100644
--- a/public/src/widgets.js
+++ b/public/src/widgets.js
@@ -71,7 +71,7 @@
 
 				var widgetAreas = $('#content [widget-area]');
 				widgetAreas.find('img:not(.user-img)').addClass('img-responsive');
-				widgetAreas.find('span.timeago').timeago();
+				widgetAreas.find('.timeago').timeago();
 				widgetAreas.find('img[title].teaser-pic,img[title].user-img').each(function() {
 					$(this).tooltip({
 						placement: 'top',
diff --git a/public/vendor/jquery/textcomplete/jquery.textcomplete.css b/public/vendor/jquery/textcomplete/jquery.textcomplete.css
new file mode 100644
index 0000000000..d33f066c5a
--- /dev/null
+++ b/public/vendor/jquery/textcomplete/jquery.textcomplete.css
@@ -0,0 +1,33 @@
+/* Sample */
+
+/*.dropdown-menu {
+    border: 1px solid #ddd;
+    background-color: white;
+}
+
+.dropdown-menu li {
+    border-top: 1px solid #ddd;
+    padding: 2px 5px;
+}
+
+.dropdown-menu li:first-child {
+    border-top: none;
+}
+
+.dropdown-menu li:hover,
+.dropdown-menu .active {
+    background-color: rgb(110, 183, 219);
+}*/
+
+
+/* SHOULD not modify */
+
+/*.dropdown-menu {
+    list-style: none;
+    padding: 0;
+    margin: 0;
+}
+
+.dropdown-menu a:hover {
+    cursor: pointer;
+}*/
\ No newline at end of file
diff --git a/public/vendor/jquery/textcomplete/jquery.textcomplete.min.js b/public/vendor/jquery/textcomplete/jquery.textcomplete.min.js
new file mode 100644
index 0000000000..941ee008de
--- /dev/null
+++ b/public/vendor/jquery/textcomplete/jquery.textcomplete.min.js
@@ -0,0 +1 @@
+/*! jquery-textcomplete - v0.4.0 - 2015-03-10 */if("undefined"==typeof jQuery)throw new Error("jQuery.textcomplete requires jQuery");+function(a){"use strict";var b=function(a){console.warn&&console.warn(a)};a.fn.textcomplete=function(c,d){var e=Array.prototype.slice.call(arguments);return this.each(function(){var f=a(this),g=f.data("textComplete");if(g||(g=new a.fn.textcomplete.Completer(this,d||{}),f.data("textComplete",g)),"string"==typeof c){if(!g)return;e.shift(),g[c].apply(g,e),"destroy"===c&&f.removeData("textComplete")}else a.each(c,function(c){a.each(["header","footer","placement","maxCount"],function(a){c[a]&&(g.option[a]=c[a],b(a+"as a strategy param is deprecated. Use option."),delete c[a])})}),g.register(a.fn.textcomplete.Strategy.parse(c))})}}(jQuery),+function(a){"use strict";function b(c,d){if(this.$el=a(c),this.id="textcomplete"+f++,this.strategies=[],this.views=[],this.option=a.extend({},b._getDefaults(),d),!this.$el.is("input[type=text]")&&!this.$el.is("textarea")&&!c.isContentEditable&&"true"!=c.contentEditable)throw new Error("textcomplete must be called on a Textarea or a ContentEditable.");if(c===document.activeElement)this.initialize();else{var e=this;this.$el.one("focus."+this.id,function(){e.initialize()})}}var c=function(a){var b,c;return function(){var d=Array.prototype.slice.call(arguments);if(b)return c=d,void 0;b=!0;var e=this;d.unshift(function f(){if(c){var d=c;c=void 0,d.unshift(f),a.apply(e,d)}else b=!1}),a.apply(this,d)}},d=function(a){return"[object String]"===Object.prototype.toString.call(a)},e=function(a){return"[object Function]"===Object.prototype.toString.call(a)},f=0;b._getDefaults=function(){return b.DEFAULTS||(b.DEFAULTS={appendTo:a("body"),zIndex:"100"}),b.DEFAULTS},a.extend(b.prototype,{id:null,option:null,strategies:null,adapter:null,dropdown:null,$el:null,initialize:function(){var b=this.$el.get(0);this.dropdown=new a.fn.textcomplete.Dropdown(b,this,this.option);var c,d;this.option.adapter?c=this.option.adapter:(d=this.$el.is("textarea")||this.$el.is("input[type=text]")?"number"==typeof b.selectionEnd?"Textarea":"IETextarea":"ContentEditable",c=a.fn.textcomplete[d]),this.adapter=new c(b,this,this.option)},destroy:function(){this.$el.off("."+this.id),this.adapter&&this.adapter.destroy(),this.dropdown&&this.dropdown.destroy(),this.$el=this.adapter=this.dropdown=null},trigger:function(a,b){this.dropdown||this.initialize(),null!=a||(a=this.adapter.getTextFromHeadToCaret());var c=this._extractSearchQuery(a);if(c.length){var d=c[1];if(b&&this._term===d)return;this._term=d,this._search.apply(this,c)}else this._term=null,this.dropdown.deactivate()},fire:function(a){var b=Array.prototype.slice.call(arguments,1);return this.$el.trigger(a,b),this},register:function(a){Array.prototype.push.apply(this.strategies,a)},select:function(a,b){this.adapter.select(a,b),this.fire("change").fire("textComplete:select",a,b),this.adapter.focus()},_clearAtNext:!0,_term:null,_extractSearchQuery:function(a){for(var b=0;b<this.strategies.length;b++){var c=this.strategies[b],f=c.context(a);if(f||""===f){var g=e(c.match)?c.match(a):c.match;d(f)&&(a=f);var h=a.match(g);if(h)return[c,h[c.index],h]}}return[]},_search:c(function(a,b,c,d){var e=this;b.search(c,function(c,d){e.dropdown.shown||(e.dropdown.activate(),e.dropdown.setPosition(e.adapter.getCaretPosition())),e._clearAtNext&&(e.dropdown.clear(),e._clearAtNext=!1),e.dropdown.render(e._zip(c,b)),d||(a(),e._clearAtNext=!0)},d)}),_zip:function(b,c){return a.map(b,function(a){return{value:a,strategy:c}})}}),a.fn.textcomplete.Completer=b}(jQuery),+function(a){"use strict";function b(c,e,f){this.$el=b.findOrCreateElement(f),this.completer=e,this.id=e.id+"dropdown",this._data=[],this.$inputEl=a(c),this.option=f,f.listPosition&&(this.setPosition=f.listPosition),f.height&&this.$el.height(f.height);var g=this;a.each(["maxCount","placement","footer","header","className"],function(a,b){null!=f[b]&&(g[b]=f[b])}),this._bindEvents(c),d[this.id]=this}var c=function(a,b){var c,d,e=b.strategy.idProperty;for(c=0;c<a.length;c++)if(d=a[c],d.strategy===b.strategy)if(e){if(d.value[e]===b.value[e])return!0}else if(d.value===b.value)return!0;return!1},d={};a(document).on("click",function(b){var c=b.originalEvent&&b.originalEvent.keepTextCompleteDropdown;a.each(d,function(a,b){a!==c&&b.deactivate()})}),a.extend(b,{findOrCreateElement:function(b){var c=b.appendTo;c instanceof a||(c=a(c));var d=c.children(".dropdown-menu");return d.length||(d=a('<ul class="dropdown-menu"></ul>').css({display:"none",left:0,position:"absolute",zIndex:b.zIndex}).appendTo(c)),d}}),a.extend(b.prototype,{$el:null,$inputEl:null,completer:null,footer:null,header:null,id:null,maxCount:10,placement:"",shown:!1,data:[],className:"",destroy:function(){this.deactivate(),this.$el.off("."+this.id),this.$inputEl.off("."+this.id),this.clear(),this.$el=this.$inputEl=this.completer=null,delete d[this.id]},render:function(b){var c=this._buildContents(b),d=a.map(this.data,function(a){return a.value});this.data.length?(this._renderHeader(d),this._renderFooter(d),c&&(this._renderContents(c),this._activateIndexedItem()),this._setScroll()):this.shown&&this.deactivate()},setPosition:function(b){this.$el.css(this._applyPlacement(b));var b="absolute";return this.$inputEl.add(this.$inputEl.parents()).each(function(){return"absolute"===a(this).css("position")?!1:"fixed"===a(this).css("position")?(b="fixed",!1):void 0}),this.$el.css({position:b}),this},clear:function(){this.$el.html(""),this.data=[],this._index=0,this._$header=this._$footer=null},activate:function(){return this.shown||(this.clear(),this.$el.show(),this.className&&this.$el.addClass(this.className),this.completer.fire("textComplete:show"),this.shown=!0),this},deactivate:function(){return this.shown&&(this.$el.hide(),this.className&&this.$el.removeClass(this.className),this.completer.fire("textComplete:hide"),this.shown=!1),this},isUp:function(a){return 38===a.keyCode||a.ctrlKey&&80===a.keyCode},isDown:function(a){return 40===a.keyCode||a.ctrlKey&&78===a.keyCode},isEnter:function(a){var b=a.ctrlKey||a.altKey||a.metaKey||a.shiftKey;return!b&&(13===a.keyCode||9===a.keyCode||this.option.completeOnSpace===!0&&32===a.keyCode)},isPageup:function(a){return 33===a.keyCode},isPagedown:function(a){return 34===a.keyCode},isEscape:function(a){return 27===a.keyCode},_data:null,_index:null,_$header:null,_$footer:null,_bindEvents:function(){this.$el.on("mousedown."+this.id,".textcomplete-item",a.proxy(this._onClick,this)),this.$el.on("mouseover."+this.id,".textcomplete-item",a.proxy(this._onMouseover,this)),this.$inputEl.on("keydown."+this.id,a.proxy(this._onKeydown,this))},_onClick:function(b){var c=a(b.target);b.preventDefault(),b.originalEvent.keepTextCompleteDropdown=this.id,c.hasClass("textcomplete-item")||(c=c.closest(".textcomplete-item"));var d=this.data[parseInt(c.data("index"),10)];this.completer.select(d.value,d.strategy);var e=this;setTimeout(function(){e.deactivate()},0)},_onMouseover:function(b){var c=a(b.target);b.preventDefault(),c.hasClass("textcomplete-item")||(c=c.closest(".textcomplete-item")),this._index=parseInt(c.data("index"),10),this._activateIndexedItem()},_onKeydown:function(a){this.shown&&(this.isUp(a)?(a.preventDefault(),this._up()):this.isDown(a)?(a.preventDefault(),this._down()):this.isEnter(a)?(a.preventDefault(),this._enter()):this.isPageup(a)?(a.preventDefault(),this._pageup()):this.isPagedown(a)?(a.preventDefault(),this._pagedown()):this.isEscape(a)&&(a.preventDefault(),this.deactivate()))},_up:function(){0===this._index?this._index=this.data.length-1:this._index-=1,this._activateIndexedItem(),this._setScroll()},_down:function(){this._index===this.data.length-1?this._index=0:this._index+=1,this._activateIndexedItem(),this._setScroll()},_enter:function(){var a=this.data[parseInt(this._getActiveElement().data("index"),10)];this.completer.select(a.value,a.strategy),this.deactivate()},_pageup:function(){var b=0,c=this._getActiveElement().position().top-this.$el.innerHeight();this.$el.children().each(function(d){return a(this).position().top+a(this).outerHeight()>c?(b=d,!1):void 0}),this._index=b,this._activateIndexedItem(),this._setScroll()},_pagedown:function(){var b=this.data.length-1,c=this._getActiveElement().position().top+this.$el.innerHeight();this.$el.children().each(function(d){return a(this).position().top>c?(b=d,!1):void 0}),this._index=b,this._activateIndexedItem(),this._setScroll()},_activateIndexedItem:function(){this.$el.find(".textcomplete-item.active").removeClass("active"),this._getActiveElement().addClass("active")},_getActiveElement:function(){return this.$el.children(".textcomplete-item:nth("+this._index+")")},_setScroll:function(){var a=this._getActiveElement(),b=a.position().top,c=a.outerHeight(),d=this.$el.innerHeight(),e=this.$el.scrollTop();0===this._index||this._index==this.data.length-1||0>b?this.$el.scrollTop(b+e):b+c>d&&this.$el.scrollTop(b+c+e-d)},_buildContents:function(a){var b,d,e,f="";for(d=0;d<a.length&&this.data.length!==this.maxCount;d++)b=a[d],c(this.data,b)||(e=this.data.length,this.data.push(b),f+='<li class="textcomplete-item" data-index="'+e+'"><a>',f+=b.strategy.template(b.value),f+="</a></li>");return f},_renderHeader:function(b){if(this.header){this._$header||(this._$header=a('<li class="textcomplete-header"></li>').prependTo(this.$el));var c=a.isFunction(this.header)?this.header(b):this.header;this._$header.html(c)}},_renderFooter:function(b){if(this.footer){this._$footer||(this._$footer=a('<li class="textcomplete-footer"></li>').appendTo(this.$el));var c=a.isFunction(this.footer)?this.footer(b):this.footer;this._$footer.html(c)}},_renderContents:function(a){this._$footer?this._$footer.before(a):this.$el.append(a)},_applyPlacement:function(a){return-1!==this.placement.indexOf("top")?a={top:"auto",bottom:this.$el.parent().height()-a.top+a.lineHeight,left:a.left}:(a.bottom="auto",delete a.lineHeight),-1!==this.placement.indexOf("absleft")?a.left=0:-1!==this.placement.indexOf("absright")&&(a.right=0,a.left="auto"),a}}),a.fn.textcomplete.Dropdown=b}(jQuery),+function(a){"use strict";function b(b){a.extend(this,b),this.cache&&(this.search=c(this.search))}var c=function(a){var b={};return function(c,d){b[c]?d(b[c]):a.call(this,c,function(a){b[c]=(b[c]||[]).concat(a),d.apply(null,arguments)})}};b.parse=function(c){return a.map(c,function(a){return new b(a)})},a.extend(b.prototype,{match:null,replace:null,search:null,cache:!1,context:function(){return!0},index:2,template:function(a){return a},idProperty:null}),a.fn.textcomplete.Strategy=b}(jQuery),+function(a){"use strict";function b(){}var c=Date.now||function(){return(new Date).getTime()},d=function(a,b){var d,e,f,g,h,i=function(){var j=c()-g;b>j?d=setTimeout(i,b-j):(d=null,h=a.apply(f,e),f=e=null)};return function(){return f=this,e=arguments,g=c(),d||(d=setTimeout(i,b)),h}};a.extend(b.prototype,{id:null,completer:null,el:null,$el:null,option:null,initialize:function(b,c,e){this.el=b,this.$el=a(b),this.id=c.id+this.constructor.name,this.completer=c,this.option=e,this.option.debounce&&(this._onKeyup=d(this._onKeyup,this.option.debounce)),this._bindEvents()},destroy:function(){this.$el.off("."+this.id),this.$el=this.el=this.completer=null},select:function(){throw new Error("Not implemented")},getCaretPosition:function(){var a=this._getCaretRelativePosition(),b=this.$el.offset();return a.top+=b.top,a.left+=b.left,a},focus:function(){this.$el.focus()},_bindEvents:function(){this.$el.on("keyup."+this.id,a.proxy(this._onKeyup,this))},_onKeyup:function(a){this._skipSearch(a)||this.completer.trigger(this.getTextFromHeadToCaret(),!0)},_skipSearch:function(a){switch(a.keyCode){case 13:case 40:case 38:return!0}if(a.ctrlKey)switch(a.keyCode){case 78:case 80:return!0}}}),a.fn.textcomplete.Adapter=b}(jQuery),+function(a){"use strict";function b(a,b,c){this.initialize(a,b,c)}b.DIV_PROPERTIES={left:-9999,position:"absolute",top:0,whiteSpace:"pre-wrap"},b.COPY_PROPERTIES=["border-width","font-family","font-size","font-style","font-variant","font-weight","height","letter-spacing","word-spacing","line-height","text-decoration","text-align","width","padding-top","padding-right","padding-bottom","padding-left","margin-top","margin-right","margin-bottom","margin-left","border-style","box-sizing","tab-size"],a.extend(b.prototype,a.fn.textcomplete.Adapter.prototype,{select:function(b,c){var d=this.getTextFromHeadToCaret(),e=this.el.value.substring(this.el.selectionEnd),f=c.replace(b);a.isArray(f)&&(e=f[1]+e,f=f[0]),d=d.replace(c.match,f),this.$el.val(d+e),this.el.selectionStart=this.el.selectionEnd=d.length},_getCaretRelativePosition:function(){var b=a("<div></div>").css(this._copyCss()).text(this.getTextFromHeadToCaret()),c=a("<span></span>").text(".").appendTo(b);this.$el.before(b);var d=c.position();return d.top+=c.height()-this.$el.scrollTop(),d.lineHeight=c.height(),b.remove(),d},_copyCss:function(){return a.extend({overflow:this.el.scrollHeight>this.el.offsetHeight?"scroll":"auto"},b.DIV_PROPERTIES,this._getStyles())},_getStyles:function(a){var c=a("<div></div>").css(["color"]).color;return"undefined"!=typeof c?function(){return this.$el.css(b.COPY_PROPERTIES)}:function(){var c=this.$el,d={};return a.each(b.COPY_PROPERTIES,function(a,b){d[b]=c.css(b)}),d}}(a),getTextFromHeadToCaret:function(){return this.el.value.substring(0,this.el.selectionEnd)}}),a.fn.textcomplete.Textarea=b}(jQuery),+function(a){"use strict";function b(b,d,e){this.initialize(b,d,e),a("<span>"+c+"</span>").css({position:"absolute",top:-9999,left:-9999}).insertBefore(b)}var c="吶";a.extend(b.prototype,a.fn.textcomplete.Textarea.prototype,{select:function(b,c){var d=this.getTextFromHeadToCaret(),e=this.el.value.substring(d.length),f=c.replace(b);a.isArray(f)&&(e=f[1]+e,f=f[0]),d=d.replace(c.match,f),this.$el.val(d+e),this.el.focus();var g=this.el.createTextRange();g.collapse(!0),g.moveEnd("character",d.length),g.moveStart("character",d.length),g.select()},getTextFromHeadToCaret:function(){this.el.focus();var a=document.selection.createRange();a.moveStart("character",-this.el.value.length);var b=a.text.split(c);return 1===b.length?b[0]:b[1]}}),a.fn.textcomplete.IETextarea=b}(jQuery),+function(a){"use strict";function b(a,b,c){this.initialize(a,b,c)}a.extend(b.prototype,a.fn.textcomplete.Adapter.prototype,{select:function(b,c){var d=this.getTextFromHeadToCaret(),e=window.getSelection(),f=e.getRangeAt(0),g=f.cloneRange();g.selectNodeContents(f.startContainer);var h=g.toString(),i=h.substring(f.startOffset),j=c.replace(b);a.isArray(j)&&(i=j[1]+i,j=j[0]),d=d.replace(c.match,j),f.selectNodeContents(f.startContainer),f.deleteContents();var k=document.createTextNode(d+i);f.insertNode(k),f.setStart(k,d.length),f.collapse(!0),e.removeAllRanges(),e.addRange(f)},_getCaretRelativePosition:function(){var b=window.getSelection().getRangeAt(0).cloneRange(),c=document.createElement("span");b.insertNode(c),b.selectNodeContents(c),b.deleteContents();var d=a(c),e=d.offset();e.left-=this.$el.offset().left,e.top+=d.height()-this.$el.offset().top,e.lineHeight=d.height(),d.remove();var f=this.$el.attr("dir")||this.$el.css("direction");return"rtl"===f&&(e.left-=this.listView.$el.width()),e},getTextFromHeadToCaret:function(){var a=window.getSelection().getRangeAt(0),b=a.cloneRange();return b.selectNodeContents(a.startContainer),b.toString().substring(0,a.startOffset)}}),a.fn.textcomplete.ContentEditable=b}(jQuery);
diff --git a/src/categories.js b/src/categories.js
index 87c540aaf1..3061ef8425 100644
--- a/src/categories.js
+++ b/src/categories.js
@@ -62,7 +62,6 @@ var async = require('async'),
 				category.nextStart = results.topics.nextStart;
 				category.pageCount = results.pageCount;
 				category.isIgnored = results.isIgnored[0];
-				category.topic_row_size = 'col-md-9';
 
 				plugins.fireHook('filter:category.get', {category: category, uid: data.uid}, function(err, data) {
 					callback(err, data ? data.category : null);
diff --git a/src/categories/create.js b/src/categories/create.js
index ce293d1c42..0db3cad0c3 100644
--- a/src/categories/create.js
+++ b/src/categories/create.js
@@ -32,7 +32,7 @@ module.exports = function(Categories) {
 				order: order,
 				link: '',
 				numRecentReplies: 1,
-				class: 'col-md-3 col-xs-6',
+				class: 'col-md-3 col-xs-12',
 				imageClass: 'auto'
 			};
 
diff --git a/src/categories/topics.js b/src/categories/topics.js
index 50dba67bbd..805ab30c4a 100644
--- a/src/categories/topics.js
+++ b/src/categories/topics.js
@@ -74,37 +74,29 @@ module.exports = function(Categories) {
 		});
 	};
 
-	Categories.onNewPostMade = function(postData, callback) {
-		topics.getTopicFields(postData.tid, ['cid', 'pinned'], function(err, topicData) {
-			if (err) {
-				return callback(err);
-			}
-
-			if (!topicData || !topicData.cid) {
-				return callback();
-			}
-
-			var cid = topicData.cid;
+	Categories.onNewPostMade = function(cid, pinned, postData, callback) {
+		if (!cid || !postData) {
+			return callback();
+		}
 
-			async.parallel([
-				function(next) {
-					db.sortedSetAdd('cid:' + cid + ':pids', postData.timestamp, postData.pid, next);
-				},
-				function(next) {
-					db.incrObjectField('category:' + cid, 'post_count', next);
-				},
-				function(next) {
-					if (parseInt(topicData.pinned, 10) === 1) {
-						next();
-					} else {
-						db.sortedSetAdd('cid:' + cid + ':tids', postData.timestamp, postData.tid, next);
-					}
-				},
-				function(next) {
-					db.sortedSetIncrBy('cid:' + cid + ':tids:posts', 1, postData.tid, next);
+		async.parallel([
+			function(next) {
+				db.sortedSetAdd('cid:' + cid + ':pids', postData.timestamp, postData.pid, next);
+			},
+			function(next) {
+				db.incrObjectField('category:' + cid, 'post_count', next);
+			},
+			function(next) {
+				if (parseInt(pinned, 10) === 1) {
+					next();
+				} else {
+					db.sortedSetAdd('cid:' + cid + ':tids', postData.timestamp, postData.tid, next);
 				}
-			], callback);
-		});
+			},
+			function(next) {
+				db.sortedSetIncrBy('cid:' + cid + ':tids:posts', 1, postData.tid, next);
+			}
+		], callback);
 	};
 
 };
diff --git a/src/controllers/accounts.js b/src/controllers/accounts.js
index 8b966fbd26..51cc3daf82 100644
--- a/src/controllers/accounts.js
+++ b/src/controllers/accounts.js
@@ -356,7 +356,7 @@ accountsController.accountSettings = function(req, res, next) {
 			},
 			userGroups: function(next) {
 				groups.getUserGroups([userData.uid], next);
-			},			
+			},
 			languages: function(next) {
 				languages.list(next);
 			}
@@ -434,7 +434,7 @@ accountsController.uploadPicture = function (req, res, next) {
 
 			user.setUserFields(updateUid, {uploadedpicture: image.url, picture: image.url});
 
-			res.json([{name: userPhoto.name, url: nconf.get('relative_path') + image.url}]);
+			res.json([{name: userPhoto.name, url: image.url.startsWith('http') ? image.url : nconf.get('relative_path') + image.url}]);
 		}
 
 		if (err) {
diff --git a/src/controllers/categories.js b/src/controllers/categories.js
index 18bbd57f8b..8896894680 100644
--- a/src/controllers/categories.js
+++ b/src/controllers/categories.js
@@ -4,6 +4,8 @@ var categoriesController = {},
 	async = require('async'),
 	nconf = require('nconf'),
 	validator = require('validator'),
+
+	db = require('../database'),
 	privileges = require('../privileges'),
 	user = require('../user'),
 	categories = require('../categories'),
@@ -249,6 +251,11 @@ categoriesController.get = function(req, res, next) {
 			categories.getCategoryById(payload, next);
 		},
 		function(categoryData, next) {
+			if (categoryData.link) {
+				db.incrObjectField('category:' + categoryData.cid, 'timesClicked');
+				return res.redirect(categoryData.link);
+			}
+
 			var breadcrumbs = [
 				{
 					text: categoryData.name,
@@ -264,16 +271,13 @@ categoriesController.get = function(req, res, next) {
 			});
 		},
 		function(categoryData, next) {
-			if (categoryData.link) {
-				return res.redirect(categoryData.link);
-			}
-
 			categories.getRecentTopicReplies(categoryData.children, uid, function(err) {
 				next(err, categoryData);
 			});
 		},
 		function (categoryData, next) {
 			categoryData.privileges = userPrivileges;
+			categoryData.showSelect = categoryData.privileges.editable;
 
 			res.locals.metaTags = [
 				{
diff --git a/src/controllers/index.js b/src/controllers/index.js
index 6aba26bb1f..74cce358e5 100644
--- a/src/controllers/index.js
+++ b/src/controllers/index.js
@@ -26,7 +26,7 @@ var Controllers = {
 	accounts: require('./accounts'),
 	static: require('./static'),
 	api: require('./api'),
-	admin: require('./admin'),
+	admin: require('./admin')
 };
 
 
@@ -52,15 +52,18 @@ Controllers.home = function(req, res, next) {
 Controllers.reset = function(req, res, next) {
 	if (req.params.code) {
 		user.reset.validate(req.params.code, function(err, valid) {
+			if (err) {
+				return next(err);
+			}
 			res.render('reset_code', {
 				valid: valid,
-				reset_code: req.params.code ? req.params.code : null,
+				code: req.params.code ? req.params.code : null,
 				breadcrumbs: helpers.buildBreadcrumbs([{text: '[[reset_password:reset_password]]', url: '/reset'}, {text: '[[reset_password:update_password]]'}])
 			});
 		});
 	} else {
 		res.render('reset', {
-			reset_code: req.params.code ? req.params.code : null,
+			code: req.params.code ? req.params.code : null,
 			breadcrumbs: helpers.buildBreadcrumbs([{text: '[[reset_password:reset_password]]'}])
 		});
 	}
diff --git a/src/controllers/search.js b/src/controllers/search.js
index 7ed16d8bb8..775d430783 100644
--- a/src/controllers/search.js
+++ b/src/controllers/search.js
@@ -23,26 +23,6 @@ searchController.search = function(req, res, next) {
 			return next(err);
 		}
 
-		if (!req.params.term) {
-			var results = {
-				time: 0,
-				search_query: '',
-				posts: [],
-				users: [],
-				tags: [],
-				categories: categories,
-				breadcrumbs: breadcrumbs,
-				expandSearch: true
-			};
-			plugins.fireHook('filter:search.build', {data: {}, results: results}, function(err, data) {
-				if (err) {
-					return next(err);
-				}
-				res.render('search', data.results);	
-			});
-			return;			
-		}
-
 		req.params.term = validator.escape(req.params.term);
 		var page = Math.max(1, parseInt(req.query.page, 10)) || 1;
 		if (req.query.categories && !Array.isArray(req.query.categories)) {
@@ -77,7 +57,7 @@ searchController.search = function(req, res, next) {
 			results.breadcrumbs = breadcrumbs;
 			results.categories = categories;
 			results.expandSearch = false;
-			
+
 			plugins.fireHook('filter:search.build', {data: data, results: results}, function(err, data) {
 				if (err) {
 					return next(err);
diff --git a/src/database/mongo.js b/src/database/mongo.js
index e7ee038f96..522f9fffdc 100644
--- a/src/database/mongo.js
+++ b/src/database/mongo.js
@@ -112,8 +112,14 @@
 				createIndex('objects', {_key: 1, value: -1}, {background:true});
 				createIndex('objects', {expireAt: 1}, {expireAfterSeconds:0, background:true});
 
-				createIndex('search', {content:'text'}, {background:true});
-				createIndex('search', {key: 1, id: 1}, {background:true});
+
+				createIndex('searchtopic', {content: 'text', uid: 1, cid: 1}, {background:true});
+				createIndex('searchtopic', {id: 1}, {background:true});
+
+
+				createIndex('searchpost', {content: 'text', uid: 1, cid: 1}, {background:true});
+				createIndex('searchpost', {id: 1}, {background:true});
+
 
 				if (typeof callback === 'function') {
 					callback();
diff --git a/src/database/mongo/main.js b/src/database/mongo/main.js
index c2cf4883bf..eba0b3b14b 100644
--- a/src/database/mongo/main.js
+++ b/src/database/mongo/main.js
@@ -5,15 +5,18 @@ var winston = require('winston');
 module.exports = function(db, module) {
 	var helpers = module.helpers.mongo;
 
-	module.searchIndex = function(key, content, id, callback) {
+	module.searchIndex = function(key, data, id, callback) {
 		callback = callback || function() {};
-		var data = {
-			id: id,
-			key: key,
-			content: content
+		var setData = {
+			id: id
 		};
+		for(var field in data) {
+			if (data.hasOwnProperty(field) && data[field]) {
+				setData[field] = data[field].toString();
+			}
+		}
 
-		db.collection('search').update({key:key, id:id}, {$set:data}, {upsert:true, w: 1}, function(err) {
+		db.collection('search' + key).update({id: id}, {$set: setData}, {upsert:true, w: 1}, function(err) {
 			if(err) {
 				winston.error('Error indexing ' + err.message);
 			}
@@ -21,13 +24,35 @@ module.exports = function(db, module) {
 		});
 	};
 
-	module.search = function(key, term, limit, callback) {
-		db.collection('search').find({ $text: { $search: term }, key: key}, {limit: limit}).toArray(function(err, results) {
-			if(err) {
+	module.search = function(key, data, limit, callback) {
+		var searchQuery = {};
+
+		if (data.content) {
+			searchQuery.$text = {$search: data.content};
+		}
+
+		if (Array.isArray(data.cid) && data.cid.length) {
+		 	if (data.cid.length > 1) {
+				searchQuery.cid = {$in: data.cid.map(String)};
+			} else {
+				searchQuery.cid = data.cid[0].toString();
+			}
+		}
+
+		if (Array.isArray(data.uid) && data.uid.length) {
+			if (data.uid.length > 1) {
+				searchQuery.uid = {$in: data.uid.map(String)};
+			} else {
+				searchQuery.uid = data.uid[0].toString();
+			}
+		}
+
+		db.collection('search' + key).find(searchQuery, {limit: limit}).toArray(function(err, results) {
+			if (err) {
 				return callback(err);
 			}
 
-			if(!results || !results.length) {
+			if (!results || !results.length) {
 				return callback(null, []);
 			}
 
@@ -41,7 +66,12 @@ module.exports = function(db, module) {
 
 	module.searchRemove = function(key, id, callback) {
 		callback = callback || helpers.noop;
-		db.collection('search').remove({key:key, id:id}, callback);
+		if (!id) {
+			return callback();
+		}
+		db.collection('search' + key).remove({id: id}, function(err, res) {
+			callback(err);
+		});
 	};
 
 	module.flushdb = function(callback) {
diff --git a/src/database/redis.js b/src/database/redis.js
index 727705c130..716a8fd715 100644
--- a/src/database/redis.js
+++ b/src/database/redis.js
@@ -9,7 +9,7 @@
 		utils = require('./../../public/src/utils.js'),
 		redis,
 		connectRedis,
-		reds,
+		redisSearch,
 		redisClient,
 		postSearch,
 		topicSearch;
@@ -41,7 +41,7 @@
 		try {
 			redis = require('redis');
 			connectRedis = require('connect-redis')(session);
-			reds = require('reds');
+			redisSearch = require('redisearch');
 		} catch (err) {
 			winston.error('Unable to initialize Redis! Is Redis installed? Error :' + err.message);
 			process.exit();
@@ -56,12 +56,8 @@
 			ttl: 60 * 60 * 24 * 14
 		});
 
-		reds.createClient = function () {
-			return reds.client || (reds.client = redisClient);
-		};
-
-		module.postSearch = reds.createSearch('nodebbpostsearch');
-		module.topicSearch = reds.createSearch('nodebbtopicsearch');
+		module.postSearch = redisSearch.createSearch('nodebbpostsearch', redisClient);
+		module.topicSearch = redisSearch.createSearch('nodebbtopicsearch', redisClient);
 
 		require('./redis/main')(redisClient, module);
 		require('./redis/hash')(redisClient, module);
diff --git a/src/database/redis/main.js b/src/database/redis/main.js
index ebad6ae8e6..b09574e10f 100644
--- a/src/database/redis/main.js
+++ b/src/database/redis/main.js
@@ -1,36 +1,30 @@
 "use strict";
 
 module.exports = function(redisClient, module) {
-	module.searchIndex = function(key, content, id, callback) {
-		if (key === 'post') {
-			module.postSearch.index(content, id, callback);
-		} else if(key === 'topic') {
-			module.topicSearch.index(content, id, callback);
-		}
+	module.searchIndex = function(key, data, id, callback) {
+		var method = key === 'post' ? module.postSearch : module.topicSearch;
+
+		method.index(data, id, function(err, res) {
+			callback(err);
+		});
 	};
 
-	module.search = function(key, term, limit, callback) {
-		function search(searchObj, callback) {
-			searchObj
-				.query(term)
-				.between(0, limit - 1)
-				.type('or')
-				.end(callback);
-		}
+	module.search = function(key, data, limit, callback) {
+		var method = key === 'post' ? module.postSearch : module.topicSearch;
 
-		if(key === 'post') {
-			search(module.postSearch, callback);
-		} else if(key === 'topic') {
-			search(module.topicSearch, callback);
-		}
+		method.query(data, 0, limit - 1, callback);
 	};
 
 	module.searchRemove = function(key, id, callback) {
-		if(key === 'post') {
-			module.postSearch.remove(id, callback);
-		} else if(key === 'topic') {
-			module.topicSearch.remove(id, callback);
+		callback = callback || function() {};
+		if (!id) {
+			return callback();
 		}
+		var method = key === 'post' ? module.postSearch : module.topicSearch;
+
+		method.remove(id, function(err, res) {
+			callback(err);
+		});
 	};
 
 	module.flushdb = function(callback) {
diff --git a/src/emailer.js b/src/emailer.js
index b358e170fb..aeca1ecab8 100644
--- a/src/emailer.js
+++ b/src/emailer.js
@@ -62,7 +62,8 @@ var	fs = require('fs'),
 						plaintext: translated[1],
 						template: template,
 						uid: uid,
-						pid: params.pid
+						pid: params.pid,
+						fromUid: params.fromUid
 					});
 					callback();
 				} else {
diff --git a/src/file.js b/src/file.js
index 543c5011fa..6be1cd37c8 100644
--- a/src/file.js
+++ b/src/file.js
@@ -8,11 +8,20 @@ var fs = require('fs'),
 	Magic = mmmagic.Magic,
 	mime = require('mime'),
 
-	meta= require('./meta');
+	meta = require('./meta'),
+	utils = require('../public/src/utils');
 
 var file = {};
 
 file.saveFileToLocal = function(filename, folder, tempPath, callback) {
+	/*
+	* remarkable doesn't allow spaces in hyperlinks, once that's fixed, remove this.
+	*/
+	filename = filename.split('.');
+	filename.forEach(function(name, idx) {
+		filename[idx] = utils.slugify(name);
+	});
+	filename = filename.join('.');
 
 	var uploadPath = path.join(nconf.get('base_dir'), nconf.get('upload_path'), folder, filename);
 
diff --git a/src/groups.js b/src/groups.js
index 25c8ff1bdc..43a376106f 100644
--- a/src/groups.js
+++ b/src/groups.js
@@ -226,9 +226,9 @@ var async = require('async'),
 					return callback(err);
 				}
 				results.base.name = !options.unescape ? validator.escape(results.base.name) : results.base.name;
-				results.base.description = options.unescape ? validator.escape(results.base.description) : results.base.description;
+				results.base.description = !options.unescape ? validator.escape(results.base.description) : results.base.description;
 				results.base.descriptionParsed = descriptionParsed;
-				results.base.userTitle = options.unescape ? validator.escape(results.base.userTitle) : results.base.userTitle;
+				results.base.userTitle = !options.unescape ? validator.escape(results.base.userTitle) : results.base.userTitle;
 				results.base.userTitleEnabled = results.base.userTitleEnabled ? !!parseInt(results.base.userTitleEnabled, 10) : true;
 				results.base.createtimeISO = utils.toISOString(results.base.createtime);
 				results.base.members = results.users.filter(Boolean);
@@ -440,7 +440,6 @@ var async = require('async'),
 						return ephemeralGroups.indexOf(slug) !== -1;
 					}));
 				},
-				async.apply(db.isObjectFields, 'groupslug:groupname', slugs),
 				async.apply(db.isSortedSetMembers, 'groups:createtime', name)
 			], function(err, results) {
 				if (err) {
@@ -448,7 +447,7 @@ var async = require('async'),
 				}
 
 				callback(null, results.map(function(result) {
-					return result[0] || result[1] || result[2];
+					return result[0] || result[1];
 				}));
 			});
 		} else {
@@ -457,16 +456,19 @@ var async = require('async'),
 				function(next) {
 					next(null, ephemeralGroups.indexOf(slug) !== -1);
 				},
-				async.apply(db.isObjectField, 'groupslug:groupname', slug),
 				async.apply(db.isSortedSetMember, 'groups:createtime', name)
 			], function(err, results) {
-				callback(err, !err ? (results[0] || results[1] || results[2]) : null);
+				callback(err, !err ? (results[0] || results[1]) : null);
 			});
 		}
 	};
 
 	Groups.existsBySlug = function(slug, callback) {
-		db.isObjectField('groupslug:groupname', slug, callback);
+		if (Array.isArray(slug)) {
+			db.isObjectFields('groupslug:groupName', slug, callback);
+		} else {
+			db.isObjectField('groupslug:groupname', slug, callback);
+		}
 	};
 
 	Groups.create = function(data, callback) {
@@ -499,7 +501,7 @@ var async = require('async'),
 					deleted: '0',
 					hidden: data.hidden || '0',
 					system: system ? '1' : '0',
-					'private': data.private || '1'
+					private: data.private || '1'
 				},
 				tasks = [
 					async.apply(db.sortedSetAdd, 'groups:createtime', now, data.name),
@@ -538,14 +540,26 @@ var async = require('async'),
 			}
 
 			var payload = {
-					userTitle: values.userTitle || '',
-					userTitleEnabled: values.userTitleEnabled === true ? '1' : '0',
-					description: values.description || '',
-					icon: values.icon || '',
-					labelColor: values.labelColor || '#000000',
-					hidden: values.hidden === true ? '1' : '0',
-					'private': values.private === false ? '0' : '1'
-				};
+				description: values.description || '',
+				icon: values.icon || '',
+				labelColor: values.labelColor || '#000000'
+			};
+
+			if (values.hasOwnProperty('userTitle')) {
+				payload.userTitle = values.userTitle || '';
+			}
+
+			if (values.hasOwnProperty('userTitleEnabled')) {
+				payload.userTitleEnabled = values.userTitleEnabled ? '1' : '0';
+			}
+
+			if (values.hasOwnProperty('hidden')) {
+				payload.hidden = values.hidden ? '1' : '0';
+			}
+
+			if (values.hasOwnProperty('private')) {
+				payload.private = values.private ? '1' : '0';
+			}
 
 			async.series([
 				async.apply(updatePrivacy, groupName, values.private),
@@ -566,9 +580,15 @@ var async = require('async'),
 	};
 
 	function updatePrivacy(groupName, newValue, callback) {
-		// Grab the group's current privacy value
+		if (!newValue) {
+			return callback();
+		}
+
 		Groups.getGroupFields(groupName, ['private'], function(err, currentValue) {
-			currentValue = currentValue.private === '1';	// Now a Boolean
+			if (err) {
+				return callback(err);
+			}
+			currentValue = currentValue.private === '1';
 
 			if (currentValue !== newValue && currentValue === true) {
 				// Group is now public, so all pending users are automatically considered members
diff --git a/src/meta.js b/src/meta.js
index b58ac1a9b2..4aa65f6719 100644
--- a/src/meta.js
+++ b/src/meta.js
@@ -29,7 +29,7 @@ var async = require('async'),
 	Meta.userOrGroupExists = function(slug, callback) {
 		async.parallel([
 			async.apply(user.exists, slug),
-			async.apply(groups.exists, slug)
+			async.apply(groups.existsBySlug, slug)
 		], function(err, results) {
 			callback(err, results ? results.some(function(result) { return result; }) : false);
 		});
diff --git a/src/meta/css.js b/src/meta/css.js
index e0f0412e1c..d45b61f280 100644
--- a/src/meta/css.js
+++ b/src/meta/css.js
@@ -49,6 +49,7 @@ module.exports = function(Meta) {
 
 				source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui-1.10.4.custom.min.css";';
 				source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";';
+				source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/jquery/textcomplete/jquery.textcomplete.css";';
 				source += '\n@import (inline) "..' + path.sep + '..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";';
 
 				acpSource = '\n@import "..' + path.sep + 'public/less/admin/admin";\n' + source;
diff --git a/src/meta/js.js b/src/meta/js.js
index 177d8b03c8..3d8ced839c 100644
--- a/src/meta/js.js
+++ b/src/meta/js.js
@@ -31,6 +31,7 @@ module.exports = function(Meta) {
 				'public/vendor/visibility/visibility.min.js',
 				'public/vendor/bootstrap/js/bootstrap.min.js',
 				'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.min.js',
+				'public/vendor/jquery/textcomplete/jquery.textcomplete.min.js',
 				'public/vendor/requirejs/require.js',
 				'public/vendor/bootbox/bootbox.min.js',
 				'public/vendor/tinycon/tinycon.js',
@@ -43,10 +44,11 @@ module.exports = function(Meta) {
 				'public/src/utils.js',
 				'public/src/app.js',
 				'public/src/ajaxify.js',
-				'public/src/variables.js',
-				'public/src/widgets.js',
+				'public/src/components.js',
+				'public/src/overrides.js',
 				'public/src/translator.js',
-				'public/src/overrides.js'
+				'public/src/variables.js',
+				'public/src/widgets.js'
 			],
 			rjs: []
 		}
diff --git a/src/postTools.js b/src/postTools.js
index 9ddc7b999e..647d796841 100644
--- a/src/postTools.js
+++ b/src/postTools.js
@@ -19,7 +19,7 @@ var winston = require('winston'),
 
 var cache = LRU({
 	max: 1048576,
-	length: function (n) { return n.length },
+	length: function (n) { return n.length; },
 	maxAge: 1000 * 60 * 60
 });
 
@@ -63,22 +63,32 @@ var cache = LRU({
 				},
 				topic: function(next) {
 					var tid = postData.tid;
-					posts.isMain(data.pid, function(err, isMainPost) {
+					async.parallel({
+						cid: function(next) {
+							topics.getTopicField(tid, 'cid', next);
+						},
+						isMain: function(next) {
+							posts.isMain(data.pid, next);
+						}
+					}, function(err, results) {
 						if (err) {
 							return next(err);
 						}
 
 						options.tags = options.tags || [];
 
-						if (!isMainPost) {
+						if (!results.isMain) {
 							return next(null, {
 								tid: tid,
+								cid: results.cid,
 								isMainPost: false
 							});
 						}
 
 						var topicData = {
 							tid: tid,
+							cid: results.cid,
+							uid: postData.uid,
 							mainPid: data.pid,
 							title: title,
 							slug: tid + '/' + utils.slugify(title)
@@ -98,8 +108,10 @@ var cache = LRU({
 							topics.getTopicTagsObjects(tid, function(err, tags) {
 								next(err, {
 									tid: tid,
+									cid: results.cid,
+									uid: postData.uid,
 									title: validator.escape(title),
-									isMainPost: isMainPost,
+									isMainPost: results.isMain,
 									tags: tags
 								});
 							});
@@ -114,6 +126,7 @@ var cache = LRU({
 				if (err) {
 					return callback(err);
 				}
+				postData.cid = results.topic.cid;
 				results.content = results.postData.content;
 
 				plugins.fireHook('action:post.edit', postData);
@@ -156,7 +169,7 @@ var cache = LRU({
 			}
 
 			if (isDelete) {
-				cache.del(postData.pid);
+				cache.del(pid);
 				posts.delete(pid, callback);
 			} else {
 				posts.restore(pid, function(err, postData) {
diff --git a/src/posts/create.js b/src/posts/create.js
index a235b2f3f0..1bce6e69c7 100644
--- a/src/posts/create.js
+++ b/src/posts/create.js
@@ -68,7 +68,13 @@ module.exports = function(Posts) {
 						topics.onNewPostMade(postData, next);
 					},
 					function(next) {
-						categories.onNewPostMade(postData, next);
+						topics.getTopicFields(tid, ['cid', 'pinned'], function(err, topicData) {
+							if (err) {
+								return next(err);
+							}
+							postData.cid = topicData.cid;
+							categories.onNewPostMade(topicData.cid, topicData.pinned, postData, next);
+						});
 					},
 					function(next) {
 						db.sortedSetAdd('posts:pid', timestamp, postData.pid, next);
diff --git a/src/posts/delete.js b/src/posts/delete.js
index bd130ade46..f47ff28e88 100644
--- a/src/posts/delete.js
+++ b/src/posts/delete.js
@@ -9,16 +9,19 @@ var async = require('async'),
 module.exports = function(Posts) {
 
 	Posts.delete = function(pid, callback) {
-		Posts.setPostField(pid, 'deleted', 1, function(err) {
-			if (err) {
-				return callback(err);
-			}
-
-			Posts.getPostFields(pid, ['pid', 'tid', 'uid', 'timestamp'], function(err, postData) {
-				if (err) {
-					return callback(err);
-				}
-
+		var postData;
+		async.waterfall([
+			function(next) {
+				Posts.setPostField(pid, 'deleted', 1, next);
+			},
+			function(next) {
+				Posts.getPostFields(pid, ['pid', 'tid', 'uid', 'timestamp'], next);
+			},
+			function(_post, next) {
+				postData = _post;
+				topics.getTopicField(_post.tid, 'cid', next);
+			},
+			function(cid, next) {
 				plugins.fireHook('action:post.delete', pid);
 
 				async.parallel([
@@ -26,7 +29,7 @@ module.exports = function(Posts) {
 						updateTopicTimestamp(postData.tid, next);
 					},
 					function(next) {
-						removeFromCategoryRecentPosts(pid, postData.tid, next);
+						db.sortedSetRemove('cid:' + cid + ':pids', pid, next);
 					},
 					function(next) {
 						Posts.dismissFlag(pid, next);
@@ -34,21 +37,25 @@ module.exports = function(Posts) {
 				], function(err) {
 					callback(err, postData);
 				});
-			});
-		});
+			}
+		], callback);
 	};
 
 	Posts.restore = function(pid, callback) {
-		Posts.setPostField(pid, 'deleted', 0, function(err) {
-			if (err) {
-				return callback(err);
-			}
-
-			Posts.getPostFields(pid, ['pid', 'tid', 'uid', 'content', 'timestamp'], function(err, postData) {
-				if (err) {
-					return callback(err);
-				}
-
+		var postData;
+		async.waterfall([
+			function(next) {
+				Posts.setPostField(pid, 'deleted', 0, next);
+			},
+			function(next) {
+				Posts.getPostFields(pid, ['pid', 'tid', 'uid', 'content', 'timestamp'], next);
+			},
+			function(_post, next) {
+				postData = _post;
+				topics.getTopicField(_post.tid, 'cid', next);
+			},
+			function(cid, next) {
+				postData.cid = cid;
 				plugins.fireHook('action:post.restore', postData);
 
 				async.parallel([
@@ -56,13 +63,13 @@ module.exports = function(Posts) {
 						updateTopicTimestamp(postData.tid, next);
 					},
 					function(next) {
-						addToCategoryRecentPosts(pid, postData.tid, postData.timestamp, next);
+						db.sortedSetAdd('cid:' + cid + ':pids', postData.timestamp, pid, next);
 					}
 				], function(err) {
 					callback(err, postData);
 				});
-			});
-		});
+			}
+		], callback);
 	};
 
 	function updateTopicTimestamp(tid, callback) {
@@ -84,26 +91,6 @@ module.exports = function(Posts) {
 		});
 	}
 
-	function removeFromCategoryRecentPosts(pid, tid, callback) {
-		topics.getTopicField(tid, 'cid', function(err, cid) {
-			if (err) {
-				return callback(err);
-			}
-
-			db.sortedSetRemove('cid:' + cid + ':pids', pid, callback);
-		});
-	}
-
-	function addToCategoryRecentPosts(pid, tid, timestamp, callback) {
-		topics.getTopicField(tid, 'cid', function(err, cid) {
-			if (err) {
-				return callback(err);
-			}
-
-			db.sortedSetAdd('cid:' + cid + ':pids', timestamp, pid, callback);
-		});
-	}
-
 	Posts.purge = function(pid, callback) {
 		Posts.exists(pid, function(err, exists) {
 			if (err || !exists) {
diff --git a/src/posts/recent.js b/src/posts/recent.js
index 4a970a6435..d41ef5342e 100644
--- a/src/posts/recent.js
+++ b/src/posts/recent.js
@@ -13,16 +13,16 @@ module.exports = function(Posts) {
 	};
 
 	Posts.getRecentPosts = function(uid, start, stop, term, callback) {
-		var since = terms.day;
+		var min = 0;
 		if (terms[term]) {
-			since = terms[term];
+			min = Date.now() - terms[term];
 		}
 
 		var count = parseInt(stop, 10) === -1 ? stop : stop - start + 1;
 
 		async.waterfall([
 			function(next) {
-				db.getSortedSetRevRangeByScore('posts:pid', start, count, '+inf', Date.now() - since, next);
+				db.getSortedSetRevRangeByScore('posts:pid', start, count, '+inf', min, next);
 			},
 			function(pids, next) {
 				privileges.posts.filter('read', pids, uid, next);
diff --git a/src/routes/feeds.js b/src/routes/feeds.js
index 0f2f6082b6..58d44cbce5 100644
--- a/src/routes/feeds.js
+++ b/src/routes/feeds.js
@@ -84,8 +84,8 @@ function generateForTopic(req, res, next) {
 					feed.item({
 						title: 'Reply to ' + topicData.title + ' on ' + dateStamp,
 						description: postData.content,
-						url: nconf.get('url') + '/topic/' + topicData.slug + '#' + postData.pid,
-						author: postData.username,
+						url: nconf.get('url') + '/topic/' + topicData.slug + (postData.index ? '/' + (postData.index + 1) : ''),
+						author: postData.user ? postData.user.username : '',
 						date: dateStamp
 					});
 				}
@@ -144,8 +144,8 @@ function generateForCategory(req, res, next) {
 			if (err) {
 				return next(err);
 			}
-			sendFeed(feed, res);	
-		});		
+			sendFeed(feed, res);
+		});
 	});
 }
 
@@ -183,8 +183,8 @@ function generateForPopular(req, res, next) {
 				return next(err);
 			}
 			sendFeed(feed, res);
-		});	
-	});	
+		});
+	});
 }
 
 function disabledRSS(req, res, next) {
@@ -201,13 +201,13 @@ function generateForTopics(options, set, req, res, next) {
 		if (err) {
 			return next(err);
 		}
-		
+
 		generateTopicsFeed(options, data.topics, function(err, feed) {
 			if (err) {
 				return next(err);
 			}
-			sendFeed(feed, res);	
-		});	
+			sendFeed(feed, res);
+		});
 	});
 }
 
@@ -215,7 +215,7 @@ function generateTopicsFeed(feedOptions, feedTopics, callback) {
 	var tids = feedTopics.map(function(topic) {
 		return topic ? topic.tid : null;
 	});
-	
+
 	topics.getMainPids(tids, function(err, pids) {
 		if (err) {
 			return callback(err);
@@ -252,7 +252,7 @@ function generateTopicsFeed(feedOptions, feedTopics, callback) {
 			});
 			callback(null, feed);
 		});
-	});	
+	});
 }
 
 function generateForRecentPosts(req, res, next) {
diff --git a/src/routes/index.js b/src/routes/index.js
index 37ce391e54..5efca3f067 100644
--- a/src/routes/index.js
+++ b/src/routes/index.js
@@ -173,50 +173,51 @@ module.exports = function(app, middleware) {
 
 function handle404(app, middleware) {
 	app.use(function(req, res, next) {
-		if (!plugins.hasListeners('action:meta.override404')) {
-			var relativePath = nconf.get('relative_path');
-			var	isLanguage = new RegExp('^' + relativePath + '/language/[\\w]{2,}/.*.json'),
-				isClientScript = new RegExp('^' + relativePath + '\\/src\\/.+\\.js');
-
-			if (isClientScript.test(req.url)) {
-				res.type('text/javascript').status(200).send('');
-			} else if (isLanguage.test(req.url)) {
-				res.status(200).json({});
-			} else if (req.accepts('html')) {
-				if (process.env.NODE_ENV === 'development') {
-					winston.warn('Route requested but not found: ' + req.url);
-				}
-
-				res.status(404);
-
-				if (res.locals.isAPI) {
-					return res.json({path: req.path, error: 'not-found'});
-				}
-
-				middleware.buildHeader(req, res, function() {
-					res.render('404', {path: req.path});
-				});
-			} else {
-				res.status(404).type('txt').send('Not found');
-			}
-		} else {
-			plugins.fireHook('action:meta.override404', {
+		if (plugins.hasListeners('action:meta.override404')) {
+			return plugins.fireHook('action:meta.override404', {
 				req: req,
 				res: res,
 				error: {}
 			});
 		}
+
+		var relativePath = nconf.get('relative_path');
+		var	isLanguage = new RegExp('^' + relativePath + '/language/[\\w]{2,}/.*.json'),
+			isClientScript = new RegExp('^' + relativePath + '\\/src\\/.+\\.js');
+
+		if (isClientScript.test(req.url)) {
+			res.type('text/javascript').status(200).send('');
+		} else if (isLanguage.test(req.url)) {
+			res.status(200).json({});
+		} else if (req.accepts('html')) {
+			if (process.env.NODE_ENV === 'development') {
+				winston.warn('Route requested but not found: ' + req.url);
+			}
+
+			res.status(404);
+
+			if (res.locals.isAPI) {
+				return res.json({path: req.path, error: 'not-found'});
+			}
+
+			middleware.buildHeader(req, res, function() {
+				res.render('404', {path: req.path});
+			});
+		} else {
+			res.status(404).type('txt').send('Not found');
+		}
 	});
 }
 
 function handleErrors(app, middleware) {
 	app.use(function(err, req, res, next) {
-		winston.error(req.path + '\n', err.stack);
-
 		if (err.code === 'EBADCSRFTOKEN') {
+			winston.error(req.path + '\n', err.message)
 			return res.sendStatus(403);
 		}
 
+		winston.error(req.path + '\n', err.stack);
+
 		if (parseInt(err.status, 10) === 302 && err.path) {
 			return res.locals.isAPI ? res.status(302).json(err.path) : res.redirect(err.path);
 		}
diff --git a/src/search.js b/src/search.js
index 8ccdf77d78..063ebfa142 100644
--- a/src/search.js
+++ b/src/search.js
@@ -43,7 +43,7 @@ search.search = function(data, callback) {
 	};
 
 	if (searchIn === 'posts' || searchIn === 'titles' || searchIn === 'titlesposts') {
-		searchInContent(query, data, done);
+		searchInContent(data, done);
 	} else if (searchIn === 'users') {
 		searchInUsers(query, data.uid, done);
 	} else if (searchIn === 'tags') {
@@ -53,91 +53,87 @@ search.search = function(data, callback) {
 	}
 };
 
-function searchInContent(query, data, callback) {
+function searchInContent(data, callback) {
 	data.uid = data.uid || 0;
 	async.parallel({
-		pids: function(next) {
-			if (data.searchIn === 'posts' || data.searchIn === 'titlesposts') {
-				search.searchQuery('post', query, next);
-			} else {
-				next(null, []);
-			}
-		},
-		tids: function(next) {
-			if (data.searchIn === 'titles' || data.searchIn === 'titlesposts') {
-				search.searchQuery('topic', query, next);
-			} else {
-				next(null, []);
-			}
+		searchCids: function(next) {
+			getSearchCids(data, next);
 		},
-		searchCategories: function(next) {
-			getSearchCategories(data, next);
+		searchUids: function(next) {
+			getSearchUids(data, next);
 		}
-	}, function (err, results) {
+	}, function(err, results) {
 		if (err) {
 			return callback(err);
 		}
 
-		var matchCount = 0;
-		if (!results || (!results.pids.length && !results.tids.length)) {
-			return callback(null, {matches: [], matchCount: matchCount});
-		}
-
-		async.waterfall([
-			function(next) {
-				getMainPids(results.tids, next);
-			},
-			function(mainPids, next) {
-				results.pids.forEach(function(pid) {
-					if (mainPids.indexOf(pid.toString()) === -1) {
-						mainPids.push(pid);
-					}
-				});
-				
-				privileges.posts.filter('read', mainPids, data.uid, next);
-			},
-			function(pids, next) {
-				filterAndSort(pids, data, results.searchCategories, next);
+		async.parallel({
+			pids: function(next) {
+				if (data.searchIn === 'posts' || data.searchIn === 'titlesposts') {
+					search.searchQuery('post', data.query, results.searchCids, results.searchUids, next);
+				} else {
+					next(null, []);
+				}
 			},
-			function(pids, next) {
-				matchCount = pids.length;
-				if (data.page) {
-					var start = Math.max(0, (data.page - 1)) * 10;
-					pids = pids.slice(start, start + 10);
+			tids: function(next) {
+				if (data.searchIn === 'titles' || data.searchIn === 'titlesposts') {
+					search.searchQuery('topic', data.query, results.searchCids, results.searchUids, next);
+				} else {
+					next(null, []);
 				}
+			}
+		}, function (err, results) {
+			if (err) {
+				return callback(err);
+			}
 
-				posts.getPostSummaryByPids(pids, data.uid, {stripTags: true, parse: false}, next);
-			},
-			function(posts, next) {
-				next(null, {matches: posts, matchCount: matchCount});
+			var matchCount = 0;
+			if (!results || (!results.pids.length && !results.tids.length)) {
+				return callback(null, {matches: [], matchCount: matchCount});
 			}
-		], callback);
+
+			async.waterfall([
+				function(next) {
+					topics.getMainPids(results.tids, next);
+				},
+				function(mainPids, next) {
+					results.pids = mainPids.concat(results.pids).filter(function(pid, index, array) {
+						return pid && array.indexOf(pid) === index;
+					});
+
+					privileges.posts.filter('read', results.pids, data.uid, next);
+				},
+				function(pids, next) {
+					filterAndSort(pids, data, next);
+				},
+				function(pids, next) {
+					matchCount = pids.length;
+					if (data.page) {
+						var start = Math.max(0, (data.page - 1)) * 10;
+						pids = pids.slice(start, start + 10);
+					}
+
+					posts.getPostSummaryByPids(pids, data.uid, {stripTags: true, parse: false}, next);
+				},
+				function(posts, next) {
+					next(null, {matches: posts, matchCount: matchCount});
+				}
+			], callback);
+		});
 	});
 }
 
-function filterAndSort(pids, data, searchCategories, callback) {
-	async.parallel({
-		posts: function(next) {
-			getMatchedPosts(pids, data, searchCategories, next);
-		},
-		postedByUid: function(next) {
-			if (data.postedBy) {
-				user.getUidByUsername(data.postedBy, next);
-			} else {
-				next();
-			}
-		}
-	}, function(err, results) {
+function filterAndSort(pids, data, callback) {
+	getMatchedPosts(pids, data, function(err, posts) {
 		if (err) {
 			return callback(err);
 		}
-		if (!results.posts) {
+
+		if (!Array.isArray(posts) || !posts.length) {
 			return callback(null, pids);
 		}
-		var posts = results.posts.filter(Boolean);
+		posts = posts.filter(Boolean);
 
-		posts = filterByUser(posts, results.postedByUid);
-		posts = filterByCategories(posts, searchCategories);
 		posts = filterByPostcount(posts, data.replies, data.repliesFilter);
 		posts = filterByTimerange(posts, data.timeRange, data.timeFilter);
 
@@ -151,25 +147,19 @@ function filterAndSort(pids, data, searchCategories, callback) {
 	});
 }
 
-function getMatchedPosts(pids, data, searchCategories, callback) {
+function getMatchedPosts(pids, data, callback) {
 	var postFields = ['pid', 'tid', 'timestamp'];
 	var topicFields = [];
 	var categoryFields = [];
 
-	if (data.postedBy) {
-		postFields.push('uid');
-	}
-
-	if (searchCategories.length || (data.sortBy && data.sortBy.startsWith('category.'))) {
-		topicFields.push('cid');
-	}
-
 	if (data.replies) {
 		topicFields.push('postcount');
 	}
 
 	if (data.sortBy) {
-		if (data.sortBy.startsWith('topic.')) {
+		if (data.sortBy.startsWith('category')) {
+			topicFields.push('cid');
+		} else if (data.sortBy.startsWith('topic.')) {
 			topicFields.push(data.sortBy.split('.')[1]);
 		} else if (data.sortBy.startsWith('user.')) {
 			postFields.push('uid');
@@ -282,25 +272,6 @@ function getMatchedPosts(pids, data, searchCategories, callback) {
 	], callback);
 }
 
-function filterByUser(posts, postedByUid) {
-	if (postedByUid) {
-		postedByUid = parseInt(postedByUid, 10);
-		posts = posts.filter(function(post) {
-			return parseInt(post.uid, 10) === postedByUid;
-		});
-	}
-	return posts;
-}
-
-function filterByCategories(posts, searchCategories) {
-	if (searchCategories.length) {
-		posts = posts.filter(function(post) {
-			return post.topic && searchCategories.indexOf(post.topic.cid) !== -1;
-		});
-	}
-	return posts;
-}
-
 function filterByPostcount(posts, postCount, repliesFilter) {
 	postCount = parseInt(postCount, 10);
 	if (postCount) {
@@ -391,7 +362,7 @@ function sortPosts(posts, data) {
 	}
 }
 
-function getSearchCategories(data, callback) {
+function getSearchCids(data, callback) {
 	if (!Array.isArray(data.categories) || !data.categories.length || data.categories.indexOf('all') !== -1) {
 		return callback(null, []);
 	}
@@ -441,6 +412,14 @@ function getChildrenCids(cids, uid, callback) {
 	});
 }
 
+function getSearchUids(data, callback) {
+	if (data.postedBy) {
+		user.getUidsByUsernames(Array.isArray(data.postedBy) ? data.postedBy : [data.postedBy], callback);
+	} else {
+		callback(null, []);
+	}
+}
+
 function searchInUsers(query, uid, callback) {
 	user.search({query: query, uid: uid}, function(err, results) {
 		if (err) {
@@ -460,26 +439,12 @@ function searchInTags(query, callback) {
 	});
 }
 
-function getMainPids(tids, callback) {
-	if (!Array.isArray(tids) || !tids.length) {
-		return callback(null, []);
-	}
-
-	topics.getTopicsFields(tids, ['mainPid'], function(err, topics) {
-		if (err) {
-			return callback(err);
-		}
-		topics = topics.map(function(topic) {
-			return topic && topic.mainPid && topic.mainPid.toString();
-		}).filter(Boolean);
-		callback(null, topics);
-	});
-}
-
-search.searchQuery = function(index, query, callback) {
+search.searchQuery = function(index, content, cids, uids, callback) {
 	plugins.fireHook('filter:search.query', {
 		index: index,
-		query: query
+		content: content,
+		cid: cids,
+		uid: uids
 	}, callback);
 };
 
diff --git a/src/socket.io/admin.js b/src/socket.io/admin.js
index 6860fb6e66..6b61d523f3 100644
--- a/src/socket.io/admin.js
+++ b/src/socket.io/admin.js
@@ -304,7 +304,7 @@ SocketAdmin.getMoreEvents = function(socket, next, callback) {
 	if (start < 0) {
 		return callback(null, {data: [], next: next});
 	}
-	var end = next + 10;
+	var end = start + 10;
 	events.getEvents(start, end, function(err, events) {
 		if (err) {
 			return callback(err);
diff --git a/src/socket.io/categories.js b/src/socket.io/categories.js
index 9f20697ef9..d98cb0c21f 100644
--- a/src/socket.io/categories.js
+++ b/src/socket.io/categories.js
@@ -118,4 +118,8 @@ SocketCategories.ignore = function(socket, cid, callback) {
 	});
 };
 
+SocketCategories.isModerator = function(socket, cid, callback) {
+	user.isModerator(socket.uid, cid, callback);
+};
+
 module.exports = SocketCategories;
diff --git a/src/socket.io/meta.js b/src/socket.io/meta.js
index c801381a1f..21fdeca35e 100644
--- a/src/socket.io/meta.js
+++ b/src/socket.io/meta.js
@@ -70,6 +70,10 @@ SocketMeta.rooms.enter = function(socket, data, callback) {
 		socket.currentRoom = data.enter;
 		if (data.enter.indexOf('topic') !== -1) {
 			data.uid = socket.uid;
+			data.picture = validator.escape(data.picture);
+			data.username = validator.escape(data.username);
+			data.userslug = validator.escape(data.userslug);
+
 			websockets.in(data.enter).emit('event:user_enter', data);
 		}
 	}
diff --git a/src/socket.io/posts.js b/src/socket.io/posts.js
index e8f5ef9a63..b49e796e7a 100644
--- a/src/socket.io/posts.js
+++ b/src/socket.io/posts.js
@@ -50,6 +50,10 @@ SocketPosts.reply = function(socket, data, callback) {
 		socket.emit('event:new_post', result);
 
 		SocketPosts.notifyOnlineUsers(socket.uid, result);
+
+		if (data.lock) {
+			socketTopics.doTopicAction('lock', 'event:topic_locked', socket, {tids: [postData.topic.tid], cid: postData.topic.cid});
+		}
 	});
 };
 
@@ -362,9 +366,9 @@ SocketPosts.purge = function(socket, data, callback) {
 			});
 
 			callback();
-		});	
+		});
 	}
-	
+
 	if (!data || !parseInt(data.pid, 10)) {
 		return callback(new Error('[[error:invalid-data]]'));
 	}
@@ -500,7 +504,7 @@ SocketPosts.flag = function(socket, pid, callback) {
 				}
 				notifications.push(notification, results.admins.concat(results.moderators), next);
 			});
-		}		
+		}
 	], callback);
 };
 
diff --git a/src/socket.io/topics.js b/src/socket.io/topics.js
index cd42d1fde4..c270082953 100644
--- a/src/socket.io/topics.js
+++ b/src/socket.io/topics.js
@@ -17,7 +17,7 @@ var nconf = require('nconf'),
 	meta = require('../meta'),
 	events = require('../events'),
 	utils = require('../../public/src/utils'),
-	SocketPosts = require('./posts'),
+
 
 	SocketTopics = {};
 
@@ -41,6 +41,10 @@ SocketTopics.post = function(socket, data, callback) {
 			return callback(err);
 		}
 
+		if (data.lock) {
+			SocketTopics.doTopicAction('lock', 'event:topic_locked', socket, {tids: [result.topicData.tid], cid: result.topicData.cid});
+		}
+
 		callback(null, result.topicData);
 		socket.emit('event:new_post', {posts: [result.postData]});
 		socket.emit('event:new_topic', result.topicData);
@@ -233,6 +237,7 @@ SocketTopics.unpin = function(socket, data, callback) {
 };
 
 SocketTopics.doTopicAction = function(action, event, socket, data, callback) {
+	callback = callback || function() {};
 	if (!socket.uid) {
 		return;
 	}
@@ -312,7 +317,7 @@ SocketTopics.movePost = function(socket, data, callback) {
 				return callback(err);
 			}
 
-			SocketPosts.sendNotificationToPostOwner(data.pid, socket.uid, 'notifications:moved_your_post');
+			require('./posts').sendNotificationToPostOwner(data.pid, socket.uid, 'notifications:moved_your_post');
 			callback();
 		});
 	});
@@ -550,4 +555,13 @@ SocketTopics.loadMoreTags = function(socket, data, callback) {
 	});
 };
 
+SocketTopics.isModerator = function(socket, tid, callback) {
+	topics.getTopicField(tid, 'cid', function(err, cid) {
+		if (err) {
+			return callback(err);
+		}
+		user.isModerator(socket.uid, cid, callback);
+	});
+};
+
 module.exports = SocketTopics;
diff --git a/src/socket.io/user.js b/src/socket.io/user.js
index 7170cf27d0..807666ff18 100644
--- a/src/socket.io/user.js
+++ b/src/socket.io/user.js
@@ -281,7 +281,7 @@ SocketUser.follow = function(socket, data, callback) {
 	if (!socket.uid || !data) {
 		return;
 	}
-
+	var userData;
 	async.waterfall([
 		function(next) {
 			toggleFollow('follow', socket.uid, data.uid, next);
@@ -289,7 +289,8 @@ SocketUser.follow = function(socket, data, callback) {
 		function(next) {
 			user.getUserFields(socket.uid, ['username', 'userslug'], next);
 		},
-		function(userData, next) {
+		function(_userData, next) {
+			userData = _userData;
 			notifications.create({
 				bodyShort: '[[notifications:user_started_following_you, ' + userData.username + ']]',
 				nid: 'follow:' + data.uid + ':uid:' + socket.uid,
@@ -297,6 +298,7 @@ SocketUser.follow = function(socket, data, callback) {
 			}, next);
 		},
 		function(notification, next) {
+			notification.user = userData;
 			notifications.push(notification, [data.uid], next);
 		}
 	], callback);
diff --git a/src/threadTools.js b/src/threadTools.js
index 96d8198291..76ea84c9df 100644
--- a/src/threadTools.js
+++ b/src/threadTools.js
@@ -21,7 +21,7 @@ var async = require('async'),
 	};
 
 	function toggleDelete(tid, uid, isDelete, callback) {
-		topics.getTopicFields(tid, ['tid', 'cid', 'deleted', 'title', 'mainPid'], function(err, topicData) {
+		topics.getTopicFields(tid, ['tid', 'cid', 'uid', 'deleted', 'title', 'mainPid'], function(err, topicData) {
 			if (err) {
 				return callback(err);
 			}
@@ -183,13 +183,16 @@ var async = require('async'),
 
 			categories.moveRecentReplies(tid, oldCid, cid);
 
-			topics.setTopicField(tid, 'cid', cid, callback);
-
-			plugins.fireHook('action:topic.move', {
-				tid: tid,
-				fromCid: oldCid,
-				toCid: cid,
-				uid: uid
+			topics.setTopicField(tid, 'cid', cid, function(err) {
+				if (err) {
+					return callback(err);
+				}
+				plugins.fireHook('action:topic.move', {
+					tid: tid,
+					fromCid: oldCid,
+					toCid: cid,
+					uid: uid
+				});
 			});
 		});
 	};
diff --git a/src/topics.js b/src/topics.js
index aecdcb1000..0cc153e85e 100644
--- a/src/topics.js
+++ b/src/topics.js
@@ -287,13 +287,17 @@ var async = require('async'),
 	};
 
 	Topics.getMainPids = function(tids, callback) {
+		if (!Array.isArray(tids) || !tids.length) {
+			return callback(null, []);
+		}
+
 		Topics.getTopicsFields(tids, ['mainPid'], function(err, topicData) {
 			if (err) {
 				return callback(err);
 			}
 
 			var mainPids = topicData.map(function(topic) {
-				return topic ? topic.mainPid : null;
+				return topic && topic.mainPid;
 			});
 			callback(null, mainPids);
 		});
diff --git a/src/topics/create.js b/src/topics/create.js
index 36a73948d7..5988f637e2 100644
--- a/src/topics/create.js
+++ b/src/topics/create.js
@@ -93,7 +93,6 @@ module.exports = function(Topics) {
 
 	Topics.post = function(data, callback) {
 		var uid = data.uid,
-			handle = data.handle,
 			title = data.title,
 			content = data.content,
 			cid = data.cid;
@@ -135,7 +134,7 @@ module.exports = function(Topics) {
 				Topics.create({uid: uid, title: title, cid: cid, thumb: data.thumb, tags: data.tags}, next);
 			},
 			function(tid, next) {
-				Topics.reply({uid:uid, tid:tid, handle: handle, content:content, req: data.req}, next);
+				Topics.reply({uid:uid, tid:tid, handle: data.handle, content:content, req: data.req}, next);
 			},
 			function(postData, next) {
 				async.parallel({
@@ -185,30 +184,23 @@ module.exports = function(Topics) {
 	Topics.reply = function(data, callback) {
 		var tid = data.tid,
 			uid = data.uid,
-			toPid = data.toPid,
-			handle = data.handle,
 			content = data.content,
 			postData;
 
 		async.waterfall([
 			function(next) {
 				async.parallel({
-					exists: function(next) {
-						Topics.exists(tid, next);
-					},
-					locked: function(next) {
-						Topics.isLocked(tid, next);
-					},
-					canReply: function(next) {
-						privileges.topics.can('topics:reply', tid, uid, next);
-					}
+					exists: async.apply(Topics.exists, tid),
+					locked: async.apply(Topics.isLocked, tid),
+					canReply: async.apply(privileges.topics.can, 'topics:reply', tid, uid),
+					isAdmin: async.apply(user.isAdministrator, uid)
 				}, next);
 			},
 			function(results, next) {
 				if (!results.exists) {
 					return next(new Error('[[error:no-topic]]'));
 				}
-				if (results.locked) {
+				if (results.locked && !results.isAdmin) {
 					return next(new Error('[[error:topic-locked]]'));
 				}
 				if (!results.canReply) {
@@ -229,7 +221,7 @@ module.exports = function(Topics) {
 				checkContentLength(content, next);
 			},
 			function(next) {
-				posts.create({uid: uid, tid: tid, handle: handle, content: content, toPid: toPid, ip: data.req ? data.req.ip : null}, next);
+				posts.create({uid: uid, tid: tid, handle: data.handle, content: content, toPid: data.toPid, ip: data.req ? data.req.ip : null}, next);
 			},
 			function(data, next) {
 				postData = data;
diff --git a/src/topics/teaser.js b/src/topics/teaser.js
index 87e2baa86f..adb4f77b5c 100644
--- a/src/topics/teaser.js
+++ b/src/topics/teaser.js
@@ -7,6 +7,7 @@ var async = require('async'),
 	db = require('../database'),
 	user = require('../user'),
 	posts = require('../posts'),
+	plugins = require('../plugins'),
 	utils = require('../../public/src/utils');
 
 
@@ -27,7 +28,7 @@ module.exports = function(Topics) {
 			}
 		});
 
-		posts.getPostsFields(teaserPids, ['pid', 'uid', 'timestamp', 'tid'], function(err, postData) {
+		posts.getPostsFields(teaserPids, ['pid', 'uid', 'timestamp', 'tid', 'content'], function(err, postData) {
 			if (err) {
 				return callback(err);
 			}
@@ -61,7 +62,9 @@ module.exports = function(Topics) {
 					return tidToPost[topic.tid];
 				});
 
-				callback(null, teasers);
+				plugins.fireHook('filter:teasers.get', {teasers: teasers}, function(err, data) {
+					callback(err, data ? data.teasers : null);
+				});
 			});
 		});
 	};
diff --git a/src/user.js b/src/user.js
index 109e67a3f0..2b9c411ab8 100644
--- a/src/user.js
+++ b/src/user.js
@@ -120,7 +120,7 @@ var	async = require('async'),
 
 			if (user.picture) {
 				if (user.picture === user.uploadedpicture) {
-					user.picture = user.uploadedpicture = user.picture.indexOf('http') === -1 ? nconf.get('relative_path') + user.picture : user.picture;
+					user.picture = user.uploadedpicture = user.picture.startsWith('http') ? user.picture : nconf.get('relative_path') + user.picture;
 				} else {
 					user.picture = User.createGravatarURLFromEmail(user.email);
 				}
@@ -158,7 +158,7 @@ var	async = require('async'),
 				if (now - parseInt(userOnlineTime, 10) < 300000) {
 					return callback();
 				}
-				db.sortedSetAdd('users:online', now, uid, next);	
+				db.sortedSetAdd('users:online', now, uid, next);
 			},
 			function(next) {
 				topics.pushUnreadCount(uid);
@@ -334,6 +334,18 @@ var	async = require('async'),
 		db.getObjectField('username:uid', username, callback);
 	};
 
+	User.getUidsByUsernames = function(usernames, callback) {
+		db.getObjectFields('username:uid', usernames, function(err, users) {
+			if (err) {
+				return callback(err);
+			}
+			var uids = usernames.map(function(username) {
+				return users[username];
+			});
+			callback(null, uids);
+		});
+	};
+
 	User.getUidByUserslug = function(userslug, callback) {
 		if (!userslug) {
 			return callback();
diff --git a/src/user/digest.js b/src/user/digest.js
index 06ea1aa967..02799c0bf3 100644
--- a/src/user/digest.js
+++ b/src/user/digest.js
@@ -46,7 +46,6 @@ var	async = require('async'),
 
 				return topicObj;
 			});
-			return;
 
 			data.interval = interval;
 
diff --git a/src/user/notifications.js b/src/user/notifications.js
index c3ea4b9c23..fe9ffc80b8 100644
--- a/src/user/notifications.js
+++ b/src/user/notifications.js
@@ -109,34 +109,38 @@ var async = require('async'),
 				return callback(err);
 			}
 
-			var pids = notifications.map(function(notification) {
-				return notification ? notification.pid : null;
-			});
+			UserNotifications.generateNotificationPaths(notifications, uid, callback);
+		});
+	};
 
-			generatePostPaths(pids, uid, function(err, pidToPaths) {
-				if (err) {
-					return callback(err);
-				}
+	UserNotifications.generateNotificationPaths = function (notifications, uid, callback) {
+		var pids = notifications.map(function(notification) {
+			return notification ? notification.pid : null;
+		});
 
-				notifications = notifications.map(function(notification, index) {
-					if (!notification) {
-						return null;
-					}
+		generatePostPaths(pids, uid, function(err, pidToPaths) {
+			if (err) {
+				return callback(err);
+			}
 
-					notification.path = pidToPaths[notification.pid] || notification.path || '';
+			notifications = notifications.map(function(notification, index) {
+				if (!notification) {
+					return null;
+				}
 
-					if (notification.nid.startsWith('chat')) {
-						notification.path = nconf.get('relative_path') + '/chats/' + notification.user.userslug;
-					} else if (notification.nid.startsWith('follow')) {
-						notification.path = nconf.get('relative_path') + '/user/' + notification.user.userslug;
-					}
+				notification.path = pidToPaths[notification.pid] || notification.path || '';
 
-					notification.datetimeISO = utils.toISOString(notification.datetime);
-					return notification;
-				});
+				if (notification.nid.startsWith('chat')) {
+					notification.path = nconf.get('relative_path') + '/chats/' + notification.user.userslug;
+				} else if (notification.nid.startsWith('follow')) {
+					notification.path = nconf.get('relative_path') + '/user/' + notification.user.userslug;
+				}
 
-				callback(null, notifications);
+				notification.datetimeISO = utils.toISOString(notification.datetime);
+				return notification;
 			});
+
+			callback(null, notifications);
 		});
 	};
 
diff --git a/src/views/admin/manage/flags.tpl b/src/views/admin/manage/flags.tpl
index a87f450a47..f463d829d2 100644
--- a/src/views/admin/manage/flags.tpl
+++ b/src/views/admin/manage/flags.tpl
@@ -50,7 +50,7 @@
 									<p class="fade-out"></p>
 								</div>
 								<small>
-									<span class="pull-right footer">
+									<span class="pull-right">
 										Posted in <a href="{relative_path}/category/{posts.category.slug}" target="_blank"><i class="fa {posts.category.icon}"></i> {posts.category.name}</a>, <span class="timeago" title="{posts.relativeTime}"></span> &bull;
 										<a href="{relative_path}/topic/{posts.topic.slug}/{posts.index}" target="_blank">Read More</a>
 									</span>
diff --git a/src/views/partials/data/category.tpl b/src/views/partials/data/category.tpl
new file mode 100644
index 0000000000..2144c8a84c
--- /dev/null
+++ b/src/views/partials/data/category.tpl
@@ -0,0 +1 @@
+data-tid="{topics.tid}" data-index="{topics.index}" data-cid="{topics.cid}" itemprop="itemListElement"
\ No newline at end of file
diff --git a/src/views/partials/data/topic.tpl b/src/views/partials/data/topic.tpl
new file mode 100644
index 0000000000..125926e94f
--- /dev/null
+++ b/src/views/partials/data/topic.tpl
@@ -0,0 +1 @@
+data-pid="{posts.pid}" data-uid="{posts.uid}" data-username="{posts.user.username}" data-userslug="{posts.user.userslug}" data-index="{posts.index}" data-timestamp="{posts.timestamp}" data-votes="{posts.votes}" itemscope itemtype="http://schema.org/Comment"
\ No newline at end of file
diff --git a/src/views/partials/requirejs-config.tpl b/src/views/partials/requirejs-config.tpl
new file mode 100644
index 0000000000..932c571f4a
--- /dev/null
+++ b/src/views/partials/requirejs-config.tpl
@@ -0,0 +1,12 @@
+<script>
+	require.config({
+		baseUrl: "{relative_path}/src/modules",
+		waitSeconds: 3,
+		urlArgs: "{cache-buster}",
+		paths: {
+			'forum': '../forum',
+			'vendor': '../../vendor',
+			'mousetrap': '../../bower/mousetrap/mousetrap'
+		}
+	});
+</script>
\ No newline at end of file
diff --git a/src/views/partials/variables/account.tpl b/src/views/partials/variables/account.tpl
new file mode 100644
index 0000000000..cc63770339
--- /dev/null
+++ b/src/views/partials/variables/account.tpl
@@ -0,0 +1,2 @@
+<input type="hidden" template-variable="yourid" value="{yourid}" />
+<input type="hidden" template-variable="theirid" value="{theirid}" />
\ No newline at end of file
diff --git a/src/views/partials/variables/account/edit.tpl b/src/views/partials/variables/account/edit.tpl
new file mode 100644
index 0000000000..d8e5d259cc
--- /dev/null
+++ b/src/views/partials/variables/account/edit.tpl
@@ -0,0 +1,3 @@
+<input type="hidden" template-variable="userslug" value="{userslug}" />
+<input type="hidden" template-variable="gravatarpicture" value="{gravatarpicture}" />
+<input type="hidden" template-variable="uploadedpicture" value="{uploadedpicture}" />
\ No newline at end of file
diff --git a/src/views/partials/variables/account/profile.tpl b/src/views/partials/variables/account/profile.tpl
new file mode 100644
index 0000000000..be91685112
--- /dev/null
+++ b/src/views/partials/variables/account/profile.tpl
@@ -0,0 +1 @@
+<input type="hidden" template-type="boolean" template-variable="isFollowing" value="{isFollowing}" />
\ No newline at end of file
diff --git a/src/views/partials/variables/category.tpl b/src/views/partials/variables/category.tpl
new file mode 100644
index 0000000000..984002411a
--- /dev/null
+++ b/src/views/partials/variables/category.tpl
@@ -0,0 +1,6 @@
+<input type="hidden" template-variable="category_id" value="{cid}" />
+<input type="hidden" template-variable="category_name" value="{name}" />
+<input type="hidden" template-variable="category_slug" value="{slug}" />
+<input type="hidden" template-variable="topic_count" value="{topic_count}" />
+<input type="hidden" template-variable="currentPage" value="{currentPage}" />
+<input type="hidden" template-variable="pageCount" value="{pageCount}" />
\ No newline at end of file
diff --git a/src/views/partials/variables/groups/details.tpl b/src/views/partials/variables/groups/details.tpl
new file mode 100644
index 0000000000..0cefdde305
--- /dev/null
+++ b/src/views/partials/variables/groups/details.tpl
@@ -0,0 +1,2 @@
+<input type="hidden" template-variable="group_name" value="{group.name}" />
+<input type="hidden" template-variable="is_owner" value="{group.isOwner}" />
\ No newline at end of file
diff --git a/src/views/partials/variables/reset_code.tpl b/src/views/partials/variables/reset_code.tpl
new file mode 100644
index 0000000000..ab630d7cff
--- /dev/null
+++ b/src/views/partials/variables/reset_code.tpl
@@ -0,0 +1 @@
+<input type="hidden" template-variable="reset_code" value="{code}" />
\ No newline at end of file
diff --git a/src/views/partials/variables/tag.tpl b/src/views/partials/variables/tag.tpl
new file mode 100644
index 0000000000..486ba8245c
--- /dev/null
+++ b/src/views/partials/variables/tag.tpl
@@ -0,0 +1 @@
+<input type="hidden" template-variable="tag" value="{tag}" />
\ No newline at end of file
diff --git a/src/views/partials/variables/topic.tpl b/src/views/partials/variables/topic.tpl
new file mode 100644
index 0000000000..4f3c15180d
--- /dev/null
+++ b/src/views/partials/variables/topic.tpl
@@ -0,0 +1,11 @@
+<input type="hidden" template-variable="topic_id" value="{tid}" />
+<input type="hidden" template-variable="topic_slug" value="{slug}" />
+<input type="hidden" template-variable="category_id" value="{category.cid}" />
+<input type="hidden" template-variable="currentPage" value="{currentPage}" />
+<input type="hidden" template-variable="pageCount" value="{pageCount}" />
+<input type="hidden" template-variable="locked" template-type="boolean" value="{locked}" />
+<input type="hidden" template-variable="deleted" template-type="boolean" value="{deleted}" />
+<input type="hidden" template-variable="pinned" template-type="boolean" value="{pinned}" />
+<input type="hidden" template-variable="topic_name" value="{title}" />
+<input type="hidden" template-variable="postcount" value="{postcount}" />
+<input type="hidden" template-variable="viewcount" value="{viewcount}" />
\ No newline at end of file
diff --git a/src/webserver.js b/src/webserver.js
index 849be56879..bd4acb231f 100644
--- a/src/webserver.js
+++ b/src/webserver.js
@@ -110,7 +110,7 @@ if(nconf.get('ssl')) {
 		emitter.emit('nodebb:ready');
 	});
 
-	server.setTimeout(10000);
+	server.setTimout && server.setTimeout(10000);
 
 	module.exports.listen = function(callback) {
 		logger.init(app);
diff --git a/tests/groups.js b/tests/groups.js
index 0025000828..a4d5c9da30 100644
--- a/tests/groups.js
+++ b/tests/groups.js
@@ -186,13 +186,22 @@ describe('Groups', function() {
 	});
 
 	describe('.update()', function() {
+		before(function(done) {
+			Groups.create({
+				name: 'updateTestGroup',
+				description: 'bar',
+				system: 0,
+				hidden: 0
+			}, done);
+		});
+
 		it('should change an aspect of a group', function(done) {
-			Groups.update('foo', {
+			Groups.update('updateTestGroup', {
 				description: 'baz'
 			}, function(err) {
 				if (err) return done(err);
 
-				Groups.get('foo', {}, function(err, groupObj) {
+				Groups.get('updateTestGroup', {}, function(err, groupObj) {
 					if (err) return done(err);
 
 					assert.strictEqual('baz', groupObj.description);
@@ -203,16 +212,16 @@ describe('Groups', function() {
 		});
 
 		it('should rename a group if the name was updated', function(done) {
-			Groups.update('foo', {
-				name: 'foobar?'
+			Groups.update('updateTestGroup', {
+				name: 'updateTestGroup?'
 			}, function(err) {
 				if (err) return done(err);
 
-				Groups.get('foobar?', {}, function(err, groupObj) {
+				Groups.get('updateTestGroup?', {}, function(err, groupObj) {
 					if (err) return done(err);
 
-					assert.strictEqual('foobar?', groupObj.name);
-					assert.strictEqual('foobar', groupObj.slug);
+					assert.strictEqual('updateTestGroup?', groupObj.name);
+					assert.strictEqual('updatetestgroup', groupObj.slug);
 
 					done();
 				});