Skip to content

Instantly share code, notes, and snippets.

@pezholio
Forked from tomger/fatc.html
Created October 19, 2010 12:03
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pezholio/634084 to your computer and use it in GitHub Desktop.
Save pezholio/634084 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html><head>
<title>Fork-A-Twitter-Client</title>
<!--
Basic OniApollo/StratifiedJS Twitter Client application scaffold.
See http://fatc.onilabs.com/
THIS FILE IS IN THE PUBLIC DOMAIN.
You can use this file however we want, but if you do something
interesting with it I'd love to hear about it!
@tomg
Make sure the following line points to the location of your oni-apollo.js!
You can hotlink to http://code.onilabs.com/0.9.2/oni-apollo.js if you
want:
-->
<script src="http://code.onilabs.com/0.9.2/oni-apollo.js"></script>
<script type="text/sjs">
/*
This is more of a scaffold than a proper twitter client.
It lacks any sophistication and basically just allows you to sign-in
and tweet, it shows and auto-updates your timeline, and it loads older
tweets when you scroll down the page.
It's easy to extend, so go on and hack it :-)
Oh, and IT RUNS COMPLETELY CLIENT-SIDE. There is no server code involved.
It talks to twitter directly. You don't even need a proper domain to
run it from; you can serve it from 'localhost'. E.g. if you have a Mac,
you can serve it via Personal Web Sharing - see
http://www.macinstruct.com/node/112 .
* HOW TO GET A TWITTER APPLICATION ID:
Log into twitter and go to http://dev.twitter.com/apps/ .
Register a new application, setting the 'Callback URL' to the site where
your app will live (this can be 'http://localhost'!).
Set the 'Default Access type' to 'Read & Write'.
Then change the application id below to the @Anywhere API key of your app.
* ABOUT STRATIFIED JAVASCRIPT / ONI APOLLO:
Oni Apollo, the library that we use here, allows you to make calls to
the Twitter API directly from the client in a NON-CALLBACK style.
See http://onilabs.com/api#twitter.
It allows you to call any function in the standard RESTful Twitter API
(see http://dev.twitter.com/doc) in this way:
var result = T.call("statuses/home_timeline", params);
StratifiedJS is the extended JS used by Oni Apollo to make the
non-callback style possible (and more!). See http://stratifiedjs.org/sjsdocs.
*/
var API_KEY = "OQTPVZIstxfxFVB2kD5oA";
//----------------------------------------------------------------------
// Initialization
// Load the @Anywhere API and init with your application id:
var T = require("twitter").initAnywhere({id:API_KEY});
// We'll use jquery; load it from Google's CDN and install stratified
// bindings ($click, etc):
require("jquery-binding").install();
// We'll also use various methods from the common module (supplant, ...)
var common = require("common");
var tweeting_button = $("#tweeting_button");
var status_el = $("#status");
var counter = $("#tweeting_status");
//----------------------------------------------------------------------
// main program loop
// Show the twitter connect button:
T("#login").connectButton();
// Run our application logic in an endless loop:
while (true) {
try {
main();
}
catch(e) {
alert("Error:"+e);
}
}
//----------------------------------------------------------------------
// main application logic:
function main() {
// First wait until we're connected:
if (!T.isConnected())
T.waitforEvent("authComplete");
$("#login").hide();
$("#welcome").hide();
$("#timeline").empty();
try {
// Let's set the last tweet and the background based on the
// user's prefs:
var profile = T.call("users/show", {user_id: T.currentUser.id});
$("#current_user").text(profile.screen_name);
$("#current_user").prepend("<img src='"+profile.profile_image_url + "'/>");
setLatestTweet(profile.status);
$("body").css({
background: common.supplant("#{color} url({image}) {repeat}", {
color: profile.profile_background_color,
image: profile.profile_background_image_url,
repeat: profile.profile_background_tile ? "repeat" : "no-repeat"
})
});
// Now we'll do several things in parallel, by combining them with
// the stratified construct waitfor/and:
waitfor {
waitfor_signout();
// Now exit main(). The other clauses in the waitfor/and will
// automatically be aborted cleanly (i.e. eventlisteners will be
// removed, etc).
return;
}
and {
// Endlessly handle tweets by the user:
tweet_loop();
}
and {
// Endlessly update the timeline:
update_timeline_loop();
}
and {
// Endlessly load older tweets when the user scrolls down:
show_more_loop();
}
and {
ui_mute_loop();
}
}
finally {
// Clean up:
$("#current_user").empty();
$("#timeline").empty();
$("#welcome").show();
$("body").css({background:""});
}
}
//----------------------------------------------------------------------
// Helper functions
function setLatestTweet(tweet) {
$("#latest")[tweet?"show":"hide"]();
if (tweet)
$("#latest span").text(tweet.text);
}
// 'characters left' counter
function update_counter() {
counter.text(140 - status_el.val().length);
}
function ui_mute_loop() {
if (!window["localStorage"]) return;
var filterArray = [];
tweetFilter = function(tweet) {
for (var i = filterArray.length; i >= 0; --i) {
var keyword = filterArray[i];
if (!keyword) continue;
if (keyword.indexOf("@") == 0 && tweet.user.screen_name == keyword.substring(1)) return true;
else if (tweet.text.indexOf(keyword) != -1) return true;
}
return false;
};
var button = $("<a href='#'>Mute list</a>").prependTo("#session_buttons");
try {
while(true) {
filterArray = localStorage.mute ? localStorage.mute.split(" ") : [];
button.$click();
var rv = prompt("Enter a space-separated list of #keywords or @users you want to mute.", localStorage.mute);
if (rv != null) {
localStorage.mute = rv;
$(window).trigger("settingschanged");
}
}
} finally {
button.remove();
}
}
// Append/prepend a tweet to the timeline
function showTweet(tweet, append) {
if (window["tweetFilter"] && tweetFilter(tweet)) return;
var date = new Date(tweet.created_at.replace(/^\w+ (\w+) (\d+) ([\d:]+) \+0000 (\d+)$/,
"$1 $2 $4 $3 UTC"));
var elapsed = Math.round(((new Date()).getTime() - date.getTime()) / 60000);
if (elapsed < 60) {
var date_string = elapsed + " minutes";
}
else if (elapsed < 60*24) {
var date_string = Math.round(elapsed / 60) + " hours";
}
else {
var date_string = date.getDate() + "/" + date.getMonth();
}
if (tweet.entities && tweet.entities.urls) {
for (var i = tweet.entities.urls.length - 1, entity; entity = tweet.entities.urls[i]; --i) {
tweet.text = common.supplant("{a}<a target='_blank' href='{b}'>{b}</a>{c}", {
a: tweet.text.substring(0, entity.indices[0]),
b: entity.url,
c: tweet.text.substring(entity.indices[1])
});
}
}
// Construct the html for the tweet. Note how we can use
// multiline strings in StratifiedJS. We also use the
// 'supplant' function from the 'common' module for getting
// values into our string:
$("#timeline")[append ? "append" : "prepend"](common.supplant("\
<div class='timeline_item user_{screenname}'>
<div class='tweet_wrapper' tweetid='{tweetid}'>
<span class='tweet_thumb'>
<img src='{image}' width='48' height='48'/>
</span>
<span class='tweet_body'>
<span class='screenname'>{screenname}</span>
<span class='content'>{text}</span>
<span class='meta'>{meta}</span>
</span>
</div>
</div>
", {
tweetid: tweet.id,
text: tweet.text,
image: tweet.user.profile_image_url,
screenname: tweet.user.screen_name,
meta: date_string
}));
}
// Helper to fetch new tweets:
function fetch_tweets(params) {
try {
return T.call("statuses/home_timeline", params);
}
catch(e) {
// Are we still connected? If not, quit the app:
if (!T.isConnected())
throw "Disconnected";
// else try again in 10s:
// XXX should really have a back-off algo here
// XXX should examine exception object.
hold(10*1000);
return fetch_tweets(params);
}
}
// Function that periodically fetches new tweets from twitter and
// displays them:
function update_timeline_loop() {
// Run an endless loop:
while (true) {
// Fetch tweets from twitter:
var timeline = fetch_tweets({
include_entities: true,
count: 30,
since_id: $(".tweet_wrapper:first").attr("tweetid")
});
if (timeline && timeline.length) {
// Prepend tweets to timeline:
for (var i = timeline.length-1, tweet; tweet = timeline[i]; --i)
showTweet(tweet, false);
}
waitfor {
// Twitter is rate-limited to ~150calls/h. Wait for 60 seconds
// until we fetch more tweets.
hold(60*1000);
}
or {
$(window).waitFor("settingschanged");
$("#timeline").empty();
}
}
}
// Helper that waits until the user scrolls to the bottom of the page:
function waitforScrollToBottom() {
do {
$(window).$scroll();
}
while ($(document).height()- $(window).height() - $(document).scrollTop() > 300)
}
// Runs a loop that waits for the user to scroll to the bottom, and
// then loads more tweets:
function show_more_loop() {
while (true) {
waitforScrollToBottom();
var timeline = null;
waitfor {
// show a cancel button:
$("#timeline").append("\
<div class='timeline_item loading_more'>
Loading more tweets... click here to cancel
</div>");
// wait for it to be clicked; if that happens before the request
// completes, the request will be cancelled, by virtue of the
// waitfor/or.
$(".timeline_item:last").$click();
}
or {
timeline = fetch_tweets({
count: 30,
max_id: $(".tweet_wrapper:last").attr("tweetid")-1
});
if (!timeline.length) return; // we've loaded all tweets there are
}
finally {
// remove the cancel button:
$(".timeline_item:last").remove();
}
if (timeline && timeline.length) {
// Append tweets to timeline:
for (var i = 0, tweet; tweet = timeline[i]; ++i)
showTweet(tweet, true);
}
}
}
// Runs an endless loop that checks for the 'tweet' button to be
// clicked and sends out a tweet if it is:
function tweet_loop() {
try {
$(".tweet_box").show();
while (true) {
tweeting_button.$click();
tweeting_button.attr("disabled", "disabled");
$("#tweeting_status").text("Tweeting...");
try {
var tweet = T.call("statuses/update", {status: status_el.val() });
status_el.val("");
showTweet(tweet);
setLatestTweet(tweet);
}
catch (e) {
alert("Error posting: " + e.response.error);
}
update_counter();
tweeting_button.removeAttr("disabled");
}
}
finally {
$(".tweet_box").hide();
}
}
// shows a signout button and blocks until it is clicked
function waitfor_signout() {
try {
// Show a signout button and wait for it to be clicked:
var signout = $("<a href='#'>Sign out of twitter</a>").appendTo("#logout");
var e = signout.$click();
e.returnValue = false;
e.preventDefault();
// Ok, signout button was clicked; sign out, hide button & clear timeline:
twttr.anywhere.signOut();
}
finally {
signout.remove();
$("#login").show();
}
}
</script>
<style>
body {
-webkit-font-smoothing: antialiased;
padding: 0; margin: 0;
background: #C0FFD8;
}
h2 {
color: #999;
text-shadow: white 0px 1px 0px;
font-size:17px;
margin: 0 0 10px 0;
}
body, textarea, input {
font: 15px sans-serif;
line-height: 19px;
}
#timeline {
margin: 0;
padding: 0;
}
#top_bar {
background: rgba(0,0,0,0.2);
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#AA000000,endColorstr=#AA000000);
height: 2.5em;
margin-bottom: 10px;
line-height: 2.5em;
}
#session_buttons {
float:right;
margin-right: 10px;
font-size: 12px;
}
#session_buttons * {
color: #fff;
text-shadow: 0px 1px rgba(0,0,0,0.2);
font-weight:bold;
}
#logout, #login, #current_user {
display:inline;
margin-left:10px;
}
#current_user img {
width: 20px; height:20px;
vertical-align:middle;
margin: -2px 5px 0 0;
}
#title_bar {
color: #fff;
font-weight:bold;
font-size: 20px;
text-shadow: 0px 1px rgba(0,0,0,0.3);
}
#wrapper, #title_bar {
width: 480px;
margin: 0 auto;
}
#wrapper {
padding: 0;
border-radius: 3px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
background:#fff;
overflow:hidden;
}
#info {
width: 441px;
padding: 0 0px 40px 0;
}
#status {
border: 1px solid #aaa;
padding: 4px 2px;
height: 2.5em;
resize: none;
overflow:auto;
margin-bottom: 5px;
width:435px;
border-radius: 3px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
-webkit-box-shadow: white 0px 1px;
-moz-box-shadow: white 0px 1px;
}
#tweeting_button_container {
float:right;
margin:0;
}
#tweeting_button {
}
#tweeting_status {
position:relative;
right: 5px;
top: 3px;
color: #555;
}
#latest {
float:left;
width: 300px;
line-height: 16px;
height: 30px;
overflow:hidden;
}
.tweet_wrapper {
margin: 0;
line-height: 16px;
display:block;
}
.tweet_wrapper, .tweet_box {
padding: 15px 30px 10px 20px;
}
.tweet_box {
background: #f8f8f8;
-moz-border-radius: 3px 3px 0 0;
display:none;
}
.timeline_item, .tweet_box {
border-bottom: 1px solid #eee;
}
.tweet_thumb {
position:absolute;
display: block;
height: 50px;
}
.tweet_body {
margin: -5px 0 0 63px;
min-height:58px;
display:block;
word-wrap: break-word;
}
.tweet_body .content {
color: #444
}
.tweet_body .meta {
display:block;
line-height: 25px;
}
.tweet_body .meta, #latest {
font-size: 11px;
color: #666;
}
.tweet_body .screenname {
color: #222;
line-height:19px;
display:block;
font-weight:bold;
}
.loading_more {
text-align:center;
color: #666;
padding: 20px;
}
#toolbar {
position:absolute;
top: 10px; right: 10px;
}
button {
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#fff), to(#ddd));
background: -moz-linear-gradient(center top, #fff, #ddd);
-webkit-box-shadow: white 0px 1px;
-moz-box-shadow: white 0px 1px;
text-shadow: 0px 1px white;
color: #555;
border: 1px solid #aaa;
border-radius: 3px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
cursor: pointer;
overflow: visible;
vertical-align: middle;
white-space: nowrap;
text-align:center;
margin:0;
font-weight: normal;
height: 2.0em;
line-height:1.2em;
padding: 0.1em 0.6em;
font-size: 15px;
}
button:hover {
border-color: #888;
}
button:active {
border-color: #444;
}
#welcome {
margin: 0 auto;
width: 280px;
font-size: 20px;
line-height: 27px;
color: #444;
background: transparent url(http://fatc.onilabs.com/fatc.png) no-repeat top right;
padding: 30px 200px 30px 0;
}
#welcome a {color: #222}
</style>
</head>
<body>
<div id="top_bar">
<div id="session_buttons">
<div id="current_user"></div>
<div id="login"></div>
<div id="logout"></div>
</div>
<div id="title_bar">Fork-A-Twitter-Client</div>
</div>
<div id="wrapper">
<div class="tweet_box">
<h2>What's happening?</h2>
<div id="info">
<textarea id="status" onkeyup="update_counter(this)"></textarea>
<span id="latest"><strong>latest: </strong><span></span></span>
<span id="tweeting_button_container">
<span id="tweeting_status"></span>
<button id="tweeting_button">Tweet</button>
</span>
</div>
</div>
<div id="timeline">
</div>
</div>
<div id="welcome">
Welcome to <strong>your</strong> Twitter client.
<p>
Connect to Twitter using the blue button at the top.<br/>
Better still, <a href="http://fatc.onilabs.com/">fork the code and hack it.</a>
</p>
</div>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment