Refactor all the things

This commit is contained in:
Lance Stout 2013-08-29 20:38:28 -07:00
parent cae595b1c0
commit 83f68a7a2b
56 changed files with 1086 additions and 4781 deletions

View File

@ -1,4 +1,5 @@
node_modules
clientapp/libraries
clientapp/modules
public
clientapp/libraries
clientapp/templates.js
clientapp/.build

View File

@ -7,17 +7,9 @@
"white": true,
"undef": true,
"browser": true,
"es5": true,
"predef": [
"$",
"me",
"confirm",
"alert",
"require",
"__dirname",
"process",
"exports",
"Buffer",
"module"
]
"node": true,
"trailing": true,
"indent": 4,
"latedef": true,
"newcap": true
}

86
clientapp/app.js Normal file
View File

@ -0,0 +1,86 @@
/*global $, app, me, client, XMPP*/
"use strict";
var _ = require('underscore');
var async = require('async');
var Backbone = require('backbone');
var MeModel = require('./models/me');
var MainView = require('./views/main');
var Router = require('./router');
var Storage = require('./storage');
var xmppEventHandlers = require('./helpers/xmppEventHandlers');
module.exports = {
launch: function () {
var self = this;
_.extend(this, Backbone.Events);
var app = window.app = this;
$(function () {
async.series([
function (cb) {
app.storage = new Storage();
app.storage.open(cb);
},
function (cb) {
var me = window.me = new MeModel();
new Router();
app.history = Backbone.history;
if (!localStorage.config) {
return app.navigate('signin');
}
app.view = new MainView({
model: me,
el: document.body
});
app.view.render();
var rosterVer = localStorage.rosterVersion;
var config = JSON.parse(localStorage.config);
config.rosterVer = rosterVer;
var client = window.client = app.client = XMPP.createClient(config);
xmppEventHandlers(client, app);
client.connect();
// we have what we need, we can now start our router and show the appropriate page
app.history.start({pushState: true, root: '/'});
cb();
}
]);
});
},
whenConnected: function (func) {
if (client.sessionStarted) {
func();
} else {
client.once('session:started', func);
}
},
navigate: function (page) {
var url = (page.charAt(0) === '/') ? page.slice(1) : page;
app.history.navigate(url, true);
},
renderPage: function (view, animation) {
var container = $('#pages');
if (app.currentPage) {
app.currentPage.hide(animation);
}
// we call render, but if animation is none, we want to tell the view
// to start with the active class already before appending to DOM.
container.append(view.render(animation === 'none').el);
view.show(animation);
}
};
module.exports.launch();

View File

@ -1,49 +0,0 @@
/* global app, XMPP */
var Backbone = require('backbone');
var MeModel = require('models/me');
var MainView = require('views/main');
var Router = require('router');
var xmppEventHandlers = require('helpers/xmppEventHandlers');
module.exports = {
launch: function () {
var app = window.app = this;
var me = window.me = new MeModel();
new Router();
app.history = Backbone.history;
if (!localStorage.config) {
return app.navigate('signin');
}
app.view = new MainView({
el: document.body,
model: me
}).render();
var config = JSON.parse(localStorage.config);
var client = window.client = app.client = XMPP.createClient(config);
xmppEventHandlers(client, app);
client.connect();
// we have what we need, we can now start our router and show the appropriate page
app.history.start({pushState: true, root: '/'});
},
navigate: function (page) {
var url = (page.charAt(0) === '/') ? page.slice(1) : page;
app.history.navigate(url, true);
},
renderPage: function (view, animation) {
var container = $('#pages');
if (app.currentPage) {
app.currentPage.hide(animation);
}
// we call render, but if animation is none, we want to tell the view
// to start with the active class already before appending to DOM.
container.append(view.render(animation === 'none').el);
view.show(animation);
}
};

View File

@ -1,116 +0,0 @@
/* global XMPP, client */
var StrictModel = require('strictmodel').Model;
var Resources = require('./resources');
var Messages = require('./messages');
var Message = require('./message');
var crypto = XMPP.crypto;
module.exports = StrictModel.extend({
init: function (attrs) {
if (attrs.jid) {
this.cid = attrs.jid;
}
if (!attrs.avatar) {
this.useDefaultAvatar();
}
this.resources.bind('add remove reset change', this.resourceChange, this);
},
type: 'contact',
props: {
jid: ['string', true],
name: ['string', true, ''],
subscription: ['string', true, 'none'],
groups: ['array', true, []]
},
derived: {
displayName: {
deps: ['name', 'jid'],
fn: function () {
if (this.name) {
return this.name;
}
return this.jid;
}
},
status: {
deps: ['topResourceStatus', 'offlineStatus'],
fn: function () {
if (this.topResourceStatus) {
return this.topResourceStatus;
}
return this.offlineStatus;
}
}
},
session: {
topResourceStatus: ['string', true, ''],
offlineStatus: ['string', true, ''],
idleSince: 'date',
avatar: 'string',
show: ['string', true, 'offline'],
chatState: ['string', true, 'gone'],
lockedResource: 'string'
},
collections: {
resources: Resources,
messages: Messages
},
useDefaultAvatar: function () {
this.avatar = 'https://gravatar.com/avatar/' + crypto.createHash('md5').update(this.jid).digest('hex') + '?s=30&d=mm';
},
resourceChange: function () {
// Manually propagate change events for properties that
// depend on the resources collection.
this.resources.sort();
var res = this.resources.first();
if (res) {
this.offlineStatus = '';
this.topResourceStatus = res.status;
this.show = res.show || 'online';
this.lockedResource = undefined;
} else {
this.topResourceStatus = '';
this.show = 'offline';
}
},
fetchHistory: function () {
var self = this;
client.getHistory({
with: this.jid,
rsm: {
count: 20,
before: true
}
}, function (err, res) {
if (err) return;
var results = res.mamQuery.results || [];
results.reverse();
results.forEach(function (result) {
result = result.toJSON();
var msg = result.mam.forwarded.message;
if (!msg.delay) {
msg.delay = result.mam.forwarded.delay;
}
if (msg.replace) {
var original = self.messages.get(msg.replace);
if (original) {
return original.correct(msg);
}
}
var message = new Message();
message.cid = msg.id;
delete msg.id;
message.set(msg);
self.messages.add(message);
});
});
}
});

View File

@ -1,27 +0,0 @@
/*global app*/
var BaseView = require('strictview'),
getOrCall = require('helpers/getOrCall');
module.exports = BaseView.extend({
show: function (animation) {
$('body').scrollTop(0);
// set the class so it comes into view
//this.$el.addClass('active');
// store reference to current page
app.currentPage = this;
// set the document title
document.title = getOrCall(this, 'title') + ' • Stanza.io';
// trigger an event to the page model in case we want to respond
this.trigger('pageloaded');
return this;
},
hide: function () {
var self = this;
// tell the model we're bailing
this.trigger('pageunloaded');
// unbind all events bound for this view
this.remove();
return this;
}
});

View File

@ -1,31 +0,0 @@
/*global app*/
var BasePage = require('pages/base');
var templates = require('templates');
var ContactListItem = require('views/contactListItem');
var ContactListItemResource = require('views/contactListItemResource');
var Message = require('views/message');
module.exports = BasePage.extend({
template: templates.pages.chat,
initialize: function (spec) {
this.render();
},
imageBindings: {
avatar: 'header .avatar'
},
contentBindings: {
name: 'header .name'
},
render: function () {
this.basicRender();
this.collectomatic(me.contacts, ContactListItem, {
containerEl: this.$('#contactList')
}, {quick: true});
this.collectomatic(this.model.messages, Message, {
containerEl: this.$('#conversation')
}, {quick: true});
this.handleBindings();
return this;
}
});

View File

@ -1,20 +0,0 @@
/*global app*/
var BasePage = require('pages/base');
var templates = require('templates');
var ContactListItem = require('views/contactListItem');
module.exports = BasePage.extend({
template: templates.pages.main,
initialize: function (spec) {
this.render();
},
render: function () {
this.basicRender();
this.collectomatic(me.contacts, ContactListItem, {
containerEl: this.$('#contactList')
}, {quick: true});
this.handleBindings();
return this;
}
});

View File

@ -1,21 +0,0 @@
/*global ui, app*/
var BasePage = require('pages/base'),
templates = require('templates');
module.exports = BasePage.extend({
template: templates.layout,
classBindings: {
},
contentBindings: {
},
hrefBindings: {
},
events: {
},
render: function () {
this.$el.html(this.template());
this.handleBindings();
return this;
}
});

View File

@ -1,3 +1,5 @@
"use strict";
// get a property that's a function or direct property
module.exports = function (obj, propName) {
if (obj[propName] instanceof Function) {

View File

@ -1,11 +1,12 @@
/* global XMPP */
/*global XMPP, me, app, client*/
"use strict";
var crypto = XMPP.crypto;
var _ = require('underscore');
var imageToDataURI = require('image-to-data-uri');
var Contact = require('models/contact');
var Resource = require('models/resource');
var Message = require('models/message');
var Contact = require('../models/contact');
var Resource = require('../models/resource');
var Message = require('../models/message');
function logScroll() {
@ -62,8 +63,12 @@ module.exports = function (client, app) {
client.getRoster(function (err, resp) {
resp = resp.toJSON();
localStorage.rosterVersion = resp.roster.ver;
_.each(resp.roster.items, function (item) {
me.contacts.add(item);
console.log(item);
me.setContact(item, true);
});
client.updateCaps();
@ -75,22 +80,22 @@ module.exports = function (client, app) {
});
client.on('roster:update', function (iq) {
var items = iq.toJSON().roster.items;
iq = iq.toJSON();
var items = iq.roster.items;
localStorage.rosterVersion = iq.roster.ver;
_.each(items, function (item) {
var contact = me.getContact(item.jid);
if (item.subscription === 'remove') {
if (contact) {
me.contacts.remove(contact);
me.removeContact(contact);
}
return;
}
if (contact) {
contact.set(item);
} else {
me.contacts.add(item);
}
me.setContact(item, false);
});
});
@ -132,16 +137,13 @@ module.exports = function (client, app) {
client.on('avatar', function (info) {
var contact = me.getContact(info.jid);
if (contact) {
var id = '';
var type = 'image/png';
if (info.avatars.length > 0) {
client.getAvatar(info.jid, info.avatars[0].id, function (err, resp) {
if (err) return;
resp = resp.toJSON();
var avatar = resp.pubsub.retrieve.item.avatarData;
contact.avatar = 'data:' + info.avatars[0].type + ';base64,' + avatar;
});
} else {
contact.useDefaultAvatar();
id = info.avatars[0].id;
type = info.avatars[0].type || 'image/png';
}
contact.setAvatar(id, type);
}
});
@ -161,8 +163,16 @@ module.exports = function (client, app) {
if (contact && !msg.replace) {
var message = new Message();
message.cid = msg.id;
delete msg.id;
message.set(msg);
if (msg.archived) {
msg.archived.forEach(function (archived) {
if (me.isMe(archived.by)) {
message.id = archived.id;
}
});
}
contact.messages.add(message);
if (!contact.lockedResource) {
contact.lockedResource = msg.from;
@ -216,16 +226,4 @@ module.exports = function (client, app) {
client.emit('message', msg);
});
client.on('message:sent', function (msg) {
var contact = me.getContact(msg.to);
msg = msg.toJSON();
if (contact && msg.body) {
var message = new Message();
message.cid = msg.id;
delete msg.id;
message.set(msg);
contact.messages.add(message);
}
});
};

File diff suppressed because one or more lines are too long

View File

@ -422,7 +422,10 @@ Client.prototype.sendMessage = function (data) {
}
var message = new Message(data);
this.send(new Message(data));
this.emit('message:sent', message);
this.send(message);
return data.id;
};
Client.prototype.sendPresence = function (data) {
@ -431,6 +434,8 @@ Client.prototype.sendPresence = function (data) {
data.id = this.nextId();
}
this.send(new Presence(data));
return data.id;
};
Client.prototype.sendIq = function (data, cb) {
@ -449,6 +454,8 @@ Client.prototype.sendIq = function (data, cb) {
});
}
this.send(new Iq(data));
return data.id;
};
Client.prototype.getRoster = function (cb) {
@ -2811,34 +2818,6 @@ Result.prototype = {
};
function Archived(data, xml) {
return stanza.init(this, xml, data);
}
Archived.prototype = {
constructor: {
value: Result
},
NS: 'urn:xmpp:mam:tmp',
EL: 'archived',
_name: 'archived',
_eventname: 'mam:archived',
toString: stanza.toString,
toJSON: stanza.toJSON,
get by() {
return stanza.getAttribute(this.xml, 'by');
},
set by(value) {
stanza.setAttribute(this.xml, 'by', value);
},
get id() {
return stanza.getAttribute(this.xml, 'id');
},
set id(value) {
stanza.setAttribute(this.xml, 'id', value);
}
};
function Prefs(data, xml) {
return stanza.init(this, xml, data);
}
@ -2904,10 +2883,37 @@ Prefs.prototype = {
stanza.extend(Iq, MAMQuery);
stanza.extend(Iq, Prefs);
stanza.extend(Message, Result);
stanza.extend(Message, Archived);
stanza.extend(Result, Forwarded);
stanza.extend(MAMQuery, RSM);
Message.prototype.__defineGetter__('archived', function () {
var self = this;
var archives = stanza.find(this.xml, 'urn:xmpp:mam:tmp', 'archived');
var results = [];
archives.forEach(function (archive) {
results.push({
by: stanza.getAttribute(archive, 'by'),
id: stanza.getAttribute(archive, 'id')
});
});
return results;
});
Message.prototype.__defineSetter__('archived', function (value) {
var self = this;
value.forEach(function (val) {
var archive = document.createElementNS('urn:xmpp:mam:tmp', 'archived');
stanza.setAttribute(archive, 'by', val.by);
stanza.setAttribute(archive, 'id', val.id);
self.xml.appendChild(archive);
});
});
exports.MAMQuery = MAMQuery;
exports.Result = Result;

View File

@ -1,3 +1,5 @@
"use strict";
// our base collection
var Backbone = require('backbone');

179
clientapp/models/contact.js Normal file
View File

@ -0,0 +1,179 @@
/*global XMPP, app, me, client*/
"use strict";
var async = require('async');
var StrictModel = require('strictmodel').Model;
var imageToDataURI = require('image-to-data-uri');
var Resources = require('./resources');
var Messages = require('./messages');
var Message = require('./message');
var crypto = XMPP.crypto;
module.exports = StrictModel.extend({
initialize: function (attrs) {
if (attrs.jid) {
this.cid = attrs.jid;
}
this.setAvatar(attrs.avatarID);
this.resources.bind('add remove reset change', this.resourceChange, this);
},
type: 'contact',
props: {
jid: ['string', true],
name: ['string', true, ''],
subscription: ['string', true, 'none'],
groups: ['array', true, []],
avatarID: ['string', true, '']
},
derived: {
displayName: {
deps: ['name', 'jid'],
fn: function () {
if (this.name) {
return this.name;
}
return this.jid;
}
},
status: {
deps: ['topResourceStatus', 'offlineStatus'],
fn: function () {
if (this.topResourceStatus) {
return this.topResourceStatus;
}
return this.offlineStatus;
}
},
lockedJID: {
deps: ['jid', 'lockedResource'],
fn: function () {
if (this.lockedResource) {
return this.jid + '/' + this.lockedResource;
}
return this.jid;
}
}
},
session: {
topResourceStatus: ['string', true, ''],
offlineStatus: ['string', true, ''],
idleSince: 'date',
avatar: 'string',
show: ['string', true, 'offline'],
chatState: ['string', true, 'gone'],
lockedResource: 'string',
lastSentMessage: 'object'
},
collections: {
resources: Resources,
messages: Messages
},
setAvatar: function (id, type) {
var self = this;
if (!id) {
var gID = crypto.createHash('md5').update(this.jid).digest('hex');
self.avatar = 'https://gravatar.com/avatar/' + gID + '?s=30&d=mm';
return;
}
app.storage.avatars.get(id, function (err, avatar) {
if (err) {
if (!type) {
// We can't find the ID, and we don't know the type, so fallback.
var gID = crypto.createHash('md5').update(self.jid).digest('hex');
self.avatar = 'https://gravatar.com/avatar/' + gID + '?s=30&d=mm';
return;
}
app.whenConnected(function () {
client.getAvatar(self.jid, id, function (err, resp) {
if (err) return;
resp = resp.toJSON();
var avatarData = resp.pubsub.retrieve.item.avatarData;
var dataURI = 'data:' + type + ';base64,' + avatarData;
app.storage.avatars.add({id: id, uri: dataURI});
self.set({
avatar: dataURI,
avatarID: id
});
self.save();
});
});
} else {
self.set({
avatar: avatar.uri,
avatarID: avatar.id
});
self.save();
}
});
},
resourceChange: function () {
// Manually propagate change events for properties that
// depend on the resources collection.
this.resources.sort();
var res = this.resources.first();
if (res) {
this.offlineStatus = '';
this.topResourceStatus = res.status;
this.show = res.show || 'online';
this.lockedResource = undefined;
} else {
this.topResourceStatus = '';
this.show = 'offline';
}
},
fetchHistory: function () {
var self = this;
app.whenConnected(function () {
client.getHistory({
with: self.jid,
rsm: {
count: 20,
before: true
}
}, function (err, res) {
if (err) return;
var results = res.mamQuery.results || [];
results.reverse();
results.forEach(function (result) {
result = result.toJSON();
var msg = result.mam.forwarded.message;
if (!msg.delay) {
msg.delay = result.mam.forwarded.delay;
}
if (msg.replace) {
var original = self.messages.get(msg.replace);
if (original) {
return original.correct(msg);
}
}
var message = new Message();
message.cid = msg.id || result.mam.id;
message.set(msg);
self.messages.add(message);
});
});
});
},
save: function () {
var data = {
jid: this.jid,
name: this.name,
groups: this.groups,
subscription: this.subscription,
avatarID: this.avatarID
};
app.storage.roster.add(data);
}
});

View File

@ -1,3 +1,7 @@
/*global app*/
"use strict";
var async = require('async');
var BaseCollection = require('./baseCollection');
var Contact = require('./contact');
@ -40,13 +44,20 @@ module.exports = BaseCollection.extend({
}
},
initialize: function (model, options) {
var self = this;
this.bind('change', this.orderChange, this);
this.bind('add', this.fetchHistory, this);
app.storage.roster.getAll(function (err, contacts) {
if (err) return;
contacts.forEach(function (contact) {
contact = new Contact(contact);
contact.save();
self.add(contact);
});
});
},
orderChange: function () {
this.sort();
},
fetchHistory: function (contact) {
contact.fetchHistory();
}
});

View File

@ -1,5 +1,9 @@
/*global app*/
"use strict";
var StrictModel = require('strictmodel');
var Contacts = require('./contacts');
var Contact = require('./contact');
module.exports = StrictModel.Model.extend({
@ -34,6 +38,21 @@ module.exports = StrictModel.Model.extend({
}
return this.contacts.get(jid);
},
setContact: function (data, create) {
var contact = this.getContact(data.jid);
if (contact) {
contact.set(data);
contact.save();
} else if (create) {
contact = new Contact(data);
contact.save();
this.contacts.add(contact);
}
},
removeContact: function (jid) {
this.contacts.remove(jid);
app.storage.roster.remove(jid);
},
isMe: function (jid) {
var hasResource = jid.indexOf('/') > 0;
if (hasResource) {

View File

@ -1,17 +1,24 @@
/*global me*/
"use strict";
var StrictModel = require('strictmodel').Model;
module.exports = StrictModel.extend({
init: function (attrs) {
initialize: function (attrs) {
console.log(attrs);
this._created = Date.now();
},
type: 'message',
idDefinition: {
type: 'string'
},
props: {
to: ['string', true, ''],
from: ['string', true, ''],
body: ['string', true, ''],
type: ['string', true, 'normal'],
acked: ['bool', true, false],
acked: ['bool', true, false]
},
derived: {
mine: {
@ -38,7 +45,10 @@ module.exports = StrictModel.extend({
formattedTime: {
deps: ['created'],
fn: function () {
return this.created.format('{MM}/{dd} {h}:{mm}{t}');
if (this.created) {
return this.created.format('{MM}/{dd} {h}:{mm}{t}');
}
return undefined;
}
}
},
@ -46,7 +56,7 @@ module.exports = StrictModel.extend({
_created: 'date',
receiptReceived: ['bool', true, false],
edited: ['bool', true, false],
delay: 'object',
delay: 'object'
},
correct: function (msg) {
if (this.from !== msg.from) return;

View File

@ -1,3 +1,5 @@
"use strict";
var BaseCollection = require('./baseCollection');
var Message = require('./message');

View File

@ -1,8 +1,10 @@
"use strict";
var StrictModel = require('strictmodel').Model;
module.exports = StrictModel.extend({
init: function () {},
initialize: function () {},
type: 'resource',
session: {
jid: ['string', true],

View File

@ -1,3 +1,5 @@
"use strict";
var BaseCollection = require('./baseCollection');
var Resource = require('./resource');

View File

@ -1,28 +0,0 @@
// follow @HenrikJoreteg and @andyet if you like this ;)
(function (window) {
var ls = window.localStorage,
out = {},
inNode = typeof process !== 'undefined';
if (inNode) {
module.exports = console;
return;
}
if (ls && ls.debug && window.console) {
out = window.console;
} else {
var methods = "assert,count,debug,dir,dirxml,error,exception,group,groupCollapsed,groupEnd,info,log,markTimeline,profile,profileEnd,time,timeEnd,trace,warn".split(","),
l = methods.length,
fn = function () {};
while (l--) {
out[methods[l]] = fn;
}
}
if (typeof exports !== 'undefined') {
module.exports = out;
} else {
window.console = out;
}
})(this);

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +0,0 @@
// simple commonJS cookie reader, best perf according to http://jsperf.com/cookie-parsing
module.exports = function (name) {
var cookie = document.cookie,
setPos = cookie.indexOf(name + '='),
stopPos = cookie.indexOf(';', setPos),
res;
if (!~setPos) return null;
res = decodeURIComponent(cookie.substring(setPos, ~stopPos ? stopPos : undefined).split('=')[1]);
return (res.charAt(0) === '{') ? JSON.parse(res) : res;
};

View File

@ -1,229 +0,0 @@
(function () {
// reference to our currently focused element
var focusedEl;
function getFluidGridFunction(selector) {
return function (focus) {
reOrganize(selector, focus);
};
}
function biggestBox(container, aspectRatio) {
var aspectRatio = aspectRatio || (3 / 4),
height = (container.width * aspectRatio),
res = {};
if (height > container.height) {
return {
height: container.height,
width: container.height / aspectRatio
};
} else {
return {
width: container.width,
height: container.width * aspectRatio
};
}
}
function reOrganize(selector, focus) {
var floor = Math.floor,
elements = $(selector),
howMany = elements.length,
howManyNonFocused = function () {
var hasFocused = !!elements.find('.focused').length;
if (hasFocused && howMany > 1) {
return howMany - 1;
} else if (hasFocused && howMany === 1) {
return 1;
} else {
return howMany;
}
}(),
totalAvailableWidth = window.innerWidth,
totalAvailableHeight = window.innerHeight - 140,
availableWidth = totalAvailableWidth,
availableHeight = totalAvailableHeight,
container = {
width: availableWidth,
height: availableHeight
},
columnPadding = 15,
minimumWidth = 290,
aspectRatio = 3 / 4,
numberOfColumns,
numberOfRows,
numberOfPaddingColumns,
numberOfPaddingRows,
itemDimensions,
totalWidth,
videoWidth,
leftMargin,
videoHeight,
usedHeight,
topMargin,
// do we have one selected?
// this is because having a single
// focused element is not treated
// differently, but we don't want to
// lose that reference.
haveFocusedEl;
// if we passed in a string here (could be "none")
// then we want to either set or clear our current
// focused element.
if (focus) focusedEl = $(focus)[0];
// make sure our cached focused element is still
// attached.
if (focusedEl && !$(focusedEl).parent().length) focusedEl = undefined;
// figure out if we should consider us as having any
// special focused elements
haveFocusedEl = focusedEl && howManyNonFocused > 1;
elements.height(availableHeight);
// how we want the to stack at different numbers
if (haveFocusedEl) {
numberOfColumns = howManyNonFocused - 1;
numberOfRows = 1;
availableHeight = totalAvailableHeight * .2;
} else if (howManyNonFocused === 0) {
return;
} else if (howManyNonFocused === 1) {
numberOfColumns = 1;
numberOfRows = 1;
} else if (howManyNonFocused === 2) {
if (availableWidth > availableHeight) {
numberOfColumns = 2;
numberOfRows = 1;
} else {
numberOfColumns = 1;
numberOfRows = 2;
}
} else if (howManyNonFocused === 3) {
if (availableWidth > availableHeight) {
numberOfColumns = 3;
numberOfRows = 1;
} else {
numberOfColumns = 1;
numberOfRows = 3;
}
} else if (howManyNonFocused === 4) {
numberOfColumns = 2;
numberOfRows = 2;
} else if (howManyNonFocused === 5) {
numberOfColumns = 3;
numberOfRows = 2;
} else if (howManyNonFocused === 6) {
if (availableWidth > availableHeight) {
numberOfColumns = 3;
numberOfRows = 2;
} else {
numberOfColumns = 2;
numberOfRows = 3;
}
}
itemDimensions = biggestBox({
width: availableWidth / numberOfColumns,
height: availableHeight / numberOfRows
});
numberOfPaddingColumns = numberOfColumns - 1;
numberOfPaddingRows = numberOfRows - 1;
totalWidth = itemDimensions.width * numberOfColumns;
videoWidth = function () {
var totalWidthLessPadding = totalWidth - (columnPadding * numberOfPaddingColumns);
return totalWidthLessPadding / numberOfColumns;
}();
leftMargin = (availableWidth - totalWidth) / 2;
videoHeight = itemDimensions.height - ((numberOfRows > 1) ? (columnPadding / numberOfRows) : 0);
usedHeight = (numberOfRows * videoHeight);
topMargin = (availableHeight - usedHeight) / 2;
if (haveFocusedEl) {
elements = elements.not('.focused');
}
elements.each(function (index) {
var order = index,
row = floor(order / numberOfColumns),
column = order % numberOfColumns,
intensity = 12,
rotation = function () {
if (numberOfColumns === 3) {
if (column === 0) {
return 1;
} else if (column === 1) {
return 0;
} else if (column === 2) {
return -1
}
} else if (numberOfColumns === 2) {
intensity = 5;
return column == 1 ? -1 : 1
} else if (numberOfColumns === 1) {
return 0;
}
}(),
transformation = 'rotateY(' + (rotation * intensity) + 'deg)';
if (rotation === 0) {
transformation += ' scale(.98)';
}
var calculatedTop;
if (haveFocusedEl) {
calculatedTop = (totalAvailableHeight * .8) + topMargin + 'px';
} else {
calculatedTop = (row * itemDimensions.height) + topMargin + 'px';
}
$(this).css({
//transform: transformation,
top: calculatedTop,
left: (column * itemDimensions.width) + leftMargin + 'px',
width: videoWidth + 'px',
height: videoHeight + 'px',
position: 'absolute'
});
});
if (haveFocusedEl) {
var focusSize = biggestBox({
height: (totalAvailableHeight * .8),
width: totalAvailableWidth
}, focusedEl.videoHeight / focusedEl.videoWidth);
$(focusedEl).css({
top: 0,
height: focusSize.height - topMargin,
width: focusSize.width,
left: (totalAvailableWidth / 2) - (focusSize.width / 2)
});
}
}
if (typeof exports !== 'undefined') {
module.exports = getFluidGridFunction;
} else {
window.getFluidGridFunction = getFluidGridFunction;
}
})();

View File

@ -1,31 +0,0 @@
// converts a URL of an image into a dataURI
module.exports = function (url, cb) {
// Create an empty canvas and image elements
var canvas = document.createElement('canvas'),
img = document.createElement('img');
img.onload = function () {
var ctx = canvas.getContext('2d');
// match size of image
canvas.width = img.width;
canvas.height = img.height;
// Copy the image contents to the canvas
ctx.drawImage(img, 0, 0);
// Get the data-URI formatted image
cb(canvas.toDataURL('image/png'));
};
img.ononerror = function () {
cb(new Error('FailedToLoadImage'));
};
// canvas is not supported
if (!canvas.getContext) {
cb(new Error('CanvasIsNotSupported'));
} else {
img.src = url;
}
};

View File

@ -1,21 +0,0 @@
// replaces all whitespace with '-' and removes
// all non-url friendly characters
(function () {
var whitespace = /\s+/g,
nonAscii = /[^A-Za-z0-9_ \-]/g;
function slugger(string, opts) {
var maintainCase = opts && opts.maintainCase || false,
replacement = opts && opts.replacement || '-',
key;
if (typeof string !== 'string') return '';
if (!maintainCase) string = string.toLowerCase();
return string.replace(nonAscii, '').replace(whitespace, replacement);
};
if (typeof module !== 'undefined') {
module.exports = slugger;
} else {
window.slugger = slugger;
}
})();

View File

@ -1,140 +0,0 @@
/*
SoundEffectManager
Loads and plays sound effects useing
HTML5 Web Audio API (as only available in webkit, at the moment).
By @HenrikJoreteg from &yet
*/
/*global webkitAudioContext define*/
(function () {
var root = this;
function SoundEffectManager() {
this.support = !!window.webkitAudioContext;
if (this.support) {
this.context = new webkitAudioContext();
}
this.sounds = {};
}
// async load a file at a given URL, store it as 'name'.
SoundEffectManager.prototype.loadFile = function (url, name, delay, cb) {
if (this.support) {
this._loadWebAudioFile(url, name, delay, cb);
} else {
this._loadWaveFile(url.replace('.mp3', '.wav'), name, delay, 3, cb);
}
};
// async load a file at a given URL, store it as 'name'.
SoundEffectManager.prototype._loadWebAudioFile = function (url, name, delay, cb) {
if (!this.support) return;
var self = this,
request = new XMLHttpRequest();
request.open("GET", url, true);
request.responseType = "arraybuffer";
request.onload = function () {
self.sounds[name] = self.context.createBuffer(request.response, true);
cb && cb();
};
setTimeout(function () {
request.send();
}, delay || 0);
};
SoundEffectManager.prototype._loadWaveFile = function (url, name, delay, multiplexLimit, cb) {
var self = this,
limit = multiplexLimit || 3;
setTimeout(function () {
var a, i = 0;
self.sounds[name] = [];
while (i < limit) {
a = new Audio();
a.src = url;
// for our callback
if (i === 0 && cb) {
a.addEventListener('canplaythrough', cb, false);
}
a.load();
self.sounds[name][i++] = a;
}
}, delay || 0);
};
SoundEffectManager.prototype._playWebAudio = function (soundName) {
var buffer = this.sounds[soundName],
source;
if (!buffer) return;
// creates a sound source
source = this.context.createBufferSource();
// tell the source which sound to play
source.buffer = buffer;
// connect the source to the context's destination (the speakers)
source.connect(this.context.destination);
// play it
source.noteOn(0);
};
SoundEffectManager.prototype._playWavAudio = function (soundName, loop) {
var self = this,
audio = this.sounds[soundName],
howMany = audio && audio.length || 0,
i = 0,
currSound;
if (!audio) return;
while (i < howMany) {
currSound = audio[i++];
// this covers case where we loaded an unplayable file type
if (currSound.error) return;
if (currSound.currentTime === 0 || currSound.currentTime === currSound.duration) {
currSound.currentTime = 0;
currSound.loop = !!loop;
i = howMany;
return currSound.play();
}
}
};
SoundEffectManager.prototype.play = function (soundName, loop) {
if (this.support) {
this._playWebAudio(soundName, loop);
} else {
return this._playWavAudio(soundName, loop);
}
};
SoundEffectManager.prototype.stop = function (soundName) {
if (this.support) {
// TODO: this
} else {
var soundArray = this.sounds[soundName],
howMany = soundArray && soundArray.length || 0,
i = 0,
currSound;
while (i < howMany) {
currSound = soundArray[i++];
currSound.pause();
currSound.currentTime = 0;
}
}
};
// attach to window or export with commonJS
if (typeof module !== "undefined") {
module.exports = SoundEffectManager;
} else if (typeof root.define === "function" && define.amd) {
root.define(SoundEffectManager);
} else {
root.SoundEffectManager = SoundEffectManager;
}
})();

View File

@ -1,565 +0,0 @@
// (c) 2013 Henrik Joreteg
// MIT Licensed
// For all details and documentation:
// https://github.com/HenrikJoreteg/StrictModel
(function () {
'use strict';
// Initial setup
// -------------
// Establish the root object, `window` in the browser, or `global` on the server.
var root = this;
// The top-level namespace. All public Backbone classes and modules will
// be attached to this. Exported for both CommonJS and the browser.
var Strict = typeof exports !== 'undefined' ? exports : root.Strict = {},
toString = Object.prototype.toString,
slice = Array.prototype.slice;
// Current version of the library. Keep in sync with `package.json`.
Strict.VERSION = '0.0.1';
// Require Underscore, if we're on the server, and it's not already present.
var _ = root._;
if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
// Require Backbone, if we're on the server, and it's not already present.
var Backbone = root.Backbone;
if (!Backbone && (typeof require !== 'undefined')) Backbone = require('backbone');
// Backbone Collection compatibility fix:
// In backbone, when you add an already instantiated model to a collection
// the collection checks to see if what you're adding is already a model
// the problem is, it does this witn an instanceof check. We're wanting to
// use completely different models so the instanceof will fail even if they
// are "real" models. So we work around this by overwriting this method from
// backbone 1.0.0. The only difference is it compares against our Strict.Model
// instead of backbone's.
Backbone.Collection.prototype._prepareModel = function (attrs, options) {
if (attrs instanceof Strict.Model) {
if (!attrs.collection) attrs.collection = this;
return attrs;
}
options || (options = {});
options.collection = this;
var model = new this.model(attrs, options);
if (!model._validate(attrs, options)) {
this.trigger('invalid', this, attrs, options);
return false;
}
return model;
};
// Helpers
// -------
// Shared empty constructor function to aid in prototype-chain creation.
var Constructor = function () {};
// Helper function to correctly set up the prototype chain, for subclasses.
// Similar to `goog.inherits`, but uses a hash of prototype properties and
// class properties to be extended.
var inherits = function (parent, protoProps, staticProps) {
var child;
// The constructor function for the new subclass is either defined by you
// (the "constructor" property in your `extend` definition), or defaulted
// by us to simply call the parent's constructor.
if (protoProps && protoProps.hasOwnProperty('constructor')) {
child = protoProps.constructor;
} else {
child = function () { return parent.apply(this, arguments); };
}
// Inherit class (static) properties from parent.
_.extend(child, parent);
// Set the prototype chain to inherit from `parent`, without calling
// `parent`'s constructor function.
Constructor.prototype = parent.prototype;
child.prototype = new Constructor();
// Add prototype properties (instance properties) to the subclass,
// if supplied.
if (protoProps) _.extend(child.prototype, protoProps);
// Add static properties to the constructor function, if supplied.
if (staticProps) _.extend(child, staticProps);
// Correctly set child's `prototype.constructor`.
child.prototype.constructor = child;
// Set a convenience property in case the parent's prototype is needed later.
child.__super__ = parent.prototype;
return child;
};
var extend = function (protoProps, classProps) {
var child = inherits(this, protoProps, classProps);
child.extend = this.extend;
return child;
};
// Mixins
// ------
// Sugar for defining properties a la ES5.
var Mixins = Strict.Mixins = {
// shortcut for Object.defineProperty
define: function (name, def) {
Object.defineProperty(this, name, def);
},
defineGetter: function (name, handler) {
this.define(name, {
get: handler.bind(this)
});
},
defineSetter: function (name, handler) {
this.define(name, {
set: handler.bind(this)
});
}
};
// Strict.Registry
// ---------------
// Internal storage for models, seperate namespace
// storage from default to prevent collision of matching
// model type+id and namespace name
var Registry = Strict.Registry = function () {
this._cache = {};
this._namespaces = {};
};
// Attach all inheritable methods to the Registry prototype.
_.extend(Registry.prototype, {
// Get the general or namespaced internal cache
_getCache: function (ns) {
if (ns) {
this._namespaces[ns] || (this._namespaces[ns] = {});
return this._namespaces[ns];
}
return this._cache;
},
// Find the cached model
lookup: function (type, id, ns) {
var cache = this._getCache(ns);
return cache && cache[type + id];
},
// Add a model to the cache if it has not already been set
store: function (model) {
var cache = this._getCache(model._namespace),
key = model.type + model.id;
// Prevent overriding a previously stored model
cache[key] = cache[key] || model;
return this;
},
// Remove a stored model from the cache, return `true` if removed
remove: function (type, id, ns) {
var cache = this._getCache(ns);
if (this.lookup.apply(this, arguments)) {
delete cache[type + id];
return true;
}
return false;
},
// Reset internal cache
clear: function () {
this._cache = {};
this._namespaces = {};
}
});
// Create the default Strict.registry.
Strict.registry = new Registry();
// Strict.Model
// ------------
var Model = Strict.Model = function (attrs, options) {
attrs = attrs || {};
options = options || {};
var modelFound,
opts = _.defaults(options || {}, {
seal: true
});
this._namespace = opts.namespace;
this._initted = false;
this._deps = {};
this._initProperties();
this._initCollections();
this._cache = {};
this._verifyRequired();
this.set(attrs, {silent: true});
this.init.apply(this, arguments);
if (attrs.id) Strict.registry.store(this);
this._previous = _.clone(this.attributes); // Should this be set right away?
this._initted = true;
};
// Attach all inheritable methods to the Model prototype.
_.extend(Model.prototype, Backbone.Events, Mixins, {
idAttribute: 'id',
idDefinition: {
type: 'number',
setOnce: true
},
// stubbed out to be overwritten
init: function () {
return this;
},
// Remove model from the registry and unbind events
remove: function () {
if (this.id) {
Strict.registry.remove(this.type, this.id, this._namespace);
}
this.trigger('remove', this);
this.off();
return this;
},
set: function (key, value, options) {
var self = this,
changing = self._changing,
opts,
changes = [],
newType,
interpretedType,
newVal,
def,
attr,
attrs,
val;
self._changing = true;
// Handle both `"key", value` and `{key: value}` -style arguments.
if (_.isObject(key) || key === null) {
attrs = key;
options = value;
} else {
attrs = {};
attrs[key] = value;
}
opts = options || {};
// For each `set` attribute...
for (attr in attrs) {
val = attrs[attr];
newType = typeof val;
newVal = val;
def = this.definition[attr] || {};
// check type if we have one
if (def.type === 'date') {
if (!_.isDate(val)) {
try {
newVal = (new Date(parseInt(val, 10))).valueOf();
newType = 'date';
} catch (e) {
newType = typeof val;
}
} else {
newType = 'date';
newVal = val.valueOf();
}
} else if (def.type === 'array') {
newType = _.isArray(val) ? 'array' : typeof val;
} else if (def.type === 'object') {
// we have to have a way of supporting "missing" objects.
// Null is an object, but setting a value to undefined
// should work too, IMO. We just override it, in that case.
if (typeof val !== 'object' && _.isUndefined(val)) {
newVal = null;
newType = 'object';
}
}
// If we have a defined type and the new type doesn't match, throw error.
// Unless it's not required and the value is undefined.
if (def.type && def.type !== newType && (!def.required && !_.isUndefined(val))) {
throw new TypeError('Property \'' + attr + '\' must be of type ' + def.type + '. Tried to set ' + val);
}
// if trying to set id after it's already been set
// reject that
if (def.setOnce && def.value !== undefined && !_.isEqual(def.value, newVal)) {
throw new TypeError('Property \'' + key + '\' can only be set once.');
}
// only change if different
if (!_.isEqual(def.value, newVal)) {
self._previous && (self._previous[attr] = def.value);
def.value = newVal;
changes.push(attr);
}
}
_.each(changes, function (key) {
if (!opts.silent) {
self.trigger('change:' + key, self, self[key]);
}
// TODO: ensure that all deps are not undefined before triggering a change event
(self._deps[key] || []).forEach(function (derTrigger) {
// blow away our cache
delete self._cache[derTrigger];
if (!opts.silent) self.trigger('change:' + derTrigger, self, self.derived[derTrigger]);
});
});
// fire general change events
if (changes.length) {
if (!opts.silent) self.trigger('change', self);
}
},
get: function (attr) {
return this[attr];
},
// convenience methods for manipulating array properties
addListVal: function (prop, value, prepend) {
var list = _.clone(this[prop]) || [];
if (!_(list).contains(value)) {
list[prepend ? 'unshift' : 'push'](value);
this[prop] = list;
}
return this;
},
previous: function (attr) {
return attr ? this._previous[attr] : _.clone(this._previous);
},
removeListVal: function (prop, value) {
var list = _.clone(this[prop]) || [];
if (_(list).contains(value)) {
this[prop] = _(list).without(value);
}
return this;
},
hasListVal: function (prop, value) {
return _.contains(this[prop] || [], value);
},
// -----------------------------------------------------------------------
_initCollections: function () {
var coll;
if (!this.collections) return;
for (coll in this.collections) {
this[coll] = new this.collections[coll]();
this[coll].parent = this;
}
},
// Check that all required attributes are present
// TODO: should this throw an error or return boolean?
_verifyRequired: function () {
var attrs = this.attributes;
for (var def in this.definition) {
if (this.definition[def].required && typeof attrs[def] === 'undefined') {
return false;
}
}
return true;
},
_initProperties: function () {
var self = this,
definition = this.definition = {},
val,
prop,
item,
type,
filler;
this.cid = _.uniqueId('model');
function addToDef(name, val, isSession) {
var def = definition[name] = {};
if (_.isString(val)) {
// grab our type if all we've got is a string
type = self._ensureValidType(val);
if (type) def.type = type;
} else {
type = self._ensureValidType(val[0] || val.type);
if (type) def.type = type;
if (val[1] || val.required) def.required = true;
// set default if defined
def.value = !_.isUndefined(val[2]) ? val[2] : val.default;
if (isSession) def.session = true;
if (val.setOnce) def.setOnce = true;
}
}
// loop through given properties
for (item in this.props) {
addToDef(item, this.props[item]);
}
// loop through session props
for (prop in this.session) {
addToDef(prop, this.session[prop], true);
}
// always add "id" as a definition or make sure it's 'setOnce'
if (definition.id) {
definition[this.idAttribute].setOnce = true;
} else {
addToDef(this.idAttribute, this.idDefinition);
}
// register derived properties as part of the definition
this._registerDerived();
this._createGettersSetters();
// freeze attributes used to define object
if (this.session) Object.freeze(this.session);
//if (this.derived) Object.freeze(this.derived);
if (this.props) Object.freeze(this.props);
},
// just makes friendlier errors when trying to define a new model
// only used when setting up original property definitions
_ensureValidType: function (type) {
return _.contains(['string', 'number', 'boolean', 'array', 'object', 'date'], type) ? type : undefined;
},
_validate: function () {
return true;
},
_createGettersSetters: function () {
var item, def, desc, self = this;
// create getters/setters based on definitions
for (item in this.definition) {
def = this.definition[item];
desc = {};
// create our setter
desc.set = function (def, item) {
return function (val, options) {
self.set(item, val);
};
}(def, item);
// create our getter
desc.get = function (def, attributes) {
return function (val) {
if (typeof def.value !== 'undefined') {
if (def.type === 'date') {
return new Date(def.value);
}
return def.value;
}
return;
};
}(def);
// define our property
this.define(item, desc);
}
this.defineGetter('attributes', function () {
var res = {};
for (var item in this.definition) res[item] = this[item];
return res;
});
this.defineGetter('keys', function () {
return Object.keys(this.attributes);
});
this.defineGetter('json', function () {
return JSON.stringify(this._getAttributes(false, true));
});
this.defineGetter('derived', function () {
var res = {};
for (var item in this._derived) res[item] = this._derived[item].fn.apply(this);
return res;
});
this.defineGetter('toTemplate', function () {
return _.extend(this._getAttributes(true), this.derived);
});
},
_getAttributes: function (includeSession, raw) {
var res = {};
for (var item in this.definition) {
if (!includeSession) {
if (!this.definition[item].session) {
res[item] = (raw) ? this.definition[item].value : this[item];
}
} else {
res[item] = (raw) ? this.definition[item].value : this[item];
}
}
return res;
},
// stores an object of arrays that specifies the derivedProperties
// that depend on each attribute
_registerDerived: function () {
var self = this, depList;
if (!this.derived) return;
this._derived = this.derived;
for (var key in this.derived) {
depList = this.derived[key].deps || [];
_.each(depList, function (dep) {
self._deps[dep] = _(self._deps[dep] || []).union([key]);
});
// defined a top-level getter for derived keys
this.define(key, {
get: _.bind(function (key) {
// is this a derived property we should cache?
if (this._derived[key].cache) {
// do we have it?
if (this._cache.hasOwnProperty(key)) {
return this._cache[key];
} else {
return this._cache[key] = this._derived[key].fn.apply(this);
}
} else {
return this._derived[key].fn.apply(this);
}
}, this, key),
set: _.bind(function (key) {
var deps = this._derived[key].deps,
msg = '"' + key + '" is a derived property, you can\'t set it directly.';
if (deps && deps.length) {
throw new TypeError(msg + ' It is dependent on "' + deps.join('" and "') + '".');
} else {
throw new TypeError(msg);
}
}, this, key)
});
}
}
});
// Set up inheritance for the model
Strict.Model.extend = extend;
// Overwrite Backbone.Model so that collections don't need to be modified in Backbone core
Backbone.Model = Strict.Model;
}).call(this);

View File

@ -1,245 +0,0 @@
var Backbone = require('backbone'),
_ = require('underscore'),
templates = require('templates');
// the base view we use to build all our other views
module.exports = Backbone.View.extend({
// ###handleBindings
// This makes it simple to bind model attributes to the view.
// To use it, add a `classBindings` and/or a `contentBindings` attribute
// to your view and call `this.handleBindings()` at the end of your view's
// `render` function. It's also used by `basicRender` which lets you do
// a complete attribute-bound views with just this:
//
// var ProfileView = BaseView.extend({
// template: 'profile',
// contentBindings: {
// 'name': '.name'
// },
// classBindings: {
// 'active': ''
// },
// render: function () {
// this.basicRender();
// return this;
// }
// });
handleBindings: function () {
var self = this;
if (this.contentBindings) {
_.each(this.contentBindings, function (selector, key) {
var func = function () {
var el = (selector.length > 0) ? self.$(selector) : $(self.el);
el.html(self.model[key]);
};
self.listenTo(self.model, 'change:' + key, func);
func();
});
}
if (this.imageBindings) {
_.each(this.imageBindings, function (selector, key) {
var func = function () {
var el = (selector.length > 0) ? self.$(selector) : $(self.el);
el.attr('src', self.model[key]);
};
self.listenTo(self.model, 'change:' + key, func);
func();
});
}
if (this.hrefBindings) {
_.each(this.hrefBindings, function (selector, key) {
var func = function () {
var el = (selector.length > 0) ? self.$(selector) : $(self.el);
el.attr('href', self.model[key]);
};
self.listenTo(self.model, 'change:' + key, func);
func();
});
}
if (this.classBindings) {
_.each(this.classBindings, function (selector, key) {
var func = function () {
var newValue = self.model[key],
prevHash = self.model.previous(),
prev = _.isFunction(prevHash) ? prevHash(key) : prevHash[key],
el = (selector.length > 0) ? self.$(selector) : $(self.el);
if (_.isBoolean(newValue)) {
if (newValue) {
el.addClass(key);
} else {
el.removeClass(key);
}
} else {
if (prev) el.removeClass(prev);
el.addClass(newValue);
}
};
self.listenTo(self.model, 'change:' + key, func);
func();
});
}
if (this.inputBindings) {
_.each(this.inputBindings, function (selector, key) {
var func = function () {
var el = (selector.length > 0) ? self.$(selector) : $(self.el);
el.val(self.model[key]);
};
self.listenTo(self.model, 'change:' + key, func);
func();
});
}
return this;
},
// ###desist
// This is method we used to remove/unbind/destroy the view.
// By default we fade it out this seemed like a reasonable default for realtime apps.
// So things to just magically disappear and to give some visual indication that
// it's going away. You can also pass an options hash `{quick: true}` to remove immediately.
desist: function (opts) {
opts || (opts = {});
_.defaults(opts, {
quick: false,
animate: true,
speed: 300,
animationProps: {
height: 0,
opacity: 0
}
});
var el = $(this.el),
kill = _.bind(this.remove, this);
if (this.interval) {
clearInterval(this.interval);
delete this.interval;
}
if (opts.quick) {
kill();
} else if (opts.animate) {
el.animate(opts.animationProps, {
speed: opts.speed,
complete: kill
});
} else {
setTimeout(kill, opts.speed);
}
},
// ###addReferences
// This is a shortcut for adding reference to specific elements within your view for
// access later. This is avoids excessive DOM queries and gives makes it easier to update
// your view if your template changes. You could argue whether this is worth doing or not,
// but I like it.
// In your `render` method. Use it like so:
//
// render: function () {
// this.basicRender();
// this.addReferences({
// pages: '#pages',
// chat: '#teamChat',
// nav: 'nav#views ul',
// me: '#me',
// cheatSheet: '#cheatSheet',
// omniBox: '#awesomeSauce'
// });
// }
//
// Then later you can access elements by reference like so: `this.$pages`, or `this.$chat`.
addReferences: function (hash) {
for (var item in hash) {
this['$' + item] = $(hash[item], this.el);
}
},
// ###basicRender
// All the usual stuff when I render a view. It assumes that the view has a `template` property
// that is the name of the ICanHaz template. You can also specify the template name by passing
// it an options hash like so: `{templateKey: 'profile'}`.
basicRender: function (opts) {
var newEl;
opts || (opts = {});
_.defaults(opts, {
templateFunc: (typeof this.template === 'string') ? templates[opts.templateKey] : this.template,
context: false
});
newEl = $(opts.templateFunc(opts.contex));
$(this.el).replaceWith(newEl);
this.setElement(newEl);
this.handleBindings();
this.delegateEvents();
},
// ###subViewRender
// This is handy for views within collections when you use `collectomatic`. Just like `basicRender` it assumes
// that the view either has a `template` property or that you pass it an options object with the name of the
// `templateKey` name of the ICanHaz template.
// Additionally, it handles appending or prepending the view to its parent container.
// It takes an options arg where you can optionally specify the `templateKey` and `placement` of the element.
// If your collections is stacked newest first, just use `{plaement: 'prepend'}`.
subViewRender: function (opts) {
opts || (opts = {});
_.defaults(opts, {
placement: 'append',
templateFunc: (typeof this.template === 'string') ? templates[opts.templateKey] : this.template
});
var data = _.isFunction(this.model.toTemplate) ? this.model.toTemplate() : this.model.toTemplate,
newEl = $(opts.templateFunc(opts.context))[0];
if (!this.el.parentNode) {
$(this.containerEl)[opts.placement](newEl);
} else {
$(this.el).replaceWith(newEl);
}
this.setElement(newEl);
this.handleBindings();
},
// ### bindomatic
// Shortcut for listening and triggering
bindomatic: function (object, events, handler, opts) {
var bound = _.bind(handler, this);
this.listenTo(object, events, bound);
if (opts && opts.trigger || opts === true) bound();
},
// ###collectomatic
// Shorthand for rendering collections and their invividual views.
// Just pass it the collection, and the view to use for the items in the
// collection. (anything in the `options` arg just gets passed through to
// view. Again, props to @natevw for this.
collectomatic: function (collection, ViewClass, options, desistOptions) {
var views = {},
self = this,
refreshResetHandler;
function addView(model, collection, opts) {
var matches = self.matchesFilters ? self.matchesFilters(model) : true;
if (matches) {
views[model.cid] = new ViewClass(_({model: model}).extend(options));
views[model.cid].parent = self;
}
}
this.listenTo(collection, 'add', addView);
this.listenTo(collection, 'remove', function (model) {
if (views[model.cid]) {
views[model.cid].desist(desistOptions);
delete views[model.cid];
}
});
this.listenTo(collection, 'move', function () {
_(views).each(function (view) {
view.desist({quick: true});
});
views = {};
collection.each(addView);
});
refreshResetHandler = function (opts) {
_(views).each(function (view) {
view.desist({quick: true});
});
views = {};
collection.each(addView);
};
this.listenTo(collection, 'refresh reset sort', refreshResetHandler);
refreshResetHandler();
}
});

View File

@ -1,90 +0,0 @@
(function () {
var root = this, exports = {};
// The jade runtime:
var jade=function(exports){Array.isArray||(Array.isArray=function(arr){return"[object Array]"==Object.prototype.toString.call(arr)}),Object.keys||(Object.keys=function(obj){var arr=[];for(var key in obj)obj.hasOwnProperty(key)&&arr.push(key);return arr}),exports.merge=function merge(a,b){var ac=a["class"],bc=b["class"];if(ac||bc)ac=ac||[],bc=bc||[],Array.isArray(ac)||(ac=[ac]),Array.isArray(bc)||(bc=[bc]),ac=ac.filter(nulls),bc=bc.filter(nulls),a["class"]=ac.concat(bc).join(" ");for(var key in b)key!="class"&&(a[key]=b[key]);return a};function nulls(val){return val!=null}return exports.attrs=function attrs(obj,escaped){var buf=[],terse=obj.terse;delete obj.terse;var keys=Object.keys(obj),len=keys.length;if(len){buf.push("");for(var i=0;i<len;++i){var key=keys[i],val=obj[key];"boolean"==typeof val||null==val?val&&(terse?buf.push(key):buf.push(key+'="'+key+'"')):0==key.indexOf("data")&&"string"!=typeof val?buf.push(key+"='"+JSON.stringify(val)+"'"):"class"==key&&Array.isArray(val)?buf.push(key+'="'+exports.escape(val.join(" "))+'"'):escaped&&escaped[key]?buf.push(key+'="'+exports.escape(val)+'"'):buf.push(key+'="'+val+'"')}}return buf.join(" ")},exports.escape=function escape(html){return String(html).replace(/&(?!(\w+|\#\d+);)/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")},exports.rethrow=function rethrow(err,filename,lineno){if(!filename)throw err;var context=3,str=require("fs").readFileSync(filename,"utf8"),lines=str.split("\n"),start=Math.max(lineno-context,0),end=Math.min(lines.length,lineno+context),context=lines.slice(start,end).map(function(line,i){var curr=i+start+1;return(curr==lineno?" > ":" ")+curr+"| "+line}).join("\n");throw err.path=filename,err.message=(filename||"Jade")+":"+lineno+"\n"+context+"\n\n"+err.message,err},exports}({});
// create our folder objects
exports.includes = {};
exports.pages = {};
// contactListItem.jade compiled template
exports.includes.contactListItem = function anonymous(locals) {
var buf = [];
with (locals || {}) {
buf.push('<li class="contact"><img' + jade.attrs({
src: contact.avatar,
"class": "avatar"
}, {
src: true
}) + '/><div class="name">' + jade.escape(null == (jade.interp = contact.displayName) ? "" : jade.interp) + '</div><div class="status">' + jade.escape(null == (jade.interp = contact.status) ? "" : jade.interp) + "</div></li>");
}
return buf.join("");
};
// contactListItemResource.jade compiled template
exports.includes.contactListItemResource = function anonymous(locals) {
var buf = [];
with (locals || {}) {
buf.push('<li><p class="jid">' + jade.escape(null == (jade.interp = resource.jid) ? "" : jade.interp) + '</p><p class="status">' + jade.escape(null == (jade.interp = resource.status) ? "" : jade.interp) + "</p></li>");
}
return buf.join("");
};
// message.jade compiled template
exports.includes.message = function anonymous(locals) {
var buf = [];
with (locals || {}) {
buf.push('<li><div class="message"><span class="timestamp">' + jade.escape(null == (jade.interp = message.created) ? "" : jade.interp) + '</span><p class="body">' + jade.escape(null == (jade.interp = message.body) ? "" : jade.interp) + "</p></div></li>");
}
return buf.join("");
};
// layout.jade compiled template
exports.layout = function anonymous(locals) {
var buf = [];
with (locals || {}) {
buf.push('<div class="wrap"><header></header><div id="me"><img class="avatar"/><p class="status"></p></div><section id="pages"></section><footer></footer></div>');
}
return buf.join("");
};
// chat.jade compiled template
exports.pages.chat = function anonymous(locals) {
var buf = [];
with (locals || {}) {
buf.push('<section class="page chat"><nav id="contactList"></nav><header class="contactInfo"><img width="30" height="30" class="avatar"/><h1 class="name"></h1></header><ul id="conversation"></ul></section>');
}
return buf.join("");
};
// main.jade compiled template
exports.pages.main = function anonymous(locals) {
var buf = [];
with (locals || {}) {
buf.push('<section class="page main"><nav id="contactList"></nav><div id="log"><h2>Event Log</h2></div></section>');
}
return buf.join("");
};
// wrapper.jade compiled template
exports.pages.wrapper = function anonymous(locals) {
var buf = [];
with (locals || {}) {
buf.push('<div class="page"></div>');
}
return buf.join("");
};
// attach to window or export with commonJS
if (typeof module !== "undefined") {
module.exports = exports;
} else if (typeof define === "function" && define.amd) {
define(exports);
} else {
root.templatizer = exports;
}
})();

File diff suppressed because it is too large Load Diff

View File

@ -1,135 +0,0 @@
/*
WildEmitter.js is a slim little event emitter by @henrikjoreteg largely based
on @visionmedia's Emitter from UI Kit.
Why? I wanted it standalone.
I also wanted support for wildcard emitters like this:
emitter.on('*', function (eventName, other, event, payloads) {
});
emitter.on('somenamespace*', function (eventName, payloads) {
});
Please note that callbacks triggered by wildcard registered events also get
the event name as the first argument.
*/
module.exports = WildEmitter;
function WildEmitter() {
this.callbacks = {};
}
// Listen on the given `event` with `fn`. Store a group name if present.
WildEmitter.prototype.on = function (event, groupName, fn) {
var hasGroup = (arguments.length === 3),
group = hasGroup ? arguments[1] : undefined,
func = hasGroup ? arguments[2] : arguments[1];
func._groupName = group;
(this.callbacks[event] = this.callbacks[event] || []).push(func);
return this;
};
// Adds an `event` listener that will be invoked a single
// time then automatically removed.
WildEmitter.prototype.once = function (event, groupName, fn) {
var self = this,
hasGroup = (arguments.length === 3),
group = hasGroup ? arguments[1] : undefined,
func = hasGroup ? arguments[2] : arguments[1];
function on() {
self.off(event, on);
func.apply(this, arguments);
}
this.on(event, group, on);
return this;
};
// Unbinds an entire group
WildEmitter.prototype.releaseGroup = function (groupName) {
var item, i, len, handlers;
for (item in this.callbacks) {
handlers = this.callbacks[item];
for (i = 0, len = handlers.length; i < len; i++) {
if (handlers[i]._groupName === groupName) {
//console.log('removing');
// remove it and shorten the array we're looping through
handlers.splice(i, 1);
i--;
len--;
}
}
}
return this;
};
// Remove the given callback for `event` or all
// registered callbacks.
WildEmitter.prototype.off = function (event, fn) {
var callbacks = this.callbacks[event],
i;
if (!callbacks) return this;
// remove all handlers
if (arguments.length === 1) {
delete this.callbacks[event];
return this;
}
// remove specific handler
i = callbacks.indexOf(fn);
callbacks.splice(i, 1);
return this;
};
// Emit `event` with the given args.
// also calls any `*` handlers
WildEmitter.prototype.emit = function (event) {
var args = [].slice.call(arguments, 1),
callbacks = this.callbacks[event],
specialCallbacks = this.getWildcardCallbacks(event),
i,
len,
item;
if (callbacks) {
for (i = 0, len = callbacks.length; i < len; ++i) {
if (callbacks[i]) {
callbacks[i].apply(this, args);
} else {
break;
}
}
}
if (specialCallbacks) {
for (i = 0, len = specialCallbacks.length; i < len; ++i) {
if (specialCallbacks[i]) {
specialCallbacks[i].apply(this, [event].concat(args));
} else {
break;
}
}
}
return this;
};
// Helper for for finding special wildcard event handlers that match the event
WildEmitter.prototype.getWildcardCallbacks = function (eventName) {
var item,
split,
result = [];
for (item in this.callbacks) {
split = item.split('*');
if (item === '*' || (split.length === 2 && eventName.slice(0, split[1].length) === split[1])) {
result = result.concat(this.callbacks[item]);
}
}
return result;
};

50
clientapp/pages/base.js Normal file
View File

@ -0,0 +1,50 @@
/*global $, app*/
"use strict";
var _ = require('underscore');
var StrictView = require('strictview');
module.exports = StrictView.extend({
show: function (animation) {
var self = this;
$('body').scrollTop(0);
if (this.detached) {
this.$('#pages').append(this.el);
this.detached = false;
} else {
this.render();
}
this.$el.addClass('active');
app.currentPage = this;
document.title = function () {
var title = _.result(self, 'title');
return title ? title + '- OTalk' : 'OTalk';
}();
this.trigger('pageloaded');
return this;
},
hide: function () {
var self = this;
this.$el.removeClass('active');
this.trigger('pageunloaded');
if (this.cache) {
this.$el.detach();
this.detached = true;
} else {
this.animateRemove();
}
return this;
}
});

93
clientapp/pages/chat.js Normal file
View File

@ -0,0 +1,93 @@
/*global $, app, me, client*/
"use strict";
var BasePage = require('./base');
var templates = require('../templates');
var ContactListItem = require('../views/contactListItem');
var ContactListItemResource = require('../views/contactListItemResource');
var Message = require('../views/message');
var MessageModel = require('../models/message');
module.exports = BasePage.extend({
template: templates.pages.chat,
initialize: function (spec) {
this.editMode = false;
this.model.fetchHistory();
this.render();
},
events: {
'submit #chatInput': 'killForm',
'keydown #chatBuffer': 'handleKeyDown'
},
srcBindings: {
avatar: 'header .avatar'
},
textBindings: {
name: 'header .name'
},
render: function () {
this.renderAndBind();
this.$chatBuffer = this.$('#chatBuffer');
this.renderCollection(this.model.messages, Message, this.$('#conversation'));
this.registerBindings();
return this;
},
killForm: function (e) {
console.log('kill form', e);
e.preventDefault();
return false;
},
handleKeyDown: function (e) {
if (e.which === 13 && !e.shiftKey) {
this.sendChat();
e.preventDefault();
return false;
} else if (e.which === 38 && this.$chatBuffer.val() === '' && this.model.lastSentMessage) {
this.editMode = true;
this.$chatBuffer.addClass('editing');
this.$chatBuffer.val(this.model.lastSentMessage.body);
e.preventDefault();
return false;
} else if (e.which === 40 && this.editMode) {
this.editMode = false;
this.$chatBuffer.removeClass('editing');
e.preventDefault();
return false;
}
},
sendChat: function () {
var message;
var val = this.$chatBuffer.val();
if (val) {
message = {
to: this.model.lockedJID,
type: 'chat',
body: val,
chatState: 'active'
};
console.log(this.editMode);
if (this.editMode) {
message.replace = this.model.lastSentMessage.id;
}
var id = client.sendMessage(message);
message.id = id;
message.from = me.jid;
console.log(message);
console.log(this.editMode);
if (this.editMode) {
this.model.lastSentMessage.correct(message);
} else {
var msgModel = new MessageModel(message);
this.model.messages.add(msgModel);
this.model.lastSentMessage = msgModel;
}
}
this.editMode = false;
this.$chatBuffer.removeClass('editing');
this.$chatBuffer.val('');
}
});

18
clientapp/pages/main.js Normal file
View File

@ -0,0 +1,18 @@
/*global app*/
"use strict";
var BasePage = require('./base');
var templates = require('../templates');
var ContactListItem = require('../views/contactListItem');
module.exports = BasePage.extend({
template: templates.pages.main,
initialize: function (spec) {
this.render();
},
render: function () {
this.renderAndBind();
return this;
}
});

View File

@ -1,5 +1,7 @@
var BasePage = require('pages/base'),
templates = require('templates');
"use strict";
var BasePage = require('pages/base');
var templates = require('templates');
module.exports = BasePage.extend({

View File

@ -1,14 +1,9 @@
/*global app*/
var Backbone = require('backbone'),
staticPage = function (url) {
return function () {
var View = require('pages/wrapper');
app.renderPage(new View({
url: url
}));
};
};
/*global app, me*/
"use strict";
var Backbone = require('backbone');
var MainPage = require('./pages/main');
var ChatPage = require('./pages/chat');
module.exports = Backbone.Router.extend({
@ -18,16 +13,14 @@ module.exports = Backbone.Router.extend({
},
// ------- ROUTE HANDLERS ---------
main: function () {
var View = require('pages/main');
app.renderPage(new View({
app.renderPage(new MainPage({
model: me
}));
},
chat: function (jid) {
var View = require('pages/chat');
var contact = me.contacts.get(jid);
if (contact) {
app.renderPage(new View({
app.renderPage(new ChatPage({
model: contact
}));
} else {

View File

@ -0,0 +1,23 @@
"use strict";
function ArchiveStorage(storage) {
this.storage = storage;
}
ArchiveStorage.prototype = {
constructor: {
value: ArchiveStorage
},
setup: function (db) {
db.createObjectStore('archive', {
keyPath: 'id'
});
},
transaction: function (mode) {
var trans = this.storage.db.transaction('archive', mode);
return trans.objectStore('archive');
}
};
module.exports = ArchiveStorage;

View File

@ -0,0 +1,59 @@
"use strict";
// SCHEMA
// id: 'sha1 hash',
// dataURI: '...'
function AvatarStorage(storage) {
this.storage = storage;
}
AvatarStorage.prototype = {
constructor: {
value: AvatarStorage
},
setup: function (db) {
db.createObjectStore('avatars', {
keyPath: 'id'
});
},
transaction: function (mode) {
var trans = this.storage.db.transaction('avatars', mode);
return trans.objectStore('avatars');
},
add: function (avatar, cb) {
cb = cb || function () {};
var request = this.transaction('readwrite').put(avatar);
request.onsuccess = function () {
cb(false, avatar);
};
request.onerror = cb;
},
get: function (id, cb) {
cb = cb || function () {};
if (!id) {
return cb('not-found');
}
var request = this.transaction('readonly').get(id);
request.onsuccess = function (e) {
var res = request.result;
if (res === undefined) {
return cb('not-found');
}
cb(false, request.result);
};
request.onerror = cb;
},
remove: function (id, cb) {
cb = cb || function () {};
var request = this.transaction('readwrite')['delete'](id);
request.onsuccess = function (e) {
cb(false, request.result);
};
request.onerror = cb;
}
};
module.exports = AvatarStorage;

View File

@ -0,0 +1,45 @@
"use strict";
function DiscoStorage(storage) {
}
DiscoStorage.prototype = {
constructor: {
value: DiscoStorage
},
setup: function (db) {
db.createObjectStore('disco', {
keyPath: 'ver'
});
},
transaction: function (mode) {
var trans = this.storage.db.transaction('disco', mode);
return trans.objectStore('disco');
},
add: function (ver, disco, cb) {
cb = cb || function () {};
var data = {
ver: ver,
disco: disco
};
},
get: function (ver, cb) {
cb = cb || function () {};
if (!ver) {
return cb('not-found');
}
var request = this.transaction('readonly').get(ver);
request.onsuccess = function (e) {
var res = request.result;
if (res === undefined) {
return cb('not-found');
}
cb(false, res.disco);
};
request.onerror = cb;
}
};
module.exports = DiscoStorage;

View File

@ -0,0 +1,45 @@
/*global indexedDB*/
"use strict";
var AvatarStorage = require('./avatars');
var RosterStorage = require('./roster');
var DiscoStorage = require('./disco');
var ArchiveStorage = require('./archive');
function Storage() {
this.db = null;
this.init = [];
this.avatars = new AvatarStorage(this);
this.roster = new RosterStorage(this);
this.disco = new DiscoStorage(this);
this.archive = new ArchiveStorage(this);
}
Storage.prototype = {
constructor: {
value: Storage
},
version: 1,
open: function (cb) {
cb = cb || function () {};
var self = this;
var request = indexedDB.open('datastorage', this.version);
request.onsuccess = function (e) {
self.db = e.target.result;
cb(false, self.db);
};
request.onupgradeneeded = function (e) {
var db = e.target.result;
self.avatars.setup(db);
self.roster.setup(db);
self.disco.setup(db);
self.archive.setup(db);
};
request.onerror = cb;
}
};
module.exports = Storage;

View File

@ -0,0 +1,78 @@
"use strict";
// SCHEMA
// jid: string
// name: string
// subscription: string
// groups: array
// rosterID: string
function RosterStorage(storage) {
this.storage = storage;
}
RosterStorage.prototype = {
constructor: {
value: RosterStorage
},
setup: function (db) {
db.createObjectStore('roster', {
keyPath: 'jid'
});
},
transaction: function (mode) {
var trans = this.storage.db.transaction('roster', mode);
return trans.objectStore('roster');
},
add: function (contact, cb) {
cb = cb || function () {};
var request = this.transaction('readwrite').put(contact);
request.onsuccess = function () {
cb(false, contact);
};
request.onerror = cb;
},
get: function (id, cb) {
cb = cb || function () {};
if (!id) {
return cb('not-found');
}
var request = this.transaction('readonly').get(id);
request.onsuccess = function (e) {
var res = request.result;
if (res === undefined) {
return cb('not-found');
}
cb(false, request.result);
};
request.onerror = cb;
},
getAll: function (cb) {
cb = cb || function () {};
var results = [];
var request = this.transaction('readonly').openCursor();
request.onsuccess = function (e) {
var cursor = e.target.result;
if (cursor) {
results.push(cursor.value);
cursor.continue();
} else {
cb(false, results);
}
};
request.onerror = cb;
},
remove: function (id, cb) {
cb = cb || function () {};
var request = this.transaction('readwrite')['delete'](id);
request.onsuccess = function (e) {
cb(false, request.result);
};
request.onerror = cb;
}
};
module.exports = RosterStorage;

98
clientapp/templates.js Normal file
View File

@ -0,0 +1,98 @@
(function () {
var root = this, exports = {};
// The jade runtime:
var jade = exports.jade=function(exports){Array.isArray||(Array.isArray=function(arr){return"[object Array]"==Object.prototype.toString.call(arr)}),Object.keys||(Object.keys=function(obj){var arr=[];for(var key in obj)obj.hasOwnProperty(key)&&arr.push(key);return arr}),exports.merge=function merge(a,b){var ac=a["class"],bc=b["class"];if(ac||bc)ac=ac||[],bc=bc||[],Array.isArray(ac)||(ac=[ac]),Array.isArray(bc)||(bc=[bc]),ac=ac.filter(nulls),bc=bc.filter(nulls),a["class"]=ac.concat(bc).join(" ");for(var key in b)key!="class"&&(a[key]=b[key]);return a};function nulls(val){return val!=null}return exports.attrs=function attrs(obj,escaped){var buf=[],terse=obj.terse;delete obj.terse;var keys=Object.keys(obj),len=keys.length;if(len){buf.push("");for(var i=0;i<len;++i){var key=keys[i],val=obj[key];"boolean"==typeof val||null==val?val&&(terse?buf.push(key):buf.push(key+'="'+key+'"')):0==key.indexOf("data")&&"string"!=typeof val?buf.push(key+"='"+JSON.stringify(val)+"'"):"class"==key&&Array.isArray(val)?buf.push(key+'="'+exports.escape(val.join(" "))+'"'):escaped&&escaped[key]?buf.push(key+'="'+exports.escape(val)+'"'):buf.push(key+'="'+val+'"')}}return buf.join(" ")},exports.escape=function escape(html){return String(html).replace(/&(?!(\w+|\#\d+);)/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")},exports.rethrow=function rethrow(err,filename,lineno){if(!filename)throw err;var context=3,str=require("fs").readFileSync(filename,"utf8"),lines=str.split("\n"),start=Math.max(lineno-context,0),end=Math.min(lines.length,lineno+context),context=lines.slice(start,end).map(function(line,i){var curr=i+start+1;return(curr==lineno?" > ":" ")+curr+"| "+line}).join("\n");throw err.path=filename,err.message=(filename||"Jade")+":"+lineno+"\n"+context+"\n\n"+err.message,err},exports}({});
// create our folder objects
exports.includes = {};
exports.pages = {};
// body.jade compiled template
exports.body = function anonymous(locals) {
var buf = [];
with (locals || {}) {
buf.push('<body><div class="wrap"><header id="me"><img class="avatar"/><p class="status"></p></header><nav id="contactList"></nav><section id="pages"></section><footer></footer></div></body>');
}
return buf.join("");
};
// head.jade compiled template
exports.head = function anonymous(locals) {
var buf = [];
with (locals || {}) {
buf.push('<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0"/><meta name="apple-mobile-web-app-capable" content="yes"/>');
}
return buf.join("");
};
// contactListItem.jade compiled template
exports.includes.contactListItem = function anonymous(locals) {
var buf = [];
with (locals || {}) {
buf.push('<li class="contact"><img' + jade.attrs({
src: contact.avatar,
"class": "avatar"
}, {
src: true
}) + '/><div class="name">' + jade.escape(null == (jade.interp = contact.displayName) ? "" : jade.interp) + '</div><div class="status">' + jade.escape(null == (jade.interp = contact.status) ? "" : jade.interp) + "</div></li>");
}
return buf.join("");
};
// contactListItemResource.jade compiled template
exports.includes.contactListItemResource = function anonymous(locals) {
var buf = [];
with (locals || {}) {
buf.push('<li><p class="jid">' + jade.escape(null == (jade.interp = resource.jid) ? "" : jade.interp) + '</p><p class="status">' + jade.escape(null == (jade.interp = resource.status) ? "" : jade.interp) + "</p></li>");
}
return buf.join("");
};
// message.jade compiled template
exports.includes.message = function anonymous(locals) {
var buf = [];
with (locals || {}) {
buf.push('<li><div class="message"><span class="timestamp">' + jade.escape(null == (jade.interp = message.created) ? "" : jade.interp) + '</span><p class="body">' + jade.escape(null == (jade.interp = message.body) ? "" : jade.interp) + "</p></div></li>");
}
return buf.join("");
};
// chat.jade compiled template
exports.pages.chat = function anonymous(locals) {
var buf = [];
with (locals || {}) {
buf.push('<section class="page chat"><header class="contactInfo"><img width="30" height="30" class="avatar"/><h1 class="name"></h1></header><ul id="conversation"></ul><div id="chatInput"><form><textarea id="chatBuffer" name="chatInput" type="text" placeholder="Send a message..." autocomplete="off"></textarea></form></div></section>');
}
return buf.join("");
};
// main.jade compiled template
exports.pages.main = function anonymous(locals) {
var buf = [];
with (locals || {}) {
buf.push('<section class="page main"><div id="log"><h2>Event Log</h2></div></section>');
}
return buf.join("");
};
// wrapper.jade compiled template
exports.pages.wrapper = function anonymous(locals) {
var buf = [];
with (locals || {}) {
buf.push('<div class="page"></div>');
}
return buf.join("");
};
// attach to window or export with commonJS
if (typeof module !== "undefined" && typeof module.exports !== "undefined") {
module.exports = exports;
} else if (typeof define === "function" && define.amd) {
define(exports);
} else {
root.templatizer = exports;
}
})();

View File

@ -0,0 +1,8 @@
body
.wrap
header#me
img.avatar
p.status
nav#contactList
section#pages
footer

View File

@ -0,0 +1,2 @@
meta(name="viewport", content="width=device-width,initial-scale=1.0,maximum-scale=1.0")
meta(name="apple-mobile-web-app-capable", content="yes")

View File

@ -1,7 +0,0 @@
.wrap
header
#me
img.avatar
p.status
section#pages
footer

View File

@ -1,6 +1,8 @@
section.page.chat
nav#contactList
header.contactInfo
img.avatar(width=30, height=30)
h1.name
ul#conversation
div#chatInput
form
textarea#chatBuffer(name='chatInput', type='text', placeholder='Send a message...', autocomplete='off')

View File

@ -1,4 +1,3 @@
section.page.main
nav#contactList
#log
h2 Event Log

View File

@ -1,8 +1,9 @@
/*global app, $*/
var StrictView = require('strictview');
var templates = require('templates');
/*global $, app, me*/
"use strict";
var _ = require('underscore');
var ContactListItemResource = require('views/contactListItemResource');
var StrictView = require('strictview');
var templates = require('../templates');
module.exports = StrictView.extend({
@ -12,29 +13,25 @@ module.exports = StrictView.extend({
subscription: '',
chatState: ''
},
contentBindings: {
textBindings: {
displayName: '.name',
status: '.status'
},
imageBindings: {
avatar: '.avatar img'
srcBindings: {
avatar: '.avatar'
},
events: {
'click': 'goChat'
},
initialize: function (opts) {
this.containerEl = opts.containerEl;
this.render();
},
render: function () {
this.subViewRender({context: {contact: this.model}});
//this.collectomatic(this.model.resources, ContactListItemResource, {
// containerEl: this.$('.resources')
//});
this.handleBindings();
this.renderAndBind({contact: this.model});
return this;
},
goChat: function () {
console.log('event?');
app.navigate('chat/' + this.model.jid);
}
});

View File

@ -1,7 +1,9 @@
/*global $*/
var StrictView = require('strictview');
var templates = require('templates');
/*global $, app, me*/
"use strict";
var _ = require('underscore');
var StrictView = require('strictview');
var templates = require('../templates');
module.exports = StrictView.extend({

17
clientapp/views/main.js Normal file
View File

@ -0,0 +1,17 @@
/*global $, app, me*/
"use strict";
var StrictView = require('strictview');
var templates = require('../templates');
var ContactListItem = require('../views/contactListItem');
module.exports = StrictView.extend({
template: templates.body,
render: function () {
$('head').append(templates.head());
this.renderAndBind();
this.renderCollection(me.contacts, ContactListItem, this.$('#contactList'));
return this;
}
});

View File

@ -1,11 +1,16 @@
/*global $*/
var StrictView = require('strictview');
var templates = require('templates');
"use strict";
var _ = require('underscore');
var StrictView = require('strictview');
var templates = require('../templates');
module.exports = StrictView.extend({
template: templates.includes.message,
initialize: function (opts) {
this.render();
},
classBindings: {
mine: '.message',
receiptReceived: '',
@ -13,17 +18,12 @@ module.exports = StrictView.extend({
delayed: '',
edited: ''
},
contentBindings: {
textBindings: {
body: '.body',
formattedTime: '.timestamp'
},
initialize: function (opts) {
this.containerEl = opts.containerEl;
this.render();
},
render: function () {
this.subViewRender({context: {message: this.model}});
this.handleBindings();
this.renderAndBind({message: this.model});
return this;
}
});

View File

@ -1,48 +1,30 @@
{
"name": "otalk.im",
"description": "OTalk: WebRTC Enabled XMPP Client, in the Browser",
"version": "0.0.1",
"repository": {
"type": "git",
"url": "git@github.com:andyet/otalk.git"
},
"dependencies": {
"express": "3.x.x",
"connect": "2.7.6",
"connect-redis": "1.4.5",
"getconfig": "0.0.5",
"jade": "0.30.0",
"moonboots": "0.0.3",
"clientmodules": "0.0.5",
"backbone": "1.0.0",
"underscore": "1.4.4",
"strictmodel": "0.0.3",
"precommit-hook": "0.3.4",
"sound-effect-manager": "0.0.5",
"andlog": "0.0.3",
"strictview": "0.0.2",
"system-requirements": "0.0.2",
"webrtc.js": "0.0.3",
"semi-static": "",
"node-uuid": "1.4.0",
"cookie-getter": "0.0.2",
"wildemitter": "0.0.5",
"image-to-data-uri": "",
"stanza.io": ""
},
"clientmodules": [
"backbone",
"strictmodel",
"underscore",
"sound-effect-manager",
"andlog",
"strictview",
"cookie-getter",
"wildemitter",
"image-to-data-uri"
],
"clientmodulesDir": "clientapp/modules",
"scripts": {
"postinstall": "node node_modules/clientmodules/install.js"
}
"name": "otalk.im",
"version": "0.0.1",
"description": "OTalk: WebRTC Enabled XMPP Client, in the Browser",
"repository": {
"type": "git",
"url": "git@github.com:andyet/otalk.git"
},
"dependencies": {
"andlog": "0.0.4",
"async": "0.2.9",
"backbone": "1.0.0",
"express": "3.3.7",
"getconfig": "0.0.5",
"jade": "0.35.0",
"moonboots": "0.4.1",
"helmet": "0.1.0",
"node-uuid": "1.4.1",
"semi-static": "0.0.4",
"sound-effect-manager": "0.0.5",
"strictmodel": "0.1.1",
"strictview": "1.0.0",
"templatizer": "0.1.2",
"underscore": "1.5.1"
},
"devDependencies": {
"precommit-hook": "0.3.6"
},
"main": "server.js"
}

View File

@ -118,14 +118,22 @@ html, body {
color: #777;
}
.chatView {
position: relative;
}
#conversation {
background: #ecf0f2;
box-sizing: border-box;
bottom: 0;
margin: 0;
padding-bottom: 30px;
max-width: 100%;
padding: 0;
overflow-x: hidden;
position: relative;
margin-top: 50px;
bottom: 50px;
}
#conversation li {
@ -187,11 +195,14 @@ html, body {
.contactInfo {
padding: 5px;
background-image: #bccad0;
background-color: #bccad0;
background-image: -moz-linear-gradient(top, #bccad0, #95a4a9);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #bccad0), color-stop(1, #95a4a9));
min-height: 30px;
position: relative;
position: fixed;
top: 0px;
width: 100%;
z-index: 1000;
}
.contactInfo .avatar {
@ -213,3 +224,43 @@ html, body {
#me {
display: none;
}
#chatInput {
position: fixed;
bottom: 0px;
left: 176px;
right: 0px;
z-index: 100;
background: #ecf0f2;
}
#chatInput form {
background-color: #e1e4e6;
background-image: -moz-linear-gradient(top, #e1e4e6, #cacdce);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #e1e4e6), color-stop(1, #cacdce));
padding: 10px;
z-index: 1000;
border-top: 1px solid #fff;
border-left: 1px solid #ddd;
border-right: 1px solid #ddd;
}
#chatBuffer {
width: 100%;
padding: 5px;
height: 30px;
line-height: 20px;
font-size: 13px;
resize: none;
outline: none;
border: 1px solid #ccc;
border-top-color: #bbb;
border-radius: 5px;
-moz-box-sizing: border-box;
box-sizing: border-box;
font-family: 'lucida grande';
}
#chatBuffer.editing {
background-color: yellow;
}

View File

@ -1,57 +1,46 @@
/*global console*/
var fs = require('fs');
var privateKey = fs.readFileSync('fakekeys/privatekey.pem').toString();
var certificate = fs.readFileSync('fakekeys/certificate.pem').toString();
var express = require('express');
var app = express();
var server = require('https').createServer({key: privateKey, cert: certificate}, app);
var connect = require('connect');
var RedisStore = require('connect-redis')(connect);
var https = require('https');
var helmet = require('helmet');
var Moonboots = require('moonboots');
var config = require('getconfig');
var semiStatic = require('semi-static');
var uuid = require('node-uuid');
var templatizer = require('templatizer');
var app = express();
app.use(express.compress());
app.use(express.static(__dirname + '/public'));
app.enable('trust proxy');
app.set('view engine', 'jade');
app.use(express.bodyParser());
app.use(express.cookieParser());
app.use(express.session({
proxy: true,
secret: config.session.secret,
store: new RedisStore({
host: config.session.host,
port: config.session.port,
db: config.session.db
}),
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 90, // 90 days
secure: config.session.secure
},
key: 'o.im'
}));
if (!config.isDev) {
app.use(helmet.xframe());
}
app.use(helmet.iexss());
app.use(helmet.contentTypeOptions());
var clientApp = new Moonboots({
fileName: 'stanzaiodemo',
dir: __dirname + '/clientapp',
main: __dirname + '/clientapp/app.js',
developmentMode: config.isDev,
libraries: [
'jquery.js',
'stanza.io.js',
'sugar-1.2.1-dates.js',
'init.js'
__dirname + '/clientapp/libraries/jquery.js',
__dirname + '/clientapp/libraries/sugar-1.2.1-dates.js',
__dirname + '/clientapp/libraries/IndexedDBShim.min.js',
__dirname + '/clientapp/libraries/stanza.io.js'
],
server: app
stylesheets: [
__dirname + '/public/style.css'
],
browserify: {
debug: false
},
server: app,
beforeBuild: function () {
var clientFolder = __dirname + '/clientapp';
templatizer(clientFolder + '/templates', clientFolder + '/templates.js');
}
});
app.get('test', function (req, res) {
res.send('<html></html>');
});
// serves app on every other url
app.get('*', clientApp.html());
server.listen(config.http.port);
app.listen(config.http.port);
console.log('demo.stanza.io running at: ' + config.http.baseUrl);