Making progress on adding the personal page feature.

parent 3b72a663
......@@ -72,17 +72,17 @@ class PublicTimelineWeaver
# Store the messages by person also so we can quickly stream them on their own pages.
personMessagesSublevel = @db.sublevel "#{accountHandle}-messages"
personMessagesSublevel.put messageID, message
personMessagesSublevel.put timestamp, message
#
# Everyone timeline
#
getPostsAfter: (timeline='public', key, limit = 20) =>
getPostsAfter: (key, limit = 20, timeline='public') =>
# console.log "Getting posts after key: #{key}…"
@getTimeline timeline, key, '\uffff', limit
getPostsBefore: (timeline='public', key, limit = 20) =>
getPostsBefore: (key, limit = 20, timeline='public') =>
@getTimeline timeline, '\x00', key, limit
#
......@@ -113,7 +113,7 @@ class PublicTimelineWeaver
getTimeline: (timeline='public', from='\x00', to='\uffff', limit=20) =>
# console.log "Streamweaver::getTimeline: #{timeline}"
# console.log "getTimeline: from: >#{from.charCodeAt(0)}<, to: >#{to.charCodeAt(0)}<, limit: #{limit}."
console.log "getTimeline: timeline:>#{timeline}< from: >#{from.charCodeAt(0)}<, to: >#{to.charCodeAt(0)}<, limit: #{limit}."
return new Promise ((fulfill, reject) =>
# console.log "getTimeline: from: >#{from.charCodeAt(0)}<, to: >#{to.charCodeAt(0)}<, limit: #{limit}."
......
......@@ -348,7 +348,12 @@ app.get '/person/:accountHandle/about/me.jpg', (request, response) =>
#
# Ajax updates
#
# TODO: Refactor these into the same call.
app.get '/posts-after/:key/for/:timeline', require('./routes/public/posts-after.coffee')(app)
app.get '/posts-after/:key', require('./routes/public/posts-after.coffee')(app)
app.get '/posts-before/:key/for/:timeline', require('./routes/public/posts-before.coffee')(app)
app.get '/posts-before/:key', require('./routes/public/posts-before.coffee')(app)
#
......
......@@ -157,6 +157,10 @@ module.exports = (app) ->
mainFolderForPerson = (new TeleportationAPI).folderForHandle(accountHandle)
aboutPersonFolder = path.join mainFolderForPerson, 'public', 'from', 'about'
profileImageURL = "/person/#{accountHandle}/about/me.jpg"
indieFriendURL = "indie://friend/#{accountHandle}"
console.log "About person folder: #{aboutPersonFolder}"
profileDataPath = path.join(aboutPersonFolder, "me.json")
......@@ -167,5 +171,5 @@ module.exports = (app) ->
(new PublicTimelineWeaver).getTimeline("#{accountHandle}-messages").then (messages) ->
# Reverse the message order to match that of the Cocoa client.
messages.reverse()
response.render 'person/index', {profile: profile, messages: messages, __set: { formatters: {messageBodyIDFormatter: messageBodyIDFormatter, messageStatusIDFormatter: messageStatusIDFormatter, profileImagePathFormatter: profileImagePathFormatter, personFormatter:personFormatter, addFriendLinkFormatter: addFriendLinkFormatter, addFriendTextFormatter:addFriendTextFormatter, postDateFormatter:postDateFormatter, timestampFormatter: timestampFormatter, permanentLinkFormatter: permanentLinkFormatter}}}
# messages.reverse()
response.render 'person/index', {indieFriendURL: indieFriendURL, profileImageURL: profileImageURL, profile: profile, messages: messages, __set: { formatters: {messageBodyIDFormatter: messageBodyIDFormatter, messageStatusIDFormatter: messageStatusIDFormatter, profileImagePathFormatter: profileImagePathFormatter, personFormatter:personFormatter, addFriendLinkFormatter: addFriendLinkFormatter, addFriendTextFormatter:addFriendTextFormatter, postDateFormatter:postDateFormatter, timestampFormatter: timestampFormatter, permanentLinkFormatter: permanentLinkFormatter}}}
......@@ -12,9 +12,13 @@ module.exports = (app) ->
# Read the details
key = request.param 'key'
console.log "Looking up posts since key: #{key}…"
timeline = request.param 'timeline'
if timeline == undefined
timeline = 'public'
(new PublicTimelineWeaver).getPostsAfter(key).then (messages) ->
console.log "Looking up posts after key #{key}, limit=20, timeline=#{timeline}…"
(new PublicTimelineWeaver).getPostsAfter(key, 20, timeline).then (messages) ->
# console.log 'Found new posts: '
# console.log messages
......
......@@ -12,10 +12,14 @@ module.exports = (app) ->
# Read the details
key = request.param 'key'
console.log "Looking up posts prior to key #{key}…"
timeline = request.param 'timeline'
if timeline == undefined
timeline = 'public'
(new PublicTimelineWeaver).getPostsBefore(key).then (messages) ->
# console.log 'Get posts before: found new posts: '
# console.log messages
console.log "Looking up posts prior to key #{key}, limit=20, timeline=#{timeline}…"
(new PublicTimelineWeaver).getPostsBefore(key, 20, timeline).then (messages) ->
console.log "Get posts before (key=#{key}, limit=20, timeline=#{timeline}): found new posts: "
console.log messages
response.end JSON.stringify messages
/* Heartbeat public web server styles. */
*
{
box-sizing: border-box;
}
body
{
max-width: 600px;
margin-left: auto;
margin-right: auto;
font-family: 'Avenir Next', 'Avenir', 'Helvetica', sans-serif;
text-align: center;
color: #333;
}
h1
{
font-size: 64px;
line-height: 1;
margin-bottom: 0;
border-bottom: 0;
}
h2
{
font-weight: 400;
font-size: 36px;
}
#profile-image
{
max-width: 148px;
border: 2px solid #ccc;
border-radius: 50%;
display: block;
margin-left: auto;
margin-right: auto;
}
.messageBody
{
line-height: 1.5em;
}
.message
{
border-bottom: 1px solid #eee;
padding: 6px;
margin-bottom: 6px;
}
.messages
{
text-align: left;
color: rgb(77, 77, 77);
}
.messages img
{
/* Make photos break the flow */
display:block;
/* Limit the size of photos so that they don’t overwhelm the display. */
max-width:100%;
/* Center */
margin-left: auto;
margin-right: auto;
}
.timestamp
{
font-size:10px;
color: #ccc;
text-align: center;
}
.bodyText
{
margin-left: 0;
}
.meta
{
margin-left: 0;
text-align: right;
}
.image-and-body
{
width: 100%;
break: both;
overflow: hidden;
margin-bottom: 6px;
}
a
{
color: #191919;
}
button
{
width: 50%;
padding: 12px;
font-size: 24px;
background-color: #025E6F;
color: white;
border: 0;
margin-top: 12px;
border-radius: 10px;
}
button:hover
{
background-color: #1D8FA4;
}
button:active
{
background-color: #63BACA;
}
header
{
border-bottom: 2px solid #333;
padding-bottom: 12px;
margin-bottom: 36px;
}
.new-person-notification
{
width: 100%;
break: both;
overflow: hidden;
}
.new-person-notification p img
{
width: 72px;
border-radius: 36px;
float: left;
margin-right: 36px;
}
.new-person-notification p:nth-child(2)
{
padding-top: 26px;
}
function insertAfter(parentNode, referenceNode, newNode) {
//
// Make sure we handle the null reference node gracefully
// (i.e., the parent node being inserted into has no children)
//
var nodeToInsertBefore = referenceNode;
if (referenceNode != null)
{
nodeToInsertBefore = referenceNode.nextSibling
}
parentNode.insertBefore(newNode, nodeToInsertBefore);
}
function getOlderMessages(){
console.log("Polling server for older public posts (infinite scroll)…")
//
// 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 lastMessage = messages.lastElementChild;
// The latest message is at the bottom for conversation-style timelines.
var oldestPostID = 0;
if (lastMessage != null) {
oldestPostID = lastMessage.getAttribute('id');
console.log("Oldest post ID: " + oldestPostID);
}
else
{
console.log("No messages.");
return;
}
console.log('Getting posts before ' + oldestPostID);
superagent
// .get('https://waystone.ind.ie/posts-before/'+oldestPostID)
.get('http://192.168.59.103:3000/posts-before/'+oldestPostID+'/for/aral1619-messages')
// .get('//' + set.meta.waystoneURL +'/posts-before/'+oldestPostID)
.end(function(error, posts) {
var loadProgress = document.getElementById('loadProgress');
var displayNoneClass = "displayNone";
// Hide the progress indicator
loadProgress.classList.add(displayNoneClass);
posts = JSON.parse(posts.text);
// console.log(posts);
if (posts.length == 0){
console.log('No older posts.');
return;
}
console.log("Got " + posts.length + "older posts.");
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'><a data-set-attribute='href message.key permanentLinkFormatter'><span class='postDate' data-set-attribute='data-timestamp message.key timestampFormatter' data-set-text='message.key postDateFormatter'></span></a> <span data-set-text='message.key personFormatter'></span> <a data-set-attribute='href message.key addFriendLinkFormatter' data-set-text='html message.key addFriendTextFormatter'></a></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 end.
insertAfter(messages, messages.lastElementChild, div);
repeaterNode = messages.lastElementChild;
// 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['profileImagePathFormatter'] = function (messageID) {
var personHandle = messageID.substr(messageID.lastIndexOf('Z-')+2);
var profileImagePath = "/public/"+personHandle+"/about/me.jpg";
return profileImagePath;
}
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 timestamp = matches[2];
// Deserialise the timestamp.
timestamp = timestamp.replace(/_/g, ':');
return timestamp;
} else {
// This should never happen and probably shows that some sort of corrupted ID got through somehow.
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 '';
}
}
set.format['addFriendLinkFormatter'] = function (messageID) {
var personHandleDelimeter = messageID.lastIndexOf('Z-');
if (personHandleDelimeter != -1) {
// From someone else
var personHandle = messageID.substr(personHandleDelimeter+2);
var addFriendLink = "indie://friend/"+personHandle;
return addFriendLink;
}
// // TODO: Filter out the person themselves.
// else
// {
// // This is the person themselves, no need to display a friend link.
// return '';
// }
}
set.format['addFriendTextFormatter'] = function (messageID) {
var personHandleDelimeter = messageID.lastIndexOf('Z-');
var profileImagePath = '';
if (personHandleDelimeter != -1){
return "<img class='add-friend-icon' src='/images/person_add@2x.png' alt='Send friend request'>"
}
// // TODO: Filter out the person themselves.
// else
// {
// //This is the person themselves, no need to display a friend link.
// return ''
// }
}
// Permanent link formatter
set.format['permanentLinkFormatter'] = function (messageID) {
// Get the person handle
var personHandleDelimeter = messageID.lastIndexOf('Z-');
var personHandle = messageID.substr(personHandleDelimeter+2);
return '/person/' + personHandle + '/post/' + messageID
}
// Update the repeater node
set(repeaterNode, {messages: posts})
});
}
window.addEventListener('load', function(){
// Set: don’t run in Node.
if (typeof this.data !== 'undefined')
{
// console.log("In Node: Not running in-browser code.");
return;
}
//
// Infinite scroll
//
var lastInfiniteScrollAttemptTime = null
var loadProgress = document.getElementById('loadProgress');
var displayNoneClass = "displayNone";
window.addEventListener('scroll', function(e){
var bodyHeight = document.body.clientHeight;
var bodyScrollHeight = document.body.scrollHeight;
// console.log("bodyHeight: " + bodyHeight);
// console.log("bodyScrollHeight: " + bodyScrollHeight);
var pageYOffset = window.pageYOffset;
var bodyScrollTop = document.body.scrollTop;
// console.log("PageYOffset: " + pageYOffset);
// console.log("Body scroll top: " + bodyScrollTop);
var currentTime = Date.now();
var locationOfContentBottom = bodyScrollHeight - bodyHeight
// Dampen to one second to avoid multiple event fires.
if(pageYOffset >= locationOfContentBottom && (lastInfiniteScrollAttemptTime == null || (currentTime - lastInfiniteScrollAttemptTime)/1000 >= 1.0 ))
{
console.log("At bottom of page… about to get older messages!");
lastInfiniteScrollAttemptTime = currentTime;
loadProgress.classList.remove(displayNoneClass);
getOlderMessages();
}
})
setInterval(function(){
// console.log("Polling server for new public posts…")
var messages = document.getElementById('messages');
var newestPostID = messages.firstElementChild.getAttribute('id');
// console.log('Getting post after ' + newestPostID)
// TODO: Do not hard code this URL.
// TODO: Fix Set injectData so we don’t have to use this metadata injection workaround.
superagent
// .get('https://waystone.ind.ie/posts-after/'+newestPostID)
.get('http://192.168.59.103:3000/posts-after/'+newestPostID+'/for/aral1619-messages')
// .get('//' + set.meta.waystoneURL +'/posts-after/'+newestPostID)
.end(function(error, posts) {
//
// 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 new posts.');
return;
}
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><span data-set-text='message.key personFormatter'></span> <a data-set-attribute='href message.key addFriendLinkFormatter' data-set-text='html message.key addFriendTextFormatter'></a></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;
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['profileImagePathFormatter'] = function (messageID) {
var personHandle = messageID.substr(messageID.lastIndexOf('Z-')+2);
var profileImagePath = "/public/"+personHandle+"/about/me.jpg";
return profileImagePath;
}
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();