This commit is contained in:
Baris Soner Usakli
2013-08-20 00:26:58 -04:00
23 changed files with 123 additions and 59 deletions

View File

@@ -1,16 +1,15 @@
Please support NodeBB development! Check out our IndieGoGo campaign and like, share, and follow us :)
[NodeBB Homepage](http://www.nodebb.org/ "NodeBB") # [IndieGoGo campaign](https://www.indiegogo.com/projects/nodebb-the-discussion-platform-of-the-future/ "IndieGoGo") # [Follow on Twitter](http://www.twitter.com/NodeBB/ "NodeBB Twitter") # [Like us on Facebook](http://www.facebook.com/NodeBB/ "NodeBB Facebook")
[NodeBB Homepage](http://www.nodebb.org/ "NodeBB") # [Follow on Twitter](http://www.twitter.com/NodeBB/ "NodeBB Twitter") # [Like us on Facebook](http://www.facebook.com/NodeBB/ "NodeBB Facebook")
# NodeBB
**NodeBB** is a robust Node.js driven forum built on a redis database. It is powered by web sockets, and is compatible down to IE8.
![NodeBB Main Category Listing](http://i.imgur.com/N8SLkcDh.png)
![NodeBB Main Category Listing](http://imgur.com/BdImzGs.png)
![NodeBB Topic Page](http://i.imgur.com/RBkFzdkh.png)
![NodeBB Topic Page](http://imgur.com/Lk7E3KR.png)
## How can I follow along/contribute?
* Our [Indiegogo campaign](https://www.indiegogo.com/projects/nodebb-the-discussion-platform-of-the-future/) is accepting contributions until August 17th, 2013
* Our feature roadmap is hosted on the project wiki's [Version History / Roadmap](https://github.com/designcreateplay/NodeBB/wiki/Version-History-%26-Roadmap)
* If you are a developer, feel free to check out the source and submit pull requests.
* If you are a designer, NodeBB needs themes! NodeBB will accept any LESS or CSS file and use it in place of the default Twitter Bootstrap theme. Consider extending Bootstrap themes by extending the base bootstrap LESS file.

4
app.js
View File

@@ -18,10 +18,10 @@
var fs = require('fs'),
winston = require('winston'),
nconf = require('nconf'),
pkg = require('./package.json'),
url = require('url');
nconf = require('nconf');
// Runtime environment
global.env = process.env.NODE_ENV || 'production',
@@ -54,7 +54,7 @@ if(nconf.get('upgrade')) {
} else if (!nconf.get('setup') && nconf.get('base_url')) {
nconf.set('url', nconf.get('base_url') + (nconf.get('use_port') ? ':' + nconf.get('port') : '') + nconf.get('relative_path') + '/');
nconf.set('upload_url', nconf.get('url') + 'uploads/');
global.nconf = nconf;
winston.info('Initializing NodeBB v' + pkg.version + ', on port ' + nconf.get('port') + ', using Redis store at ' + nconf.get('redis:host') + ':' + nconf.get('redis:port') + '.');
winston.info('Base Configuration OK.');

View File

@@ -1,5 +1,9 @@
@import "mixins";
html {
overflow-y: scroll;
}
body {
/*background: #fdfdfd;*/ // port to default theme when it is implemented.
-webkit-transition: margin-bottom 250ms ease;

View File

@@ -32,6 +32,7 @@
}
.profile-block, .post-block {
position:relative;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 5px;
padding: 10px;
@@ -98,12 +99,16 @@
}
}
&.deleted {
-moz-opacity: 0.30;
opacity: 0.30;
}
/*http://stackoverflow.com/questions/11037517/bootstrap-making-responsive-changes-to-layout*/
@media (max-width: 979px) {
.span12-tablet {
width: 100% !important;
margin-left:0px;
@@ -111,6 +116,36 @@
}
}
@media (min-width: 979px) {
.speech-bubble:after
{
content: "";
position: absolute;
top: 9px;
left: -7px;
border-style: solid;
border-width: 7px 7px 7px 0;
border-color: transparent #FFFFFF;
display: block;
width: 0;
z-index: 1;
}
.speech-bubble:before
{
content: "";
position: absolute;
top: 9px;
left: -8px;
border-style: solid;
border-width: 7px 7px 7px 0;
border-color: transparent rgba(0, 0, 0, 0.125);
display: block;
width: 0;
z-index: 0;
}
}
.main-post {
h3 {
margin: 0;

View File

@@ -36,7 +36,6 @@
]);
function onNewTopic(data) {
var html = templates.prepare(templates['category'].blocks['topics']).parse({ topics: [data] }),
topic = document.createElement('div'),
container = document.getElementById('topics-container'),
@@ -61,6 +60,8 @@
container.insertBefore(topic, null);
$(topic).hide().fadeIn('slow');
}
socket.emit('api:categories.getRecentReplies', cid);
}
socket.on('event:new_topic', onNewTopic);

View File

@@ -389,14 +389,20 @@
socket.on('api:posts.favourite', function(data) {
if (data.status === 'ok' && data.pid) {
var favEl = document.querySelector('.post_rep_' + data.pid).nextSibling;
if (favEl) favEl.className = 'icon-star';
if (favEl) {
favEl.className = 'icon-star';
$(favEl).parent().addClass('btn-warning');
}
}
});
socket.on('api:posts.unfavourite', function(data) {
if (data.status === 'ok' && data.pid) {
var favEl = document.querySelector('.post_rep_' + data.pid).nextSibling;
if (favEl) favEl.className = 'icon-star-empty';
if (favEl) {
favEl.className = 'icon-star-empty';
$(favEl).parent().removeClass('btn-warning');
}
}
});

View File

@@ -78,7 +78,10 @@
if(data.topics && data.topics.length) {
onTopicsLoaded(data.topics);
$('#topics-container').attr('data-next-start', data.nextStart);
} else {
$('#load-more-btn').hide();
}
loadingMoreTopics = false;
});
}
@@ -92,7 +95,7 @@
});
if($("body").height() <= $(window).height() && $('#topics-container').children().length)
if($("body").height() <= $(window).height() && $('#topics-container').children().length >= 20)
$('#load-more-btn').show();
$('#load-more-btn').on('click', function() {

View File

@@ -181,7 +181,7 @@
if (!templates[tpl_url] || !template_data) return;
if(typeof global !== "undefined")
template_data['relative_path'] = global.nconf.get('relative_path');
template_data['relative_path'] = nconf.get('relative_path');
else
template_data['relative_path'] = RELATIVE_PATH;

View File

@@ -10,13 +10,8 @@
<div id="category_active_users"></div>
</ul>
</div>
<div class="alert alert-warning hide {no_topics_message}" id="category-no-topics">
<strong>There are no topics in this category.</strong><br />
Why don't you try posting one?
</div>
<div>
<button id="new_post" class="btn btn-primary btn-large {show_topic_button}">New Topic</button>
<div class="inline-block pull-right">
@@ -27,7 +22,12 @@
</div>
</div>
<hr class="{show_sidebar}" />
<hr/>
<div class="alert alert-warning hide {no_topics_message}" id="category-no-topics">
<strong>There are no topics in this category.</strong><br />
Why don't you try posting one?
</div>
<div class="category row">
<div class="{topic_row_size}">

View File

@@ -31,7 +31,10 @@
"unread": "unread",
"popular": "category",
"active": "category",
"search": "search"
"search": "search",
"reset/[^]*": "reset_code",
"reset": "reset"
},
"force_refresh": {
"logout": true

View File

@@ -42,7 +42,7 @@
<button id="ids_{main_posts.pid}_{main_posts.uid}" class="btn delete {main_posts.display_moderator_tools}" type="button" title="Delete"><i class="icon-trash"></i></button>
<div class="btn-group">
<button class="btn follow" type="button" title="Be notified of new replies in this topic"><i class="icon-eye-open"></i></button>
<button id="favs_{main_posts.pid}_{main_posts.uid}" class="favourite btn" type="button">
<button id="favs_{main_posts.pid}_{main_posts.uid}" class="favourite btn {main_posts.fav_button_class}" type="button">
<span>Favourite</span>
<span class="post_rep_{main_posts.pid}">{main_posts.post_rep} </span><i class="{main_posts.fav_star_class}"></i>
</button>
@@ -92,7 +92,7 @@
<span class="label label-important {posts.show_banned}">banned</span>
</div>
<div class="span11 span12-tablet">
<div class="post-block">
<div class="post-block speech-bubble">
<div id="content_{posts.pid}" class="post-content">{posts.content}</div>
<div id="images_{posts.pid}" class="post-images">
<!-- BEGIN uploadedImages -->

View File

@@ -39,9 +39,9 @@ var RDB = require('./redis.js'),
'category_id': category_id,
'active_users': [],
'topics' : [],
'twitter-intent-url': 'https://twitter.com/intent/tweet?url=' + encodeURIComponent(global.nconf.get('url') + 'category/' + category_slug) + '&text=' + encodeURIComponent(category_name),
'facebook-share-url': 'https://www.facebook.com/sharer/sharer.php?u=' + encodeURIComponent(global.nconf.get('url') + 'category/' + category_slug),
'google-share-url': 'https://plus.google.com/share?url=' + encodeURIComponent(global.nconf.get('url') + 'category/' + category_slug)
'twitter-intent-url': 'https://twitter.com/intent/tweet?url=' + encodeURIComponent(nconf.get('url') + 'category/' + category_slug) + '&text=' + encodeURIComponent(category_name),
'facebook-share-url': 'https://www.facebook.com/sharer/sharer.php?u=' + encodeURIComponent(nconf.get('url') + 'category/' + category_slug),
'google-share-url': 'https://plus.google.com/share?url=' + encodeURIComponent(nconf.get('url') + 'category/' + category_slug)
};
function getTopics(next) {

View File

@@ -48,9 +48,9 @@ var utils = require('./../public/src/utils.js'),
if (exists) {
fs.readFile(themeConfPath, function(err, conf) {
conf = JSON.parse(conf);
conf.src = global.nconf.get('url') + 'themes/' + themeDir + '/' + conf.src;
if (conf.screenshot) conf.screenshot = global.nconf.get('url') + 'themes/' + themeDir + '/' + conf.screenshot;
else conf.screenshot = global.nconf.get('url') + 'images/themes/default.png';
conf.src = nconf.get('url') + 'themes/' + themeDir + '/' + conf.src;
if (conf.screenshot) conf.screenshot = nconf.get('url') + 'themes/' + themeDir + '/' + conf.screenshot;
else conf.screenshot = nconf.get('url') + 'images/themes/default.png';
themeArr.push(conf);
next();
});

View File

@@ -169,7 +169,7 @@ var RDB = require('./redis.js'),
if (md && md.length > 0) {
var parsedContentDOM = cheerio.load(marked(md));
var domain = global.nconf.get('url');
var domain = nconf.get('url');
parsedContentDOM('a').each(function() {
this.attr('rel', 'nofollow');

View File

@@ -39,7 +39,7 @@ var RDB = require('./redis.js'),
post.user_rep = userData.reputation || 0;
post.user_postcount = userData.postcount || 0;
post.user_banned = userData.banned || '0';
post.picture = userData.picture || require('gravatar').url('', {}, https=global.nconf.get('https'));
post.picture = userData.picture || require('gravatar').url('', {}, https=nconf.get('https'));
post.signature = postTools.markdownToHTML(userData.signature, true);
if(post.editor !== '') {
@@ -213,7 +213,6 @@ var RDB = require('./redis.js'),
Posts.create(uid, tid, content, images, function(postData) {
if (postData) {
topics.addPostToTopic(tid, postData.pid);
topics.markUnRead(tid);
@@ -227,7 +226,8 @@ var RDB = require('./redis.js'),
postData.content = postTools.markdownToHTML(postData.content);
postData.post_rep = 0;
postData.relativeTime = utils.relativeTime(postData.timestamp)
postData.relativeTime = utils.relativeTime(postData.timestamp);
postData.fav_button_class = '';
postData.fav_star_class = 'icon-star-empty';
postData['edited-class'] = 'none';
postData.show_banned = 'hide';
@@ -280,6 +280,7 @@ var RDB = require('./redis.js'),
RDB.hmset('post:' + pid, postData);
topics.addPostToTopic(tid, pid);
topics.increasePostCount(tid);
topics.updateTimestamp(tid, timestamp);

View File

@@ -18,7 +18,7 @@ var user = require('./../user.js'),
Admin.build_header = function(res) {
return templates['admin/header'].parse({
csrf:res.locals.csrf_token,
relative_path: global.nconf.get('relative_path')
relative_path: nconf.get('relative_path')
});
}

View File

@@ -161,11 +161,11 @@ var user = require('./../user.js'),
if (url) {
res.json({
url: url,
home: global.nconf.get('url')
home: nconf.get('url')
});
} else {
res.status(404);
res.redirect(global.nconf.get('relative_path') + '/404');
res.redirect(nconf.get('relative_path') + '/404');
}
});
@@ -224,6 +224,14 @@ var user = require('./../user.js'),
});
});
app.get('/api/reset', function(req, res) {
res.json({});
});
app.get('/api/reset/:code', function(req, res) {
res.json({ reset_code: req.params.code });
});
app.get('/api/404', function(req, res) {
res.json({});
});

View File

@@ -127,13 +127,13 @@
app.get('/reset/:code', function(req, res) {
app.build_header({ req: req, res: res }, function(err, header) {
res.send(header + templates['reset_code'].parse({ reset_code: req.params.code }) + templates['footer']);
res.send(header + app.create_route('reset/'+req.params.code) + templates['footer']);
});
});
app.get('/reset', function(req, res) {
app.build_header({ req: req, res: res }, function(err, header) {
res.send(header + templates['reset'] + templates['footer']);
res.send(header + app.create_route('reset') + templates['footer']);
});
});
@@ -160,10 +160,10 @@
req.login({
uid: uid
}, function() {
res.redirect(global.nconf.get('relative_path') + '/');
res.redirect(nconf.get('relative_path') + '/');
});
} else {
res.redirect(global.nconf.get('relative_path') + '/register');
res.redirect(nconf.get('relative_path') + '/register');
}
});
});

View File

@@ -56,7 +56,7 @@ var path = require('path'),
async.parallel([sitemap.getStaticUrls, sitemap.getDynamicUrls], function(err, urls) {
var urls = urls[0].concat(urls[1]),
map = sm.createSitemap({
hostname: global.nconf.get('url'),
hostname: nconf.get('url'),
cacheTime: 600000,
urls: urls
}),

View File

@@ -79,6 +79,7 @@ marked.setOptions({
privileges = results[2];
for(var i=0; i<postData.length; ++i) {
postData[i].fav_button_class = fav_data[postData[i].pid]? 'btn-warning' : '';
postData[i].fav_star_class = fav_data[postData[i].pid] ? 'icon-star' : 'icon-star-empty';
postData[i]['display_moderator_tools'] = (postData[i].uid == current_user || privileges.editable) ? 'show' : 'none';
postData[i].show_banned = postData[i].user_banned === '1'?'show':'hide';
@@ -612,7 +613,6 @@ marked.setOptions({
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);

View File

@@ -376,7 +376,7 @@ var utils = require('./../public/src/utils.js'),
options.forcedefault = 'y';
}
return require('gravatar').url(email, options, https=global.nconf.get('https'));
return require('gravatar').url(email, options, https=nconf.get('https'));
}
User.hashPassword = function(password, callback) {
@@ -589,7 +589,7 @@ var utils = require('./../public/src/utils.js'),
topics.getTopicField(tid, 'slug', function(err, slug) {
var message = username + ' made a new post';
notifications.create(message, 5, global.nconf.get('url') + 'topic/' + slug + '#' + pid, 'notification_'+ Date.now(), function(nid) {
notifications.create(message, 5, nconf.get('url') + 'topic/' + slug + '#' + pid, 'notification_'+ Date.now(), function(nid) {
notifications.push(nid, followers);
});
});
@@ -792,6 +792,7 @@ var utils = require('./../public/src/utils.js'),
User.reset = {
validate: function(socket, code, callback) {
if (typeof callback !== 'function') {
callback = null;
}
@@ -841,13 +842,13 @@ var utils = require('./../public/src/utils.js'),
RDB.set('reset:' + reset_code + ':uid', uid);
RDB.set('reset:' + reset_code + ':expiry', (60*60)+new Date()/1000|0); // Active for one hour
var reset_link = config.url + 'reset/' + reset_code,
var reset_link = nconf.get('url') + 'reset/' + reset_code,
reset_email = global.templates['emails/reset'].parse({'RESET_LINK': reset_link}),
reset_email_plaintext = global.templates['emails/reset_plaintext'].parse({ 'RESET_LINK': reset_link });
var message = emailjs.message.create({
text: reset_email_plaintext,
from: config.mailer.from,
from: config.mailer?config.mailer.from:'localhost@example.org',
to: email,
subject: 'Password Reset Requested',
attachment: [
@@ -884,14 +885,17 @@ var utils = require('./../public/src/utils.js'),
});
},
commit: function(socket, code, password) {
this.validate(code, function(validated) {
this.validate(socket, code, function(validated) {
if (validated) {
RDB.get('reset:' + code + ':uid', function(err, uid) {
if (err) {
RDB.handle(err);
}
User.setUserField(uid, 'password', password);
User.hashPassword(password, function(hash) {
User.setUserField(uid, 'password', hash);
});
RDB.del('reset:' + code + ':uid');
RDB.del('reset:' + code + ':expiry');

View File

@@ -5,7 +5,7 @@ var express = require('express'),
RedisStore = require('connect-redis')(express),
path = require('path'),
redis = require('redis'),
redisServer = redis.createClient(global.nconf.get('redis:port'), global.nconf.get('redis:host')),
redisServer = redis.createClient(nconf.get('redis:port'), nconf.get('redis:host')),
marked = require('marked'),
utils = require('../public/src/utils.js'),
pkg = require('../package.json'),
@@ -40,11 +40,11 @@ var express = require('express'),
],
metaString = utils.buildMetaTags(defaultMetaTags.concat(options.metaTags || [])),
templateValues = {
cssSrc: global.config['theme:src'] || global.nconf.get('relative_path') + '/vendor/bootstrap/css/bootstrap.min.css',
cssSrc: global.config['theme:src'] || nconf.get('relative_path') + '/vendor/bootstrap/css/bootstrap.min.css',
title: global.config['title'] || 'NodeBB',
browserTitle: global.config['title'] || 'NodeBB',
csrf: options.res.locals.csrf_token,
relative_path: global.nconf.get('relative_path'),
relative_path: nconf.get('relative_path'),
meta_tags: metaString
};
@@ -60,7 +60,7 @@ var express = require('express'),
// Middlewares
app.use(express.favicon(path.join(__dirname, '../', 'public', 'favicon.ico')));
app.use(require('less-middleware')({ src: path.join(__dirname, '../', 'public') }));
app.use(global.nconf.get('relative_path'), express.static(path.join(__dirname, '../', 'public')));
app.use(nconf.get('relative_path'), express.static(path.join(__dirname, '../', 'public')));
app.use(express.bodyParser()); // Puts POST vars in request.body
app.use(express.cookieParser()); // If you want to parse cookies (res.cookies)
app.use(express.compress());
@@ -69,7 +69,7 @@ var express = require('express'),
client: redisServer,
ttl: 60*60*24*14
}),
secret: global.nconf.get('secret'),
secret: nconf.get('secret'),
key: 'express.sid'
}));
app.use(express.csrf());
@@ -86,7 +86,7 @@ var express = require('express'),
app.use(function(req, res, next) {
global.nconf.set('https', req.secure);
nconf.set('https', req.secure);
// Don't bother with session handling for API requests
if (/^\/api\//.test(req.url)) return next();
@@ -109,7 +109,7 @@ var express = require('express'),
// respond with html page
if (req.accepts('html')) {
//res.json('404', { url: req.url });
res.redirect(global.nconf.get('relative_path') + '/404');
res.redirect(nconf.get('relative_path') + '/404');
return;
}
@@ -141,7 +141,7 @@ var express = require('express'),
};
app.namespace(global.nconf.get('relative_path'), function() {
app.namespace(nconf.get('relative_path'), function() {
auth.create_routes(app);
admin.create_routes(app);
@@ -243,7 +243,7 @@ var express = require('express'),
{ name: "title", content: topicData.topic_name },
{ property: 'og:title', content: topicData.topic_name + ' | ' + (global.config.title || 'NodeBB') },
{ property: "og:type", content: 'article' },
{ property: "og:url", content: global.nconf.get('url') + 'topic/' + topicData.slug },
{ property: "og:url", content: nconf.get('url') + 'topic/' + topicData.slug },
{ property: 'og:image', content: topicData.main_posts[0].picture },
{ property: "article:published_time", content: new Date(parseInt(topicData.main_posts[0].timestamp, 10)).toISOString() },
{ property: 'article:modified_time', content: new Date(lastMod).toISOString() },
@@ -338,7 +338,7 @@ var express = require('express'),
res.send( "User-agent: *\n" +
"Disallow: \n" +
"Disallow: /admin/\n" +
"Sitemap: " + global.nconf.get('url') + "sitemap.xml");
"Sitemap: " + nconf.get('url') + "sitemap.xml");
});
app.get('/cid/:cid', function(req, res) {

View File

@@ -16,12 +16,12 @@ var SocketIO = require('socket.io').listen(global.server, { log:false }),
async = require('async'),
RedisStoreLib = require('connect-redis')(express),
redis = require('redis'),
redisServer = redis.createClient(global.nconf.get('redis:port'), global.nconf.get('redis:host')),
redisServer = redis.createClient(nconf.get('redis:port'), nconf.get('redis:host')),
RedisStore = new RedisStoreLib({
client: redisServer,
ttl: 60*60*24*14
}),
socketCookieParser = express.cookieParser(global.nconf.get('secret')),
socketCookieParser = express.cookieParser(nconf.get('secret')),
admin = {
'categories': require('./admin/categories.js'),
'user': require('./admin/user.js')
@@ -187,7 +187,7 @@ var SocketIO = require('socket.io').listen(global.server, { log:false }),
uid:0,
username: "Anonymous User",
email: '',
picture: require('gravatar').url('', {s:'24'}, https=global.nconf.get('https'))
picture: require('gravatar').url('', {s:'24'}, https=nconf.get('https'))
});
}