diff --git a/package.json b/package.json index f37bfca434..054d3bd315 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,9 @@ "emailjs": "0.3.4", "cookie": "0.0.6", "connect-redis": "1.4.5", - "passport": "0.1.16", + "passport": "0.1.17", "passport-local": "0.1.6", - "passport-twitter": "0.1.4", + "passport-twitter": "0.1.5", "passport-google-oauth": "0.1.5", "passport-facebook": "0.1.5", "less-middleware": "0.1.12", diff --git a/public/css/style.less b/public/css/style.less index 53e4d8688e..b3ade8d0b5 100644 --- a/public/css/style.less +++ b/public/css/style.less @@ -504,8 +504,8 @@ body .navbar .nodebb-inline-block { .post-window { position: fixed; bottom: 45px; - height: 450px; display: none; + height: 450px; > div { position: absolute; @@ -528,7 +528,7 @@ body .navbar .nodebb-inline-block { input { width: 100%; text-align: center; - background: rgba(255, 255, 255, 0.9); + background: rgba(255, 255, 255, 0.5); border: none; padding: 0.5em 0; -webkit-border-radius: 0px; @@ -547,6 +547,29 @@ body .navbar .nodebb-inline-block { color: white; height: 330px; } + + #imagedrop { + background: rgba(64, 64, 64, 0.95); + padding: 0.5em; + display: block; + width: 90%; + min-height:25px; + margin: 1em auto; + resize: none; + color:white; + font-size:20px; + div { + margin-right:10px; + } + span { + line-height:20px; + float:left; + } + button { + padding-left:5px; + } + } + } } diff --git a/public/css/topic.less b/public/css/topic.less index 1bd3cbf9e9..4cb218dd28 100644 --- a/public/css/topic.less +++ b/public/css/topic.less @@ -48,6 +48,10 @@ padding: 2px 5px 0 5px; word-wrap: break-word; } + + .post-images{ + padding: 2px 5px 0 5px; + } .post-block { .post-buttons { diff --git a/public/src/modules/composer.js b/public/src/modules/composer.js index 0d5445fb1e..feff528dc8 100644 --- a/public/src/modules/composer.js +++ b/public/src/modules/composer.js @@ -7,10 +7,88 @@ define(['taskbar'], function(taskbar) { postContainer: undefined, }; + function loadFile(file) { + var reader = new FileReader(); + var dropDiv = $('#imagedrop'); + var uuid = dropDiv.parents('[data-uuid]').attr('data-uuid'); + var posts = composer.posts[uuid]; + + $(reader).on('loadend', function(e) { + var bin = this.result; + bin = bin.split(',')[1]; + + var img = { + name: file.name, + data: bin + }; + + posts.images.push(img); + + var imageLabel = $('
'+ file.name +'
'); + var closeButton = $(''); + closeButton.on('click', function(e) { + + imageLabel.remove(); + var index = posts.images.indexOf(img); + if(index !== -1) { + posts.images.splice(index, 1); + } + + if(!dropDiv.children().length) { + dropDiv.html('Drag and drop images here'); + } + }); + + imageLabel.append(closeButton); + dropDiv.append(imageLabel); + + }); + + reader.readAsDataURL(file); + } + + function initializeFileReader() { + jQuery.event.props.push( "dataTransfer" ); + + if(window.FileReader) { + var drop = $('#imagedrop'); + + $(composer.postContainer).on('dragenter dragover', function() { + drop.show(); + }); + + function cancel(e) { + e.preventDefault(); + return false; + } + + drop.on('dragover', cancel); + drop.on('dragenter', cancel); + + drop.on('drop', function(e) { + e.preventDefault(); + var uuid = drop.parents('[data-uuid]').attr('data-uuid'); + var posts = composer.posts[uuid]; + + var dt = e.dataTransfer; + var files = dt.files; + + if(!posts.images.length) + drop.html(''); + + for (var i=0; i Discard' + '' + '' + + ''+ '' + ''; document.body.insertBefore(composer.postContainer, taskbar); + initializeFileReader(); + socket.on('api:composer.push', function(threadData) { if (!threadData.error) { - var uuid = utils.generateUUID(); + var uuid = utils.generateUUID(); composer.taskbar.push('composer', uuid, { title: (!threadData.cid ? (threadData.title || '') : 'New Topic'), @@ -48,7 +129,8 @@ define(['taskbar'], function(taskbar) { cid: threadData.cid, pid: threadData.pid, title: threadData.title || '', - body: threadData.body || '' + body: threadData.body || '', + images: [] }; composer.load(uuid); } else { @@ -156,15 +238,18 @@ define(['taskbar'], function(taskbar) { } composer.load = function(post_uuid) { - var post_data = composer.posts[post_uuid], + var post_data = composer.posts[post_uuid], titleEl = composer.postContainer.querySelector('input'), bodyEl = composer.postContainer.querySelector('textarea'), postWindowEl = composer.postContainer.querySelector('.span5'), taskbarBtn = document.querySelector('#taskbar [data-uuid="' + post_uuid + '"]'), btnRect = taskbarBtn.getBoundingClientRect(), taskbarRect = document.getElementById('taskbar').getBoundingClientRect(), + dropDiv = $(composer.postContainer).find('#imagedrop'), windowRect, leftPos; + dropDiv.html('Drag and drop images here').hide(); + composer.postContainer.style.display = 'block'; windowRect = postWindowEl.getBoundingClientRect(); leftPos = btnRect.left + btnRect.width - windowRect.width; @@ -184,6 +269,8 @@ define(['taskbar'], function(taskbar) { } bodyEl.value = post_data.body + + // Direct user focus to the correct element if ((parseInt(post_data.tid) || parseInt(post_data.pid)) > 0) { bodyEl.focus(); @@ -225,18 +312,21 @@ define(['taskbar'], function(taskbar) { socket.emit('api:topics.post', { 'title' : titleEl.value, 'content' : bodyEl.value, - 'category_id' : postData.cid + 'category_id' : postData.cid, + images: composer.posts[post_uuid].images }); } else if (parseInt(postData.tid) > 0) { socket.emit('api:posts.reply', { 'topic_id' : postData.tid, - 'content' : bodyEl.value + 'content' : bodyEl.value, + images: composer.posts[post_uuid].images }); } else if (parseInt(postData.pid) > 0) { socket.emit('api:posts.edit', { pid: postData.pid, content: bodyEl.value, - title: titleEl.value + title: titleEl.value, + images: composer.posts[post_uuid].images }); } @@ -245,6 +335,7 @@ define(['taskbar'], function(taskbar) { composer.discard = function(post_uuid) { if (composer.posts[post_uuid]) { + $(composer.postContainer).find('#imagedrop').html(''); delete composer.posts[post_uuid]; composer.minimize(); taskbar.discard('composer', post_uuid); diff --git a/public/templates/topic.tpl b/public/templates/topic.tpl index 682e3db238..397d3a8f59 100644 --- a/public/templates/topic.tpl +++ b/public/templates/topic.tpl @@ -53,6 +53,11 @@
{main_posts.content}
+
+ + {main_posts.uploadedImages.name}
+ +
{main_posts.signature}
posted by {main_posts.username} {main_posts.relativeTime} ago @@ -81,6 +86,11 @@
{posts.content}
+
+ + {posts.uploadedImages.name}
+ +
{posts.signature}
diff --git a/src/imgur.js b/src/imgur.js new file mode 100644 index 0000000000..d8178bc332 --- /dev/null +++ b/src/imgur.js @@ -0,0 +1,33 @@ + + +var request = require('request'); + + +(function(imgur) { + var clientID = ''; + + imgur.upload = function(image, type, callback) { + var options = { + url: 'https://api.imgur.com/3/upload.json', + headers: { + 'Authorization': 'Client-ID ' + clientID + } + }; + + var post = request.post(options, function(err, req, body){ + try{ + callback(err, JSON.parse(body)); + } catch(e) { + callback(err, body); + } + }); + + var upload = post.form({type:type, image:image}); + } + + imgur.setClientID = function(id) { + clientID = id; + } + +}(exports)); + diff --git a/src/posts.js b/src/posts.js index b4e07be059..0feeca6b02 100644 --- a/src/posts.js +++ b/src/posts.js @@ -117,6 +117,11 @@ marked.setOptions({ postData['edited-class'] = postData.editor !== '' ? '' : 'none'; postData['relativeEditTime'] = postData.edited !== '0' ? utils.relativeTime(postData.edited) : ''; postData.content = marked(postData.content || ''); + if(postData.uploadedImages) { + postData.uploadedImages = JSON.parse(postData.uploadedImages); + } else { + postData.uploadedImages = []; + } posts.push(postData); } callback(null); @@ -185,7 +190,7 @@ marked.setOptions({ }); } - Posts.reply = function(socket, tid, uid, content) { + Posts.reply = function(socket, tid, uid, content, images) { if (uid < 1) { socket.emit('event:alert', { title: 'Reply Unsuccessful', @@ -211,13 +216,13 @@ marked.setOptions({ return; } - Posts.create(uid, tid, content, function(pid) { - if (pid > 0) { - RDB.rpush('tid:' + tid + ':posts', pid); + Posts.create(uid, tid, content, images, function(postData) { + if (postData) { + RDB.rpush('tid:' + tid + ':posts', postData.pid); RDB.del('tid:' + tid + ':read_by_uid'); - Posts.get_cid_by_pid(pid, function(cid) { + Posts.get_cid_by_pid(postData.pid, function(cid) { RDB.del('cid:' + cid + ':read_by_uid', function(err, data) { topics.markAsRead(tid, uid); }); @@ -228,7 +233,6 @@ marked.setOptions({ // Send notifications to users who are following this topic threadTools.notify_followers(tid, uid); - socket.emit('event:alert', { title: 'Reply Successful', message: 'You have successfully replied. Click here to view your reply.', @@ -236,21 +240,16 @@ marked.setOptions({ timeout: 2000 }); + postData.content = marked(postData.content); + postData.post_rep = 0; + postData.relativeTime = utils.relativeTime(postData.timestamp) + postData.fav_star_class = 'icon-star-empty'; + postData['edited-class'] = 'none'; + postData.uploadedImages = JSON.parse(postData.uploadedImages); - var timestamp = Date.now(); var socketData = { 'posts' : [ - { - 'pid' : pid, - 'content' : marked(content || ''), - 'uid' : uid, - 'post_rep' : 0, - 'timestamp' : timestamp, - 'relativeTime': utils.relativeTime(timestamp), - 'fav_star_class' :'icon-star-empty', - 'edited-class': 'none', - 'editor': '', - } + postData ] }; @@ -258,6 +257,7 @@ marked.setOptions({ io.sockets.in('topic_' + tid).emit('event:new_post', socketData); io.sockets.in('recent_posts').emit('event:new_post', socketData); }); + } else { socket.emit('event:alert', { @@ -270,10 +270,10 @@ marked.setOptions({ }); }); }; - - Posts.create = function(uid, tid, content, callback) { + + Posts.create = function(uid, tid, content, images, callback) { if (uid === null) { - callback(-1); + callback(null); return; } @@ -285,7 +285,7 @@ marked.setOptions({ var timestamp = Date.now(); - RDB.hmset('post:' + pid, { + var postData = { 'pid': pid, 'uid': uid, 'tid': tid, @@ -294,8 +294,11 @@ marked.setOptions({ 'reputation': 0, 'editor': '', 'edited': 0, - 'deleted': 0 - }); + 'deleted': 0, + 'uploadedImages': '' + }; + + RDB.hmset('post:' + pid, postData); topics.increasePostCount(tid); topics.updateTimestamp(tid, timestamp); @@ -321,11 +324,42 @@ marked.setOptions({ user.onNewPostMade(uid, tid, pid, timestamp); - if (callback) - callback(pid); + var imgur = require('./imgur'); + // move clientID to config + imgur.setClientID('09f3955fee9a0a6'); + + var uploadedImages = []; + + function uploadImage(image, callback) { + imgur.upload(image.data, 'base64', function(err, data) { + if(err) { + callback(err); + } else { + if(data.success) { + var img= {url:data.data.link, name:image.name}; + uploadedImages.push(img); + callback(null); + } else { + callback(data); + } + } + }); + } + + async.each(images, uploadImage, function(err) { + if(!err) { + postData.uploadedImages = JSON.stringify(uploadedImages); + Posts.setPostField(pid, 'uploadedImages', postData.uploadedImages); + + callback(postData); + } else { + console.log(err); + callback(null); + } + }); }); } else { - callback(-1); + callback(null); } }); } diff --git a/src/routes/authentication.js b/src/routes/authentication.js index fa026425ae..a1cf0c9e37 100644 --- a/src/routes/authentication.js +++ b/src/routes/authentication.js @@ -5,7 +5,7 @@ passportGoogle = require('passport-google-oauth').OAuth2Strategy, passportFacebook = require('passport-facebook').Strategy, login_strategies = [], - + nconf = require('nconf'), user_module = require('./../user.js'), login_module = require('./../login.js'); @@ -20,7 +20,7 @@ passport.use(new passportTwitter({ consumerKey: global.config['social:twitter:key'], consumerSecret: global.config['social:twitter:secret'], - callbackURL: config.url + 'auth/twitter/callback' + callbackURL: nconf.get('url') + 'auth/twitter/callback' }, function(token, tokenSecret, profile, done) { login_module.loginViaTwitter(profile.id, profile.username, function(err, user) { if (err) { return done(err); } @@ -35,7 +35,7 @@ passport.use(new passportGoogle({ clientID: global.config['social:google:id'], clientSecret: global.config['social:google:secret'], - callbackURL: config.url + 'auth/google/callback' + callbackURL: nconf.get('url') + 'auth/google/callback' }, function(accessToken, refreshToken, profile, done) { login_module.loginViaGoogle(profile.id, profile.displayName, profile.emails[0].value, function(err, user) { if (err) { return done(err); } @@ -50,7 +50,7 @@ passport.use(new passportFacebook({ clientID: global.config['social:facebook:app_id'], clientSecret: global.config['social:facebook:secret'], - callbackURL: config.url + 'auth/facebook/callback' + callbackURL: nconf.get('url') + 'auth/facebook/callback' }, function(accessToken, refreshToken, profile, done) { login_module.loginViaFacebook(profile.id, profile.displayName, profile.emails[0].value, function(err, user) { if (err) { return done(err); } diff --git a/src/topics.js b/src/topics.js index 773c91bf47..b304fc5f20 100644 --- a/src/topics.js +++ b/src/topics.js @@ -116,7 +116,7 @@ marked.setOptions({ privileges = results[2]; var main_posts = topicPosts.splice(0, 1); - + callback(null, { 'topic_name':topicData.title, 'category_name':topicData.category_name, @@ -313,7 +313,7 @@ marked.setOptions({ }); } - Topics.post = function(socket, uid, title, content, category_id) { + Topics.post = function(socket, uid, title, content, category_id, images) { if (!category_id) throw new Error('Attempted to post without a category_id'); @@ -379,9 +379,9 @@ marked.setOptions({ RDB.set('topicslug:' + slug + ':tid', tid); - posts.create(uid, tid, content, function(pid) { - if (pid > 0) { - RDB.lpush(schema.topics(tid).posts, pid); + posts.create(uid, tid, content, images, function(postData) { + if (postData) { + RDB.lpush(schema.topics(tid).posts, postData.pid); // Auto-subscribe the post creator to the newly created topic threadTools.toggleFollow(tid, uid); diff --git a/src/webserver.js b/src/webserver.js index ae641d95f4..1733afaea5 100644 --- a/src/webserver.js +++ b/src/webserver.js @@ -169,7 +169,7 @@ var express = require('express'), app.get('/topic/:topic_id/:slug?', function(req, res) { var tid = req.params.topic_id; - if (tid.match('^\d+\.rss$')) { + if (tid.match(/^\d+\.rss$/)) { fs.readFile('feeds/topics/' + tid, function (err, data) { if (err) { res.type('text').send(404, "Unable to locate an rss feed at this location."); @@ -181,7 +181,6 @@ var express = require('express'), return; } - var topic_url = tid + (req.params.slug ? '/' + req.params.slug : ''); topics.getTopicWithPosts(tid, ((req.user) ? req.user.uid : 0), function(err, topic) { if (err) return res.redirect('404'); @@ -197,7 +196,8 @@ var express = require('express'), app.get('/category/:category_id/:slug?', function(req, res) { var cid = req.params.category_id; - if (cid.match('^\d+\.rss$')) { + + if (cid.match(/^\d+\.rss$/)) { fs.readFile('feeds/categories/' + cid, function (err, data) { if (err) { res.type('text').send(404, "Unable to locate an rss feed at this location."); diff --git a/src/websockets.js b/src/websockets.js index ed510bffd6..5cc3f1743c 100644 --- a/src/websockets.js +++ b/src/websockets.js @@ -313,11 +313,11 @@ var SocketIO = require('socket.io').listen(global.server, { log:false }), }); socket.on('api:topics.post', function(data) { - topics.post(socket, uid, data.title, data.content, data.category_id); + topics.post(socket, uid, data.title, data.content, data.category_id, data.images); }); socket.on('api:posts.reply', function(data) { - posts.reply(socket, data.topic_id, uid, data.content); + posts.reply(socket, data.topic_id, uid, data.content, data.images); }); socket.on('api:user.active.get', function() {