Commit initial somewhat working version
+www/build/
+node_modules/
+[submodule "www/enyo"]
+ path = www/enyo
+ url = https://github.com/enyojs/enyo
+[submodule "www/lib/onyx"]
+ path = www/lib/onyx
+ url = https://github.com/enyojs/onyx
+[submodule "www/lib/layout"]
+ path = www/lib/layout
+ url = https://github.com/enyojs/layout
+var sx = require('simple-xmpp');
+var EventEmitter = require('events').EventEmitter;
+
+function Agent() {
+ this.roster = {};
+ this.presence = {};
+ this.events = new EventEmitter();
+ ['on', 'addListener', 'once', 'removeListener'].forEach(function(v, i) {
+ this[v] = this.events[v].bind(this.events);
+ }.bind(this));
+
+ var xmpp = this.xmpp = new sx.SimpleXMPP();
+
+ xmpp.on('error', function(err) {
+ console.log(err);
+ });
+
+ xmpp.on('chat', function(from, message) {
+ // TODO: Pull timestamp from XMPP stanza?
+ this.events.emit('message', {
+ outbound: false,
+ jid: from,
+ message: message,
+ receivedTimestamp: (new Date()).getTime()
+ });
+ }.bind(this));
+
+ xmpp.on('buddy', function(jid, status, statusText) {
+ switch(status) {
+ case xmpp.STATUS.OFFLINE:
+ delete this.presence[jid];
+ console.log(jid + ' offline');
+ this.events.emit('buddy', {
+ jid: jid,
+ status: status
+ });
+ break;
+ case xmpp.STATUS.ONLINE:
+ case xmpp.STATUS.AWAY:
+ case xmpp.STATUS.XA:
+ case xmpp.STATUS.DND:
+ this.presence[jid] = {
+ jid: jid,
+ status: status,
+ statusText: statusText
+ };
+ this.events.emit('buddy', this.presence[jid]);
+ console.log(jid + ' online (' + status + ')');
+ break;
+ default:
+ console.log('unknown status ' + status + ' for ' + jid);
+ }
+ }.bind(this));
+}
+
+Agent.prototype.login = function(username, password, callback) {
+ this.xmpp.on('online', function connectCallback() {
+ console.log('connected');
+ callback();
+ this.jid = username;
+ this.xmpp.removeListener('online', connectCallback);
+ }.bind(this));
+
+ this.xmpp.on('error', function errorCallback(err) {
+ callback(err);
+ this.xmpp.removeListener('error', errorCallback);
+ }.bind(this));
+
+ this.xmpp.connect({
+ jid: username,
+ password: password
+ });
+}
+
+Agent.prototype.logout = function() {
+ this.xmpp.disconnect();
+}
+
+Agent.prototype.getRoster = function(callback) {
+ this.xmpp.conn.on('stanza', function rosterReply(stanza) {
+ this.xmpp.conn.removeListener('stanza', rosterReply);
+ if (stanza.attrs.id == 'roster1') {
+ if (stanza.attrs.type == 'error') {
+ console.log('error getting roster');
+ return callback('error getting roster');
+ }
+ var items = stanza.getChild('query').getChildren('item');
+ this.roster = {};
+ for (var i = 0; i < items.length; i++) {
+ var attrs = items[i].attrs;
+ var groups = items[i].getChildren('group');
+
+ this.roster[attrs.jid] = {
+ jid: attrs.jid,
+ name: attrs.name,
+ subscription: attrs.subscription,
+ groups: groups.map(function(v) {
+ return v.getText();
+ })
+ };
+ }
+
+ callback(null, this.roster);
+ }
+ }.bind(this));
+ var msg = new this.xmpp.Element('iq', {
+ from: this.jid,
+ type: 'get',
+ id: 'roster1'
+ }).c('query', {xmlns: 'jabber:iq:roster'});
+ this.xmpp.conn.send(msg);
+}
+
+Agent.prototype.sendMessage = function(data) {
+ this.xmpp.send(data.jid, data.message);
+}
+
+module.exports = Agent;
+var xmpp = require('simple-xmpp');
+var express = require('express');
+var app = express();
+var server = require('http').createServer(app);
+var io = require('socket.io').listen(server);
+var cookie = require('./node_modules/express/node_modules/cookie');
+var connect = require('./node_modules/express/node_modules/connect');
+var Agent = require('./agent.js');
+
+var cookie_secret = 'Du:drKkl8&<\\I~Tc~y$nnJbX?Zt<M}1K4lx*N<iJOc|UpSK$w#iZvxqJ1vOrv(088Ev!~2;1="M!)BVD{%:7U1{0#';
+var sessionStore = new express.session.MemoryStore();
+app.use(express.cookieParser(cookie_secret));
+app.use(express.session({
+ store: sessionStore,
+ key: 'connect.sess'
+}));
+app.use(express.json());
+app.use(express.static(__dirname + '/www'));
+
+io.configure(function() {
+ io.set('authorization', function (handshakeData, callback) {
+ if (handshakeData.headers.cookie) {
+ var cookies = cookie.parse(handshakeData.headers.cookie);
+ var signedCookies = connect.utils.parseSignedCookies(cookies, cookie_secret);
+ var sid = signedCookies['connect.sess'];
+ sessionStore.load(sid, function(err, sess) {
+ if (err) return callback(err);
+ if (!sess) return callback('No session');
+
+ handshakeData.session = sess;
+ callback(null, true);
+ });
+ } else {
+ callback('No session cookie');
+ }
+ });
+});
+
+server.listen(4000);
+
+var agents = {};
+
+io.sockets.on('connection', function(socket) {
+ var session = socket.handshake.session;
+ var agent;
+
+ socket.on('login', function(data) {
+ agent = new Agent();
+
+ agent.on('message', function(data) {
+ socket.emit('message', data);
+ });
+
+ agent.on('buddy', function(data) {
+ socket.emit('buddy', data);
+ });
+
+ agent.login(data.username, data.password, function(err) {
+ if (err) {
+ console.log(err);
+ socket.emit('login-failed');
+ } else {
+ socket.emit('login-successful', {
+ username: data.username
+ });
+ session.jid = data.username;
+ session.save();
+ agents[session.jid] = agent;
+ console.log('logged in');
+ agent.getRoster(function(err, roster) {
+ socket.emit('roster', roster);
+ });
+ }
+ });
+ });
+
+ socket.on('logout', function() {
+ agent.logout();
+ session.destroy();
+ socket.disconnect();
+ });
+
+ socket.on('get-roster', function() {
+ agent.getRoster(function(err, roster) {
+ socket.emit('roster', roster);
+ });
+ });
+
+ socket.on('send-message', function(data) {
+ agent.sendMessage(data);
+ });
+});
+{
+ "name": "xmpp-ftw-test",
+ "dependencies": {
+ "express": "3.x",
+ "socket.io": "*",
+ "simple-xmpp": "*"
+ }
+}
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+ <title>Chat</title>
+ <link rel="shortcut icon" href="assets/favicon.ico"/>
+ <!-- -->
+ <meta http-equiv="Content-Type" content="text/html; charset=utf8"/>
+ <meta name="apple-mobile-web-app-capable" content="yes"/>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
+ <!-- enyo (debug) -->
+ <script src="enyo/enyo.js" charset="utf-8"></script>
+ <!-- application (debug) -->
+ <script src="package.js" charset="utf-8"></script>
+ <link rel="stylesheet" href="style.css">
+ <script src="/socket.io/socket.io.js"></script>
+ </head>
+ <body class="enyo-unselectable">
+ <script>
+ var conversationStore = new ConversationStore();
+ new ChatClient().renderInto(document.body);
+ </script>
+ </body>
+</html>
+Subproject commit ad50ee2de0a568a4826c11f931a7aa209c309161
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="48"
+ height="48"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.4 r9939"
+ sodipodi:docname="New document 1">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#f0f0f0"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="1"
+ inkscape:pageshadow="2"
+ inkscape:zoom="11.2"
+ inkscape:cx="22.139681"
+ inkscape:cy="20.547239"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ showguides="false"
+ inkscape:window-width="1855"
+ inkscape:window-height="1177"
+ inkscape:window-x="1592"
+ inkscape:window-y="-8"
+ inkscape:window-maximized="1" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1004.3622)">
+ <path
+ style="fill:#b8b8b8;fill-opacity:1;stroke:none"
+ d="m 8.6607143,1047.8265 30.6250007,0 c -5.714286,-25.8036 -24.196429,-25.5357 -30.6250007,0 z"
+ id="path3753"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccc" />
+ <path
+ sodipodi:type="arc"
+ style="fill:#b8b8b8;fill-opacity:1;stroke:none"
+ id="path3755"
+ sodipodi:cx="25.3125"
+ sodipodi:cy="12.955357"
+ sodipodi:rx="11.473214"
+ sodipodi:ry="11.473214"
+ d="m 36.785714,12.955357 a 11.473214,11.473214 0 1 1 -22.946428,0 11.473214,11.473214 0 1 1 22.946428,0 z"
+ transform="translate(-1.1607143,1006.2551)" />
+ </g>
+</svg>
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+ <title>Chat</title>
+ <link rel="shortcut icon" href="assets/favicon.ico"/>
+ <!-- -->
+ <meta http-equiv="Content-Type" content="text/html; charset=utf8"/>
+ <meta name="apple-mobile-web-app-capable" content="yes"/>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
+ <!-- css -->
+ <link href="build/chat.css" rel="stylesheet"/>
+ <link rel="stylesheet" href="style.css">
+ <!-- js -->
+ <script src="build/chat.js" charset="utf-8"></script>
+ <script src="/socket.io/socket.io.js"></script>
+ </head>
+ <body class="enyo-unselectable">
+ <script>
+ var conversationStore = new ConversationStore();
+ new ChatClient().renderInto(document.body);
+ </script>
+ </body>
+</html>
+Subproject commit 288dfb1c68a028804521fb6f0255160753403b50
+Subproject commit f00190a2dbc04fcc92bc689865dbfcae5f7bca88
+enyo.depends(
+ 'enyo/minify',
+ 'package.js'
+);
+enyo.depends(
+ '$lib/onyx',
+ '$lib/layout',
+ 'src/'
+);
+enyo.kind({
+ name: "ChatClient",
+ kind: "Control",
+ loggedIn: false,
+ components: [
+ {name: "mainPanels", kind: "Panels", arrangerKind: "CollapsingArranger", style: "height: 100%", components: [
+ {name: "picker", kind: "Picker", onConversationSwitched: "conversationSwitched"},
+ {name: "conversation", kind: "Conversation", classes: "panel-shadow"},
+ ]},
+ {name: "loginDialog", kind: "LoginDialog", onTryLogin: "tryLogin"},
+ {kind: "Signals",
+ onStartLogin: "startLogin",
+ onStartLogout: "startLogout",
+ onMessageSent: "messageSent"}
+ ],
+ create: function() {
+ this.inherited(arguments);
+ this.socketSetup();
+ },
+ socketSetup: function() {
+ this.socket = io.connect(location.protocol + '//' + location.host);
+
+ this.socket.on('login-successful', function(data) {
+ enyo.Signals.send('onLoginSuccessful', {
+ username: data.username
+ });
+ }.bind(this));
+
+ this.socket.on('login-failed', function() {
+ enyo.Signals.send('onLoginFailed');
+ }.bind(this));
+
+ this.socket.on('roster', function(data) {
+ this.$.picker.setRoster(data);
+ }.bind(this));
+
+ this.socket.on('message', function(data) {
+ enyo.Signals.send('onMessageReceived', data);
+ conversationStore.appendConversation(data.jid, data);
+ this.$.picker.updateConversationList();
+ }.bind(this));
+
+ this.socket.on('buddy', function(data) {
+ this.$.picker.setPresence(data);
+ }.bind(this));
+
+ this.socket.on('disconnect', function() {
+ enyo.Signals.send('onLogout');
+ console.log('logged out');
+ }.bind(this));
+
+ this.socket.on('error', function() {
+ socket.disconnect();
+ // Retry a bit later
+ setTimeout(this.socketSetup.bind(this), 1000);
+ }.bind(this));
+ },
+ startLogin: function(inSender, inEvent) {
+ this.$.loginDialog.reset();
+ this.$.loginDialog.show();
+ },
+ startLogout: function(inSender, inEvent) {
+ this.socket.emit('logout');
+ },
+ tryLogin: function(inSender, inEvent) {
+ console.log('attempting login');
+ this.socket.emit('login', this.$.loginDialog.getData());
+ },
+ messageSent: function(inSender, inEvent) {
+ this.socket.emit('send-message', inEvent);
+ conversationStore.appendConversation(inEvent.jid, inEvent);
+ this.$.picker.updateConversationList();
+ },
+ conversationSwitched: function(inSender, inEvent) {
+ this.$.conversation.setJid(inEvent.jid);
+ this.$.conversation.setName(inEvent.name);
+ if (enyo.Panels.isScreenNarrow()) {
+ this.$.mainPanels.setIndex(1);
+ }
+ }
+});
+enyo.kind({
+ name: "Conversation",
+ kind: "FittableRows",
+ classes: "conversation",
+ messages: [],
+ properties: {
+ jid: '',
+ name: '',
+ },
+ components: [
+ {kind: "onyx.Toolbar", components: [
+ {kind: "StatusCircle"},
+ {name: "conversationJid", content: 'None'}
+ ]},
+ {name: "messageList", kind: "List", onSetupItem: "messageItemSetup", fit: 1, components: [
+ {name: "messageItem", classes: "message-item", components: [
+ {classes: "message-image-container", components: [
+ {name: "messageBuddyImage", classes: "buddy-image"}
+ ]},
+ {components: [
+ {name: "messageData", classes: "message-data"},
+ {name: "messageTimestamp", classes: "message-timestamp"}
+ ]}
+ ]}
+ ]},
+ {kind: "onyx.Toolbar", classes: "table-fit", components: [
+ {components: [
+ {kind: "onyx.Grabber"},
+ ]},
+ {kind: "onyx.InputDecorator", alwaysLooksFocused: true, classes: "conversation-input-decorator table-fit-fill", components: [
+ {name: "conversationInput", kind: "onyx.Input", classes: "conversation-input", onkeypress: "inputKeypress"}
+ ]}
+ ]},
+ {kind: "Signals", onMessageReceived: "messageReceived"}
+ ],
+ create: function() {
+ this.inherited(arguments);
+ this.updateConversationList();
+ },
+ setJid: function(jid) {
+ this.jid = jid;
+ this.messages = conversationStore.getConversation(jid);
+ this.$.conversationJid.setContent(jid);
+ this.updateConversationList();
+ },
+ setName: function(name) {
+ this.name = name;
+ this.$.conversationJid.setContent(name || this.jid);
+ },
+ updateConversationList: function() {
+ this.$.messageList.setCount(this.messages.length);
+ this.$.messageList.refresh();
+ setTimeout(function() {
+ this.$.messageList.scrollToBottom();
+ }.bind(this));
+ },
+ messageReceived: function(inSender, inEvent) {
+ if (inEvent.jid == this.jid) {
+ var data = inEvent;
+ this.messages.push(data);
+ this.updateConversationList();
+ }
+ },
+ messageItemSetup: function(inSender, inEvent) {
+ var row = this.messages[inEvent.index];
+ if (!row) return;
+ this.$.messageData.setContent(row.message);
+ var timestamp = new Date(row.receivedTimestamp).toLocaleTimeString();
+ this.$.messageTimestamp.setContent(timestamp);
+ this.$.messageItem.addRemoveClass('outbound', row.outbound);
+ },
+ inputKeypress: function(inSender, inEvent) {
+ if (inEvent.keyCode == 13) {
+ var message = this.$.conversationInput.getValue();
+ this.$.conversationInput.setValue('');
+ var data = {
+ outbound: true,
+ jid: this.jid,
+ receivedTimestamp: (new Date()).getTime(),
+ message: message
+ };
+ this.messages.push(data);
+ enyo.Signals.send('onMessageSent', data);
+ this.updateConversationList();
+ }
+ }
+});
+enyo.kind({
+ name: "ConversationStore",
+ kind: "Component",
+ conversations: {},
+ retentionWindow: 86400,
+ loadConversations: function() {
+ var len = localStorage.length;
+ for (var i = 0; i < len; i++) {
+ var key = localStorage.key(i);
+ if (key.match(/^conversation:/)) {
+ var jid = key.replace(/^conversation:/, '');
+ this.conversations[jid] = JSON.parse(localStorage[key]);
+ }
+ }
+ },
+ saveConversation: function(jid) {
+ if (!(jid in this.conversations)) return;
+ localStorage['conversation:' + jid] = JSON.stringify(this.conversations[jid]);
+ },
+ getConversation: function(jid) {
+ if (jid in this.conversations) {
+ // Be sure to return a copy
+ return this.conversations[jid].messages.slice(0);
+ } else {
+ return [];
+ }
+ },
+ getConversationSummary: function() {
+ var res = []
+ for (jid in this.conversations) {
+ var len = this.conversations[jid].messages.length;
+ var lastMessage = len > 0 ? this.conversations[jid].messages[len - 1] : '';
+ res.push({
+ jid: jid,
+ outbound: lastMessage.outbound,
+ message: lastMessage.message,
+ receivedTimestamp: lastMessage.receivedTimestamp
+ });
+ }
+
+ return res.sort(function(a, b) {
+ if (a.receivedTimestamp > b.receivedTimestamp)
+ return -1;
+ if (a.receivedTimestamp < b.receivedTimestamp)
+ return 1;
+ return 0;
+ });
+ },
+ appendConversation: function(jid, data) {
+ if (!(jid in this.conversations)) {
+ this.conversations[jid] = {
+ lastMessageTimestamp: 0,
+ messages: []
+ };
+ }
+ this.conversations[jid].messages.push(data);
+ this.conversations[jid].lastMessageTimestamp = (new Date()).getTime();
+ this.cullConversation(jid);
+ this.saveConversation(jid);
+ },
+ cullConversation: function(jid) {
+ // TODO: implement message culling by retentionWindow
+ }
+});
+enyo.kind({
+ name: "LoginDialog",
+ kind: "onyx.Popup",
+ classes: "login-dialog",
+ centered: true,
+ floating: true,
+ modal: true,
+ scrim: true,
+ scrimWhenModal: true,
+ events: {
+ onTryLogin: ""
+ },
+ components: [
+ {tag: "h2", content: "Login"},
+ {kind: "onyx.Groupbox", components: [
+ {kind: "onyx.InputDecorator", components: [
+ {name: "username", kind: "onyx.Input", placeholder: "Username", onkeypress: "keypress"}
+ ]},
+ {kind: "onyx.InputDecorator", components: [
+ {name: "password", kind: "onyx.Input", placeholder: "Password", type: "password", onkeypress: "keypress"}
+ ]}
+ ]},
+ {name: "loginError", tag: "p", showing: false, classes: "error", content: "I couldn't log you in."},
+ {name: "loginButton", kind: "onyx.Button", content: "Login", onclick: "loginClick", classes: "onyx-affirmative"},
+ {name: "cancelButton", kind: "onyx.Button", content: "Cancel", onclick: "cancelClick"},
+ {kind: "Signals", onLoginSuccessful: "success", onLoginFailed: "failure"}
+ ],
+ getData: function() {
+ return {
+ username: this.$.username.getValue(),
+ password: this.$.password.getValue()
+ };
+ },
+ loginClick: function() {
+ this.$.loginError.hide();
+ this.$.loginButton.setDisabled(true);
+ this.doTryLogin(this.getData());
+ },
+ reset: function() {
+ this.$.username.setValue('');
+ this.$.password.setValue('');
+ },
+ failure: function(inSender, inEvent) {
+ this.$.loginButton.setDisabled(false);
+ this.$.loginError.show();
+ },
+ success: function(inSender, inEvent) {
+ this.reset();
+ this.$.loginButton.setDisabled(false);
+ this.hide();
+ },
+ cancelClick: function(inSender, inEvent) {
+ // Eh, it's kind of like success.
+ this.success();
+ },
+ keypress: function(inSender, inEvent) {
+ if (inEvent.keyCode == 13) {
+ this.loginClick(this);
+ }
+ }
+});
+enyo.kind({
+ name: "Picker",
+ kind: "FittableRows",
+ classes: "picker",
+ events: {
+ onConversationSwitched: ''
+ },
+ conversations: [],
+ buddies: [],
+ buddiesByJid: {},
+ roster: {},
+ components: [
+ {name: "tabButtons", kind: "onyx.RadioGroup", classes: "picker-radiobuttons", components: [
+ {content: "Conversations", active: true, ontap: "selectConversations"},
+ {content: "Buddies", ontap: "selectBuddies"}
+ ]},
+ {name: "tabPanels", kind: "Panels", classes: "picker-panels", fit: true, draggable: false, components: [
+ {name: "conversationList", kind: "List", onSetupItem: "conversationItemSetup", components: [
+ {classes: "conversation-list-item", onclick: "conversationItemClick", components: [
+ {classes: "conversation-status", components: [
+ {name: "conversationBuddyStatus", kind: "StatusCircle"},
+ ]},
+ {classes: "conversation-content", components: [
+ {name: "conversationBuddyName", classes: "conversation-buddy-name"},
+ {name: "conversationLastMessage", classes: "conversation-last-message"},
+ ]},
+ {classes: "conversation-image-container", components: [
+ {name: "conversationImage", classes: "buddy-image"},
+ ]}
+ ]}
+ ]},
+ {name: "buddyList", kind: "List", onSetupItem: "buddyItemSetup", components: [
+ {classes: "buddy-list-item", onclick: "buddyItemClick", components: [
+ {classes: "buddy-status", components: [
+ {name: "buddyStatus", kind: "StatusCircle"},
+ ]},
+ {classes: "buddy-content", components: [
+ {name: "buddyName", classes: "buddy-name"},
+ {name: "buddyStatusText", classes: "buddy-status-text"},
+ ]},
+ {classes: "buddy-image-container", components: [
+ {name: "buddyImage", classes: "buddy-image"},
+ ]}
+ ]}
+ ]}
+ ]},
+ {kind: "onyx.Toolbar", components: [
+ {name: "loginButton", kind: "onyx.Button", content: "Login", onclick: "loginClick"},
+ {name: "logoutButton", kind: "onyx.Button", content: "Logout", showing: false, onclick: "logoutClick"},
+ ]},
+ {kind: "Signals", onLoginSuccessful: "didLogin", onLogout: "didLogout"}
+ ],
+ create: function() {
+ this.inherited(arguments);
+ conversationStore.loadConversations();
+ this.updateConversationList();
+ },
+ selectConversations: function(inSender, inEvent) {
+ this.$.tabPanels.setIndex(0);
+ },
+ updateConversationList: function() {
+ this.conversations = conversationStore.getConversationSummary();
+ this.$.conversationList.setCount(this.conversations.length);
+ this.$.conversationList.refresh();
+ },
+ selectBuddies: function(inSender, inEvent) {
+ this.$.tabPanels.setIndex(1);
+ },
+ setRoster: function(data) {
+ this.roster = data;
+ for (jid in data) {
+ this.mergeBuddyInfo(data[jid]);
+ }
+ this.updateBuddyList();
+ },
+ setPresence: function(data) {
+ this.mergeBuddyInfo(data);
+ this.updateBuddyList();
+ },
+ mergeBuddyInfo: function(data) {
+ var b = this.buddiesByJid[data.jid];
+ if (!b) {
+ b = this.buddiesByJid[data.jid] = {};
+ this.buddies.push(b);
+ }
+ for (i in data) {
+ b[i] = data[i];
+ }
+ if (!b.name) {
+ b.name = b.jid;
+ }
+ b.lastUpdate = new Date().getTime();
+ },
+ sortBuddies: function() {
+ this.buddies.sort(function(a, b) {
+ if (a.lastUpdate > b.lastUpdate)
+ return -1;
+ if (a.lastUpdate < b.lastUpdate)
+ return 1;
+ return 0;
+ });
+ },
+ updateBuddyList: function() {
+ this.sortBuddies();
+ this.$.buddyList.setCount(this.buddies.length);
+ this.$.buddyList.refresh();
+ this.$.conversationList.refresh();
+ },
+ conversationItemSetup: function(inSender, inEvent) {
+ var row = this.conversations[inEvent.index];
+ if (!row) return;
+ var status;
+ if (this.buddiesByJid[row.jid]) {
+ status = this.buddiesByJid[row.jid].status;
+ } else {
+ status = 'unknown';
+ }
+ this.$.conversationBuddyStatus.setStatus(status);
+ var name = row.jid;
+ if (row.jid in this.roster && this.roster[row.jid].name) {
+ name = this.roster[row.jid].name;
+ }
+ this.$.conversationBuddyName.setContent(name);
+ this.$.conversationLastMessage.setContent(row.message);
+ },
+ buddyItemSetup: function(inSender, inEvent) {
+ var row = this.buddies[inEvent.index];
+ if (!row) return;
+ this.$.buddyStatus.setStatus(row.status);
+ this.$.buddyName.setContent(row.name);
+ this.$.buddyStatusText.setContent(row.statusText);
+ },
+ loginClick: function(inSender, inEvent) {
+ enyo.Signals.send('onStartLogin');
+ },
+ logoutClick: function(inSender, inEvent) {
+ enyo.Signals.send('onStartLogout');
+ },
+ didLogin: function(inSender, inEvent) {
+ this.$.loginButton.hide();
+ this.$.logoutButton.show();
+ },
+ didLogout: function(inSender, inEvent) {
+ this.$.logoutButton.hide();
+ this.$.loginButton.show();
+ this.buddies = [];
+ this.buddiesByJid = {};
+ this.updateBuddyList();
+ },
+ conversationItemClick: function(inSender, inEvent) {
+ this.doConversationSwitched({
+ jid: this.conversations[inEvent.index].jid,
+ name: this.conversations[inEvent.index].jid
+ });
+ },
+ buddyItemClick: function(inSender, inEvent) {
+ this.doConversationSwitched({
+ jid: this.buddies[inEvent.index].jid,
+ name: this.buddies[inEvent.index].name
+ });
+ }
+});
+enyo.kind({
+ name: "StatusCircle",
+ kind: "Control",
+ classes: "status-circle",
+ setStatus: function(s) {
+ switch(s) {
+ case 'online':
+ this.applyStyle('background-color', '#00DF00');
+ break;
+ case 'away':
+ this.applyStyle('background-color', 'red');
+ break;
+ case 'offline':
+ default:
+ this.applyStyle('background-color', null);
+ break;
+ }
+ }
+});
+enyo.depends(
+ 'ConversationStore.js',
+ 'StatusCircle.js',
+ 'Picker.js',
+ 'Conversation.js',
+ 'LoginDialog.js',
+ 'ChatClient.js'
+);
+body {
+ background-color: #F0F0F0;
+}
+
+.picker {
+ width: 400px;
+}
+
+.picker, .conversation {
+ background-color: #F0F0F0;
+}
+
+.picker-radiobuttons {
+ width: 100%;
+ margin-top: 1%;
+ text-align: center;
+}
+
+.picker-radiobuttons > button {
+ width: 49%;
+}
+
+.picker-panels {
+ min-width: 320px;
+}
+
+.status-circle {
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+ border-radius: 10px;
+ background-color: lightgray;
+ box-shadow: inset 0 1px 3px -1px;
+ vertical-align: middle;
+ margin: 4px;
+}
+
+.conversation-list-item {
+ margin: 6px;
+ padding: 8px;
+ border-radius: 8px;
+ background-image: linear-gradient(to right, rgb(208,208,255), rgba(208,208,255,0));
+ box-shadow: inset 0 1px 3px -1px;
+}
+
+.conversation-list-item > * {
+ display: table-cell;
+}
+
+.conversation-content, .buddy-content {
+ padding-left: 4px;
+ width: 100%;
+}
+
+.conversation-image-container {
+ vertical-align: middle;
+}
+
+.conversation-buddy-name, .buddy-name {
+ font-size: 140%;
+}
+
+.conversation-last-message, .buddy-status-text {
+ width: 225px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.buddy-image {
+ border: 2px solid white;
+ border-radius: 6px;
+ width: 48px;
+ height: 48px;
+ background-size: cover;
+ background-image: url(images/unknown-user.svg);
+ box-shadow: 0 2px 4px -2px;
+}
+
+.buddy-list-item {
+ padding: 8px;
+ border-top: 1px solid white;
+ border-bottom: 1px solid #C0C0C0;
+}
+
+.buddy-list-item > * {
+ display: table-cell;
+ vertical-align: middle;
+}
+
+.panel-shadow {
+ box-shadow: -6px 0 6px rgba(0,0,0,0.3);
+}
+
+.onyx-input {
+ width: 100%;
+}
+
+.table-fit > * {
+ display: table-cell;
+}
+
+.table-fit .onyx-grabber {
+ margin: 0 10px 0 4px;
+}
+
+.table-fit > .table-fit-fill {
+ width: 100%;
+}
+
+.message-item {
+ margin: 6px;
+ padding: 8px;
+ background-image: linear-gradient(to left, rgb(255,255,208), rgba(255,255,208,0));
+ box-shadow: inset 0 1px 3px -1px;
+ border-radius: 10px;
+ overflow: hidden;
+}
+
+.message-item.outbound {
+ background-image: linear-gradient(to right, rgb(208,208,255), rgba(208,208,255,0));
+}
+
+.message-image-container {
+ height: 100%;
+ float: right;
+ margin-left: 8px;
+}
+
+.outbound .message-image-container {
+ float: left;
+ margin-right: 8px;
+ margin-left: 0;
+}
+
+.message-timestamp {
+ color: #606060;
+ font-size: 70%;
+ font-style: italic;
+}
+
+.onyx-popup {
+ width: 320px;
+}
+
+.onyx-popup h2 {
+ font-size: 20px;
+ margin-top: 0;
+}
+
+.onyx-popup .onyx-button {
+ width: 100%;
+ margin-top: 8px;
+}
+
+.error {
+ color: red;
+}