From 50c34e4f336a826ca3b78047e221ad29143d0e93 Mon Sep 17 00:00:00 2001
From: Baris Soner Usakli <barisusakli@gmail.com>
Date: Sat, 10 Aug 2013 16:14:50 -0400
Subject: [PATCH] added infinite scrolling to unread page, issue #141

---
 public/src/forum/recent.js  |  14 -----
 public/src/forum/unread.js  | 103 ++++++++++++++++++++++++++++++++++++
 public/templates/unread.tpl |   5 +-
 src/posts.js                |  49 ++++++-----------
 src/routes/api.js           |   2 +-
 src/topics.js               |  63 ++++++++--------------
 src/webserver.js            |   1 +
 src/websockets.js           |  88 +++++++++++++++++++++++++++++-
 8 files changed, 231 insertions(+), 94 deletions(-)
 create mode 100644 public/src/forum/unread.js

diff --git a/public/src/forum/recent.js b/public/src/forum/recent.js
index 84d22ea8b8..44a078a278 100644
--- a/public/src/forum/recent.js
+++ b/public/src/forum/recent.js
@@ -47,20 +47,6 @@
 		++newPostCount;
 		updateAlertText();
 	});
-	
-	$('#mark-allread-btn').on('click', function() {
-		var btn = $(this);
-		socket.emit('api:topics.markAllRead', {} , function(success) {
-			if(success) {
-				btn.remove();
-				$('#topics-container').empty();
-				$('#category-no-topics').removeClass('hidden');			
-				app.alertSuccess('All topics marked as read!');
-			} else {
-				app.alertError('There was an error marking topics read!');
-			}
-		});
-	});
 
 	function onTopicsLoaded(topics) {
 
diff --git a/public/src/forum/unread.js b/public/src/forum/unread.js
new file mode 100644
index 0000000000..8b2342df36
--- /dev/null
+++ b/public/src/forum/unread.js
@@ -0,0 +1,103 @@
+(function() {
+	var loadingMoreTopics = false;
+
+	app.enter_room('recent_posts');
+
+	ajaxify.register_events([
+		'event:new_topic',
+		'event:new_post'
+	]);
+
+	var newTopicCount = 0, newPostCount = 0;
+
+	$('#new-topics-alert').on('click', function() {
+		$(this).hide();
+	});
+
+	socket.on('event:new_topic', function(data) {
+		
+		++newTopicCount;
+		updateAlertText();
+	
+	});
+	
+	function updateAlertText() {
+		var text = '';
+		
+		if(newTopicCount > 1)
+			text = 'There are ' + newTopicCount + ' new topics';
+		else if(newTopicCount === 1)
+			text = 'There is 1 new topic';
+		else
+			text = 'There are no new topics';
+			
+		if(newPostCount > 1)
+			text += ' and ' + newPostCount + ' new posts.';
+		else if(newPostCount === 1)
+			text += ' and 1 new post.';
+		else
+			text += ' and no new posts.';
+
+		text += ' Click here to reload.';
+
+		$('#new-topics-alert').html(text).fadeIn('slow');
+	}
+	
+	socket.on('event:new_post', function(data) {
+		++newPostCount;
+		updateAlertText();
+	});
+	
+	$('#mark-allread-btn').on('click', function() {
+		var btn = $(this);
+		socket.emit('api:topics.markAllRead', {} , function(success) {
+			if(success) {
+				btn.remove();
+				$('#topics-container').empty();
+				$('#category-no-topics').removeClass('hidden');			
+				app.alertSuccess('All topics marked as read!');
+			} else {
+				app.alertError('There was an error marking topics read!');
+			}
+		});
+	});
+
+	function onTopicsLoaded(topics) {
+
+		var html = templates.prepare(templates['unread'].blocks['topics']).parse({ topics: topics }),
+			container = $('#topics-container');
+
+		$('#category-no-topics').remove();
+
+		container.append(html);
+	}
+
+	function loadMoreTopics() {
+		loadingMoreTopics = true;
+		socket.emit('api:topics.loadMoreUnreadTopics', {after:parseInt($('#topics-container').attr('data-next-start'), 10)}, function(data) {
+			if(data.topics && data.topics.length) {
+				onTopicsLoaded(data.topics);
+				$('#topics-container').attr('data-next-start', data.nextStart);
+			}
+			loadingMoreTopics = false;
+		});
+	}
+
+	$(window).off('scroll').on('scroll', function() {
+		var windowHeight = document.body.offsetHeight - $(window).height(),
+			half = windowHeight / 2;
+
+		if (document.body.scrollTop > half && !loadingMoreTopics) {
+			loadMoreTopics();
+		}
+	});
+
+
+	if($("body").height() <= $(window).height() && $('#topics-container').children().length)
+		$('#load-more-btn').show();
+
+	$('#load-more-btn').on('click', function() {
+		loadMoreTopics();
+	});
+
+})();
\ No newline at end of file
diff --git a/public/templates/unread.tpl b/public/templates/unread.tpl
index a44d30aa1b..7e4481aeac 100644
--- a/public/templates/unread.tpl
+++ b/public/templates/unread.tpl
@@ -21,7 +21,7 @@
 
 <div class="category row">
 	<div class="{topic_row_size}">
-		<ul id="topics-container">
+		<ul id="topics-container" data-next-start="{nextStart}">
 		<!-- BEGIN topics -->
 		<a href="../../topic/{topics.slug}" id="tid-{topics.tid}">
 			<li class="category-item {topics.deleted-class}">
@@ -48,7 +48,8 @@
 		</a>
 		<!-- END topics -->
 		</ul>
+		<button id="load-more-btn" class="btn hide">Load More</button>
 	</div>
 </div>
 
-<script type="text/javascript" src="{relative_path}/src/forum/recent.js"></script>
\ No newline at end of file
+<script type="text/javascript" src="{relative_path}/src/forum/unread.js"></script>
\ No newline at end of file
diff --git a/src/posts.js b/src/posts.js
index 0a2bcd1cab..9a0dda28e4 100644
--- a/src/posts.js
+++ b/src/posts.js
@@ -178,34 +178,30 @@ var	RDB = require('./redis.js'),
 			alert_id: 'post_error'
 		});
 	}
+	
+	Posts.emitTooManyPostsAlert = function(socket) {
+		socket.emit('event:alert', {
+			title: 'Too many posts!',
+			message: 'You can only post every '+ (config.post_delay / 1000) + ' seconds.',
+			type: 'error',
+			timeout: 2000
+		});
+	}
 
-	Posts.reply = function(socket, tid, uid, content, images) {
+	Posts.reply = function(tid, uid, content, images, callback) {
 		if(content) {
 			content = content.trim();
 		}
 		
-		if (uid < 1) {
-			socket.emit('event:alert', {
-				title: 'Reply Unsuccessful',
-				message: 'You don&apos;t seem to be logged in, so you cannot reply.',
-				type: 'error',
-				timeout: 2000
-			});
-			return;
-		} else if (!content || content.length < Posts.minimumPostLength) {
-			Posts.emitContentTooShortAlert(socket);
+		if (!content || content.length < Posts.minimumPostLength) {
+			callback(new Error('content-too-short'), null);
 			return;
 		}
 
 		user.getUserField(uid, 'lastposttime', function(lastposttime) {
 
 			if(Date.now() - lastposttime < config.post_delay) {
-				socket.emit('event:alert', {
-					title: 'Too many posts!',
-					message: 'You can only post every '+ (config.post_delay / 1000) + ' seconds.',
-					type: 'error',
-					timeout: 2000
-				});
+				callback(new Error('too-many-posts'), null);
 				return;
 			}
 
@@ -221,18 +217,8 @@ var	RDB = require('./redis.js'),
 						});
 					});
 
-					Posts.getTopicPostStats(socket);
-
-					// 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.',
-						type: 'notify',
-						timeout: 2000
-					});
-
 					postData.content = postTools.markdownToHTML(postData.content);
 					postData.post_rep = 0;
 					postData.relativeTime = utils.relativeTime(postData.timestamp)
@@ -251,14 +237,9 @@ var	RDB = require('./redis.js'),
 						io.sockets.in('recent_posts').emit('event:new_post', socketData);
 					});					
 			
-				
+					callback(null, 'Reply successful');
 				} else {
-					socket.emit('event:alert', {
-						title: 'Reply Unsuccessful',
-						message: 'Your reply could not be posted at this time. Please try again later.',
-						type: 'notify',
-						timeout: 2000
-					});
+					callback(new Error('reply-error'), null);
 				}
 			});
 		});
diff --git a/src/routes/api.js b/src/routes/api.js
index a47a7a35ca..0414b1570f 100644
--- a/src/routes/api.js
+++ b/src/routes/api.js
@@ -115,7 +115,7 @@ var user = require('./../user.js'),
 
 		app.get('/api/unread', function(req, res) {
 			var uid = (req.user) ? req.user.uid : 0;
-			topics.getUnreadTopics(uid, 0, -1, function(data) {
+			topics.getUnreadTopics(uid, 0, 19, function(data) {
 				res.json(data);
 			});
 		});
diff --git a/src/topics.js b/src/topics.js
index 3d3ba5bf86..6730ee00ac 100644
--- a/src/topics.js
+++ b/src/topics.js
@@ -148,6 +148,7 @@ marked.setOptions({
 			function sendUnreadTopics(topicIds) {
 				Topics.getTopicsByTids(topicIds, uid, function(topicData) {
 					unreadTopics.topics = topicData;
+					unreadTopics.nextStart = start + tids.length;
 					callback(unreadTopics);
 				});
 			}
@@ -532,7 +533,7 @@ marked.setOptions({
 			});
 	}
 
-	Topics.post = function(socket, uid, title, content, category_id, images) {
+	Topics.post = function(uid, title, content, category_id, images, callback) {
 		if (!category_id) 
 			throw new Error('Attempted to post without a category_id');
 		
@@ -542,33 +543,20 @@ marked.setOptions({
 			title = title.trim();
 		
 		if (uid === 0) {
-			socket.emit('event:alert', {
-				title: 'Thank you for posting',
-				message: 'Since you are unregistered, your post is awaiting approval. Click here to register now.',
-				type: 'warning',
-				timeout: 7500,
-				clickfn: function() {
-					ajaxify.go('register');
-				}
-			});
-			return; // for now, until anon code is written.
+			callback(new Error('not-logged-in'), null);
+			return;
 		} else if(!title || title.length < Topics.minimumTitleLength) {
-			Topics.emitTitleTooShortAlert(socket);
+			callback(new Error('title-too-short'), null);
 			return;
 		} else if (!content || content.length < posts.miminumPostLength) {
-			posts.emitContentTooShortAlert(socket);
+			callback(new Error('content-too-short'), null);
 			return;
 		}
 		
 		user.getUserField(uid, 'lastposttime', function(lastposttime) {
 
 			if(Date.now() - lastposttime < config.post_delay) {
-				socket.emit('event:alert', {
-					title: 'Too many posts!',
-					message: 'You can only post every '+ (config.post_delay / 1000) + ' seconds.',
-					type: 'error',
-					timeout: 2000
-				});
+				callback(new Error('too-many-posts'), null);
 				return;
 			}
 
@@ -604,23 +592,6 @@ marked.setOptions({
 				topicSearch.index(title, tid);
 				RDB.set('topicslug:' + slug + ':tid', tid);
 
-				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);
-
-						// Notify any users looking at the category that a new topic has arrived
-						Topics.getTopicForCategoryView(tid, uid, function(topicData) {
-							io.sockets.in('category_' + category_id).emit('event:new_topic', topicData);
-							io.sockets.in('recent_posts').emit('event:new_topic', topicData);
-						});
-
-						posts.getTopicPostStats(socket);
-					}
-				});
-
 				user.addTopicIdToUser(uid, tid);
 
 				// let everyone know that there is an unread topic in this category
@@ -636,11 +607,21 @@ marked.setOptions({
 
 				feed.updateCategory(category_id);
 
-				socket.emit('event:alert', {
-					title: 'Thank you for posting',
-					message: 'You have successfully posted. Click here to view your post.',
-					type: 'notify',
-					timeout: 2000
+				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);
+
+						// Notify any users looking at the category that a new topic has arrived
+						Topics.getTopicForCategoryView(tid, uid, function(topicData) {
+							io.sockets.in('category_' + category_id).emit('event:new_topic', topicData);
+							io.sockets.in('recent_posts').emit('event:new_topic', topicData);
+						});
+
+						callback(null, postData);
+					}
 				});
 			});
 		});
diff --git a/src/webserver.js b/src/webserver.js
index aa2be7a669..379bec362f 100644
--- a/src/webserver.js
+++ b/src/webserver.js
@@ -400,6 +400,7 @@ var express = require('express'),
 				}
 			});
 		});
+		
 	});
 
 }(WebServer));
diff --git a/src/websockets.js b/src/websockets.js
index f7dc80c28b..57b6e1973f 100644
--- a/src/websockets.js
+++ b/src/websockets.js
@@ -304,7 +304,41 @@ 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, data.images);
+
+			topics.post(uid, data.title, data.content, data.category_id, data.images, function(err, result) {
+				if(err) {
+					if(err.message === 'not-logged-in') {
+						socket.emit('event:alert', {
+							title: 'Thank you for posting',
+							message: 'Since you are unregistered, your post is awaiting approval. Click here to register now.',
+							type: 'warning',
+							timeout: 7500,
+							clickfn: function() {
+								ajaxify.go('register');
+							}
+						});
+					} else if(err.message === 'title-too-short') {
+						topics.emitTitleTooShortAlert(socket);
+					} else if(err.message === 'content-too-short') {
+						posts.emitContentTooShortAlert(socket);
+					} else if (err.message === 'too-many-posts') {
+						posts.emitTooManyPostsAlert(socket);
+					}
+					return;
+				}
+				
+				if(result) {
+					posts.getTopicPostStats(socket);
+					
+					socket.emit('event:alert', {
+						title: 'Thank you for posting',
+						message: 'You have successfully posted. Click here to view your post.',
+						type: 'notify',
+						timeout: 2000
+					});
+				}				
+			});
+			
 		});
 		
 		socket.on('api:topics.markAllRead', function(data, callback) {
@@ -318,7 +352,47 @@ var SocketIO = require('socket.io').listen(global.server, { log:false }),
 		});
 
 		socket.on('api:posts.reply', function(data) {
-			posts.reply(socket, data.topic_id, uid, data.content, data.images);
+			if(uid < 1) {
+				socket.emit('event:alert', {
+					title: 'Reply Unsuccessful',
+					message: 'You don&apos;t seem to be logged in, so you cannot reply.',
+					type: 'error',
+					timeout: 2000
+				});
+				return;
+			}
+			
+			posts.reply(data.topic_id, uid, data.content, data.images, function(err, result) {
+				if(err) {
+					if(err.message === 'content-too-short') {
+						posts.emitContentTooShortAlert(socket);
+					} else if(err.messages === 'too-many-posts') {
+						posts.emitTooManyPostsAlert(socket);
+					} else if(err.message === 'reply-error') {
+						socket.emit('event:alert', {
+							title: 'Reply Unsuccessful',
+							message: 'Your reply could not be posted at this time. Please try again later.',
+							type: 'notify',
+							timeout: 2000
+						});
+					}
+					return;
+				}
+				
+				if(result) {
+					
+					posts.getTopicPostStats(socket);
+					
+					socket.emit('event:alert', {
+						title: 'Reply Successful',
+						message: 'You have successfully replied. Click here to view your reply.',
+						type: 'notify',
+						timeout: 2000
+					});
+					
+				}
+				
+			});
 		});
 
 		socket.on('api:user.active.get', function() {
@@ -578,6 +652,16 @@ var SocketIO = require('socket.io').listen(global.server, { log:false }),
 				callback(latestTopics);
 			});
 		});
+		
+		socket.on('api:topics.loadMoreUnreadTopics', function(data, callback) {
+			var start = data.after,
+				end = start + 9;
+			
+			console.log(start, end);
+			topics.getUnreadTopics(uid, start, end, function(unreadTopics) {
+				callback(unreadTopics);
+			});
+		});
 
 		socket.on('api:admin.topics.getMore', function(data) {
 			topics.getAllTopics(data.limit, data.after, function(topics) {