Add client. And it mostly works!
+[submodule "client/enyo"]
+ path = client/enyo
+ url = https://github.com/enyojs/enyo.git
+[submodule "client/lib/layout"]
+ path = client/lib/layout
+ url = https://github.com/enyojs/layout.git
+[submodule "client/lib/onyx"]
+ path = client/lib/onyx
+ url = https://github.com/enyojs/onyx.git
+<!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">
+ </head>
+ <body class="enyo-unselectable">
+ <script>
+ var conversationStore = new ConversationStore();
+ new ChatClient().renderInto(document.body);
+ </script>
+ </body>
+</html>
+Subproject commit 78ef04637aeb7e9a202703342cd0a99f5a847360
+<?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>
+ </head>
+ <body class="enyo-unselectable">
+ <script>
+ var conversationStore = new ConversationStore();
+ new ChatClient().renderInto(document.body);
+ </script>
+ </body>
+</html>
+Subproject commit 91c00634ccf9dd5aa8f12cdc2158bf787965c2c5
+Subproject commit 4e1c1186165bfb615fa8910bbd4f941efc3441e8
+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);
+ },
+ socketSetup: function() {
+ this.socket.onopen = function(event) {
+ enyo.log('WebSocket open');
+ this.ws_send_login(this.$.loginDialog.getData().password);
+ }.bind(this);
+
+ this.socket.onmessage = function(e) {
+ try {
+ var data = JSON.parse(e.data);
+ } catch(e) {
+ enyo.log('Malformed message: ' + e.data);
+ return;
+ }
+ switch (data.type) {
+ case 'login-successful':
+ enyo.Signals.send('onLoginSuccessful', {
+ username: this.jid
+ });
+ this.ws_send('get-roster');
+ break;
+
+ case 'login-failed':
+ enyo.Signals.send('onLoginFailed');
+ break;
+
+ case 'roster':
+ this.$.picker.setRoster(data.contacts);
+ break;
+
+ case 'message':
+ var mobj = {
+ jid: data.from,
+ message: data.body,
+ receivedTimestamp: (new Date()).getTime(),
+ outbound: false
+ };
+ conversationStore.appendConversation(data.from, mobj);
+ enyo.Signals.send('onMessageReceived', mobj);
+ this.$.picker.updateConversationList();
+ break;
+
+ case 'presence':
+ this.$.picker.setPresence(data);
+ break;
+
+ case 'disconnect':
+ enyo.Signals.send('onLogout');
+ enyo.log('logged out');
+ break;
+
+ case 'error':
+ socket.close();
+ setTimeout(this.socketSetup.bind(this), 1000);
+ break;
+ }
+ }.bind(this);
+ },
+ ws_send: function(type, args) {
+ var d = {
+ type: type
+ };
+ for (var i in args) {
+ d[i] = args[i];
+ }
+ this.socket.send(JSON.stringify(d));
+ },
+ ws_send_login: function(password) {
+ this.ws_send('login', {
+ password: password
+ });
+ },
+ startLogin: function(inSender, inEvent) {
+ this.$.loginDialog.reset();
+ this.$.loginDialog.show();
+ },
+ startLogout: function(inSender, inEvent) {
+ this.ws_send('logout');
+ this.socket.close();
+ this.socket = null;
+ },
+ tryLogin: function(inSender, inEvent) {
+ console.log('attempting login');
+ var logindata = this.$.loginDialog.getData();
+ this.jid = logindata.username;
+
+ if (this.socket) {
+ this.socket.close();
+ }
+ this.socket = new WebSocket('ws://' + location.host + '/chatnoir/ws/' + logindata.username);
+ this.socketSetup();
+ },
+ messageSent: function(inSender, inEvent) {
+ this.ws_send('message', {
+ to: inEvent.jid,
+ body: inEvent.message,
+ });
+ 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",
+ properties: {
+ jid: '',
+ name: '',
+ },
+ components: [
+ {kind: "onyx.Toolbar", components: [
+ {kind: "PresenceCircle"},
+ {name: "conversationJid", content: 'None'}
+ ]},
+ {name: "messageList", kind: "DataList", fit: 1, components: [
+ {components: [
+ {name: "messageItem", kind: "MessageItem"}
+ ], bindings: [
+ {from: "model.message", to: "$.messageItem.message"},
+ {from: "model.receivedTimestamp", to: "$.messageItem.receivedTimestamp"},
+ {from: "model.outbound", to: "$.messageItem.outbound"}
+ ]}
+ ]},
+ {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.$.messageList.set('collection', conversationStore.getConversationCollection(jid));
+ this.$.conversationJid.setContent(jid);
+ this.$.messageList.reset();
+ this.updateConversationList();
+ },
+ setName: function(name) {
+ this.name = name;
+ this.$.conversationJid.setContent(name || this.jid);
+ },
+ updateConversationList: function() {
+ this.$.messageList.refresh();
+ /*
+ setTimeout(function() {
+ var lastIndex = this.$.messageList.data().length - 1;
+ this.$.messageList.scrollToIndex(lastIndex);
+ }.bind(this), 0);
+ */
+ },
+ messageReceived: function(inSender, inEvent) {
+ if (inEvent.jid == this.jid) {
+ this.updateConversationList();
+ }
+ },
+ 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
+ };
+ conversationStore.appendConversation(this.jid, data);
+ enyo.Signals.send('onMessageSent', data);
+ this.updateConversationList();
+ }
+ }
+});
+// Register the LocalStorage source
+new enyo.LocalStorageSource({name: "localstorage"});
+
+enyo.kind({
+ name: "MessageModel",
+ kind: "enyo.Model",
+ source: "localstorage",
+ options: {
+ commit: true
+ },
+ attributes: {
+ message: "",
+ receivedTimestamp: 0,
+ outbound: false
+ }
+});
+
+enyo.kind({
+ name: "ConversationCollection",
+ kind: "enyo.Collection",
+ options: {
+ commit: true
+ },
+ model: "MessageModel",
+ source: "localstorage",
+ // Because LocalStorageSource doesn't support dynamic getUrl()
+ constructor: enyo.inherit(function(sup) {
+ return function() {
+ sup.apply(this, arguments);
+ this.set('url', 'conversation:' + this.jid);
+ }
+ })
+});
+
+enyo.kind({
+ name: "ConversationStore",
+ kind: "Component",
+ conversations: {},
+ retentionWindow: 86400,
+ create: function() {
+ this.inherited(arguments);
+ },
+ loadConversations: function() {
+ if (!localStorage.conversationJids) {
+ return;
+ }
+ var jids = localStorage.conversationJids.split(' ');
+ var len = jids.length;
+ for (var i = 0; i < len; i++) {
+ var jid = jids[i];
+ this.conversations[jid] = new ConversationCollection({jid: jid});
+ this.conversations[jid].fetch();
+ }
+ },
+ saveConversationList: function() {
+ localStorage.conversationJids = Object.keys(this.conversations).join(' ');
+ },
+ getConversationCollection: function(jid) {
+ if (!(jid in this.conversations)) {
+ this.conversations[jid] = new ConversationCollection({jid: jid});
+ }
+ return this.conversations[jid];
+ },
+ getConversationSummary: function() {
+ var res = []
+ for (jid in this.conversations) {
+ var len = this.conversations[jid].length;
+ var lastMessage = len > 0 ? this.conversations[jid].at(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] = new ConversationCollection({jid: jid});
+ }
+ this.conversations[jid].add(data);
+ this.cullConversation(jid);
+ this.saveConversationList();
+ },
+ clearConversation: function(jid) {
+ if (!(jid in this.conversations)) {
+ return;
+ }
+ this.conversations[jid].forEach(function(v) {
+ v.destroy();
+ });
+ },
+ deleteConversation: function(jid) {
+ if (!(jid in this.conversations)) {
+ return;
+ }
+ this.clearConversation(jid);
+ this.conversations[jid].destroy();
+ delete this.conversations[jid];
+ this.saveConversationList();
+ },
+ 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: "MessageItem",
+ kind: "Control",
+ classes: "message-item",
+ properties: {
+ message: "",
+ receivedTimestamp: 0,
+ outbound: true
+ },
+ components: [
+ {classes: "message-image-container", components: [
+ {name: "messageBuddyImage", classes: "buddy-image"}
+ ]},
+ {components: [
+ {name: "messageData", classes: "message-data"},
+ {name: "messageTimestamp", classes: "message-timestamp"}
+ ]}
+ ],
+ bindings: [
+ {from: "message", to: "$.messageData.content"},
+ {from: "receivedTimestamp", to: "$.messageTimestamp.content", transform: function(val) { return new Date(val).toLocaleTimeString() }}
+ ],
+ outboundChanged: function() {
+ this.addRemoveClass('outbound', this.outbound);
+ }
+});
+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-presence", components: [
+ {name: "conversationBuddyPresence", kind: "PresenceCircle"},
+ ]},
+ {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-presence", components: [
+ {name: "buddyPresence", kind: "PresenceCircle"},
+ ]},
+ {classes: "buddy-content", components: [
+ {name: "buddyName", classes: "buddy-name"},
+ {name: "buddyPresenceText", classes: "buddy-presence-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 = [];
+ for (var i = 0; i < data.length; i++) {
+ this.roster[data[i].jid] = data[i];
+ this.mergeBuddyInfo(data[i]);
+ }
+ 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 presence;
+ if (this.buddiesByJid[row.jid]) {
+ presence = this.buddiesByJid[row.jid].presence;
+ } else {
+ presence = 'unknown';
+ }
+ this.$.conversationBuddyPresence.setPresence(presence);
+ 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.$.buddyPresence.setPresence(row.presence);
+ this.$.buddyName.setContent(row.name);
+ this.$.buddyPresenceText.setContent(row.presenceText);
+ },
+ 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: "PresenceCircle",
+ kind: "Control",
+ classes: "presence-circle",
+ setPresence: 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',
+ 'PresenceCircle.js',
+ 'MessageItem.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;
+}
+
+.presence-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-presence-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;
+}