From 48af82659ed16abd0f8d9df2ddd6b5d00b4807e3 Mon Sep 17 00:00:00 2001
From: Julian Lam <julian@designcreateplay.com>
Date: Fri, 3 Jul 2015 16:41:21 -0400
Subject: [PATCH] removing the composer from core, out to its own plugin:
 nodebb-plugin-composer-default, closes #3288

---
 package.json                                |   3 +-
 public/src/app.js                           |  32 +-
 public/src/client/category.js               |   3 +-
 public/src/client/topic/postTools.js        |  92 +--
 public/src/modules/composer.js              | 595 --------------------
 public/src/modules/composer/categoryList.js |  44 --
 public/src/modules/composer/controls.js     |  46 --
 public/src/modules/composer/drafts.js       |  69 ---
 public/src/modules/composer/formatting.js   |  58 --
 public/src/modules/composer/preview.js      |  83 ---
 public/src/modules/composer/resize.js       | 195 -------
 public/src/modules/composer/tags.js         |  92 ---
 public/src/modules/composer/uploads.js      | 324 -----------
 src/messaging.js                            |   3 +-
 src/upgrade.js                              |  28 +-
 15 files changed, 100 insertions(+), 1567 deletions(-)
 delete mode 100644 public/src/modules/composer.js
 delete mode 100644 public/src/modules/composer/categoryList.js
 delete mode 100644 public/src/modules/composer/controls.js
 delete mode 100644 public/src/modules/composer/drafts.js
 delete mode 100644 public/src/modules/composer/formatting.js
 delete mode 100644 public/src/modules/composer/preview.js
 delete mode 100644 public/src/modules/composer/resize.js
 delete mode 100644 public/src/modules/composer/tags.js
 delete mode 100644 public/src/modules/composer/uploads.js

diff --git a/package.json b/package.json
index 60b101277c..38713a683c 100644
--- a/package.json
+++ b/package.json
@@ -28,18 +28,19 @@
     "daemon": "~1.1.0",
     "express": "^4.9.5",
     "express-session": "^1.8.2",
-    "lwip": "0.0.7",
     "gravatar": "^1.1.0",
     "heapdump": "^0.3.0",
     "less": "^2.0.0",
     "logrotate-stream": "^0.2.3",
     "lru-cache": "^2.6.1",
+    "lwip": "0.0.7",
     "mime": "^1.3.4",
     "minimist": "^1.1.1",
     "mkdirp": "~0.5.0",
     "mmmagic": "^0.3.13",
     "morgan": "^1.3.2",
     "nconf": "~0.7.1",
+    "nodebb-plugin-composer-default": "^1.0.0",
     "nodebb-plugin-dbsearch": "^0.2.12",
     "nodebb-plugin-emoji-extended": "^0.4.8",
     "nodebb-plugin-markdown": "^3.0.0",
diff --git a/public/src/app.js b/public/src/app.js
index 74829543af..30a7d8bc68 100644
--- a/public/src/app.js
+++ b/public/src/app.js
@@ -466,21 +466,23 @@ app.cacheBuster = null;
 
 	function handleNewTopic() {
 		$('#content').on('click', '#new_topic', function() {
-			require(['composer'], function(composer) {
-				var cid = ajaxify.variables.get('category_id');
-				if (cid) {
-					composer.newTopic(cid);
-				} else {
-					socket.emit('categories.getCategoriesByPrivilege', 'topics:create', function(err, categories) {
-						if (err) {
-							return app.alertError(err.message);
-						}
-						if (categories.length) {
-							composer.newTopic(categories[0].cid);
-						}
-					});
-				}
-			});
+			var cid = ajaxify.variables.get('category_id');
+			if (cid) {
+				$(window).trigger('action:composer.topic.new', {
+					cid: cid
+				});
+			} else {
+				socket.emit('categories.getCategoriesByPrivilege', 'topics:create', function(err, categories) {
+					if (err) {
+						return app.alertError(err.message);
+					}
+					if (categories.length) {
+						$(window).trigger('action:composer.topic.new', {
+							cid: categories[0].cid
+						});
+					}
+				});
+			}
 		});
 	}
 
diff --git a/public/src/client/category.js b/public/src/client/category.js
index 777d3f808a..d60c643462 100644
--- a/public/src/client/category.js
+++ b/public/src/client/category.js
@@ -2,7 +2,6 @@
 /* global define, config, templates, app, utils, ajaxify, socket */
 
 define('forum/category', [
-	'composer',
 	'forum/pagination',
 	'forum/infinitescroll',
 	'share',
@@ -11,7 +10,7 @@ define('forum/category', [
 	'sort',
 	'components',
 	'translator'
-], function(composer, pagination, infinitescroll, share, navigator, categoryTools, sort, components, translator) {
+], function(pagination, infinitescroll, share, navigator, categoryTools, sort, components, translator) {
 	var Category = {};
 
 	$(window).on('action:ajaxify.start', function(ev, data) {
diff --git a/public/src/client/topic/postTools.js b/public/src/client/topic/postTools.js
index 4a227252a8..0ddeb8a700 100644
--- a/public/src/client/topic/postTools.js
+++ b/public/src/client/topic/postTools.js
@@ -108,9 +108,9 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator
 
 		postContainer.on('click', '[component="post/edit"]', function(e) {
 			var btn = $(this);
-			require(['composer'], function(composer) {
-				composer.editPost(getData(btn, 'data-pid'));
-			});
+			$(window).trigger('action:composer.post.edit', {
+				pid: getData(btn, 'data-pid')
+			})
 		});
 
 		postContainer.on('click', '[component="post/delete"]', function(e) {
@@ -135,51 +135,61 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator
 	}
 
 	function onReplyClicked(button, tid, topicName) {
-		require(['composer'], function(composer) {
-			var selectionText = '',
-				selection = window.getSelection ? window.getSelection() : document.selection.createRange(),
-				topicUUID = composer.findByTid(tid);
-
-			if ($(selection.baseNode).parents('[component="post/content"]').length > 0) {
-				var snippet = selection.toString();
-				if (snippet.length) {
-					selectionText = '> ' + snippet.replace(/\n/g, '\n> ') + '\n\n';
-				}
-			}
+		var selectionText = '',
+			selection = window.getSelection ? window.getSelection() : document.selection.createRange();
 
-			var username = getUserName(selectionText ? $(selection.baseNode) : button);
-			if (getData(button, 'data-uid') === '0') {
-				username = '';
-			}
-			if (selectionText.length) {
-				composer.addQuote(tid, ajaxify.variables.get('topic_slug'), getData(button, 'data-index'), getData(button, 'data-pid'), topicName, username, selectionText, topicUUID);
-			} else {
-				composer.newReply(tid, getData(button, 'data-pid'), topicName, username ? username + ' ' : '');
+		if ($(selection.baseNode).parents('[component="post/content"]').length > 0) {
+			var snippet = selection.toString();
+			if (snippet.length) {
+				selectionText = '> ' + snippet.replace(/\n/g, '\n> ') + '\n\n';
 			}
-		});
+		}
 
+		var username = getUserName(selectionText ? $(selection.baseNode) : button);
+		if (getData(button, 'data-uid') === '0') {
+			username = '';
+		}
+		if (selectionText.length) {
+			$(window).trigger('action:composer.addQuote', {
+				tid: tid,
+				slug: ajaxify.variables.get('topic_slug'),
+				index: getData(button, 'data-index'),
+				pid: getData(button, 'data-pid'),
+				topicName: topicName,
+				username: username,
+				text: selectionText
+			});
+		} else {
+			$(window).trigger('action:composer.post.new', {
+				tid: tid,
+				pid: ajaxify.variables.get('pid'),
+				topicName: topicName,
+				text: username + ' ' || ''
+			});
+		}
 	}
 
 	function onQuoteClicked(button, tid, topicName) {
-		require(['composer'], function(composer) {
-			var username = getUserName(button),
-				pid = getData(button, 'data-pid'),
-				topicUUID = composer.findByTid(tid);
-
-			socket.emit('posts.getRawPost', pid, function(err, post) {
-				if(err) {
-					return app.alertError(err.message);
-				}
-				var quoted = '';
-				if(post) {
-					quoted = '> ' + post.replace(/\n/g, '\n> ') + '\n\n';
-				}
+		var username = getUserName(button),
+			pid = getData(button, 'data-pid');
 
-				if(topicUUID) {
-					composer.addQuote(tid, ajaxify.variables.get('topic_slug'), getData(button, 'data-index'), pid, topicName, username, quoted, topicUUID);
-				} else {
-					composer.newReply(tid, pid, topicName, '[[modules:composer.user_said, ' + username + ']]\n' + quoted);
-				}
+		socket.emit('posts.getRawPost', pid, function(err, post) {
+			if(err) {
+				return app.alertError(err.message);
+			}
+			var quoted = '';
+			if(post) {
+				quoted = '> ' + post.replace(/\n/g, '\n> ') + '\n\n';
+			}
+
+			$(window).trigger('action:composer.addQuote', {
+				tid: tid,
+				slug: ajaxify.variables.get('topic_slug'),
+				index: getData(button, 'data-index'),
+				pid: pid,
+				username: username,
+				topicName: topicName,
+				text: quoted
 			});
 		});
 	}
diff --git a/public/src/modules/composer.js b/public/src/modules/composer.js
deleted file mode 100644
index 1e32892617..0000000000
--- a/public/src/modules/composer.js
+++ /dev/null
@@ -1,595 +0,0 @@
-'use strict';
-
-/* globals define, socket, app, config, ajaxify, utils, templates, bootbox */
-
-define('composer', [
-	'taskbar',
-	'translator',
-	'composer/controls',
-	'composer/uploads',
-	'composer/formatting',
-	'composer/drafts',
-	'composer/tags',
-	'composer/categoryList',
-	'composer/preview',
-	'composer/resize'
-], function(taskbar, translator, controls, uploads, formatting, drafts, tags, categoryList, preview, resize) {
-	var composer = {
-		active: undefined,
-		posts: {},
-		bsEnvironment: undefined,
-		formatting: []
-	};
-
-	$(window).off('resize', onWindowResize).on('resize', onWindowResize);
-
-	$(window).on('action:composer.topics.post', function(ev, data) {
-		localStorage.removeItem('category:' + data.data.cid + ':bookmark');
-		localStorage.removeItem('category:' + data.data.cid + ':bookmark:clicked');
-	});
-
-	$(window).on('popstate', function(ev, data) {
-		var env = utils.findBootstrapEnvironment();
-
-		if (composer.active && (env === 'xs' || env ==='sm')) {
-			if (!composer.posts[composer.active].modified) {
-				discard(composer.active);
-				return;
-			}
-
-			translator.translate('[[modules:composer.discard]]', function(translated) {
-				bootbox.confirm(translated, function(confirm) {
-					if (confirm) {
-						discard(composer.active);
-					}
-				});
-			});
-		}
-	});
-
-	function removeComposerHistory() {
-		var env = utils.findBootstrapEnvironment();
-		if (env === 'xs' || env ==='sm') {
-			history.back();
-		}
-	}
-
-	// Query server for formatting options
-	socket.emit('modules.composer.getFormattingOptions', function(err, options) {
-		composer.formatting = options;
-	});
-
-	function onWindowResize() {
-		if (composer.active !== undefined) {
-			resize.reposition($('#cmp-uuid-' + composer.active));
-		}
-	}
-
-	function alreadyOpen(post) {
-		// If a composer for the same cid/tid/pid is already open, return the uuid, else return bool false
-		var	type, id;
-
-		if (post.hasOwnProperty('cid')) {
-			type = 'cid';
-		} else if (post.hasOwnProperty('tid')) {
-			type = 'tid';
-		} else if (post.hasOwnProperty('pid')) {
-			type = 'pid';
-		}
-
-		id = post[type];
-
-		// Find a match
-		for(var uuid in composer.posts) {
-			if (composer.posts[uuid].hasOwnProperty(type) && id === composer.posts[uuid][type]) {
-				return uuid;
-			}
-		}
-
-		// No matches...
-		return false;
-	}
-
-	function push(post) {
-		var uuid = utils.generateUUID(),
-			existingUUID = alreadyOpen(post);
-
-		if (existingUUID) {
-			taskbar.updateActive(existingUUID);
-			return composer.load(existingUUID);
-		}
-
-		translator.translate('[[topic:composer.new_topic]]', function(newTopicStr) {
-			taskbar.push('composer', uuid, {
-				title: post.title ? post.title : newTopicStr
-			});
-		});
-
-		// Construct a save_id
-		if (0 !== parseInt(app.user.uid, 10)) {
-			if (post.hasOwnProperty('cid')) {
-				post.save_id = ['composer', app.user.uid, 'cid', post.cid].join(':');
-			} else if (post.hasOwnProperty('tid')) {
-				post.save_id = ['composer', app.user.uid, 'tid', post.tid].join(':');
-			} else if (post.hasOwnProperty('pid')) {
-				post.save_id = ['composer', app.user.uid, 'pid', post.pid].join(':');
-			}
-		}
-
-		composer.posts[uuid] = post;
-		composer.load(uuid);
-	}
-
-	function composerAlert(post_uuid, message) {
-		$('#cmp-uuid-' + post_uuid).find('.composer-submit').removeAttr('disabled');
-		app.alert({
-			type: 'danger',
-			timeout: 3000,
-			title: '',
-			message: message,
-			alert_id: 'post_error'
-		});
-	}
-
-	composer.findByTid = function(tid) {
-		// Iterates through the initialised composers and returns the uuid of the matching composer
-		for(var uuid in composer.posts) {
-			if (composer.posts.hasOwnProperty(uuid) && composer.posts[uuid].hasOwnProperty('tid') && parseInt(composer.posts[uuid].tid, 10) === parseInt(tid, 10)) {
-				return uuid;
-			}
-		}
-
-		return null;
-	};
-
-	composer.addButton = function(iconClass, onClick) {
-		formatting.addButton(iconClass, onClick);
-	};
-
-	composer.newTopic = function(cid) {
-		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
-			});
-		});
-	};
-
-	composer.addQuote = function(tid, topicSlug, postIndex, pid, title, username, text, uuid) {
-		uuid = uuid || composer.active;
-
-		if (uuid === undefined) {
-			composer.newReply(tid, pid, title, '[[modules:composer.user_said, ' + username + ']]\n' + text);
-			return;
-		} else if (uuid !== composer.active) {
-			// If the composer is not currently active, activate it
-			composer.load(uuid);
-		}
-
-		var postContainer = $('#cmp-uuid-' + uuid);
-		var bodyEl = postContainer.find('textarea');
-		var prevText = bodyEl.val();
-		if (parseInt(tid, 10) !== parseInt(composer.posts[uuid].tid, 10)) {
-			var link = '[' + title + '](/topic/' + topicSlug + '/' + (parseInt(postIndex, 10) + 1) + ')';
-			translator.translate('[[modules:composer.user_said_in, ' + username + ', ' + link + ']]\n', config.defaultLang, onTranslated);
-		} else {
-			translator.translate('[[modules:composer.user_said, ' + username + ']]\n', config.defaultLang, onTranslated);
-		}
-
-		function onTranslated(translated) {
-			composer.posts[uuid].body = (prevText.length ? prevText + '\n\n' : '') + translated + text;
-			bodyEl.val(composer.posts[uuid].body);
-			focusElements(postContainer);
-			preview.render(postContainer);
-		}
-	};
-
-	composer.newReply = function(tid, pid, title, text) {
-		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: $('<div/>').text(title).html(),
-					body: translated,
-					modified: false,
-					isMain: false,
-					isMod: isMod
-				});
-			});
-		});
-	};
-
-	composer.editPost = function(pid) {
-		socket.emit('modules.composer.push', pid, function(err, threadData) {
-			if(err) {
-				return app.alertError(err.message);
-			}
-
-			push({
-				pid: pid,
-				uid: threadData.uid,
-				handle: threadData.handle,
-				title: threadData.title,
-				body: threadData.body,
-				modified: false,
-				isMain: threadData.isMain,
-				topic_thumb: threadData.topic_thumb,
-				tags: threadData.tags
-			});
-		});
-	};
-
-	composer.load = function(post_uuid) {
-		var postContainer = $('#cmp-uuid-' + post_uuid);
-		if (postContainer.length) {
-			activate(post_uuid);
-			resize.reposition(postContainer);
-			focusElements(postContainer);
-		} else {
-			createNewComposer(post_uuid);
-		}
-
-		startNotifyTyping(composer.posts[post_uuid]);
-	};
-
-	function startNotifyTyping(postData) {
-		function emit() {
-			socket.emit('modules.composer.notifyTyping', {
-				tid: postData.tid,
-				uid: app.user.uid
-			});
-		}
-
-		if (!parseInt(postData.tid, 10)) {
-			return;
-		}
-
-		stopNotifyInterval(postData);
-
-		emit();
-		postData.notifyTypingIntervalId = setInterval(emit, 5000);
-	}
-
-	function stopNotifyTyping(postData) {
-		if (!parseInt(postData.tid, 10)) {
-			return;
-		}
-		socket.emit('modules.composer.stopNotifyTyping', {
-			tid: postData.tid,
-			uid: app.user.uid
-		});
-	}
-
-	function stopNotifyInterval(postData) {
-		if (postData.notifyTypingIntervalId) {
-			clearInterval(postData.notifyTypingIntervalId);
-			postData.notifyTypingIntervalId = 0;
-		}
-	}
-
-	function createNewComposer(post_uuid) {
-		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();
-
-		// see
-		// https://github.com/NodeBB/NodeBB/issues/2994 and
-		// https://github.com/NodeBB/NodeBB/issues/1951
-		// remove when 1951 is resolved
-
-		var title = postData.title.replace(/%/g, '&#37;').replace(/,/g, '&#44;');
-
-		var data = {
-			title: title,
-			mobile: composer.bsEnvironment === 'xs' || composer.bsEnvironment === 'sm',
-			allowTopicsThumbnail: allowTopicsThumbnail,
-			isTopicOrMain: isTopic || isMain,
-			minimumTagLength: config.minimumTagLength,
-			maximumTagLength: config.maximumTagLength,
-			isTopic: isTopic,
-			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
-		};
-
-		if (data.mobile) {
-			var qs = '?p=' + window.location.pathname;
-			ajaxify.go('compose', function() {
-				renderComposer();
-			}, false, qs);
-		} else {
-			renderComposer();
-		}
-
-		function renderComposer() {
-			parseAndTranslate('composer', data, function(composerTemplate) {
-				if ($('#cmp-uuid-' + post_uuid).length) {
-					return;
-				}
-				composerTemplate = $(composerTemplate);
-
-				composerTemplate.attr('id', 'cmp-uuid-' + post_uuid);
-
-				$(document.body).append(composerTemplate);
-
-				var postContainer = $(composerTemplate[0]),
-					bodyEl = postContainer.find('textarea'),
-					draft = drafts.getDraft(postData.save_id),
-					submitBtn = postContainer.find('.composer-submit');
-
-				preview.handleToggler(postContainer);
-				tags.init(postContainer, composer.posts[post_uuid]);
-				categoryList.init(postContainer, composer.posts[post_uuid]);
-
-				activate(post_uuid);
-				resize.reposition(postContainer);
-
-				if (config.allowFileUploads || config.hasImageUploadPlugin) {
-					uploads.initialize(post_uuid);
-				}
-
-				formatting.addHandler(postContainer);
-
-				if (allowTopicsThumbnail) {
-					uploads.toggleThumbEls(postContainer, composer.posts[post_uuid].topic_thumb || '');
-				}
-
-				postContainer.on('change', 'input, textarea', function() {
-					composer.posts[post_uuid].modified = true;
-				});
-
-				submitBtn.on('click', function() {
-					var action = $(this).attr('data-action');
-
-					switch(action) {
-						case 'post-lock':
-							$(this).attr('disabled', true);
-							post(post_uuid, {lock: true});
-							break;
-
-						case 'post':	// intentional fall-through
-						default:
-							$(this).attr('disabled', true);
-							post(post_uuid);
-							break;
-					}
-				});
-
-				postContainer.on('click', 'a[data-switch-action]', function() {
-					var action = $(this).attr('data-switch-action'),
-						label = $(this).html();
-
-					submitBtn.attr('data-action', action).html(label);
-				});
-
-				postContainer.find('.composer-discard').on('click', function() {
-					if (!composer.posts[post_uuid].modified) {
-						removeComposerHistory();
-						discard(post_uuid);
-						return;
-					}
-					var btn = $(this).prop('disabled', true);
-					translator.translate('[[modules:composer.discard]]', function(translated) {
-						bootbox.confirm(translated, function(confirm) {
-							if (confirm) {
-								removeComposerHistory();
-								discard(post_uuid);
-							}
-							btn.prop('disabled', false);
-						});
-					});
-				});
-
-				postContainer.on('click', function() {
-					if (!taskbar.isActive(post_uuid)) {
-						taskbar.updateActive(post_uuid);
-					}
-				});
-
-				bodyEl.on('input propertychange', function() {
-					preview.render(postContainer);
-				});
-
-				bodyEl.on('scroll', function() {
-					preview.matchScroll(postContainer);
-				});
-
-				bodyEl.val(draft ? draft : postData.body);
-
-				preview.render(postContainer, function() {
-					preview.matchScroll(postContainer);
-				});
-
-				drafts.init(postContainer, postData);
-
-				resize.handleResize(postContainer);
-
-				handleHelp(postContainer);
-
-				$(window).trigger('action:composer.loaded', {
-					post_uuid: post_uuid,
-					composerData: composer.posts[post_uuid]
-				});
-
-				formatting.addComposerButtons();
-				focusElements(postContainer);
-			});
-		}
-	}
-
-	function parseAndTranslate(template, data, callback) {
-		templates.parse(template, data, function(composerTemplate) {
-			translator.translate(composerTemplate, callback);
-		});
-	}
-
-	function handleHelp(postContainer) {
-		var helpBtn = postContainer.find('.help');
-		socket.emit('modules.composer.renderHelp', function(err, html) {
-			if (!err && html && html.length > 0) {
-				helpBtn.removeClass('hidden');
-				helpBtn.on('click', function() {
-					bootbox.alert(html);
-				});
-			}
-		});
-	}
-
-	function activate(post_uuid) {
-		if(composer.active && composer.active !== post_uuid) {
-			composer.minimize(composer.active);
-		}
-
-		composer.active = post_uuid;
-	}
-
-	function focusElements(postContainer) {
-		var title = postContainer.find('input.title');
-		if (title.length) {
-			title.focus();
-		} else {
-			postContainer.find('textarea').focus().putCursorAtEnd();
-		}
-	}
-
-	function post(post_uuid, options) {
-		var postData = composer.posts[post_uuid],
-			postContainer = $('#cmp-uuid-' + post_uuid),
-			handleEl = postContainer.find('.handle'),
-			titleEl = postContainer.find('.title'),
-			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) {
-			thumbEl.val(thumbEl.val().trim());
-		}
-
-		var checkTitle = (parseInt(postData.cid, 10) || parseInt(postData.pid, 10)) && postContainer.find('input.title').length;
-
-		if (uploads.inProgress[post_uuid] && uploads.inProgress[post_uuid].length) {
-			return composerAlert(post_uuid, '[[error:still-uploading]]');
-		} else if (checkTitle && titleEl.val().length < parseInt(config.minimumTitleLength, 10)) {
-			return composerAlert(post_uuid, '[[error:title-too-short, ' + config.minimumTitleLength + ']]');
-		} else if (checkTitle && titleEl.val().length > parseInt(config.maximumTitleLength, 10)) {
-			return composerAlert(post_uuid, '[[error:title-too-long, ' + config.maximumTitleLength + ']]');
-		} else if (checkTitle && !utils.slugify(titleEl.val()).length) {
-			return composerAlert(post_uuid, '[[error:invalid-title]]');
-		} else if (bodyEl.val().length < parseInt(config.minimumPostLength, 10)) {
-			return composerAlert(post_uuid, '[[error:content-too-short, ' + config.minimumPostLength + ']]');
-		} else if (bodyEl.val().length > parseInt(config.maximumPostLength, 10)) {
-			return composerAlert(post_uuid, '[[error:content-too-long, ' + config.maximumPostLength + ']]');
-		}
-
-		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),
-				lock: options.lock || false
-			};
-		} 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,
-				lock: options.lock || false
-			};
-		} else if (parseInt(postData.pid, 10) > 0) {
-			action = 'posts.edit';
-			composerData = {
-				pid: postData.pid,
-				handle: handleEl ? handleEl.val() : undefined,
-				content: bodyEl.val(),
-				title: titleEl.val(),
-				topic_thumb: thumbEl.val() || '',
-				tags: tags.getTags(post_uuid)
-			};
-		}
-
-		socket.emit(action, composerData, function (err, data) {
-			postContainer.find('.composer-submit').removeAttr('disabled');
-			if (err) {
-				if (err.message === '[[error:email-not-confirmed]]') {
-					return app.showEmailConfirmWarning(err);
-				}
-
-				return app.alertError(err.message);
-			}
-
-			discard(post_uuid);
-			drafts.removeDraft(postData.save_id);
-
-			if (action === 'topics.post') {
-				ajaxify.go('topic/' + data.slug);
-			} else {
-				removeComposerHistory();
-			}
-
-			$(window).trigger('action:composer.' + action, {composerData: composerData, data: data});
-		});
-	}
-
-	function discard(post_uuid) {
-		if (composer.posts[post_uuid]) {
-			$('#cmp-uuid-' + post_uuid).remove();
-			drafts.removeDraft(composer.posts[post_uuid].save_id);
-			stopNotifyInterval(composer.posts[post_uuid]);
-			stopNotifyTyping(composer.posts[post_uuid]);
-
-			delete composer.posts[post_uuid];
-			composer.active = undefined;
-			taskbar.discard('composer', post_uuid);
-			$('body').css({'margin-bottom': 0});
-			$('[data-action="post"]').removeAttr('disabled');
-
-
-			$('html').removeClass('composing mobile');
-
-		}
-	}
-
-	composer.minimize = function(post_uuid) {
-		var postContainer = $('#cmp-uuid-' + post_uuid);
-		postContainer.css('visibility', 'hidden');
-		composer.active = undefined;
-		taskbar.minimize('composer', post_uuid);
-
-		stopNotifyInterval(composer.posts[post_uuid]);
-		stopNotifyTyping(composer.posts[post_uuid]);
-
-		$('body').css({'margin-bottom': '0px'});
-	};
-
-	return composer;
-});
diff --git a/public/src/modules/composer/categoryList.js b/public/src/modules/composer/categoryList.js
deleted file mode 100644
index 1eb86051bf..0000000000
--- a/public/src/modules/composer/categoryList.js
+++ /dev/null
@@ -1,44 +0,0 @@
-
-'use strict';
-
-/*globals define, config, socket, app*/
-
-define('composer/categoryList', function() {
-	var categoryList = {};
-
-	categoryList.init = function(postContainer, postData) {
-		var listEl = postContainer.find('.category-list');
-		if (!listEl.length) {
-			return;
-		}
-
-		socket.emit('categories.getCategoriesByPrivilege', 'topics:create', function(err, categories) {
-			if (err) {
-				return app.alertError(err.message);
-			}
-
-			// Remove categories that are just external links
-			categories = categories.filter(function(category) {
-				return !category.link;
-			});
-
-			categories.forEach(function(category) {
-				$('<option value="' + category.cid + '">' + category.name + '</option>').appendTo(listEl);
-			});
-
-			if (postData.cid) {
-				listEl.find('option[value="' + postData.cid + '"]').prop('selected', true);
-			}
-		});
-
-		listEl.on('change', function() {
-			if (postData.cid) {
-				postData.cid = this.value;
-			}
-
-			$('[tabindex=' + (parseInt($(this).attr('tabindex'), 10) + 1) + ']').trigger('focus');
-		});
-	};
-
-	return categoryList;
-});
diff --git a/public/src/modules/composer/controls.js b/public/src/modules/composer/controls.js
deleted file mode 100644
index 05b6bdcf3c..0000000000
--- a/public/src/modules/composer/controls.js
+++ /dev/null
@@ -1,46 +0,0 @@
-"use strict";
-
-/*global define*/
-
-define('composer/controls', function() {
-	var controls = {};
-
-	/*************************************************/
-	/* Rich Textarea Controls                        */
-	/*************************************************/
-	controls.insertIntoTextarea = function(textarea, value) {
-		var $textarea = $(textarea);
-		var currentVal = $textarea.val();
-
-		$textarea.val(
-			currentVal.slice(0, textarea.selectionStart) +
-			value +
-			currentVal.slice(textarea.selectionStart)
-		);
-	};
-
-	controls.wrapSelectionInTextareaWith = function(textarea, leading, trailing){
-		if(trailing === undefined){
-			trailing = leading;
-		}
-
-		var $textarea = $(textarea);
-		var currentVal = $textarea.val();
-
-		$textarea.val(
-			currentVal.slice(0, textarea.selectionStart) +
-			leading +
-			currentVal.slice(textarea.selectionStart, textarea.selectionEnd) +
-			trailing +
-			currentVal.slice(textarea.selectionEnd)
-		);
-	};
-
-	controls.updateTextareaSelection = function(textarea, start, end){
-		textarea.setSelectionRange(start, end);
-		$(textarea).focus();
-	};
-
-
-	return controls;
-});
\ No newline at end of file
diff --git a/public/src/modules/composer/drafts.js b/public/src/modules/composer/drafts.js
deleted file mode 100644
index 27370607ca..0000000000
--- a/public/src/modules/composer/drafts.js
+++ /dev/null
@@ -1,69 +0,0 @@
-'use strict';
-
-/* globals define */
-
-define('composer/drafts', function() {
-
-	var drafts = {};
-	var	saveThrottleId;
-	var saving = false;
-
-	drafts.init = function(postContainer, postData) {
-
-		var bodyEl = postContainer.find('textarea');
-		bodyEl.on('keyup', function() {
-			resetTimeout();
-
-			saveThrottleId = setTimeout(function() {
-				saveDraft(postContainer, postData);
-			}, 1000);
-		});
-	};
-
-	function resetTimeout() {
-		if (saveThrottleId) {
-			clearTimeout(saveThrottleId);
-			saveThrottleId = 0;
-		}
-	}
-
-	drafts.getDraft = function(save_id) {
-		return localStorage.getItem(save_id);
-	};
-
-	function saveDraft(postContainer, postData) {
-		var raw;
-
-		if (canSave() && postData && postData.save_id && postContainer.length) {
-			raw = postContainer.find('textarea').val();
-			if (raw.length) {
-				localStorage.setItem(postData.save_id, raw);
-			} else {
-				drafts.removeDraft(postData.save_id);
-			}
-		}
-	}
-
-	drafts.removeDraft = function(save_id) {
-		resetTimeout();
-		return localStorage.removeItem(save_id);
-	};
-
-	function canSave() {
-		if (saving) {
-			return saving;
-		}
-
-		try {
-			localStorage.setItem('test', 'test');
-			localStorage.removeItem('test');
-			saving = true;
-			return true;
-		} catch(e) {
-			saving = false;
-			return false;
-		}
-	}
-
-	return drafts;
-});
\ No newline at end of file
diff --git a/public/src/modules/composer/formatting.js b/public/src/modules/composer/formatting.js
deleted file mode 100644
index 05315a7edf..0000000000
--- a/public/src/modules/composer/formatting.js
+++ /dev/null
@@ -1,58 +0,0 @@
-'use strict';
-
-/* globals define */
-
-define('composer/formatting', ['composer/controls', 'composer/preview'], function(controls, preview) {
-
-	var formatting = {};
-
-	var formattingDispatchTable = {
-		'picture': function(){
-			$('#files').click();
-		},
-
-		upload: function(){
-			$('#files').click();
-		},
-
-		tags: function() {
-			$('.tags-container').toggleClass('hidden');
-		}
-	};
-
-	var buttons = [];
-
-	formatting.addComposerButtons = function() {
-		for(var x=0,numButtons=buttons.length;x<numButtons;x++) {
-			$('.formatting-bar .btn-group form').before('<span class="btn btn-link" tabindex="-1" data-format="' + buttons[x].name + '"><i class="' + buttons[x].iconClass + '"></i></span>');
-		}
-	};
-
-	formatting.addButton = function(iconClass, onClick) {
-		var name = iconClass.replace('fa fa-', '');
-
-		formattingDispatchTable[name] = onClick;
-		buttons.push({
-			name: name,
-			iconClass: iconClass
-		});
-	};
-
-	formatting.addButtonDispatch = function(name, onClick) {
-		formattingDispatchTable[name] = onClick;
-	};
-
-	formatting.addHandler = function(postContainer) {
-		postContainer.on('click', '.formatting-bar span', function () {
-			var format = $(this).attr('data-format'),
-				textarea = $(this).parents('.composer').find('textarea')[0];
-
-			if(formattingDispatchTable.hasOwnProperty(format)){
-				formattingDispatchTable[format](textarea, textarea.selectionStart, textarea.selectionEnd);
-				preview.render(postContainer);
-			}
-		});
-	};
-
-	return formatting;
-});
diff --git a/public/src/modules/composer/preview.js b/public/src/modules/composer/preview.js
deleted file mode 100644
index e6bab0cb4e..0000000000
--- a/public/src/modules/composer/preview.js
+++ /dev/null
@@ -1,83 +0,0 @@
-'use strict';
-
-/* globals define, socket*/
-
-define('composer/preview', function() {
-	var preview = {};
-
-	var timeoutId = 0;
-
-	preview.render = function(postContainer, callback) {
-		callback = callback || function() {};
-		if (timeoutId) {
-			clearTimeout(timeoutId);
-			timeoutId = 0;
-		}
-		var textarea = postContainer.find('textarea');
-
-		timeoutId = setTimeout(function() {
-			socket.emit('modules.composer.renderPreview', textarea.val(), function(err, preview) {
-				timeoutId = 0;
-				if (err) {
-					return;
-				}
-				preview = $(preview);
-				preview.find('img').addClass('img-responsive');
-				postContainer.find('.preview').html(preview);
-				$(window).trigger('action:composer.preview');
-				callback();
-			});
-		}, 250);
-	};
-
-	preview.matchScroll = function(postContainer) {
-		var textarea = postContainer.find('textarea');
-		var preview = postContainer.find('.preview');
-		var diff = textarea[0].scrollHeight - textarea.height();
-
-		if (diff === 0) {
-			return;
-		}
-
-		var scrollPercent = textarea.scrollTop() / diff;
-
-		preview.scrollTop(Math.max(preview[0].scrollHeight - preview.height(), 0) * scrollPercent);
-	};
-
-	preview.handleToggler = function(postContainer) {
-		function hidePreview() {
-			togglePreview(false);
-			localStorage.setItem('composer:previewToggled', true);
-		}
-
-		function showPreview() {
-			togglePreview(true);
-			localStorage.removeItem('composer:previewToggled');
-		}
-
-		function togglePreview(show) {
-			previewContainer.toggleClass('hide', !show);
-			writeContainer.toggleClass('maximized', !show);
-			showBtn.toggleClass('hide', show);
-
-			$('.write').focus();
-			preview.matchScroll(postContainer);
-		}
-
-		var showBtn = postContainer.find('.write-container .toggle-preview'),
-			hideBtn = postContainer.find('.preview-container .toggle-preview'),
-			previewContainer = $('.preview-container'),
-			writeContainer = $('.write-container');
-
-		hideBtn.on('click', hidePreview);
-		showBtn.on('click', showPreview);
-
-		if (localStorage.getItem('composer:previewToggled')) {
-			hidePreview();
-		} else {
-			showPreview();
-		}
-	};
-
-	return preview;
-});
\ No newline at end of file
diff --git a/public/src/modules/composer/resize.js b/public/src/modules/composer/resize.js
deleted file mode 100644
index 23b5f67adb..0000000000
--- a/public/src/modules/composer/resize.js
+++ /dev/null
@@ -1,195 +0,0 @@
-
-'use strict';
-
-/* globals app, define, config, utils*/
-
-define('composer/resize', ['autosize'], function(autosize) {
-	var resize = {},
-		oldPercentage = 0;
-
-	resize.reposition = function(postContainer) {
-		var	percentage = localStorage.getItem('composer:resizePercentage') || 0.5;
-
-		doResize(postContainer, percentage);
-	};
-
-	function doResize(postContainer, percentage) {
-		var env = utils.findBootstrapEnvironment();
-
-
-		// todo, lump in browsers that don't support transform (ie8) here
-		// at this point we should use modernizr
-		if (env === 'sm' || env === 'xs' || window.innerHeight < 480) {
-			$('html').addClass('composing mobile');
-			autosize(postContainer.find('textarea')[0]);
-			percentage = 1;
-		} else {
-			$('html').removeClass('composing mobile');
-		}
-
-		if (percentage) {
-			var max = getMaximumPercentage();
-
-			if (percentage < 0.25) {
-				percentage = 0.25;
-			} else if (percentage > max) {
-				percentage = max;
-			}
-
-			if (env === 'md' || env === 'lg') {
-				var transform = 'translate(0, ' + (Math.abs(1-percentage) * 100) + '%)';
-				postContainer.css({
-					'-webkit-transform': transform,
-					'-moz-transform': transform,
-					'-ms-transform': transform,
-					'-o-transform': transform,
-					'transform': transform
-				});
-			} else {
-				postContainer.removeAttr('style');
-			}
-		}
-
-		postContainer.percentage = percentage;
-
-		if (config.hasImageUploadPlugin) {
-			postContainer.find('.img-upload-btn').removeClass('hide');
-			postContainer.find('#files.lt-ie9').removeClass('hide');
-		}
-
-		if (config.allowFileUploads) {
-			postContainer.find('.file-upload-btn').removeClass('hide');
-			postContainer.find('#files.lt-ie9').removeClass('hide');
-		}
-
-		postContainer.css('visibility', 'visible');
-
-		// Add some extra space at the bottom of the body so that the user can still scroll to the last post w/ composer open
-		$('body').css({'margin-bottom': postContainer.css('height')});
-
-		resizeWritePreview(postContainer);
-	}
-
-	resize.handleResize = function(postContainer) {
-		function resizeStart(e) {
-			var resizeRect = resizeEl[0].getBoundingClientRect(),
-				resizeCenterY = resizeRect.top + (resizeRect.height / 2);
-
-			resizeOffset = (resizeCenterY - e.clientY) / 2;
-			resizeActive = true;
-			resizeDown = e.clientY;
-
-			$(window).on('mousemove', resizeAction);
-			$(window).on('mouseup', resizeStop);
-			$('body').on('touchmove', resizeTouchAction);
-		}
-
-		function resizeStop(e) {
-			resizeActive = false;
-
-			postContainer.find('textarea').focus();
-			$(window).off('mousemove', resizeAction);
-			$(window).off('mouseup', resizeStop);
-			$('body').off('touchmove', resizeTouchAction);
-
-			var position = (e.clientY - resizeOffset),
-				newHeight = $(window).height() - position,
-				windowHeight = $(window).height();
-
-			if (newHeight > windowHeight - $('#header-menu').height() - (windowHeight / 15)) {
-				snapToTop = true;
-			} else {
-				snapToTop = false;
-			}
-
-			toggleMaximize(e);
-		}
-
-		function toggleMaximize(e) {
-			if (e.clientY - resizeDown === 0 || snapToTop) {
-				var newPercentage = getMaximumPercentage();
-
-				if (!postContainer.hasClass('maximized') || !snapToTop) {
-					oldPercentage = postContainer.percentage;
-					doResize(postContainer, newPercentage);
-					postContainer.addClass('maximized');
-				} else {
-					doResize(postContainer, oldPercentage);
-					postContainer.removeClass('maximized');
-				}
-			}
-		}
-
-		function resizeTouchAction(e) {
-			e.preventDefault();
-			resizeAction(e.touches[0]);
-		}
-
-		function resizeAction(e) {
-			if (resizeActive) {
-				var position = (e.clientY - resizeOffset),
-					newHeight = $(window).height() - position;
-
-				doResize(postContainer, newHeight / $(window).height());
-
-				resizeWritePreview(postContainer);
-				resizeSavePosition(newHeight);
-
-				if (Math.abs(e.clientY - resizeDown) > 0) {
-					postContainer.removeClass('maximized');
-				}
-			}
-
-			e.preventDefault();
-			return false;
-		}
-
-		function resizeSavePosition(px) {
-			var	percentage = px / $(window).height(),
-				max = getMaximumPercentage();
-			localStorage.setItem('composer:resizePercentage', percentage < max ? percentage : max);
-		}
-
-		var	resizeActive = false,
-			resizeOffset = 0,
-            resizeDown = 0,
-            snapToTop = false,
-			resizeEl = postContainer.find('.resizer');
-
-		resizeEl
-			.on('mousedown', resizeStart)
-			.on('touchstart', function(e) {
-				e.preventDefault();
-				resizeStart(e.touches[0]);
-			})
-			.on('touchend', function(e) {
-				e.preventDefault();
-				resizeStop();
-			});
-	};
-
-	function getMaximumPercentage() {
-		return ($(window).height() - $('#header-menu').height() - 1) / $(window).height();
-	}
-
-	function resizeWritePreview(postContainer) {
-		var total = getFormattingHeight(postContainer);
-		postContainer
-			.find('.write-preview-container')
-			.css('height', postContainer.percentage * $(window).height() - $('#header-menu').height() - total);
-	}
-
-	function getFormattingHeight(postContainer) {
-		return [
-			postContainer.find('.title-container').outerHeight(true),
-			postContainer.find('.formatting-bar').outerHeight(true),
-			postContainer.find('.topic-thumb-container').outerHeight(true),
-			$('.taskbar').height()
-		].reduce(function(a, b) {
-			return a + b;
-		});
-	}
-
-
-	return resize;
-});
diff --git a/public/src/modules/composer/tags.js b/public/src/modules/composer/tags.js
deleted file mode 100644
index 8e516e83b8..0000000000
--- a/public/src/modules/composer/tags.js
+++ /dev/null
@@ -1,92 +0,0 @@
-
-'use strict';
-
-/*globals define, config, socket, app*/
-
-define('composer/tags', function() {
-	var tags = {};
-
-	tags.init = function(postContainer, postData) {
-		var tagEl = postContainer.find('.tags');
-		if (!tagEl.length) {
-			return;
-		}
-
-		tagEl.tagsinput({
-			maxTags: config.tagsPerTopic,
-			maxChars: config.maximumTagLength,
-			confirmKeys: [13, 44],
-			trimValue: true
-		});
-
-		tagEl.on('beforeItemAdd', function(event) {
-			event.cancel = event.item.length < config.minimumTagLength || event.item.length > config.maximumTagLength;
-			if (event.item.length < config.minimumTagLength) {
-				app.alertError('[[error:tag-too-short, ' + config.minimumTagLength + ']]');
-			} else if (event.item.length > config.maximumTagLength) {
-				app.alertError('[[error:tag-too-long, ' + config.maximumTagLength + ']]');
-			}
-		});
-
-		tagEl.on('itemAdded', function(event) {
-			$(window).trigger('action:tag.added', {cid: postData.cid, tagEl: tagEl, tag: event.item});
-		});
-
-		addTags(postData.tags, tagEl);
-
-		var input = postContainer.find('.bootstrap-tagsinput input');
-
-		app.loadJQueryUI(function() {
-			input.autocomplete({
-				delay: 100,
-				open: function() {
-					$(this).autocomplete('widget').css('z-index', 20000);
-				},
-				source: function(request, response) {
-					socket.emit('topics.searchTags', {query: request.term, cid: postData.cid}, function(err, tags) {
-						if (err) {
-							return app.alertError(err.message);
-						}
-						if (tags) {
-							response(tags);
-						}
-						$('.ui-autocomplete a').attr('data-ajaxify', 'false');
-					});
-				},
-				select: function(event, ui) {
-					// when autocomplete is selected from the dropdown simulate a enter key down to turn it into a tag
-					triggerEnter(input);
-				}
-			});
-		});
-
-		input.attr('tabIndex', tagEl.attr('tabIndex'));
-		input.on('blur', function() {
-			triggerEnter(input);
-		});
-	};
-
-	function triggerEnter(input) {
-		// http://stackoverflow.com/a/3276819/583363
-		var e = jQuery.Event('keypress');
-		e.which = 13;
-		e.keyCode = 13;
-		setTimeout(function() {
-			input.trigger(e);
-		}, 100);
-	}
-
-	function addTags(tags, tagEl) {
-		if (tags && tags.length) {
-			for(var i=0; i<tags.length; ++i) {
-				tagEl.tagsinput('add', tags[i]);
-			}
-		}
-	}
-
-	tags.getTags = function(post_uuid) {
-		return $('#cmp-uuid-' + post_uuid + ' .tags').tagsinput('items');
-	};
-
-	return tags;
-});
diff --git a/public/src/modules/composer/uploads.js b/public/src/modules/composer/uploads.js
deleted file mode 100644
index e9d7bfa3c3..0000000000
--- a/public/src/modules/composer/uploads.js
+++ /dev/null
@@ -1,324 +0,0 @@
-'use strict';
-
-/* globals define, utils, config, app */
-
-define('composer/uploads', ['composer/preview', 'csrf'], function(preview, csrf) {
-	var uploads = {
-		inProgress: {}
-	};
-
-	uploads.initialize = function(post_uuid) {
-
-		initializeDragAndDrop(post_uuid);
-		initializePaste(post_uuid);
-
-		addChangeHandlers(post_uuid);
-		addTopicThumbHandlers(post_uuid);
-	};
-
-	function addChangeHandlers(post_uuid) {
-		var postContainer = $('#cmp-uuid-' + post_uuid);
-
-		postContainer.find('#files').on('change', function(e) {
-			var files = (e.target || {}).files || ($(this).val() ? [{name: $(this).val(), type: utils.fileMimeType($(this).val())}] : null);
-			if(files) {
-				uploadContentFiles({files: files, post_uuid: post_uuid, route: '/api/post/upload'});
-			}
-		});
-
-		postContainer.find('#topic-thumb-file').on('change', function(e) {
-			var files = (e.target || {}).files || ($(this).val() ? [{name: $(this).val(), type: utils.fileMimeType($(this).val())}] : null),
-				fd;
-
-			if(files) {
-				if (window.FormData) {
-					fd = new FormData();
-					for (var i = 0; i < files.length; ++i) {
-						fd.append('files[]', files[i], files[i].name);
-					}
-				}
-				uploadTopicThumb({files: files, post_uuid: post_uuid, route: '/api/topic/thumb/upload', formData: fd});
-			}
-		});
-	}
-
-	function addTopicThumbHandlers(post_uuid) {
-		var postContainer = $('#cmp-uuid-' + post_uuid);
-
-		postContainer.on('click', '.topic-thumb-clear-btn', function(e) {
-			postContainer.find('input#topic-thumb-url').val('').trigger('change');
-			resetInputFile(postContainer.find('input#topic-thumb-file'));
-			$(this).addClass('hide');
-			e.preventDefault();
-		});
-
-		postContainer.on('paste change keypress', 'input#topic-thumb-url', function() {
-			var urlEl = $(this);
-			setTimeout(function(){
-				var url = urlEl.val();
-				if (url) {
-					postContainer.find('.topic-thumb-clear-btn').removeClass('hide');
-				} else {
-					resetInputFile(postContainer.find('input#topic-thumb-file'));
-					postContainer.find('.topic-thumb-clear-btn').addClass('hide');
-				}
-				postContainer.find('img.topic-thumb-preview').attr('src', url);
-			}, 100);
-		});
-	}
-
-	uploads.toggleThumbEls = function(postContainer, url) {
-		var thumbToggleBtnEl = postContainer.find('.topic-thumb-toggle-btn');
-
-		postContainer.find('input#topic-thumb-url').val(url);
-		postContainer.find('img.topic-thumb-preview').attr('src', url);
-		if (url) {
-			postContainer.find('.topic-thumb-clear-btn').removeClass('hide');
-		}
-		thumbToggleBtnEl.removeClass('hide');
-		thumbToggleBtnEl.off('click').on('click', function() {
-			var container = postContainer.find('.topic-thumb-container');
-			container.toggleClass('hide', !container.hasClass('hide'));
-		});
-	};
-
-	function resetInputFile($el) {
-		$el.wrap('<form />').closest('form').get(0).reset();
-		$el.unwrap();
-	}
-
-	function initializeDragAndDrop(post_uuid) {
-
-		function onDragEnter() {
-			if(draggingDocument) {
-				return;
-			}
-			drop.css('top', postContainer.find('.write-preview-container').position().top + 'px');
-			drop.css('height', textarea.height());
-			drop.css('line-height', textarea.height() + 'px');
-			drop.show();
-
-			drop.on('dragleave', function() {
-				drop.hide();
-				drop.off('dragleave');
-			});
-		}
-
-		function onDragDrop(e) {
-			e.preventDefault();
-			var files = e.files || (e.dataTransfer || {}).files || (e.target.value ? [e.target.value] : []),
-				fd;
-
-			if(files.length) {
-				if (window.FormData) {
-					fd = new FormData();
-					for (var i = 0; i < files.length; ++i) {
-						fd.append('files[]', files[i], files[i].name);
-					}
-				}
-
-				uploadContentFiles({
-					files: files,
-					post_uuid: post_uuid,
-					route: '/api/post/upload',
-					formData: fd
-				});
-			}
-
-			drop.hide();
-			return false;
-		}
-
-		function cancel(e) {
-			e.preventDefault();
-			return false;
-		}
-
-		if($.event.props.indexOf('dataTransfer') === -1) {
-			$.event.props.push('dataTransfer');
-		}
-
-		var draggingDocument = false;
-
-		var postContainer = $('#cmp-uuid-' + post_uuid),
-			drop = postContainer.find('.imagedrop'),
-			textarea = postContainer.find('textarea');
-
-		$(document).off('dragstart').on('dragstart', function() {
-			draggingDocument = true;
-		}).off('dragend').on('dragend', function() {
-			draggingDocument = false;
-		});
-
-		textarea.on('dragenter', onDragEnter);
-
-		drop.on('dragover', cancel);
-		drop.on('dragenter', cancel);
-		drop.on('drop', onDragDrop);
-	}
-
-	function initializePaste(post_uuid) {
-		$(window).off('paste').on('paste', function(event) {
-
-			var items = (event.clipboardData || event.originalEvent.clipboardData || {}).items,
-				fd;
-
-			if(items && items.length) {
-
-				var blob = items[0].getAsFile();
-				if(blob) {
-					blob.name = 'upload-' + utils.generateUUID();
-
-					if (window.FormData) {
-						fd = new FormData();
-						fd.append('files[]', blob, blob.name);
-					}
-
-					uploadContentFiles({
-						files: [blob],
-						post_uuid: post_uuid,
-						route: '/api/post/upload',
-						formData: fd
-					});
-				}
-			}
-		});
-	}
-
-	function escapeRegExp(text) {
-		return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
-	}
-
-	function maybeParse(response) {
-		if (typeof response === 'string')  {
-			try {
-				return $.parseJSON(response);
-			} catch (e) {
-				return {status: 500, message: 'Something went wrong while parsing server response'};
-			}
-		}
-		return response;
-	}
-
-	function insertText(str, index, insert) {
-		return str.slice(0, index) + insert + str.slice(index);
-	}
-
-	function uploadContentFiles(params) {
-		var files = params.files,
-			post_uuid = params.post_uuid,
-			formData = params.formData,
-			postContainer = $('#cmp-uuid-' + post_uuid),
-			textarea = postContainer.find('textarea'),
-			text = textarea.val(),
-			uploadForm = postContainer.find('#fileForm');
-
-		uploadForm.attr('action', config.relative_path + params.route);
-
-		for(var i = 0; i < files.length; ++i) {
-			var isImage = files[i].type.match(/image./);
-
-			text = insertText(text, textarea.getCursorPosition(), (isImage ? '!' : '') + '[' + files[i].name + '](uploading...) ');
-
-			if(files[i].size > parseInt(config.maximumFileSize, 10) * 1024) {
-				uploadForm[0].reset();
-				return app.alertError('[[error:file-too-big, ' + config.maximumFileSize + ']]');
-			}
-		}
-
-		textarea.val(text);
-
-		uploadForm.off('submit').submit(function() {
-			function updateTextArea(filename, text) {
-				var current = textarea.val();
-				var re = new RegExp(escapeRegExp(filename) + "]\\([^)]+\\)", 'g');
-				textarea.val(current.replace(re, filename + '](' + text + ')'));
-			}
-
-			uploads.inProgress[post_uuid] = uploads.inProgress[post_uuid] || [];
-			uploads.inProgress[post_uuid].push(1);
-
-			$(this).ajaxSubmit({
-				headers: {
-					'x-csrf-token': csrf.get()
-				},
-				resetForm: true,
-				clearForm: true,
-				formData: formData,
-
-				error: onUploadError,
-
-				uploadProgress: function(event, position, total, percent) {
-					for(var i=0; i < files.length; ++i) {
-						updateTextArea(files[i].name, 'uploading ' + percent + '%');
-					}
-				},
-
-				success: function(uploads) {
-					uploads = maybeParse(uploads);
-
-					if(uploads && uploads.length) {
-						for(var i=0; i<uploads.length; ++i) {
-							updateTextArea(uploads[i].name, uploads[i].url);
-						}
-					}
-					preview.render(postContainer);
-					textarea.focus();
-				},
-
-				complete: function() {
-					uploadForm[0].reset();
-					uploads.inProgress[post_uuid].pop();
-				}
-			});
-
-			return false;
-		});
-
-		uploadForm.submit();
-	}
-
-	function uploadTopicThumb(params) {
-		var post_uuid = params.post_uuid,
-			formData = params.formData,
-			postContainer = $('#cmp-uuid-' + post_uuid),
-			spinner = postContainer.find('.topic-thumb-spinner'),
-			thumbForm = postContainer.find('#thumbForm');
-
-		thumbForm.attr('action', config.relative_path + params.route);
-
-		thumbForm.off('submit').submit(function() {
-			spinner.removeClass('hide');
-
-			uploads.inProgress[post_uuid] = uploads.inProgress[post_uuid] || [];
-			uploads.inProgress[post_uuid].push(1);
-
-			$(this).ajaxSubmit({
-				headers: {
-					'x-csrf-token': csrf.get()
-				},
-				formData: formData,
-				error: onUploadError,
-				success: function(uploads) {
-					uploads = maybeParse(uploads);
-
-					postContainer.find('#topic-thumb-url').val((uploads[0] || {}).url || '').trigger('change');
-				},
-				complete: function() {
-					uploads.inProgress[post_uuid].pop();
-					spinner.addClass('hide');
-				}
-			});
-			return false;
-		});
-		thumbForm.submit();
-	}
-
-	function onUploadError(xhr) {
-		xhr = maybeParse(xhr);
-		app.alertError(xhr.responseText);
-	}
-
-	return uploads;
-});
-
diff --git a/src/messaging.js b/src/messaging.js
index 3affeedf19..9edd99d836 100644
--- a/src/messaging.js
+++ b/src/messaging.js
@@ -390,7 +390,8 @@ var db = require('./database'),
 			bodyShort: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]',
 			bodyLong: messageObj.content,
 			nid: 'chat_' + fromuid + '_' + touid,
-			from: fromuid
+			from: fromuid,
+			path: '/chats/' + messageObj.fromUser.username
 		}, function(err, notification) {
 			if (!err && notification) {
 				notifications.push(notification, [touid], callback);
diff --git a/src/upgrade.js b/src/upgrade.js
index dbeada80fa..1fdbd984f7 100644
--- a/src/upgrade.js
+++ b/src/upgrade.js
@@ -21,7 +21,7 @@ var db = require('./database'),
 	schemaDate, thisSchemaDate,
 
 	// IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema
-	latestSchema = Date.UTC(2015, 5, 2);
+	latestSchema = Date.UTC(2015, 6, 3);
 
 Upgrade.check = function(callback) {
 	db.get('schemaDate', function(err, value) {
@@ -420,6 +420,32 @@ Upgrade.upgrade = function(callback) {
 				winston.info('[2015/06/02] Creating group sorted sets skipped');
 				next();
 			}
+		},
+		function(next) {
+			thisSchemaDate = Date.UTC(2015, 6, 3);
+			if (schemaDate < thisSchemaDate) {
+				updatesMade = true;
+				winston.info('[2015/07/03] Enabling default composer plugin');
+
+				db.isSortedSetMember('plugins:active', 'nodebb-plugin-composer-default', function(err, active) {
+					if (!active) {
+						Plugins.toggleActive('nodebb-plugin-composer-default', function(err) {
+							if (err) {
+								return next(err);
+							}
+
+							winston.info('[2015/07/03] Enabling default composer plugin done');
+							Upgrade.update(thisSchemaDate, next);
+						});
+					} else {
+						winston.info('[2015/07/03] Enabling default composer plugin done');
+						Upgrade.update(thisSchemaDate, next);
+					}
+				});
+			} else {
+				winston.info('[2015/07/03] Enabling default composer plugin skipped');
+				next();
+			}
 		}