Unverified Commit 297e8dcb authored by Aral Balkan's avatar Aral Balkan
Browse files

Implemented infinite scroll for private conversations.

parent b79f215e
......@@ -78,6 +78,7 @@ class PrivateWebServer
app.get '/timeline/public', require('./private/routes/public.coffee')(app)
app.get '/timeline/conversation-with/:person', require('./private/routes/conversation.coffee')(app)
app.get '/timeline/posts-after/:timeline/:key', require('./private/routes/posts-after.coffee')(app)
app.get '/timeline/posts-before/:timeline/:key', require('./private/routes/posts-before.coffee')(app)
#
# Catchall error handler.
......
......@@ -174,11 +174,11 @@ class StreamWeaver
# Ajax methods for delta updates.
#
getPostsAfter: (timeline, key, limit = 20) =>
getPostsAfter: (timeline, key, limit = 10) =>
# console.log "Getting posts after key: #{key}…"
@getTimeline timeline, key, '\uffff', limit
getPostsBefore: (timeline, key, limit = 20) =>
getPostsBefore: (timeline, key, limit = 10) =>
@getTimeline timeline, '\x00', key, limit
......@@ -209,7 +209,7 @@ class StreamWeaver
#
# Also known as document/blog order. All other
# timelines are displayed in conversation order (latest post last).
options.reverse = (timeline == @timeline.public || timeline == @timeline.private || timeline == @timeline.allFriends)
options.reverse = true #(timeline == @timeline.public || timeline == @timeline.private || timeline == @timeline.allFriends)
# console.log "TIMELINE REVERSED? #{options.reverse}"
# console.log "@timelines[timeline] = #{@timelines[timeline]}"
......
......@@ -128,7 +128,9 @@ module.exports = (app) ->
# Massage the messages.
#
streamWeaver = app.get('config').streamWeaver
streamWeaver.getTimeline(conversationTimelineForPerson).then (messages) ->
streamWeaver.getTimeline(conversationTimelineForPerson, '\x00', '\uffff', 10).then (messages) ->
messages.reverse()
# Massage the asset URLs
for message in messages
......
#
# Ajax updates for all private timelines.
# Gets posts after a given lexographical key.
# TODO: Refactor w. Waystone to remove redundancy.
#
......
#
# Gets posts before a given lexographical key.
# TODO: Refactor w. Waystone to remove redundancy.
#
streamWeaver = new (require '../../StreamWeaver')
module.exports = (app) ->
route = (request, response) ->
# Read the details
key = request.param 'key'
timeline = request.param 'timeline'
console.log "Looking up posts prior to key #{key} on timeline #{timeline}."
streamWeaver.getPostsBefore(timeline, key).then (messages) ->
console.log 'Get posts before: found new posts: '
console.log messages
# Reverse the message order to match that of the Cocoa client.
# messages.reverse()
response.end JSON.stringify messages
......@@ -11,11 +11,30 @@
<script type='text/javascript' src='/js/zenscroll.min.js'></script>
<script type='text/javascript' src='/js/conversation.js'></script>
<script type='text/javascript' data-set-text='meta'></script>
<style type="text/css">
.displayNone
{
display:none;
}
.progress-spinner
{
display:inline;
width:16px;
vertical-align:top;
padding-top: 2px;
padding-right:5px;
}
</style>
</head>
<body>
<section id='public-timeline'>
<h1 id='person' data-set-text='personsName' data-set-attribute='data-name personsAccountHandle'></h1>
<div id='loadProgress' class='displayNone'>
<p style='text-align:center;'><img class='progress-spinner' src='/images/progress-spinner@2x.gif'></p>
</div>
<!-- Notice to display if there are no messages yet. -->
<div data-set-if='not:messages' id='notice'>
<p><strong>No messages yet.</strong></p>
......
......@@ -12,6 +12,212 @@ function insertAfter(parentNode, referenceNode, newNode) {
parentNode.insertBefore(newNode, nodeToInsertBefore);
}
var topmostElementBeforeLoadingOlderMessages = null
function getOlderMessages(){
console.log("Polling server for older messages…")
topmostElementBeforeLoadingOlderMessages = document.getElementById('messages').firstElementChild
var loadProgress = document.getElementById('loadProgress');
var displayNoneClass = "displayNone";
//
// Get the ID of the last message loaded so we can use this
// to poll for new messages that have been received since the
// timeline initially loaded. Compensate for an empty timeline
// with no messages.
//
var messages = document.getElementById('messages');
var firstMessage = messages.firstElementChild;
var firstMessageScrollOffset = firstMessage.scrollOffset;
var newestPostID = 0;
if (firstMessage != null) {
newestPostID = firstMessage.getAttribute('id');
}
var person = document.getElementById('person').getAttribute('data-name')
console.log(person);
superagent
.get('http://127.0.0.1:42003/timeline/posts-before/'+person+'/'+newestPostID)
.end(function(error, posts) {
// Hide the progress indicator
loadProgress.classList.add(displayNoneClass);
//
// Optimisation: clone the repeater seed node and only apply the set
// template on that so we don’t end up rendering the whole list of messages.
// (I should pave this cowpath: https://source.ind.ie/project/set/issues/4)
//
posts = JSON.parse(posts.text);
// console.log(posts);
if (posts.length == 0){
console.log('No older posts.');
return;
}
posts.reverse();
// Make sure that the notice is hidden and that the main section is showing.
// (Set can’t handle this automatically as we are not running it on the
// entire document but only on the delta repeat block below for performance reasons.)
document.getElementById('notice').style.display = 'none';
document.getElementById('messages').style.display = 'block';
var repeaterNodeInnerHTML =
" <div class='messageBody' data-set-attribute='id message.key messageBodyIDFormatter'>"
+ " <div class='image-and-body'>"
+ " <img class='profileImage' data-set-attribute='src message.key profileImagePathFormatter'>"
+ " <div class='bodyText' data-set-text='html message.value'>Message body HTML</div>"
+ " <div class='meta'><span class='postDate' data-set-attribute='data-timestamp message.key timestampFormatter' data-set-text='message.key postDateFormatter'></span></div>"
+ " </div>"
+ " </div>";
var messages = document.getElementById('messages');
// Create the repeater node.
var div = document.createElement('div');
div.setAttribute('data-set-repeat', 'message messages');
div.setAttribute('class', 'message');
div.setAttribute('data-set-attribute', 'id message.key');
div.innerHTML = repeaterNodeInnerHTML;
// Insert new items at the beginning.
messages.insertBefore(div, messages.firstElementChild);
// messages.innerHTML = repeaterNodeHTML + messages.innerHTML;
repeaterNode = messages.firstElementChild;
// Bug: Formatters are not being passed from the server correctly
// when injectData is true. As a workaround: I’m duplicating them on the
// client also.
set.format['messageBodyIDFormatter'] = function (messageID) {
return messageID + "-body";
}
set.format['messageStatusIDFormatter'] = function (messageID) {
return messageID + "-status";
}
set.format['postDateFormatter'] = function (messageID) {
//
// Parses message IDs in the following forms into separate groups for
// * timeline clock (deprecated)
// * message time (replace underscores with colons to convert to valid timestamp)
// * account handle (optional)
//
// 000000001-2015-08-10T18_49_10.467Z-laura
// 000000001-2015-08-10T18_49_10.467Z
// 2015-08-10T18_49_10.467Z-laura
// 2015-08-10T18_49_10.467Z
//
var messageIDParserRegExp = /^(\d{9})?-?(\d{4}-\d{2}-\d{2}T\d{2}_\d{2}_\d{2}\.\d{3}Z)-?(.*)?/;
var matches = messageID.match(messageIDParserRegExp);
if (matches != null){
var depracatedOptionalMessageClock = matches[1];
var timestamp = matches[2];
var optionalAccountHandle = matches[3];
// Desearialise the timestamp.
timestamp = timestamp.replace (/_/g, ':');
var now = new Date();
var timeOfPost = new Date(timestamp);
// to secs -> mins -> hours -> days
var timeSincePostInDays = (now - timeOfPost)/1000/60/60/24;
var humanTime = moment(timeOfPost).fromNow();
return humanTime + " ";
}
else
{
// This should never happen and probably shows that some sort of corrupted date got through somehow.
return 'No date.';
}
}
set.format['timestampFormatter'] = function (messageID) {
var messageIDParserRegExp = /^(\d{9})?-?(\d{4}-\d{2}-\d{2}T\d{2}_\d{2}_\d{2}\.\d{3}Z)-?(.*)?/
var matches = messageID.match(messageIDParserRegExp);
if (matches != null) {
var depracatedOptionalMessageClock = matches[1];
var timestamp = matches[2];
var optionalAccountHandle = matches[3];
// Desearialise the timestamp.
timestamp = timestamp.replace(/_/g, ':');
return timestamp;
} else {
// This should not happen. Return the current date.
return new Date()
}
}
// Format the person’s name
set.format['personFormatter'] = function (messageID) {
var personHandleDelimeter = messageID.lastIndexOf('Z-');
if (personHandleDelimeter != -1) {
// From someone else
personHandle = messageID.substr(personHandleDelimeter+2);
// TODO: Once public profile pages are implemented, link to them.
return " by "+personHandle+".";
}
else {
// This is the person themselves.
// TODO: Once the timestamps are in there, just return that.
return '';
}
}
// Custom formatter for the profile image
set.format['profileImagePathFormatter'] = function (messageID) {
console.log("Formatting profile image for " + messageID)
var personHandleDelimeter = messageID.lastIndexOf('Z-');
var profileImagePath = '';
if (personHandleDelimeter != -1) {
// From someone else
var personHandle = messageID.substr(personHandleDelimeter+2);
console.log("Person handle for message: "+personHandle+".");
profileImagePath = "http://127.0.0.1:42003/all-friends/from/"+personHandle+"/about/me.jpg";
}
else {
// From you
console.log("Message is from you.");
profileImagePath = "http://localhost:42000/about/me.jpg";
}
return profileImagePath;
}
// Update the repeater node
set(repeaterNode, {messages: posts});
});
}
window.addEventListener('load', function(){
// Set: don’t run in Node.
......@@ -21,6 +227,30 @@ window.addEventListener('load', function(){
return;
}
//
// Infinite scroll
//
var lastInfiniteScrollAttemptTime = null
var bodyHeight = document.body.clientHeight;
var bodyScrollHeight = document.body.scrollHeight;
var loadProgress = document.getElementById('loadProgress');
var displayNoneClass = "displayNone";
window.addEventListener('scroll', function(e){
var pageYOffset = window.pageYOffset;
var currentTime = Date.now();
// Dampen to one second to avoid multiple event fires.
if(pageYOffset < 0 && (lastInfiniteScrollAttemptTime == null || (currentTime - lastInfiniteScrollAttemptTime)/1000 >= 1.0 ))
{
lastInfiniteScrollAttemptTime = currentTime;
// loadProgress.classList.remove(displayNoneClass);
getOlderMessages();
}
})
// Check if there are no messages to begin with, remove the pre-rendered repeater.
// (It’s ID will be different if there are.)
var messageRepeater = document.getElementById('messageRepeater');
......@@ -35,16 +265,33 @@ window.addEventListener('load', function(){
// e.g., scroll to show a message when one arrives.
//
var mutationObserver = new MutationObserver(function(mutations){
mutations.forEach(function(mutation){
if(mutation.type == "childList") {
var addedNodes = mutation.addedNodes;
var forEach = Array.prototype.forEach;
forEach.call(addedNodes, function(node){
// Scroll the added node into view.
zenscroll.intoView(node, 750);
});
if (addedNodes.length == 1)
{
// Scroll the sent message into view.
console.log("Scrolling new message into view…");
var node = addedNodes[0];
// Only scroll to the node if it is the last message.
// (i.e., the latest one sent/received).
if (node.nextSibling == null || node.nextSibling.nodeType != 1 /* element type */)
{
zenscroll.intoView(node, 750);
}
else
{
// Loaded older posts.
// Attempt to keep the scroll position on the last element before we loaded the older posts
// so as to disorient the person as little as possible.
console.log("Attempting to compensate for older message loading (infinite scroll).");
topmostElementBeforeLoadingOlderMessages.scrollIntoView();
}
}
}
});
});
......@@ -83,7 +330,7 @@ window.addEventListener('load', function(){
newestPostID = lastMessage.getAttribute('id');
}
// console.log('Getting post after ' + newestPostID)
console.log('Getting post after ' + newestPostID)
var person = document.getElementById('person').getAttribute('data-name')
......@@ -111,22 +358,23 @@ window.addEventListener('load', function(){
document.getElementById('notice').style.display = 'none';
document.getElementById('messages').style.display = 'block';
var repeaterNodeHTML =
" <div style='background-color: rgb(200, 50, 50, 0.5);' class='message' data-set-repeat='message messages' data-set-attribute='id message.key' >"
+ " <div class='messageBody' data-set-attribute='id message.key messageBodyIDFormatter'>"
var repeaterNodeInnerHTML =
" <div class='messageBody' data-set-attribute='id message.key messageBodyIDFormatter'>"
+ " <div class='image-and-body'>"
+ " <img class='profileImage' data-set-attribute='src message.key profileImagePathFormatter'>"
+ " <div class='bodyText' data-set-text='html message.value'>Message body HTML</div>"
+ " <div class='meta'><span class='postDate' data-set-attribute='data-timestamp message.key timestampFormatter' data-set-text='message.key postDateFormatter'></span></div>"
+ " </div>"
+ " </div>"
+ " </div>";
+ " </div>";
var messages = document.getElementById('messages');
var div = document.createElement('div');
div.setAttribute('id', posts[0]['key']+'-0');
div.innerHTML = repeaterNodeHTML;
// Create the repeater node.
var div = document.createElement('div');
div.setAttribute('data-set-repeat', 'message messages');
div.setAttribute('class', 'message');
div.setAttribute('data-set-attribute', 'id message.key');
div.innerHTML = repeaterNodeInnerHTML;
// Insert new items at the end.
insertAfter(messages, messages.lastElementChild, div);
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment