156 changes: 81 additions & 75 deletions lib/authentication.js
@@ -1,92 +1,98 @@
var everyauth = require('everyauth');
var https = require('https');
module.exports = function Server(expressInstance, siteConf) {
everyauth.debug = siteConf.debug;
module.exports = function Server(streamLib, siteConf) {
var everyauth = require('everyauth');
var types = streamLib.types;

everyauth.everymodule.handleLogout( function (req, res) {
delete req.session.user;
req.logout();
res.writeHead(303, { 'Location': this.logoutRedirectPath() });
res.end();
});
var mongoose = require("mongoose")
, Schema = mongoose.Schema
, mongooseAuth = require('mongoose-auth')
, User;

// Facebook
if (siteConf.external && siteConf.external.facebook) {
everyauth.facebook
.appId(siteConf.external.facebook.appId)
.appSecret(siteConf.external.facebook.appSecret)
.findOrCreateUser(function (session, accessToken, accessTokenExtra, facebookUserMetaData) {return true;})
.redirectPath('/');
}

// Twitter
if (siteConf.external && siteConf.external.twitter) {
everyauth.twitter
.myHostname(siteConf.uri)
.consumerKey(siteConf.external.twitter.consumerKey)
.consumerSecret(siteConf.external.twitter.consumerSecret)
.findOrCreateUser(function (session, accessToken, accessSecret, twitterUser) {return true;})
.redirectPath('/');
}

// Github
if (siteConf.external && siteConf.external.github) {
everyauth.github
.myHostname(siteConf.uri)
.appId(siteConf.external.github.appId)
.appSecret(siteConf.external.github.appSecret)
.findOrCreateUser(function (session, accessToken, accessTokenExtra, githubUser) {return true;})
.redirectPath('/');
}

everyauth.helpExpress(expressInstance);
types.UserSchema.plugin(mongooseAuth, {
facebook: {
everyauth: {
myHostname: siteConf.uri
, appId: siteConf.external.facebook.appId
, appSecret: siteConf.external.facebook.appSecret
, redirectPath: '/'
, scope: 'user_location, email, user_about_me'
}
},
twitter: {
everyauth: {
myHostname: siteConf.uri
, consumerKey: siteConf.external.twitter.consumerKey
, consumerSecret: siteConf.external.twitter.consumerSecret
, redirectPath: '/'
}
},
github: {
everyauth: {
myHostname: siteConf.uri
, appId: siteConf.external.github.appId
, appSecret: siteConf.external.github.appSecret
, redirectPath: '/'
}
},
// Here, we attach your User model to every module
everymodule: {
everyauth: {
User: function () {
return streamLib.asmsDB.User;
},
handleLogout: function (req, res) {
delete req.session.user;
req.logout();
res.writeHead(303, { 'Location': this.logoutRedirectPath() });
res.end();
}
}
}
}
);

// Fetch and format data so we have an easy object with user data to work with.
function normalizeUserData() {
function handler(req, res, next) {
if (req.session && !req.session.user && req.session.auth && req.session.auth.loggedIn) {
console.log("Logged in a user");
console.dir(req.session.auth);
var user = {};
if (req.session.auth.github) {
user.image = 'http://1.gravatar.com/avatar/'+req.session.auth.github.user.gravatar_id+'?s=48';
user.name = req.session.auth.github.user.name;
user.id = 'github-'+req.session.auth.github.user.id;
}
if (req.session.auth.twitter) {
user.image = req.session.auth.twitter.user.profile_image_url;
user.name = req.session.auth.twitter.user.name;
user.id = 'twitter-'+req.session.auth.twitter.user.id_str;
}
if (req.session.auth.facebook) {
user.image = req.session.auth.facebook.user.picture;
user.name = req.session.auth.facebook.user.name;
user.id = 'facebook-'+req.session.auth.facebook.user.id;
var id = null;
var provider = null;

// Need to fetch the users image...
https.get({
'host': 'graph.facebook.com'
, 'path': '/me/picture?access_token='+req.session.auth.facebook.accessToken
}, function(response) {
user.image = response.headers.location;
req.session.user = user;
next();
}).on('error', function(e) {
req.session.user = user;
next();
});
return;
}
req.session.user = user;
}
next();
if (req.session.auth.github && req.session.auth.github.user) {
provider = 'github';
id = req.session.auth.github.user.id;
} else if (req.session.auth.twitter && req.session.auth.twitter.user) {
provider = 'twit';
id = req.session.auth.twitter.user.id_str;
} else if (req.session.auth.facebook && req.session.auth.facebook.user) {
provider = 'fb';
id = req.session.auth.facebook.user.id;
}
if (provider && id) {
console.log("Looking for user with id" + id);
streamLib.asmsDB.User.findOne().where(provider + '.id', id).exec(function(err, user){
if (err) {
// Should we create the user here if we can't find it ?
throw(new Error("Did you delete the DB ? I couldn't find the current user in the db"));
}

console.log("Found user with id " + id)
req.session.user = user;
next();
})
} else {
throw(new Error("Bad session data - Couldn't read the ID for the current user"));
}
} else {
next();
}
}

return handler;
}

return {
'middleware': {
'auth': everyauth.middleware
'mongooseAuth': mongooseAuth.middleware
, 'normalizeUserData': normalizeUserData
}
};
Expand Down
116 changes: 31 additions & 85 deletions lib/socket-io-server.js
@@ -1,9 +1,10 @@
module.exports = function Server(expressInstance, sessionStore) {
var parseCookie = require('connect').utils.parseCookie;
var io = require('socket.io').listen(expressInstance);
var asmsServer = expressInstance.asmsDB;
var thisApp = expressInstance.thisApp;
var thisInstance = expressInstance.thisInstance;

var asmsClient = expressInstance.asmsClient;
var asmsDB = asmsClient.asmsDB;


io.configure(function () {
io.set('log level', 0);
Expand All @@ -19,103 +20,48 @@ module.exports = function Server(expressInstance, sessionStore) {
});

io.sockets.on('connection', function(client) {
var user = client.handshake.session.user ? client.handshake.session.user.name : 'UID: '+(client.handshake.session.uid || 'has no UID');

var desiredStream = "firehose";

if (client.handshake.session && client.handshake.session.desiredStream) {
desiredStream = client.handshake.session.desiredStream;
console.log("User " + user + " has chosen to view this stream " + desiredStream);
} else {
console.log("No desired stream");
console.dir(client.handshake);
}

// Join user specific channel, this is good so content is send across user tabs.
// Join user specific channel, this is good so content is send across user tabs.
client.join(client.handshake.sid);

var session = client.handshake.session;

var avatarUrl = ((client.handshake.session.auth && client.handshake.session.user.image) ? client.handshake.session.user.image : '/img/codercat-sm.jpg');
var currentUser = {displayName: user, image: {url: avatarUrl}};


asmsServer.subscribe(desiredStream, function(channel, json) {

client.send(json);
var desiredStream = session.desiredStream ? session.desiredStream : asmsClient.default.stream;
asmsDB.Activity.subscribe(desiredStream, function(channel, json) {
if (channel == desiredStream) {
client.send(json);
}
});

var cf_provider = new asmsServer.ActivityObject({'displayName': 'Cloud Foundry', icon:{url: 'http://www.cloudfoundry.com/images/favicon.ico'}});
cf_provider.save(function(err) {
if (err == null) {
if (client.handshake.session.user && client.handshake.session.user.name) {
var act = new asmsServer.Activity({
id: 1,
actor: currentUser,
verb: 'connect',
object: thisInstance,
target: thisApp._id,
title: "connected to",
provider: cf_provider._id
});
}
console.dir(act);
asmsServer.publish(desiredStream, act);

} else {
console.log("Got error publishing welcome message")
}
});

var provider = new asmsServer.ActivityObject({'displayName': 'The Internet', icon: {url: 'http://www.w3.org/favicon.ico'}});
if (client.handshake.session.auth) {
if (client.handshake.session.auth.github) {
provider.displayName = 'GitHub';
provider.icon = {url: 'http://github.com/favicon.ico'};
} else if (client.handshake.session.auth.facebook) {
provider.displayName = 'Facebook';
provider.icon = {url: 'http://facebook.com/favicon.ico'};
} else if (client.handshake.session.auth.twitter) {
provider.displayName = 'Twitter';
provider.icon = {url: 'http://twitter.com/favicon.ico'};
}
}
provider.save();
var currentUser = asmsClient.helpers.getCurrentUserObject(session);

client.on('message', function(actHash) {
console.log("Got a new message from the client");
console.dir(actHash);
if (currentUser._id) {
asmsClient.helpers.publishActivity(desiredStream, currentUser, {
verb: 'connect',
object: asmsClient.default.instance,
target: asmsClient.default.app,
provider: asmsClient.default.provider,
title: "connected to"
});

actHash.actor = currentUser;
actHash.provider = provider._id;

if (actHash.verb == "post") {
actHash.title = "posted a " + actHash.object.objectType;

}

var act = new asmsServer.Activity(actHash);

// Send back the message to the users room.
asmsServer.publish(desiredStream, act);

console.log("Published act");
console.dir(act);
});

if (client.handshake.session.user && client.handshake.session.user.name) {
client.on('disconnect', function() {
//console.log('disconnect');
asmsServer.publish(desiredStream, new asmsServer.Activity({
actor: currentUser,
asmsClient.helpers.publishActivity(desiredStream, currentUser, {
verb: 'disconnect',
object: thisInstance,
target: thisApp._id,
title: "disconnected from",
provider: cf_provider._id
}));
provider: asmsClient.default.provider,
object: asmsClient.default.instance,
target: asmsClient.default.app
});

});
}

client.on('create-activity', function(actHash) {
actHash.provider = asmsClient.default.provider;
asmsClient.helpers.publishActivity(desiredStream, currentUser, actHash);
});

});

io.sockets.on('error', function(){ console.log(arguments); });
Expand Down
410 changes: 410 additions & 0 deletions npm-shrinkwrap.json

Large diffs are not rendered by default.

22 changes: 15 additions & 7 deletions package.json
@@ -1,26 +1,34 @@
{
"name" : "node-activities-boilerplate",
"description" : "A boilerplate Activity Stream Client for any Cloud Foundry instance. Based on the work from Mathias Pettersson <mape@mape.me> on node-express-boilerplate",
"version" : "0.0.6",
"version" : "0.0.7",
"author" : "Monica Wilkinson <ciberch@gmail.com>",
"engines" : ["node"],
"repository" : { "type":"git", "url":"http://github.com/ciberch/node-express-boilerplate" },
"scripts": {
"test": "make test"
},
"dependencies" : {
"connect" : ">=1.6.0",
"connect" : "1.x",
"connect-assetmanager" : ">=0.0.21",
"connect-assetmanager-handlers" : ">=0.0.17",
"jade": "",
"express" : ">=2.4.3",
"express" : "2.x",
"socket.io" : ">=0.7.8",
"connect-redis" : ">=1.0.7",
"connect-notifo" : ">=0.0.1",
"airbrake" : ">=0.2.0",
"everyauth" : ">=0.2.18",
"everyauth" : "0.2.x",
"cloudfoundry" : ">=0.1.0",
"activity-streams-mongoose": ">=0.0.9",
"mongoose": "",
"activity-streams-mongoose": "0.1.1",
"hiredis": "",
"underscore": ""
"underscore": "",
"mongoose-auth" : ">= 0.0.12",
"guid" : "",
"imagemagick" : "",
"mongoose": "3.x",
"mocha" : "1.4.1",
"should" : "1.1.0"

}
}
1,166 changes: 760 additions & 406 deletions public/css/bootstrap-responsive.css

Large diffs are not rendered by default.

21 changes: 9 additions & 12 deletions public/css/bootstrap-responsive.min.css

Large diffs are not rendered by default.

4,150 changes: 2,892 additions & 1,258 deletions public/css/bootstrap.css

Large diffs are not rendered by default.

698 changes: 9 additions & 689 deletions public/css/bootstrap.min.css

Large diffs are not rendered by default.

92 changes: 82 additions & 10 deletions public/css/client.css
Expand Up @@ -10,19 +10,19 @@
background: #3daeb9;
}

.timestamp {
font-size: 7pt;
font-style: italic;
.timestamp, .timestamp span, .timestamp a {
font-size: 8pt;
}

.activity .avatar {
width: 48px;
float: left;
display: inline;
margin-right: 10px;
img.service {
margin: 5px;
}

.activity_object .avatar {
.right {
text-align: right;
}

.activity .avatar {
width: 48px;
float: left;
display: inline;
Expand All @@ -45,13 +45,85 @@

}

#details {
border-left: dotted 3px gray;
padding-left: 20px;
}

body {
}

#main_stream {
background-color: #fff;
padding: 10px;

}
.nav-tabs-container .nav-tabs{
margin-bottom: 0px;
padding-bottom: 0px;
}

.nav-tabs .active a {
background-color: #fff;
}

#new_activity .avatar{
.main-hero, .second-hero, #new_activity {
background: #f7fbfc; /* Old browsers */
background: -moz-linear-gradient(top, #f7fbfc 0%, #d9edf2 40%, #add9e4 100%); /* FF3.6+ */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f7fbfc), color-stop(40%,#d9edf2), color-stop(100%,#add9e4)); /* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, #f7fbfc 0%,#d9edf2 40%,#add9e4 100%); /* Chrome10+,Safari5.1+ */
background: -o-linear-gradient(top, #f7fbfc 0%,#d9edf2 40%,#add9e4 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(top, #f7fbfc 0%,#d9edf2 40%,#add9e4 100%); /* IE10+ */
background: linear-gradient(to bottom, #f7fbfc 0%,#d9edf2 40%,#add9e4 100%); /* W3C */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f7fbfc', endColorstr='#add9e4',GradientType=0 ); /* IE6-9 */
}

#map {
height: 400px;
width: 600px;
}
.avatar-small.avatar{
width: 60px;
}

#new_activity {
margin-bottom: 30px;
padding:15px;
}

#new_activity .row {
margin-bottom: 10px;
}

#new_activity .avatar {
float: left;
display: inline;
margin-right: 10px;
}

.large-text, .large-text span {
font-size: 18px;
}

#new_photo {
margin-bottom: 20px;
}

.arch {
border: solid 4px white;
border-radius: 3px;
}

/*.input-file {*/
/*overflow: hidden;*/
/*width: 50px;*/
/*height: 50px;*/
/*background-color: gray;*/
/*border: solid 5px #aaa;*/
/*}*/
#input-file-input{
cursor: pointer;
}



Binary file added public/facebook.ico
Binary file not shown.
Binary file added public/github.ico
Binary file not shown.
Binary file added public/img/asms-blog.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/generic_photo_icon.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/glyphicons-halflings-white.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/glyphicons-halflings.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/me.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
100 changes: 100 additions & 0 deletions public/js/backbone/activity_create_view.js
@@ -0,0 +1,100 @@
var ActivityCreateView = Backbone.View.extend({
el: '#new_activity',
initialize: function(){
_.bindAll(this, 'newAct', 'render', 'changeType', 'includeLocation', 'sendMessage', 'fileSelected');

this.trimForServer = App.helper.trimForServer;

var streamName = this.$el.find('#streamName').val();
var verb = this.trimForServer(this.$el.find('#verb-show'));
var objectType = this.trimForServer(this.$el.find('#object-show'));

this.newAct(streamName, verb, objectType);
this.render();
},
events: {
"click .type-select" : "changeType",
"click #includeLocation" : "includeLocation",
"click #send-message" : "sendMessage",
"change #input-file-input" : "fileSelected"
},
newAct : function(streamName, verb, objectType) {
this.streamName = streamName;
this.model = new Activity({
object: {content: '', objectType: objectType, title: '', url: ''},
verb: verb,
streams: [streamName]
});
},
render: function(){
var actData = this.model.toJSON();
this.$el.find("#specific-activity-input").html(jade.templates[actData.object.objectType]({act: actData}));

if(!actData.object.image) {
var actView = this;
$('#new_photo').ajaxForm(function(data) {
actView.model.set('object', data.object);
actView.render();
});
}

return this; // for chainable calls, like .render().el
},
changeType : function(event) {
console.log(event);
var itemName = $(event.target).data("type-show");
if (itemName) {
$("#" + itemName)[0].innerHTML = event.target.text + " &nbsp;";
var val = this.trimForServer(event.target.text);
if (itemName == "verb-show") {
this.model.set('verb', val);
} else {
var obj = this.model.get('object');
obj.objectType = val;
this.model.set('object', obj);
}
}
this.render();
},
includeLocation : function(event) {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(App.helper.getLocation);
} else {
alert("Geo Location is not supported on your device");
}
},
sendMessage : function() {
console.log("In send message");

var obj = this.model.get('object');
obj.content = $("#msg").val();
obj.url = $('#url').val();
obj.title = $('#title').val();
obj.objectType = this.trimForServer($('#object-show'));
this.model.set('object', obj);

var streamName = $('#streamName').val();
this.model.set('streams', [streamName]);

var verb = this.trimForServer($('#verb-show'));
this.model.set('verb', verb);

if (this.model.isValid()) {
if (this.model.save()) {
this.newAct(streamName, verb, obj.objectType);
this.render();
}
}

},
fileSelected : function(event){

if (event.target.files && event.target.files[0]) {
var file = event.target.files[0];
this.file = file;
console.dir(file);
$('#title').val(file.name);
}
this.$el.find("#upload-file").show();
}
});
1,431 changes: 1,431 additions & 0 deletions public/js/backbone/backbone-0.9.2.js

Large diffs are not rendered by default.

77 changes: 77 additions & 0 deletions public/js/backbone/models.js
@@ -0,0 +1,77 @@
var Activity = Backbone.Model.extend({
url : "/activities",
// From activity-streams-mongoose/lib/activityMongoose.js
defaults: {
verb: 'post',
object: null, //ActivityObject
actor: null, //ActivityObject
url: '',
title: '',
content: '',
icon: null, // MediaLinkHash
target: null, //ActivityObject
published: Date.now,
updated: Date.now,
inReplyTo: null, //Activity
provider: null, //ActivityObject
generator: null, //ActivityObject
streams: ['firehose'],
likes: {},
likes_count: 0,
comments: [],
comments_count: 0,
userFriendlyDate: 'No idea when'
},
validate: function(attrs) {

if (! attrs.object) {
return "Object is missing"
}
if (!attrs.object.title) {
return "Title is missing";
}
}
});
var ActivityList = Backbone.Collection.extend({
model: Activity,
url : "/activities"
});

var ActivityObject = Backbone.Model.extend({
defaults : {
image: null, // MediaLinkHash
icon: null, // MediaLinkHash
displayName: '',
summary: '',
content: '',
url: '',
author: null, //ActivityObject
published: Date.now,
objectType: '',
attachments: [], //ActivityObject
upstreamDuplicates: [], //string
downstreamDuplicates: [], //string
updated: Date.now
}

});

var ActivityObjectType = Backbone.Model.extend({});

var ActivityObjectTypeList = Backbone.Collection.extend({
model : ActivityObjectType,
url : "/objectTypes"
});

var ActivityVerb = Backbone.Model.extend({});
var ActivityVerbList = Backbone.Collection.extend({
model : ActivityVerb,
url : "/verbs"
});

var User = Backbone.Model.extend({
defaults : {
signedIn : false,
displayName : 'User'
}
});
77 changes: 77 additions & 0 deletions public/js/backbone/views.js
@@ -0,0 +1,77 @@
var ActivityView = Backbone.View.extend({
initialize: function(){
_.bindAll(this, 'render', 'like', 'comment'); // every function that uses 'this' as the current object should be in here
this.model.bind('change', this.render, this);
// this.model.on('change:likes_count', 'render');
// this.model.on('change:comments_count', 'render');
},
events: {
"click .like-button" : "like",
"click .comment-button" : "comment"
},
render: function(){
var actData = this.model.toJSON();
var date = Date.parse(this.model.get('published'));
var awesomeDate = App.helper.fuzzy(date);
actData['userFriendlyDate'] = awesomeDate;
this.$el.html(jade.templates["activity"]({activities: [actData]}));
return this; // for chainable calls, like .render().el
},
like : function() {
var likes = this.model.get("likes");
if (!likes) {
likes = {};
}
likes[this.user] = true;
this.model.set("likes", likes);

var likes_count = _.keys(likes).length;
this.model.set("likes_count", likes_count)
this.model.save();
return this;
},
comment: function() {
var content = this.$el.find(".comment-area").val();
var comments = this.model.get("comments");
if (!comments){
comments = [];
}
comments.push({actor : App.currentUser, object: { objectType : 'comment', content: content}, published : Date.new});
var comments_count = comments.length;
this.model.set("comments_count", comments_count)
this.model.save();
return this;
}


});

var ActivityStreamView = Backbone.View.extend({
el: '#main_stream', // el attaches to existing element

initialize: function(){
_.bindAll(this, 'render', 'appendItem'); // every function that uses 'this' as the current object should be in here
this.collection = new ActivityList();
this.collection.bind('add', this.appendItem); // collection event binder
this.maxSize = 20;
},
render: function(){
_(this.collection.models).each(function(item){ // in case collection is not empty
self.appendItem(item);
}, this);
},

appendItem: function(item){
var itemView = new ActivityView({ model: item });

this.$el.prepend(itemView.render().el);

if (this.el.children.count > this.maxSize) {
this.el.children.last.remove();
}
}
});




1,021 changes: 661 additions & 360 deletions public/js/bootstrap.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions public/js/bootstrap.min.js

Large diffs are not rendered by default.

68 changes: 68 additions & 0 deletions public/js/helper.js
@@ -0,0 +1,68 @@
function AppHelper(){

this.fuzzy = function(date) {
// Make a fuzzy time
var delta = Math.round((+new Date - date) / 1000);

var minute = 60,
hour = minute * 60,
day = hour * 24,
week = day * 7;

var fuzzy;

if (delta < 30) {
fuzzy = 'just now.';
} else if (delta < minute) {
fuzzy = delta + ' seconds ago.';
} else if (delta < 2 * minute) {
fuzzy = 'a minute ago.'
} else if (delta < hour) {
fuzzy = Math.floor(delta / minute) + ' minutes ago.';
} else if (Math.floor(delta / hour) == 1) {
fuzzy = '1 hour ago.'
} else if (delta < day) {
fuzzy = Math.floor(delta / hour) + ' hours ago.';
} else if (delta < day * 2) {
fuzzy = 'yesterday';
}

return fuzzy;
};

this.trimForServer = function (items) {
if (items) {
if (typeof(items) === "object") {
var val = items[0];
return val.innerText.trimRight().toLowerCase();
} else {
return items.trimRight().toLowerCase();
}
}
return null;
};

this.getLocation = function(position) {
var mapOptions = {
zoom: 15,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
App.map = new google.maps.Map(document.getElementById('map'),mapOptions);
var pos = new google.maps.LatLng(position.coords.latitude, position.coords.longitude);
App.map.setCenter(pos);
var marker = new google.maps.Marker({
position: pos,
map: App.map,
title: 'Drag to the proper location',
draggable:true
});
google.maps.event.addListener(marker, 'click', function() {
console.log("New position is ");
console.dir(marker.getPosition());
App.map.setCenter(marker.getPosition());
});

$("#map").show();

}
};
69 changes: 41 additions & 28 deletions public/js/jquery.client.js
Expand Up @@ -14,63 +14,76 @@
};
})();

var socketIoClient = io.connect(null, {
App.socketIoClient = io.connect(null, {
'port': '#socketIoPort#'
, 'rememberTransport': true
, 'transports': ['xhr-polling']
});
socketIoClient.on('connect', function () {
App.socketIoClient.on('connect', function () {
$$('#connected').addClass('on').find('strong').text('Online');
});


var image = $.trim($('#image').val());
var service = $.trim($('#service').val());

var $ul = $('#main_stream');
App.map = null;
var streamView = new ActivityStreamView();

var defaultSync = Backbone.sync;

Backbone.sync = function(method, model, options) {
console.dir(model);
if (model.url === "/activities") {
if (method === "create") {
var act = model.toJSON();
App.socketIoClient.emit("create-activity", act);
} else if (method === "save") {
var act = model.toJSON();
App.socketIoClient.emit("save-activity", act);
alert("Saving activity");
}
return true;
} else {
alert("Doing a different kind of operation " + model.urlRoot);
defaultSync(method, model, options);
}
}

var newActView = new ActivityCreateView();

// set up the router here - remember the router is like a controller in Rails
//var dashboardRouter = new DashboardRouter({filterView: filterView, colorView: colorView, carView: carListView});

// the required Backbone way to start up the router
//Backbone.history.start({pushState: true});

socketIoClient.on('message', function(json) {
App.socketIoClient.on('message', function(json) {
var doc = JSON.parse(json);
if (doc) {
console.log(doc);
var $li = $(jade.templates["activity"]({activities: [doc]}));
$ul.prepend($li);
}
if ($ul.children.count > 20) {
$ul.children.last.remove();
streamView.collection.add(new Activity(doc));
}
});

App.socketIoClient.on('disconnect', function() {
$$('#connected').removeClass('on').find('strong').text('Offline');
});

$(document).ready(function(){

$(".filter-checkbox").live("click", function(){
$(".filter-checkbox").on("click", function(){
if (this.checked == false) {
$("#main_stream ." + this.name + "-" +this.value).hide();
} else {
$("#main_stream ." + this.name + "-" +this.value).show();
}
});

$("#send-message").click(function() {
var msg = $("#msg").val();
var title = $('#title').val();
var streamName = $('#streamName').val();

if (msg && msg.length > 0) {
$("#msg").val('');
$('#title').val('');
});

var act = {object: {content: msg, objectType: 'note', title: title}, verb: 'post', streams: [streamName]};

console.dir(act);
socketIoClient.emit("message", act);
}
return false;
});
});

socketIoClient.on('disconnect', function() {
$$('#connected').removeClass('on').find('strong').text('Offline');
});
})(jQuery);


1,089 changes: 1,089 additions & 0 deletions public/js/jquery.form.js

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions public/js/jquery.min.js

Large diffs are not rendered by default.

60 changes: 60 additions & 0 deletions public/js/routers/dashboard.js
@@ -0,0 +1,60 @@
// The DashboardRouter acts as the controller in this MVC setup
// It is responsible for listening to the events from each view, and then dispatching them back to all the other views
// It also handles the responsibility of the routing, so using Backbone's internal routing mechanisms, it
// listens to routes and handles setting up the views appropriately.
var DashboardRouter = Backbone.Router.extend({

// set up the router with all the views on our page, and then listen to all the events that
// can get triggered
initialize : function(options) {
var _this = this;

this.activityEditView = options.activityEditView,
this.filterView = options.filterView;
this.streamView = options.streamView;
this.activityView = options.activityView;

this.filterView.on("change_verb change_actor change_object", function(text) {
Backbone.history.navigate(_this.streamView.stream + "/" + _this.filterView.verb + "/" + _this.filterView.objectType + "/" + _this.filterView.actorType, {trigger: true});
});

this.streamView.on("change_stream", function(text) {
Backbone.history.navigate(_this.streamView.stream + "/" + _this.filterView.verb + "/" + _this.filterView.objectType + "/" + _this.filterView.actorType , {trigger: true});
});
},

// set up all the routes in this object
// make sure to handle every possible scenario, since we want to handle every possibility a user can type in
routes : {
"" : "all",
":stream/" : "show",
":stream/:verb" : "show",
":stream/:verb/:objectType" : "show",
":stream/:verb/:objectType/:actorType" : "show"
},

// handle the index case specially, and reroute it to the dashboard
all : function() {
Backbone.history.navigate("firehose/", {trigger: true});
},

// this function is called by Backbone when any route is matched from our routes object
// it defaults the parameters if they aren't passed in, and then sets these values on the views themselves
// finally, it is responsible for re-rendering the views
// remember, the views now have new data, so they must be re-rendered to show the new data
show : function(stream, verb, objectType, actorType) {
stream = stream || "firehose";
verb = verb || "";
objectType = objectType || "";
actorType = actorType || "";
this.filterView.setFilter(verb, objectType, actorType);
this.filterView.render();

this.streamView.setStream(stream);
this.streamView.render();

this.activityView.setFilters(stream, verb, objectType, actorType);
this.activityView.render();
}

});
2,660 changes: 2,604 additions & 56 deletions public/js/templates.js

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions public/js/underscore-min.js
56 changes: 56 additions & 0 deletions public/js/views/activity_create.js
@@ -0,0 +1,56 @@
var ActivityDropDownView = Backbone.View.extend({
tagName : "li",
// we don't need a template file if the view is this simple
template : _.template('<a href="#" class="type-select" data-filter-type="<%=name%>"><%=name%></a>'),

// a standard render function
render : function() {
var html = this.template(this.model.toJSON());
this.$el.html(html);
return this;
},

// bind to the event when a country is selected
events : {
"click .type-select" : "selectType"
},

// handle the event and trigger it so listeners can respond to it
selectType : function(e) {
this.trigger("select", e, this);
}
});

var ActivityObjectEditorView = Backbone.View.extend({
tagName : "ul",

initialize: function(){
this.typeSelector = new ActivityDropDownView();

},

render: function(){
$(this.el).append(this.typeSelector.render());
}

});

var ActivityEditorView = Backbone.View.extend({
el: $("#new_activity"),

initialize: function(){
this.actorEditor = new ActivityObjectEditorView();
this.objectEditor = new ActivityObjectEditorView();

},

events: {
"click .verb-select": "openVerbs",
"change #streamName" : "setStream",
"click #send-message" : "save"
},

render: function(){

}
});
239 changes: 239 additions & 0 deletions public/js/views/filters.js
@@ -0,0 +1,239 @@
// These objects handle the 3 comboboxes in the application
// Note how the code is extremly long to handle seemingly simple things
// It points out one of the criticisms of Backbone, that it contains too much boiler plate



// this object is larger view that contains the previous 3 views
// it acts as the controller for the other 3 views and captures events from them and reacts to them
var FilterView = Backbone.View.extend({

tagName : "div",

initialize : function(options) {

var _this = this;

// get the template from the JST file
this.template = JST["templates/filter"];

// default values
this.country = "all";
this.make = "all";
this.carmodel = "all";

// grab the Collection objects from the options object
this.activityObjectCollection = options.activityObjectCollection;
this.verbCollection = options.verbCollection;
this.carmodelCollection = options.carmodelCollection;

// in these blocks of code, we bind to the "reset" event of each collection.
// notice we only render the specific combobox that had the "reset" event triggered, and don't just
// call a blind "render()" - that would cause unnecessary repaints
//
// we also mixin the ParamFetchCollection mixin to add some functionality to the Collections
if (this.activityObjectCollection) {
this.activityObjectCollection.on("reset", this.renderCountry, this);
_.extend(this.countryCollection, Backbone.ParamFetchCollection.prototype);
}
if (this.makeCollection) {
this.makeCollection.on("reset", this.renderMake, this);
_.extend(this.makeCollection, Backbone.ParamFetchCollection.prototype);
}
if (this.carmodelCollection) {
this.carmodelCollection.on("reset", this.renderCarmodel, this);
_.extend(this.carmodelCollection, Backbone.ParamFetchCollection.prototype);
}
},

// handle the events from the comboboxes, but only the "all" choice (the others are handled by their respective views)
events : {
"click .all-country-filter" : "selectCountry",
"click .all-make-filter" : "selectMake",
"click .all-carmodel-filter" : "selectCarmodel"
},

// render the entire view
// notice we construct our own JSON object here, to keep it simple and error-proof
// also notice we simply delegate the majority of the rendering to 3 small methods
render : function() {

var c = (this.countryCollection !== undefined);
var m = (this.makeCollection !== undefined);
var r = (this.carmodelCollection !== undefined);

var json = '{"country":' + c + ', "make":' + m + ', "carmodel":' + r + '}';

var html = this.template($.parseJSON(json));
this.$el.html(html);

if (this.countryCollection) {
this.renderCountry();
}
if (this.makeCollection) {
this.renderMake();
}
if (this.carmodelCollection) {
this.renderCarmodel();
}

return this;
},

// renders the country combobox
renderCountry : function() {
var _this = this;
// keep the first one, which is always "All Countries"
this.$el.find(".country-filter").slice(1).remove();
// loop through each country, create a view, and have it rendered
// bind to the "select" event so we can capture events from this view in this object
this.countryCollection.each(function(country){
var view = new CountryView({model:country});
view.on("select", function(e, v){
_this.selectCountry(e, v);
});
_this.$el.find(".country-dropdown-menu").append(view.render().el);
});
$(".country-filter").each(function(){
if (_this.country == $(this).data("filter-type"))
{
$("#all-countries").find(".choice-title").text($(this).text());
}
});
},

// renders the make combobox
renderMake : function() {
var _this = this;
// keep the first one, which is always "All Makes"
this.$el.find(".make-filter").slice(1).remove();
// loop through each make, create a view, and have it rendered
// bind to the "select" event so we can capture events from this view in this object
this.makeCollection.each(function(make){
var view = new MakeView({model:make});
view.on("select", function(e, v){
_this.selectMake(e, v);
});
_this.$el.find(".make-dropdown-menu").append(view.render().el);
});
$(".make-filter").each(function(){
if (_this.make == $(this).data("filter-type"))
{
$("#all-makes").find(".choice-title").text($(this).text());
}
});
},

// renders the carmodel combobox
renderCarmodel : function() {
var _this = this;
// keep the first one, which is always "All Cars"
this.$el.find(".carmodel-filter").slice(1).remove();
// loop through each carmodel, create a view, and have it rendered
// bind to the "select" event so we can capture events from this view in this object
this.carmodelCollection.each(function(carmodel){
var view = new CarModelView({model:carmodel});
view.on("select", function(e, v){
_this.selectCarmodel(e, v);
});
_this.$el.find(".carmodel-dropdown-menu").append(view.render().el);
});
$(".carmodel-filter").each(function(){
if (_this.carmodel == $(this).data("filter-type"))
{
$("#all-carmodels").find(".choice-title").text($(this).text());
}
});
},

// handles the events when one of the countries are selected
// we pass it the event "e", and the "view" so we can work with the objects
selectCountry : function(e, view) {
if (!view) {
this.country = "all";
}
else {
// get the country from the view that triggered the event
this.country = view.model.get("name");
}
this.make = "all";
this.carmodel = "all";

// fire an event so the Router can be aware of changes to the data
this.trigger("change_country", this.country);

// update the make combobox
this.makeCollection.fetchWithParams({}, {country_name: this.country});

// update the car combobox
this.carmodelCollection.fetchWithParams({}, {country_name: this.country, make_name: this.make});

// a little trick/hack to prevent the URL from putting a "#" at the end
// this is only required because we are overriding some undesired Bootstrap code
e.preventDefault();

},

selectMake : function(e, view) {
if (!view) {
this.make = "all";
}
else
{
// get the country and make from the view that triggered the event
this.country = view.model.get("country_name");
this.make = view.model.get("name");
}
this.carmodel = "all";

// fire an event so that Router can be aware of changes to the data
this.trigger("change_make", this.make);

// update the car combobox
this.carmodelCollection.fetchWithParams({},{make_name: this.make, country_name: this.country});

// a little trick/hack to prevent the URL from putting a "#" at the end
// this is only required because we are overriding some undesired Bootstrap code
e.preventDefault();
},

selectCarmodel : function(e, view) {
if (!view) {
this.carmodel = "all";
}
else
{
// get the country, make, and carmodel from the view that triggered the event
this.country = view.model.get("country_name");
this.make = view.model.get("make_name");
this.carmodel = view.model.get("name");
}

// fire and event so that the Router can be aware of changes to the data
this.trigger("change_carmodel", this.carmodel);

// a little trick/hack to prevent the URL from putting a "#" at the end
// this is only required because we are overriding some undesired Bootstrap code
e.preventDefault();
},

// convenience functions to make setting data on this object easier
setFilter : function(country, make, carmodel) {
this.setCountry(country);
this.setMake(make);
this.setCarmodel(carmodel);
},

setCountry : function(country) {
this.country = country;
},

setMake : function(make) {
this.make = make;
},

setCarmodel : function(carmodel) {
this.carmodel = carmodel;
}

});
Binary file added public/twitter.ico
Binary file not shown.
459 changes: 320 additions & 139 deletions server.js

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions siteConfig.js
Expand Up @@ -53,9 +53,18 @@ if (cf.cloud) {
console.dir(settings.redisOptions);
}

var mongolab = 'mongolab_dev-2.0';

if (cf.mongodb['mongo-asms']) {
var cfg = cf.mongodb['mongo-asms'].credentials;
settings.mongoUrl = ["mongodb://", cfg.username, ":", cfg.password, "@", cfg.hostname, ":", cfg.port,"/" + cfg.db].join('');
} else if (cf.services[mongolab] && cf.services[mongolab].length == 1) {
console.log("Using MongoLab Dev version");
var svc = cf.services[mongolab][0];
settings.mongoUrl = svc['credentials']['MONGOLAB_URI'];
} else {
console.log("Could not find a MongoDB :(")
console.dir(cf.services);
}
settings.user_email = cf.app['users'][0];
}
Expand Down
75 changes: 75 additions & 0 deletions test/fixtures/index.json
@@ -0,0 +1,75 @@
{
"currentUser":{
"displayName":"Monica Wilkinson", // Should have more info on user
"image":{
"url":"https://fbcdn-profile-a.akamaihd.net/hprofile-ak-snc4/276322_608201527_2138067735_q.jpg"
}
},
"providerFavicon": "/facebook.ico", // FIX to use Absolute
"streams":{
"firehose":{
"name":"firehose",
"items":[
{
"title":"started",
"_id":"503e9cf914d6db492e000002", //ugly
"__v":0, // ugly
"streams":["firehose"],
"generator":null,
"provider":null,
"updated":"2012-08-29T22:51:37.503Z",
"published":"2012-08-29T22:51:37.503Z",
"target":"503e9cf914d6db492e000001",
"actor":{
"displayName":"mwilkinson@vmware.com",
"image":{
"url":"img/me.jpg" // FIX to use Absolute
}
},
"object":{
"displayName":"Instance 0 -- Local" // Can also have title, content, url, location
},
"icon":null,
"verb":"start"
}
]
}
},
"desiredStream":"firehose",
"objectTypes":["Application", "Article", "Bookmark", "Comment", "Event", "File", "Folder", "Group", "List", "Note", "Person", "Photo", "Place", "Playlist", "Product", "Review", "Stream", "Service", "Song", "Status", "Video"],
"actorTypes":["Person", "Group", "Application", "Service"],
"verbs":["Post", "Favorite", "Follow", "Join", "Like", "Friend", "Play", "Save", "Share", "Tag", "Create", "Update", "Read", "Delete", "Check In"],
"usedVerbs":["start", "connect", "post", "disconnect", "like"],
"usedObjects":[
{
"displayName":"Instance 0 -- Local"
},
{
"url":"http://asms.cloudfoundry.com",
"title":"ASMS",
"objectType":"application",
"content":"Streams Data"
},
{
"url":"http://ciberch.cloudfoundry.com",
"title":"A photo of Awesome",
"objectType":"photo",
"content":"Smell"
}
],
"usedObjectTypes":["none", "application", "photo"],
"usedActorObjectTypes":["none"],
"usedActors":[
{
"image":{
"url":"img/me.jpg" // FIX to use Absolute
},
"displayName":"mwilkinson@vmware.com"
},
{
"image":{
"url":"https://fbcdn-profile-a.akamaihd.net/hprofile-ak-snc4/276322_608201527_2138067735_q.jpg"
},
"displayName":"Monica Wilkinson"
}
]}
40 changes: 40 additions & 0 deletions test/server-test.js
@@ -0,0 +1,40 @@
var should = require('should')
, mongoose = require('mongoose')
, UserSchema = new mongoose.Schema()
, authPlugin = require('mongoose-auth')
, User;

UserSchema.plugin(authPlugin, {
password: true
});

mongoose.model('User', UserSchema);
User = mongoose.model('User');

describe('User', function () {
it('should generate a salt and set a hash when password is set', function () {
var user = new User();
should.strictEqual(undefined, user.salt);
should.strictEqual(undefined, user.hash);
user.password = 'hello';
user.password.should.equal('hello');
user.salt.should.not.be.undefined;
user.hash.should.not.be.undefined;
});
it('should authenticate with a correct password', function (done) {
var user = new User();
user.password = 'hello';
user.authenticate('hello', function (err, matched) {
matched.should.be.true;
done();
});
});
it('should fail authentication with an incorrect password', function (done) {
var user = new User();
user.password = 'correct';
user.authenticate('incorrect', function (err, matched) {
matched.should.be.false;
done();
});
});
});
Empty file added tmp/.gitkeep
Empty file.
50 changes: 50 additions & 0 deletions views/actions.jade
@@ -0,0 +1,50 @@
.row.actions
.span2
small
a.btn.btn-mini.btn-warning.like-button(href="#")
i.icon-ok.icon-white
small &nbsp; Like
.span5
.row
.span5.right
textarea.span4.comment-area()
.row
.span2.offset3.right#comment-post
small
a.btn.btn-mini.btn-success.comment-button(href="#")
i.icon-pencil.icon-white
small &nbsp; Post
.row.action_results
.span2
-if (act.likes_count && act.likes_count > 0) {
small#like_count
#{act.likes_count}
small.rest
| &nbsp; liked this
-}
-if (act.comments) {
.span4
ul
-for(var i=0; i < act.comments.length; i++) {
- var comment = comments[i];
li
.row.comment
.span1.actor
- var actor = comment.actor;
include activity_stream_actor
.span6.action
.row.title
.span5
strong=actor.displayName
&nbsp;
=comment.title
.row.activity_object
- var object = comment.object;
include activity_object
.row.details
.span5
- var timedItem = comment;
include activity_details

- }
- }
36 changes: 9 additions & 27 deletions views/activity.jade
@@ -1,43 +1,25 @@
each act in activities
- if (!act.object) continue;
- var objType = " objectType-" + (act.object.objectType ? act.object.objectType : 'none');
- var actorType = " actorType-" + (act.actor.objectType ? act.actor.objectType : 'none');
- var className = "verb-" + act.verb + objType+actorType;
li(class=className)
.row.activity
.span1.actor
-if (act.actor.image && act.actor.image.url) {
img.avatar(src= act.actor.image.url)
-} else {
h2 :-)
-}
.span5.action
- var actor = act.actor;
include activity_stream_actor
.span6.action
.row.title
.span5
strong=act.actor.displayName
&nbsp;
=act.title
.row.activity_object
.span5
br
blockquote
-if (act.object.image && act.object.image.url) {
img.avatar(src= act.object.image.url)
-}
-if (act.object.displayName) {
strong.activity-displayName=act.object.displayName
-} else if (act.object.title) {
strong.activity-title=act.object.title
-}
-if (act.object.content) {
.activity-content=act.object.content
-}
- var object = act.object;
include activity_object
.row.details
.span5
.timestamp.small=act.published
- if (act.provider && act.provider.icon) {
.span1
Via
.span2
img.service(src= act.provider.icon.url)
-}
- var timedItem = act;
include activity_details
include actions

95 changes: 33 additions & 62 deletions views/activity_create.jade
@@ -1,63 +1,34 @@
form#new_activity
.row
.span8
h3 Share something...
#new_activity
.row
.span4
.well.create_mod
h3 Actor
hr
#actor
p You are the actor here. Login above to change your name and avatar.
.btn-group
a.btn.btn-warning.dropdown-toggle(data-toggle="dropdown")
span Person &nbsp;
span.caret
ul.dropdown-menu
-if (locals.objectTypes)
each objectType in locals.objectTypes
li=objectType
br
img.avatar(src="#{locals.currentUser.image.url}")

label Display Name
.input-prepend
span.add-on @
input.prepend-fix(readonly=true, type="text", value="#{locals.currentUser.displayName}").span2#displayName
.span4#verb
.well.create_mod
h3 Verb
hr
p The verb describes the type of action the actor performs on the object.
.btn-group
a.btn.btn-danger.pull-right.dropdown-toggle(data-toggle="dropdown")
span Post &nbsp;
span.caret
ul.dropdown-menu
-if (locals.verbs)
each verb in locals.verbs
li=verb
br
#streamInfo
h3 Stream
br
.input-prepend
span.add-on #
input#streamName.span3.prepend-fix(type="text", value=locals.desiredStream)
.span4
.well.create_mod
h3 Object
hr
#object
.btn-group
a.btn.btn-info.dropdown-toggle(data-toggle="dropdown")
span Note &nbsp;
span.caret
ul.dropdown-menu
-if (locals.objectTypes)
each objectType in locals.objectTypes
li=objectType
br
label Title
input(type="text")#title
label Content
textarea#msg
button.btn.btn-success.pull-right#send-message Create New Activity

include user-box
.span5.large-text
| Did you&nbsp;
span.btn-group#verb
a.btn.btn-info.dropdown-toggle(data-toggle="dropdown")
span#verb-show=chosenVerb
ul.dropdown-menu
each verb in locals.verbs
li
a.type-select(href="#", data-type-show="verb-show")=verb
| &nbsp;a new&nbsp;
span.btn-group#objectType
a.btn.btn-info.dropdown-toggle(data-toggle="dropdown")
span#object-show=chosenObject
ul.dropdown-menu
each objectType in locals.objectTypes
li
a.type-select(href="#", data-type-show="object-show")=objectType
| &nbsp;?
.row
#specific-activity-input
.row#finish
.span10.offset2
span.large-text Final Step:&nbsp;
button.btn.btn-success#send-message Publish
| &nbsp;the activity to the&nbsp;
span.input-prepend
span.add-on #
input#streamName.span2.prepend-fix(type="text", value=locals.desiredStream)
9 changes: 9 additions & 0 deletions views/activity_details.jade
@@ -0,0 +1,9 @@
.span3.timestamp
.small=(timedItem.userFriendlyDate ? timedItem.userFriendlyDate : timedItem.published)
- if (timedItem.provider && timedItem.provider.icon) {
.span3.timestamp
span Via&nbsp;
a(href="#{timedItem.provider.url}", target="_blank")
span=timedItem.provider.displayName
img.service(src= timedItem.provider.icon.url)
-}
16 changes: 16 additions & 0 deletions views/activity_object.jade
@@ -0,0 +1,16 @@
- var objUrl = object.url ? object.url : '#';
.span6
br
blockquote
-if (object.image && object.image.url) {
a(href="#{objUrl}")
img.img-rounded.avatar(src= object.image.url)
-}
-if (object.displayName) {
strong.activity-displayName=object.displayName
-} else if (object.title) {
strong.activity-title=object.title
-}
-if (object.content) {
.activity-content=object.content
-}
7 changes: 7 additions & 0 deletions views/activity_stream_actor.jade
@@ -0,0 +1,7 @@
- var actUrl = actor.url? actor.url: '#';
-if (actor.image && actor.image.url) {
a.actor(href="#{actUrl}")
img.img-rounded.avatar(src= actor.image.url)
-} else {
h2 :-)
-}
17 changes: 17 additions & 0 deletions views/application.jade
@@ -0,0 +1,17 @@
.row
.span8.offset1
.row
.span8
input.span8#input-title(type="text", placeholder="What is it called ?")#title
.row
.span8
textarea.span8(placeholder="What does it do?")#msg
.row
.span8
input.span8(type="text", placeholder="What is it's url ?")#url
.row
.span6
label.checkbox
input#includeLocation(type="checkbox")
span Include Location ?
#map.hide
11 changes: 11 additions & 0 deletions views/article.jade
@@ -0,0 +1,11 @@
.row
.span8.offset1
.row
.span8
input.span8#input-title(type="text", placeholder="Title of Article")#title
.row
.span8
textarea.span8(placeholder="Article Summary")#msg
.row
.span8
input.span8(type="text", placeholder="Article's url ?")#url
13 changes: 13 additions & 0 deletions views/auth.jade
@@ -0,0 +1,13 @@
ul.dropdown-menu
li.facebook
a(href="/auth/facebook")
img(src="/facebook.ico", alt="Facebook")
span Facebook
li.twitter
a(href="/auth/twitter")
img(src="/twitter.ico", alt="Twitter")
span Twitter
li.github
a(href="/auth/github")
img(src="/github.ico", alt="GitHub")
span Github
15 changes: 12 additions & 3 deletions views/filters.jade
Expand Up @@ -7,21 +7,30 @@ form#form_filters
-if (locals.usedVerbs)
each verb in locals.usedVerbs
label.checkbox=verb
input.filter-checkbox(type="checkbox", name="verb", value=verb, checked=true)
- if (included.verbs.indexOf(verb) > -1)
input.filter-checkbox(type="checkbox", name="verb", value=verb, checked=true)
- else
input.filter-checkbox(type="checkbox", name="verb", value=verb)
hr
label
strong Object Types
-if (locals.usedObjectTypes)
each objType in locals.usedObjectTypes
label.checkbox=objType
input.filter-checkbox(type="checkbox", name="objectType", value=objType, checked=true)
- if (included.objectTypes.indexOf(objType) > -1)
input.filter-checkbox(type="checkbox", name="objectType", value=objType, checked=true)
- else
input.filter-checkbox(type="checkbox", name="objectType", value=objType)
hr
label
strong Actor Object Types
-if (locals.usedActorObjectTypes)
each objType in locals.usedActorObjectTypes
label.checkbox=objType
input.filter-checkbox(type="checkbox", name="actorType", value=objType, checked=true)
- if (included.actorObjectTypes.indexOf(objType) > -1)
input.filter-checkbox(type="checkbox", name="actorType", value=objType, checked=true)
- else
input.filter-checkbox(type="checkbox", name="actorType", value=objType)
hr
label
strong Distinct Objects
Expand Down
56 changes: 37 additions & 19 deletions views/index.jade
@@ -1,28 +1,46 @@
include nav_bar

.container
include activity_create
include start-modal
- var chosenVerb = locals.verbs[0];
- var chosenObject = locals.objectTypes[0];
- if(session.auth)
include activity_create
- else
.hero-unit.second-hero
.row
.span5
a(href="http://blog.cloudfoundry.com/tag/asms/", target="_blank")
img.arch(src="/img/asms-blog.png")
.span5
h1 Welcome
p This is a boilerplate Activity Streams App. Login or Sign Up to Share Activities !
.btn-group
a.btn.btn-primary.dropdown-toggle(href="#", "data-toggle"="dropdown") Get Started via
include auth

.row
.span4#filters
.well
include filters
.well.second-hero
- var included = locals.included
include filters
.span8#stream
h3 Activity Stream
hr
-if (locals.streams) {
ul.nav.nav-tabs
each stream in locals.streams
-if (stream.items && stream.items.length>0)
li.active
a(href="/streams/#{stream.name}")=stream.name
-else
li
a(href="/streams/#{stream.name}")=stream.name
-}
ul.unstyled#main_stream
- var activities = locals.streams[desiredStream].items
-if (activities && activities.length) {
include activity
-}
.wello
.nav-tabs-container
-if (locals.streams) {
ul.nav.nav-tabs
each stream in locals.streams
-if (stream.items && stream.items.length>0)
li.active
a(href="/streams/#{stream.name}")=stream.name
-else
li
a(href="/streams/#{stream.name}")=stream.name
-}
ul.unstyled#main_stream
- var activities = locals.streams[desiredStream].items
-if (activities && activities.length) {
include activity
-}

11 changes: 9 additions & 2 deletions views/layout.jade
Expand Up @@ -6,5 +6,12 @@ html
link(rel="stylesheet", href="/css/client.css", type="text/css")
title Activity Streams
body!=body
script(src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js", type="text/javascript")
script(src="/static/js/#{assetsCacheHashes.js||0}/client.js", type="text/javascript")
script(src="/js/jquery.min.js", type="text/javascript")
script(src="/js/underscore-min.js", type="text/javascript")
script(src="/js/helper.js", type="text/javascript")
script(type="text/javascript")
var App = {
helper : new AppHelper()
}
script(src="/static/js/#{assetsCacheHashes.js||0}/client.js", type="text/javascript")
script(src="https://maps.googleapis.com/maps/api/js?sensor=true")
22 changes: 5 additions & 17 deletions views/nav_bar.jade
@@ -1,11 +1,11 @@
.navbar
.navbar.navbar-inverse
.navbar-inner
.container
a.btn.btn-navbar("data-target"=".nav-collapse", "data-toggle"="collapse")
span.icon-bar
span.icon-bar
span.icon-bar
a#connected.brand(:href = "#") Activity Streams
a#connected.brand(href="#start-modal", role="button", data-toggle="modal") node-activities-boilerplate
.nav-collapse
ul.nav
li
Expand All @@ -18,22 +18,10 @@
- if(!session.auth)
ul.nav
li.dropdown
a(href="#", "data-toggle"="dropdown")
span Login
a#auth-button(href="#", "data-toggle"="dropdown")
span Login / Signup
b.caret
ul.dropdown-menu
li.facebook
a(href="/auth/facebook")
img(src="//facebook.com/favicon.ico", alt="Facebook")
span Facebook
li.twitter
a(href="/auth/twitter")
img(src="//twitter.com/favicon.ico", alt="Twitter")
span Twitter
li.github
a(href="/auth/github")
img(src="//github.com/favicon.ico", alt="GitHub")
span Github
include auth
ul.nav
li.dropdown
a(href="#", "data-toggle"="dropdown")
Expand Down
8 changes: 8 additions & 0 deletions views/person.jade
@@ -0,0 +1,8 @@
.row
.span8.offset1
.row
.span8
input.span8#input-title(type="text", placeholder="Display Name")#title
.row
.span8
input.span8(type="text", placeholder="Person's web page ?")#url
39 changes: 39 additions & 0 deletions views/photo.jade
@@ -0,0 +1,39 @@
.row
- var photoTitle = act.object ? act.object.title : '';
- var photoUrl = act.object ? act.object.url : '';
- if(act.object && act.object.image) {
.span3#thumbnail.offset1
img(src="#{act.object.image.url}")
- } else {
.span3#photo.offset1
form#new_photo(method="post", enctype="multipart/form-data", action="/photos")
.row
.span3
h4 Step 1: Select Photo
.row
.span3
.input-file
input#input-file-input(type="file", name="image", accept="image/*", required="required")
.clearfix
hr
h4
| Step 2: Upload to Cloud
i.icon-upload
input.btn.btn-warning#upload-file(type="submit", value="Upload File")
- }
.span6.offset1#details
.row
.span6
h4 Step 3: Complete details
.row
.span6
input.span6#input-title(type="text", placeholder="Describe the photo", value="#{photoTitle}")#title
.row
.span6
input.span6(type="text", placeholder="Url of photo on another site ?", value="#{photoUrl}")#url
.row
.span6
label.checkbox
input#includeLocation(type="checkbox")
span Include Location ?
#map.hide
17 changes: 17 additions & 0 deletions views/place.jade
@@ -0,0 +1,17 @@
.row
.span8.offset1
.row
.span8
input.span8#input-title(type="text", placeholder="What is it called ?")#title
.row
.span8
textarea.span8(placeholder="Describe the place")#msg
.row
.span8
input.span8(type="text", placeholder="Website for the place")#url
.row
.span6
label.checkbox
input#includeLocation(type="checkbox")
span Where is this place ?
#map.hide
17 changes: 17 additions & 0 deletions views/service.jade
@@ -0,0 +1,17 @@
.row
.span8.offset1
.row
.span8
input.span8#input-title(type="text", placeholder="What is it called ?")#title
.row
.span8
textarea.span8(placeholder="Describe the service.")#msg
.row
.span8
input.span8(type="text", placeholder="What is it's url ?")#url
.row
.span6
label.checkbox
input#includeLocation(type="checkbox")
span Include Location ?
#map.hide
22 changes: 22 additions & 0 deletions views/start-modal.jade
@@ -0,0 +1,22 @@
.modal.hide.fade.main-hero#start-modal(role="dialog", aria-labelledby="Welcome Screen", aria-hidden="true")
.modal-header
button(type="button", class="close",data-dismiss="modal",aria-hidden="true") &times;
h2
a(href="#") Node.js Activities Boilerplate App
span Publish activities to the stream of your choice.&nbsp;
span Consume streams in Real-time.

.modal-body
h4 To Use
p Start by describing new activities and content others may find interesting or you want to remember.
hr
h4 Note: &nbsp;
ul
li
span To change the actor you can&nbsp;
a(href="#") Login&nbsp;
span into different accounts via&nbsp;
a(href="#") OAuth
li This app can also be used programmatically via the API &nbsp;
.modal-footer
button.btn(data-dismiss="modal", aria-hidden="true") OK
4 changes: 4 additions & 0 deletions views/user-box.jade
@@ -0,0 +1,4 @@
.user-box.span1
a(href="#", rel="tooltip", title="#{locals.currentUser.displayName}")
img.avatar-small.img-rounded.avatar(src="#{locals.currentUser.image.url}")
.clearfix