diff --git a/public/language/en_GB/global.json b/public/language/en_GB/global.json index 709e0bc94a..0dbacf8ec2 100644 --- a/public/language/en_GB/global.json +++ b/public/language/en_GB/global.json @@ -28,6 +28,7 @@ "header.admin": "Admin", "header.recent": "Recent", "header.unread": "Unread", + "header.tags": "Tags", "header.popular": "Popular", "header.users": "Users", "header.chats": "Chats", diff --git a/public/language/en_GB/tags.json b/public/language/en_GB/tags.json new file mode 100644 index 0000000000..dbd88abbfc --- /dev/null +++ b/public/language/en_GB/tags.json @@ -0,0 +1,5 @@ +{ + "no_tag_topics": "There are no topics with this tag.", + "tags": "Tags", + "enter_tags_here": "Enter tags here..." +} \ No newline at end of file diff --git a/public/src/forum/tag.js b/public/src/forum/tag.js new file mode 100644 index 0000000000..84d922ecfc --- /dev/null +++ b/public/src/forum/tag.js @@ -0,0 +1,41 @@ +'use strict'; + +/* globals define, app, socket */ + +define(['forum/recent', 'forum/infinitescroll'], function(recent, infinitescroll) { + var Tag = {}; + + Tag.init = function() { + app.enterRoom('tags'); + + if ($('body').height() <= $(window).height() && $('#topics-container').children().length >= 20) { + $('#load-more-btn').show(); + } + + $('#load-more-btn').on('click', function() { + loadMoreTopics(); + }); + + infinitescroll.init(loadMoreTopics); + + function loadMoreTopics(direction) { + if(direction < 0 || !$('#topics-container').length) { + return; + } + + infinitescroll.loadMore('topics.loadMoreFromSet', { + set: 'tag:' + ajaxify.variables.get('tag') + ':topics', + after: $('#topics-container').attr('data-nextstart') + }, function(data) { + if (data.topics && data.topics.length) { + recent.onTopicsLoaded('tag', data.topics, false); + $('#topics-container').attr('data-nextstart', data.nextStart); + } else { + $('#load-more-btn').hide(); + } + }); + } + }; + + return Tag; +}); diff --git a/public/src/forum/unread.js b/public/src/forum/unread.js index 169acf28f7..b2ca82be7c 100644 --- a/public/src/forum/unread.js +++ b/public/src/forum/unread.js @@ -3,8 +3,7 @@ /* globals define, app, socket */ define(['forum/recent', 'topicSelect', 'forum/infinitescroll'], function(recent, topicSelect, infinitescroll) { - var Unread = {}, - loadingMoreTopics = false; + var Unread = {}; $(window).on('action:ajaxify.start', function(ev, data) { if(data.url.indexOf('unread') !== 0) { diff --git a/public/src/modules/composer.js b/public/src/modules/composer.js index f747d98e03..40df400a9e 100644 --- a/public/src/modules/composer.js +++ b/public/src/modules/composer.js @@ -2,7 +2,7 @@ /* globals define, socket, app, config, ajaxify, utils, translator, templates, bootbox */ -define(['taskbar', 'composer/controls', 'composer/uploads', 'composer/formatting', 'composer/drafts'], function(taskbar, controls, uploads, formatting, drafts) { +define(['taskbar', 'composer/controls', 'composer/uploads', 'composer/formatting', 'composer/drafts', 'composer/tags'], function(taskbar, controls, uploads, formatting, drafts, tags) { var composer = { active: undefined, posts: {} @@ -152,7 +152,8 @@ define(['taskbar', 'composer/controls', 'composer/uploads', 'composer/formatting body: threadData.body, modified: false, isMain: !threadData.index, - topic_thumb: threadData.topic_thumb + topic_thumb: threadData.topic_thumb, + tags: threadData.tags }); }); }; @@ -176,8 +177,10 @@ define(['taskbar', 'composer/controls', 'composer/uploads', 'composer/formatting function createNewComposer(post_uuid) { var allowTopicsThumbnail = config.allowTopicsThumbnail && composer.posts[post_uuid].isMain && (config.hasImageUploadPlugin || config.allowFileUploads); + var isTopic = composer.posts[post_uuid] ? !!composer.posts[post_uuid].cid : false; + var isMain = composer.posts[post_uuid] ? !!composer.posts[post_uuid].isMain : false; - templates.parse('composer', {allowTopicsThumbnail: allowTopicsThumbnail}, function(composerTemplate) { + templates.parse('composer', {allowTopicsThumbnail: allowTopicsThumbnail, showTags: isTopic || isMain}, function(composerTemplate) { translator.translate(composerTemplate, function(composerTemplate) { composerTemplate = $(composerTemplate); @@ -185,10 +188,12 @@ define(['taskbar', 'composer/controls', 'composer/uploads', 'composer/formatting $(document.body).append(composerTemplate); - activateReposition(post_uuid); - var postContainer = $(composerTemplate[0]); + tags.init(postContainer, composer.posts[post_uuid]); + + activateReposition(post_uuid); + if(config.allowFileUploads || config.hasImageUploadPlugin) { uploads.initialize(post_uuid); } @@ -266,6 +271,8 @@ define(['taskbar', 'composer/controls', 'composer/uploads', 'composer/formatting }); formatting.addComposerButtons(); + + }); }); } @@ -321,15 +328,16 @@ define(['taskbar', 'composer/controls', 'composer/uploads', 'composer/formatting if (resizeActive) { var position = (e.clientY + resizeOffset); var newHeight = $(window).height() - position; - var paddingBottom = parseInt(postContainer.css('padding-bottom'), 10); + if(newHeight > $(window).height() - $('#header-menu').height() - 20) { newHeight = $(window).height() - $('#header-menu').height() - 20; - } else if (newHeight < paddingBottom) { - newHeight = paddingBottom; + } else if (newHeight < 100) { + newHeight = 100; } postContainer.css('height', newHeight); $('body').css({'margin-bottom': newHeight}); + resizeTabContent(postContainer); resizeSavePosition(newHeight); } e.preventDefault(); @@ -395,6 +403,19 @@ define(['taskbar', 'composer/controls', 'composer/uploads', 'composer/formatting $('body').css({'margin-bottom': postContainer.css('height')}); focusElements(post_uuid); + resizeTabContent(postContainer); + } + + function resizeTabContent(postContainer) { + var h1 = postContainer.find('.title').outerHeight(true); + var h2 = postContainer.find('.tags-container').outerHeight(true); + var h3 = postContainer.find('.formatting-bar').outerHeight(true); + var h4 = postContainer.find('.nav-tabs').outerHeight(true); + var h5 = postContainer.find('.instructions').outerHeight(true); + var h6 = postContainer.find('.topic-thumb-container').outerHeight(true); + var h7 = $('.taskbar').height(); + var total = h1 + h2 + h3 + h4 + h5 + h6 + h7; + postContainer.find('.tab-content').css('height', postContainer.height() - total); } function focusElements(post_uuid) { @@ -441,7 +462,8 @@ define(['taskbar', 'composer/controls', 'composer/uploads', 'composer/formatting title: titleEl.val(), content: bodyEl.val(), topic_thumb: thumbEl.val() || '', - category_id: postData.cid + category_id: postData.cid, + tags: tags.getTags(post_uuid) }, function(err, topic) { done(err); if (!err) { @@ -459,7 +481,8 @@ define(['taskbar', 'composer/controls', 'composer/uploads', 'composer/formatting pid: postData.pid, content: bodyEl.val(), title: titleEl.val(), - topic_thumb: thumbEl.val() || '' + topic_thumb: thumbEl.val() || '', + tags: tags.getTags(post_uuid) }, done); } diff --git a/public/src/modules/composer/tags.js b/public/src/modules/composer/tags.js new file mode 100644 index 0000000000..1a020b3dde --- /dev/null +++ b/public/src/modules/composer/tags.js @@ -0,0 +1,60 @@ + +'use strict'; + +/*globals define*/ + +define(function() { + var tags = {}; + + tags.init = function(postContainer, postData) { + var tagEl = postContainer.find('.tags'); + if (!tagEl.length) { + return; + } + + tagEl.tagsinput(); + addTags(postData.tags, tagEl); + + var input = postContainer.find('.bootstrap-tagsinput input'); + input.autocomplete({ + delay: 100, + source: function(request, response) { + socket.emit('topics.searchTags', request.term, function(err, tags) { + if (err) { + return app.alertError(err.message) + } + if (tags) { + response(tags); + $('.ui-autocomplete a').attr('href', '#'); + } + }); + }, + select: function(event, ui) { + // when autocomplete is selected from the dropdown simulate a enter key down to turn it into a tag + // http://stackoverflow.com/a/3276819/583363 + var e = jQuery.Event('keydown'); + e.which = 13; + e.keyCode = 13; + setTimeout(function() { + input.trigger(e); + }, 100); + } + }); + + input.attr('tabIndex', tagEl.attr('tabIndex')); + }; + + function addTags(tags, tagEl) { + if (tags && tags.length) { + for(var i=0; i'),this.$input=a('').appendTo(this.$container),this.$element.after(this.$container),this.build(c)}function c(a,b){if("function"!=typeof a[b]){var c=a[b];a[b]=function(a){return a[c]}}}function d(a,b){if("function"!=typeof a[b]){var c=a[b];a[b]=function(){return c}}}function e(a){return a?h.text(a).html():""}function f(a){var b=0;if(document.selection){a.focus();var c=document.selection.createRange();c.moveStart("character",-a.value.length),b=c.text.length}else(a.selectionStart||"0"==a.selectionStart)&&(b=a.selectionStart);return b}var g={tagClass:function(){return"label label-info"},itemValue:function(a){return a?a.toString():a},itemText:function(a){return this.itemValue(a)},freeInput:!0,maxTags:void 0,confirmKeys:[13],onTagExists:function(a,b){b.hide().fadeIn()}};b.prototype={constructor:b,add:function(b,c){var d=this;if(!(d.options.maxTags&&d.itemsArray.length>=d.options.maxTags||b!==!1&&!b)){if("object"==typeof b&&!d.objectItems)throw"Can't add objects when itemValue option is not set";if(!b.toString().match(/^\s*$/)){if(d.isSelect&&!d.multiple&&d.itemsArray.length>0&&d.remove(d.itemsArray[0]),"string"==typeof b&&"INPUT"===this.$element[0].tagName){var f=b.split(",");if(f.length>1){for(var g=0;g'+e(i)+'');if(m.data("item",b),d.findInputWrapper().before(m),m.after(" "),d.isSelect&&!a('option[value="'+escape(h)+'"]',d.$element)[0]){var n=a("");n.data("item",b),n.attr("value",h),d.$element.append(n)}c||d.pushVal(),d.options.maxTags===d.itemsArray.length&&d.$container.addClass("bootstrap-tagsinput-max"),d.$element.trigger(a.Event("itemAdded",{item:b}))}}}},remove:function(b,c){var d=this;d.objectItems&&(b="object"==typeof b?a.grep(d.itemsArray,function(a){return d.options.itemValue(a)==d.options.itemValue(b)})[0]:a.grep(d.itemsArray,function(a){return d.options.itemValue(a)==b})[0]),b&&(a(".tag",d.$container).filter(function(){return a(this).data("item")===b}).remove(),a("option",d.$element).filter(function(){return a(this).data("item")===b}).remove(),d.itemsArray.splice(a.inArray(b,d.itemsArray),1)),c||d.pushVal(),d.options.maxTags>d.itemsArray.length&&d.$container.removeClass("bootstrap-tagsinput-max"),d.$element.trigger(a.Event("itemRemoved",{item:b}))},removeAll:function(){var b=this;for(a(".tag",b.$container).remove(),a("option",b.$element).remove();b.itemsArray.length>0;)b.itemsArray.pop();b.pushVal(),b.options.maxTags&&!this.isEnabled()&&this.enable()},refresh:function(){var b=this;a(".tag",b.$container).each(function(){var c=a(this),d=c.data("item"),f=b.options.itemValue(d),g=b.options.itemText(d),h=b.options.tagClass(d);if(c.attr("class",null),c.addClass("tag "+e(h)),c.contents().filter(function(){return 3==this.nodeType})[0].nodeValue=e(g),b.isSelect){var i=a("option",b.$element).filter(function(){return a(this).data("item")===d});i.attr("value",f)}})},items:function(){return this.itemsArray},pushVal:function(){var b=this,c=a.map(b.items(),function(a){return b.options.itemValue(a).toString()});b.$element.val(c,!0).trigger("change")},build:function(b){var e=this;e.options=a.extend({},g,b);var h=e.options.typeahead||{};e.objectItems&&(e.options.freeInput=!1),c(e.options,"itemValue"),c(e.options,"itemText"),c(e.options,"tagClass"),e.options.source&&(h.source=e.options.source),h.source&&a.fn.typeahead&&(d(h,"source"),e.$input.typeahead({source:function(b,c){function d(a){for(var b=[],d=0;d$1")}})),e.$container.on("click",a.proxy(function(){e.$input.focus()},e)),e.$container.on("keydown","input",a.proxy(function(b){var c=a(b.target),d=e.findInputWrapper();switch(b.which){case 8:if(0===f(c[0])){var g=d.prev();g&&e.remove(g.data("item"))}break;case 46:if(0===f(c[0])){var h=d.next();h&&e.remove(h.data("item"))}break;case 37:var i=d.prev();0===c.val().length&&i[0]&&(i.before(d),c.focus());break;case 39:var j=d.next();0===c.val().length&&j[0]&&(j.after(d),c.focus());break;default:e.options.freeInput&&a.inArray(b.which,e.options.confirmKeys)>=0&&(e.add(c.val()),c.val(""),b.preventDefault())}c.attr("size",Math.max(this.inputSize,c.val().length))},e)),e.$container.on("click","[data-role=remove]",a.proxy(function(b){e.remove(a(b.target).closest(".tag").data("item"))},e)),e.options.itemValue===g.itemValue&&("INPUT"===e.$element[0].tagName?e.add(e.$element.val()):a("option",e.$element).each(function(){e.add(a(this).attr("value"),!0)}))},destroy:function(){var a=this;a.$container.off("keypress","input"),a.$container.off("click","[role=remove]"),a.$container.remove(),a.$element.removeData("tagsinput"),a.$element.show()},focus:function(){this.$input.focus()},input:function(){return this.$input},findInputWrapper:function(){for(var b=this.$input[0],c=this.$container[0];b&&b.parentNode!==c;)b=b.parentNode;return a(b)}},a.fn.tagsinput=function(c,d){var e=[];return this.each(function(){var f=a(this).data("tagsinput");if(f){var g=f[c](d);void 0!==g&&e.push(g)}else f=new b(this,c),a(this).data("tagsinput",f),e.push(f),"SELECT"===this.tagName&&a("option",a(this)).attr("selected","selected"),a(this).val(a(this).val())}),"string"==typeof c?e.length>1?e:e[0]:e},a.fn.tagsinput.Constructor=b;var h=a("
");a(function(){a("input[data-role=tagsinput], select[multiple][data-role=tagsinput]").tagsinput()})}(window.jQuery); +/* +*/ \ No newline at end of file diff --git a/src/controllers/categories.js b/src/controllers/categories.js index 12d5f8346f..8b813eb95f 100644 --- a/src/controllers/categories.js +++ b/src/controllers/categories.js @@ -10,7 +10,7 @@ var categoriesController = {}, topics = require('./../topics'); categoriesController.recent = function(req, res, next) { - var uid = (req.user) ? req.user.uid : 0; + var uid = req.user ? req.user.uid : 0; topics.getLatestTopics(uid, 0, 19, req.params.term, function (err, data) { if(err) { return next(err); @@ -21,7 +21,7 @@ categoriesController.recent = function(req, res, next) { }; categoriesController.popular = function(req, res, next) { - var uid = (req.user) ? req.user.uid : 0; + var uid = req.user ? req.user.uid : 0; var set = 'topics:' + req.params.set; if(!req.params.set) { set = 'topics:posts'; @@ -37,7 +37,7 @@ categoriesController.popular = function(req, res, next) { }; categoriesController.unread = function(req, res, next) { - var uid = req.user.uid; + var uid = req.user ? req.user.uid : 0; topics.getUnreadTopics(uid, 0, 20, function (err, data) { if(err) { @@ -49,7 +49,7 @@ categoriesController.unread = function(req, res, next) { }; categoriesController.unreadTotal = function(req, res, next) { - var uid = req.user.uid; + var uid = req.user ? req.user.uid : 0; topics.getTotalUnread(uid, function (err, data) { if(err) { diff --git a/src/controllers/index.js b/src/controllers/index.js index 9e92b369fc..1e5c920463 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -2,6 +2,7 @@ var topicsController = require('./topics'), categoriesController = require('./categories'), + tagsController = require('./tags'), usersController = require('./users'), accountsController = require('./accounts'), staticController = require('./static'), @@ -24,6 +25,7 @@ var topicsController = require('./topics'), var Controllers = { topics: topicsController, categories: categoriesController, + tags: tagsController, users: usersController, accounts: accountsController, static: staticController, diff --git a/src/controllers/tags.js b/src/controllers/tags.js new file mode 100644 index 0000000000..70626e8f27 --- /dev/null +++ b/src/controllers/tags.js @@ -0,0 +1,53 @@ +"use strict"; + +var tagsController = {}, + async = require('async'), + topics = require('./../topics'); + +tagsController.getTag = function(req, res, next) { + var tag = req.params.tag; + var uid = req.user ? req.user.uid : 0; + + topics.getTagTids(tag, 0, 19, function(err, tids) { + if (err) { + return next(err); + } + + topics.getTopics('tag:' + tag + ':topics', uid, tids, function(err, data) { + if (err) { + return next(err); + } + data.tag = tag; + res.render('tag', data); + }); + }); +}; + +tagsController.getTags = function(req, res, next) { + topics.getTagsObjects(function(err, tags) { + if (err) { + return next(err); + } + + async.map(tags, function(tag, next) { + topics.getTagTopicCount(tag.name, function(err, count) { + if (err) { + return next(err); + } + tag.topicCount = count; + next(null, tag); + }); + }, function(err, tags) { + if (err) { + return next(err); + } + tags = tags.sort(function(a, b) { + return parseInt(b.topicCount, 10) - parseInt(a.topicCount, 10); + }); + res.render('tags', {tags: tags}); + }); + }); + +}; + +module.exports = tagsController; diff --git a/src/meta.js b/src/meta.js index cd30e022ac..90199e29fa 100644 --- a/src/meta.js +++ b/src/meta.js @@ -235,6 +235,7 @@ var fs = require('fs'), 'vendor/jquery/js/jquery.form.min.js', 'vendor/jquery/serializeObject/jquery.ba-serializeobject.min.js', 'vendor/bootstrap/js/bootstrap.min.js', + 'vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.min.js', 'vendor/requirejs/require.js', 'vendor/bootbox/bootbox.min.js', 'vendor/tinycon/tinycon.js', @@ -354,6 +355,7 @@ var fs = require('fs'), } 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";'; var parser = new (less.Parser)({ paths: paths diff --git a/src/postTools.js b/src/postTools.js index 47a2daa15b..1e5ef0dfcd 100644 --- a/src/postTools.js +++ b/src/postTools.js @@ -61,6 +61,8 @@ var winston = require('winston'), topics.setTopicField(tid, 'thumb', options.topic_thumb); + topics.updateTags(tid, options.tags); + plugins.fireHook('action:topic.edit', tid); } diff --git a/src/routes/index.js b/src/routes/index.js index 247350d77a..faa1f5a52a 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -55,6 +55,15 @@ function topicRoutes(app, middleware, controllers) { app.get('/api/topic/:topic_id/:slug?', controllers.topics.get); } +function tagRoutes(app, middleware, controllers) { + + app.get('/tags/:tag', middleware.buildHeader, controllers.tags.getTag); + app.get('/api/tags/:tag', controllers.tags.getTag); + + app.get('/tags', middleware.buildHeader, controllers.tags.getTags); + app.get('/api/tags', controllers.tags.getTags); +} + function categoryRoutes(app, middleware, controllers) { app.get('/popular/:set?', middleware.buildHeader, controllers.categories.popular); app.get('/api/popular/:set?', controllers.categories.popular); @@ -152,6 +161,7 @@ module.exports = function(app, middleware) { mainRoutes(app, middleware, controllers); staticRoutes(app, middleware, controllers); topicRoutes(app, middleware, controllers); + tagRoutes(app, middleware, controllers); categoryRoutes(app, middleware, controllers); accountRoutes(app, middleware, controllers); userRoutes(app, middleware, controllers); diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index 2b224a8274..97f0803b30 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -49,7 +49,7 @@ var stopTracking = function(replyObj) { }; SocketModules.composer.push = function(socket, pid, callback) { - posts.getPostFields(pid, ['content'], function(err, postData) { + posts.getPostFields(pid, ['content', 'tid'], function(err, postData) { if(err || (!postData && !postData.content)) { return callback(err || new Error('[[error:invalid-pid]]')); } @@ -58,6 +58,9 @@ SocketModules.composer.push = function(socket, pid, callback) { topic: function(next) { topics.getTopicDataByPid(pid, next); }, + tags: function(next) { + topics.getTopicTags(postData.tid, next); + }, index: function(next) { posts.getPidIndex(pid, next); } @@ -71,6 +74,7 @@ SocketModules.composer.push = function(socket, pid, callback) { body: postData.content, title: results.topic.title, topic_thumb: results.topic.thumb, + tags: results.tags, index: results.index }); }); diff --git a/src/socket.io/posts.js b/src/socket.io/posts.js index cd272c758a..ac7b1e017f 100644 --- a/src/socket.io/posts.js +++ b/src/socket.io/posts.js @@ -161,7 +161,7 @@ SocketPosts.edit = function(socket, data, callback) { return callback(new Error('[[error:content-too-short, ' + meta.config.minimumPostLength + ']]')); } - postTools.edit(socket.uid, data.pid, data.title, data.content, {topic_thumb: data.topic_thumb}, function(err, results) { + postTools.edit(socket.uid, data.pid, data.title, data.content, {topic_thumb: data.topic_thumb, tags: data.tags}, function(err, results) { if(err) { return callback(err); } diff --git a/src/socket.io/topics.js b/src/socket.io/topics.js index 7323a225e3..43951f707b 100644 --- a/src/socket.io/topics.js +++ b/src/socket.io/topics.js @@ -27,6 +27,7 @@ SocketTopics.post = function(socket, data, callback) { content: data.content, cid: data.category_id, thumb: data.topic_thumb, + tags: data.tags, req: websockets.reqFromSocket(socket) }, function(err, result) { if(err) { @@ -376,4 +377,8 @@ SocketTopics.getTidIndex = function(socket, tid, callback) { categories.getTopicIndex(tid, callback); }; +SocketTopics.searchTags = function(socket, query, callback) { + topics.searchTags(query, callback); +}; + module.exports = SocketTopics; diff --git a/src/topics.js b/src/topics.js index 45d71dfd5c..105a9f3e4a 100644 --- a/src/topics.js +++ b/src/topics.js @@ -20,6 +20,7 @@ var async = require('async'), require('./topics/fork')(Topics); require('./topics/posts')(Topics); require('./topics/follow')(Topics); + require('./topics/tags')(Topics); Topics.getTopicData = function(tid, callback) { Topics.getTopicsData([tid], function(err, topics) { @@ -275,6 +276,9 @@ var async = require('async'), }, threadTools: function(next) { plugins.fireHook('filter:topic.thread_tools', [], next); + }, + tags: function(next) { + Topics.getTopicTagsObjects(tid, next); } }, function(err, results) { if (err) { @@ -283,6 +287,7 @@ var async = require('async'), topicData.category = results.category; topicData.posts = results.posts; + topicData.tags = results.tags; topicData.thread_tools = results.threadTools; topicData.pageCount = results.pageCount; topicData.unreplied = parseInt(topicData.postcount, 10) === 1; diff --git a/src/topics/create.js b/src/topics/create.js index e23c7c18b3..64c95983af 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -60,6 +60,8 @@ module.exports = function(Topics) { db.incrObjectField('category:' + cid, 'topic_count'); db.incrObjectField('global', 'topicCount'); + Topics.createTags(data.tags, tid, timestamp); + callback(null, tid); }); }); @@ -111,7 +113,7 @@ module.exports = function(Topics) { user.isReadyToPost(uid, next); }, function(next) { - Topics.create({uid: uid, title: title, cid: cid, thumb: data.thumb}, next); + Topics.create({uid: uid, title: title, cid: cid, thumb: data.thumb, tags: data.tags}, next); }, function(tid, next) { Topics.reply({uid:uid, tid:tid, content:content, req: data.req}, next); diff --git a/src/topics/tags.js b/src/topics/tags.js new file mode 100644 index 0000000000..92f4b089a6 --- /dev/null +++ b/src/topics/tags.js @@ -0,0 +1,115 @@ + +'use strict'; + +var async = require('async'), + db = require('../database'); + +module.exports = function(Topics) { + + Topics.createTags = function(tags, tid, timestamp) { + if(Array.isArray(tags)) { + for (var i=0; i_> + Topics.getTagsObjects = function(callback) { + Topics.getTags(function(err, tags) { + callback(err, mapToObject(tags)); + }); + }; + + Topics.getTopicTags = function(tid, callback) { + db.getSetMembers('topic:' + tid + ':tags', callback); + }; + + //returns tags as objects cuz templates.js cant do arrays yet >_> + Topics.getTopicTagsObjects = function(tid, callback) { + Topics.getTopicTags(tid, function(err, tags) { + callback(err, mapToObject(tags)); + }); + }; + + function mapToObject(tags) { + if (!tags) { + return tags; + } + return tags.map(function(tag) { + return {name: tag}; + }); + } + + Topics.updateTags = function(tid, tags) { + async.parallel({ + timestamp: function(next) { + Topics.getTopicField(tid, 'timestamp', next); + }, + currentTags: function(next) { + Topics.getTopicTags(tid, next); + } + }, function(err, results) { + removeTopicTags(tid, results.currentTags, function(err) { + if (!err) { + Topics.createTags(tags, tid, results.timestamp); + } + }); + }); + }; + + function removeTopicTags(tid, tags, callback) { + async.parallel([ + function(next) { + db.delete('topic:' + tid + ':tags', next); + }, + function(next) { + async.each(tags, function(tag, next) { + db.sortedSetRemove('tag:' + tag + ':topics', tid, next); + }, next); + } + ], callback); + } + + Topics.searchTags = function(query, callback) { + if (!query || query.length === 0) { + return callback(null, []); + } + + db.getSetMembers('tags', function(err, tags) { + if (err) { + return callback(null, []); + } + + query = query.toLowerCase(); + var matches = []; + for(var i=0; i b; + }); + + callback(null, matches); + }); + }; + +}; \ No newline at end of file