Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
637 lines (571 sloc) 20.7 KB
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!--
// Copyright 2009 Paul Buchheit
// Licensed under the Apache License, Version 2.0
// Source available at http://github.com/paulbuchheit/paulbuchheit.github.com/blob/master/xfeed.html
-->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<base target="_blank"/>
<title>Feed</title>
<link rel="icon" type="image/png" href="http://friendfeed.com/favicon.ico"/>
<style type="text/css">
html, body {
height: auto;
background: #000;
}
body, input, textarea, td, select, option {
color: black;
font-family: Arial, sans-serif;
font-size: 11pt;
}
a, a:visited {
color: #0000cc;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
td {
vertical-align: top;
}
#logodiv {
position: relative;
background: #fff;
margin: 0.5em auto 0.5em auto;
width:41em;
padding:0.6em 2em 0.6em 2em;
-moz-border-radius:10px;
-webkit-border-radius:10px;
}
#info {
position:absolute;
right:20px;
top:16px;
font-size:9pt;
width:25em;
border:0px solid red;
color:#888;
line-height:1.2em;
}
#main {
background: #888;
background: #fff;
margin: 0em auto 0px auto;
width:41em;
-moz-border-radius:10px;
-webkit-border-radius:10px;
padding:2em;
padding-top:0.5em;
}
.profile {
font-size: 14pt;
padding: 0.7em;
margin-top:1em;
margin-bottom:0.5em;
background: rgb(192,215,243);
-moz-border-radius:7px;
-webkit-border-radius:7px;
}
.profile td {
font-size: 15pt;
}
.profile a {
font-size: 11pt;
}
.profile .userpic {
margin-right: 10px;
width: 75px;
height: 75px;
}
.message {
position: relative;
padding:9px 5px 9px 5px;
background: white;
width: 40em;
border-top: 1px solid #e0e0e0;
}
.message .userpic {
border: 1px solid #eee;
margin-right: 10px;
margin-top:2px;
width: 50px;
height: 50px;
}
.message .smile img {
padding-left:1px;
padding-right:1px;
margin-right: 3px;
margin-bottom: -2px;
}
.message .nickname {
font-weight: bold;
}
.message .date {
color: #aaa;
font-size:9pt;
margin-left:0.5em;
font-style: italic;
white-space: nowrap;
}
.message .content {
width: 100%;
font-size:12pt;
}
.message .comlike a {
color: #aaa;
}
.message .comlike img {
margin-left:0.4em;
}
.message .comlike {
white-space: nowrap;
font-size:9pt;
font-style: italic;
white-space: nowrap;
}
.message .icon {
float:right;
padding-left:10px;
border-width: 0px;
}
.message .media {
margin-top: 8px;
white-space:nowrap;
overflow:hidden;
width:520px;
}
.message .media img {
border: 1px solid #ccc;
padding: 1px;
margin-right:4px;
}
.submsgs {
padding-left: 2em;
}
.submsgs .message {
margin-top:0em;
width: 38em;
}
.submsgs .message .userpic {
margin-left: -4px;
width: 25px;
height: 25px;
}
.submsgs .message .smile img {
padding-left:2px;
padding-right:5px;
margin-right: 10px;
}
#msgmenu {
position:absolute;
top: -1px;
left: 40em;
background: #fff;
padding:10px;
-moz-border-radius:5px;
-webkit-border-radius:5px;
z-index: 100;
line-height: 1.5em;
border: 1px solid #ccc;
}
#msgmenu a {
white-space: nowrap;
}
</style>
</head>
<body>
<div id="logodiv"><img src="http://friendfeed.com/static/images/logo.png"/>
<div id="info">
This is an experimental "message oriented" <a href="http://friendfeed.com/">FriendFeed</a> ui
written with browser-side JS using the
<a href="http://code.google.com/p/friendfeed-api/wiki/ApiDocumentation">JSON API</a>.
The <a href="http://github.com/paulbuchheit/paulbuchheit.github.com/blob/master/xfeed.html">source</a>
is available, so it's easy to make your own.
</div>
</div>
<div id="main">Loading...</div>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js"></script>
<script type="text/javascript" src="http://paulbuchheit.github.com/debug.js"></script>
<script type="text/javascript">
//<![CDATA[
// Using a function is faster than 4 regexs when there are few matches
var htmlCharMap = {'&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;'};
function htmlCharEsc(c) { return htmlCharMap[c]; } // don't create a closure, and don't keep recreating htmlCharMap
function htmlEscape(s) { return String(s).replace(/[&<>\"]/g, htmlCharEsc); }
if (!String.prototype.endsWith)
String.prototype.endsWith = function(s) { return this.indexOf(s, this.length - s.length) != -1; }
if (!String.prototype.startsWith)
String.prototype.startsWith = function(s) { return this.lastIndexOf(s, 0) != -1; }
function template(t) {
var r = /{\w+}/g;
var match, prev = 0, parts = [], keys = [];
while ((match = r.exec(t)) != null) {
parts.push(t.substring(prev, match.index));
parts.push(null);
keys.push(t.substring(match.index+1, r.lastIndex-1));
prev = r.lastIndex;
}
parts.push(t.substring(prev));
return function(vals) {
var escaped = {}
for (var i in keys) {
var k = keys[i];
var v = escaped[k];
if (v === undefined) {
v = vals[k];
if (v === undefined) dprint("Error: Missing template key: " + k);
if (!k.endsWith("html")) v = htmlEscape(v);
escaped[k] = v;
}
parts[i*2 + 1] = v;
}
return parts.join('');
};
}
function make_short_link(url) {
var max_len = 30
var href = url
if (url.length > max_len) {
url = url.substring(0, max_len);
var amp = url.lastIndexOf('&');
// avoid splitting html char entities
if (amp > max_len - 5)
url = url.substring(0, amp);
url += "...";
}
return '<a href="' + href + '" title="' + href + '">' + url + '</a>';
}
function linkify(text) {
var html = htmlEscape(text);
return html.replace(/(https?:\/\/(?:[^\s&]|&amp;)+[^\s\.,\!)?>\]:;'"&])/g, make_short_link);
}
function parse_date(d) {
// e.g. 2009-01-08T23:59:02Z
d = d.split(/[T:Z-]/g);
return new Date(Date.UTC(d[0], d[1] - 1, d[2], d[3], d[4], d[5]));
}
// Since this ui is more "message oriented", we first need to translate the FriendFeed
// entries into a list of individual messages, where each entry, comment, or like is
// a separate message.
function make_messages(who, entries, subscriptions) {
var submap = {}
for (var i in subscriptions) {
var s = subscriptions[i];
submap[s.id] = s;
}
var r = []
for (var i in entries) {
var estart = r.length;
var e = entries[i];
e.date = e.updated;
e.type = "e";
e.entry = e;
r.push(e)
// If the first comment is from the same person as the entry,
// make it part of the entry instead of a being a separate message.
if (e.comments.length && e.comments[0].user.id == e.user.id) {
e.attached_comment = e.comments[0];
e.comments = e.comments.slice(1);
}
for (var x in e.comments) {
var c = e.comments[x];
c.type = "c";
c.entry = e;
r.push(c)
}
for (var x in e.likes) {
var l = e.likes[x];
l.type = "l";
l.entry = e;
r.push(l)
}
var active = e.user.nickname == who;
for (var x = estart; x < r.length; x++) {
var m = r[x];
m.active = active;
m.user.is_friend = !!submap[m.user.id];
}
}
var good = []
for (var i in r) {
var m = r[i];
if (!m.user.nickname) continue;
// Only include messages from friends unless they are in a room
// or 'who' posted this entry
if (m.active || m.room || m.user.is_friend) good.push(m);
}
return good;
}
function sort_messages(msgs) {
function cmp(a, b) {
return (a.date > b.date) ? -1 : (a.date < b.date) ? 1 : 0;
}
msgs.sort(cmp);
}
function use_link(m) {
if (m.type != 'e') return false;
var s = m.service.id;
if (s == "twitter") return false;
if (s == "internal" && m.service.entryType == "message") return false;
if (s == "facebook" && m.service.entryType == "status") return false;
return true;
}
var thumb_template = template('<a href="{link}"><img src="{url}" width="{width}" height="{height}"/></a>');
function media_html(m) {
if (!m.media || !m.media.length) return '';
var imgs = '';
var tw = 0;
for (var i in m.media) {
var media = m.media[i];
if (!media.thumbnails || !media.thumbnails.length) continue;
for (var x in media.thumbnails) {
var t = media.thumbnails[x];
if (!t.width || !t.height) continue;
if (t.width > 525 || t.height > 175) continue;
tw += t.width;
if (tw > 525) break;
t.link = media.link || m.link;
imgs += thumb_template(t);
break;
}
}
if (!imgs) return '';
return '<div class="media">' + imgs + '</div>';
}
function entry_url(m) { return "http://friendfeed.com/e/" + m.entry.id; }
function time_html(date) {
date = parse_date(date);
var delta = ((new Date()) - date) / 1000;
if (delta < 90) return Math.round(delta) + " seconds ago";
delta /= 60;
if (delta < 90) return Math.round(delta) + " minutes ago";
delta /= 60;
if (delta < 48) return Math.round(delta) + " hours ago";
delta /= 24;
return Math.round(delta) + " days ago";
}
var msg_template = template(
'<div class="message" eurl="{eurl}"><table><tr><td>' +
'<img class="userpic" src="http://friendfeed.com/{pic_path}/picture?size={pic_size}"/></td>' +
'<td class="content"><a href="{profile_url}"><img class="icon" src="{icon_url}"/></a>' +
'{user_html} {body_html} <span class="date">{time}</span> {comlike_html} {media_html}' +
'</td></tr></table></div>');
var likemsg_template = template(
'<div class="message" eurl="{eurl}"><table><tr><td class="smile">' +
'<img src="http://friendfeed.com/static/images/smile.png"/></td>' +
'<td>{who_html} Liked "<i>{title_html}</i>"' +
'</td></tr></table></div>');
var userlink_template = template('<a target="_self" title="{name}" href="#{nickname}" class="nickname">{nickname}</a>');
var roomlink_template = template('<a href="http://friendfeed.com/rooms/{nickname}" class="nickname">{name}</a>');
var comments_template = template('<img src="http://friendfeed.com/static/images/comment-lighter.png"/> <a href="{eurl}">{desc}</a> ');
var likes_template = template('<img src="http://friendfeed.com/static/images/smile.png" style="margin-bottom:-2px"/> <a href="{eurl}">{desc}</a> ');
function msg_html(m, is_main) {
// Likes are extra-special since we generate a single entry for all the likes on an entry
if (m.type == 'l') {
var names = [];
for (var i in m.morelikes) {
if (i > 0) names.push(i == m.morelikes.length-1 ? " and " : ", ");
names.push(userlink_template(m.morelikes[i].user));
}
return likemsg_template({
eurl: entry_url(m),
who_html: names.join(''),
title_html: linkify(m.entry.title) + (use_link(m.entry) ? ' ' + make_short_link(htmlEscape(m.entry.link)) : '')
});
}
var args = {
eurl: entry_url(m),
pic_path: '',
pic_size: is_main ? "medium" : "small",
profile_url: '',
icon_url: '',
user_html: userlink_template(m.user),
body_html: '',
time: time_html(m.date),
comlike_html: '',
media_html: media_html(m)
}
if (m.room) {
args.pic_path = 'rooms/' + m.room.nickname;
args.user_html = roomlink_template(m.room) + (m.anonymous ? '' : ': ' + args.user_html);
} else {
args.pic_path = 'users/' + m.user.nickname;
}
if (m.type == 'c') {
args.icon_url = 'http://friendfeed.com/static/images/comment-' + (m.user.is_friend ? 'friend' : 'lighter') + '.png?v=2';
args.profile_url = 'http://friendfeed.com/' + m.user.nickname + '/comments';
args.body_html = linkify(m.body);
} else {
args.icon_url = m.service.iconUrl;
args.profile_url = m.service.profileUrl;
if (use_link(m)) {
if (m.attached_comment) args.body_html = linkify(m.attached_comment.body) + " - ";
args.body_html += linkify(m.title) + ' ' + make_short_link(htmlEscape(m.link));
} else {
args.body_html = linkify(m.title);
if (m.attached_comment) args.body_html += ' ' + linkify(m.attached_comment.body);
}
if (m.comments.length || m.likes.length) {
args.comlike_html =
'<span class="comlike">' +
(m.comments.length == 0 ? '' :
comments_template({eurl:args.eurl, desc: m.comments.length + (m.comments.length == 1 ? ' comment' : ' comments')})) +
(m.likes.length == 0 ? '' :
likes_template({eurl:args.eurl, desc: m.likes.length + (m.likes.length == 1 ? ' like' : ' likes')})) +
'</span>';
}
}
return msg_template(args);
}
function show_feed(cur_user) {
var profile_data = cur_user.profile_data;
var msgs = make_messages(profile_data.nickname, cur_user.feed_data.entries, profile_data.subscriptions);
sort_messages(msgs);
// Even though this is message-oriented, we still want to group all recent messages belonging to the
// same entry, ordered by the newest message. Likes are somewhat exceptional though, since they
// never cause other messages to be "promoted" in the ordering.
var dmsgs = []; // list of result clusters
var dmap = {};
var likemap = {};
for (var i in msgs) {
var m = msgs[i];
var dlist = dmap[m.entry.id];
if (m.type == 'l') {
// gather all of the likes in a single list attached to the first like.
var first = likemap[m.entry.id];
if (!first) {
likemap[m.entry.id] = m;
m.morelikes = [m];
if (dlist) {
// if there's already a cluster of normal messages, add the like to that group
dlist.push(m);
} else {
// otherwise, make it a separate group of it's own
dmsgs.push([m]);
}
} else {
first.morelikes.push(m);
}
continue;
}
if (!dlist) {
if (dmsgs.length && dmsgs[dmsgs.length-1][0].entry.id == m.entry.id) {
// the previous was a like-only cluster, promote to regular cluster
dlist = dmsgs[dmsgs.length-1];
} else {
dlist = [];
dmsgs.push(dlist);
}
dmap[m.entry.id] = dlist;
}
dlist.push(m);
if (dmsgs.length >= cur_user.feed_data.entries.length && i > 2*cur_user.feed_data.entries.length) break;
}
var profile_template = template(
'<div class="profile"><table><tr><td style="line-height:0px">' +
'<img class="userpic" src="http://friendfeed.com/{nickname}/picture?size=large"/></td><td style="width:100%">' +
'{nickname} ({name}) + friends <div style="font-size:11pt"><a id="refresh" href="#">Refresh</a> - ' +
'<a href="http://friendfeed.com/{nickname}">View {nickname} on FriendFeed</a></div></td></tr></table></div>');
var html = [];
html.push(profile_template(profile_data));
for (var i in dmsgs) {
var dlist = dmsgs[i];
var last = dlist[dlist.length-1];
if (last.type != 'e' && (last.type != 'l' || dlist.length > 1)) // always include the entry, unless it's a like-only cluster
dlist.push(last.entry);
html.push(msg_html(dlist[0], true));
if (dlist.length < 2) continue;
html.push('<div class="submsgs">');
for (var x = 1; x < dlist.length; x++)
html.push(msg_html(dlist[x]));
html.push('</div>');
}
$("#main").html(html.join(''))
$(".message:first").css("border-top-width", "0px");
$("#refresh").click(function () {
auto_refresher.cur_user.fetch_feed();
$("#refresh").replaceWith("Loading...");
return false;
});
var menu_template = template(
'<div style="display:none" id="msgmenu"><a href="{eurl}">View on FriendFeed</a><br/>' +
'<img src="http://friendfeed.com/static/images/comment-lighter.png"/> <a href="{comurl}">Comment</a><br/>' +
'<img src="http://friendfeed.com/static/images/smile.png" style="margin-bottom:-2px"/> <a href="{eurl}">Like</a> ' +
'</div>');
var menu_timeout;
$(".message").mouseover(function (e) {
if ($(this).find("#msgmenu").length) return;
$('#msgmenu').remove();
clearTimeout(menu_timeout);
var eurl = $(this).attr("eurl");
$(this).append(menu_template({eurl:eurl, comurl: eurl + '/comment?comment=' + eurl.split('/')[4]}));
$('#msgmenu').css('left', this.offsetWidth-2);
menu_timeout = setTimeout(function() { $('#msgmenu').fadeIn(400)}, 300);
});
$('body').mouseover(function (e) {
if (!$(e.target).parents().andSelf().is(".message"))
$('#msgmenu').remove();
});
}
var auto_refresher = {
start: function() {
this.last_active = this.last_refresh = new Date();
setInterval(function() { auto_refresher.maybe_refresh() }, 1000);
$(document).mousemove(this.activity).keypress(this.activity).scroll(this.activity);
},
activity: function() { auto_refresher.last_active = new Date(); },
was_recently_active: function() { return (new Date()) - this.last_active < 20000; },
maybe_refresh: function() {
var now = new Date();
if (now - this.last_refresh < 60000 || this.was_recently_active()) return;
this.last_refresh = now;
if (this.cur_user) {
dprint('auto');
this.cur_user.fetch_feed(true);
}
}
};
function load_user(nickname) {
$("#main").html("Loading...");
var cur_user = {nickname: nickname};
function check_loaded() {
if (cur_user.profile_data && cur_user.feed_data)
show_feed(cur_user);
}
$.getJSON("http://friendfeed.com/api/user/" + nickname + "/profile?callback=?", function(data) {
cur_user.profile_data = data;
check_loaded();
});
cur_user.fetch_feed = function(is_auto) {
$.getJSON("http://friendfeed.com/api/feed/user/" + nickname + "/friends?callback=?", function(data) {
if (cur_user !== auto_refresher.cur_user) {
dprint("user changed");
return;
}
if (is_auto && auto_refresher.was_recently_active()) {
dprint("was active");
return;
}
cur_user.feed_data = data;
check_loaded();
});
}
auto_refresher.cur_user = cur_user;
cur_user.fetch_feed();
}
$(function() {
var cur_nick = '';
setInterval(function() {
var nick = (document.location + '#').split('#')[1] || 'paul';
if (cur_nick == nick) return;
cur_nick = nick;
load_user(nick);
}, 200);
auto_refresher.start();
});
//]]>
</script>
</body>
</html>