vines 0.3.2 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README +5 -9
- data/Rakefile +11 -9
- data/conf/config.rb +30 -4
- data/lib/vines/cluster/connection.rb +26 -0
- data/lib/vines/cluster/publisher.rb +55 -0
- data/lib/vines/cluster/pubsub.rb +92 -0
- data/lib/vines/cluster/sessions.rb +125 -0
- data/lib/vines/cluster/subscriber.rb +108 -0
- data/lib/vines/cluster.rb +246 -0
- data/lib/vines/command/init.rb +21 -24
- data/lib/vines/config/host.rb +48 -8
- data/lib/vines/config/port.rb +5 -0
- data/lib/vines/config/pubsub.rb +108 -0
- data/lib/vines/config.rb +74 -20
- data/lib/vines/jid.rb +14 -0
- data/lib/vines/router.rb +69 -55
- data/lib/vines/stanza/iq/disco_info.rb +22 -9
- data/lib/vines/stanza/iq/disco_items.rb +6 -3
- data/lib/vines/stanza/iq/ping.rb +1 -1
- data/lib/vines/stanza/iq/private_storage.rb +4 -8
- data/lib/vines/stanza/iq/roster.rb +6 -14
- data/lib/vines/stanza/iq/session.rb +2 -7
- data/lib/vines/stanza/iq/vcard.rb +4 -6
- data/lib/vines/stanza/iq/version.rb +1 -1
- data/lib/vines/stanza/iq.rb +8 -10
- data/lib/vines/stanza/presence/subscribe.rb +3 -11
- data/lib/vines/stanza/presence/subscribed.rb +16 -29
- data/lib/vines/stanza/presence/unsubscribe.rb +3 -15
- data/lib/vines/stanza/presence/unsubscribed.rb +3 -16
- data/lib/vines/stanza/presence.rb +30 -0
- data/lib/vines/stanza/pubsub/create.rb +39 -0
- data/lib/vines/stanza/pubsub/delete.rb +41 -0
- data/lib/vines/stanza/pubsub/publish.rb +66 -0
- data/lib/vines/stanza/pubsub/subscribe.rb +44 -0
- data/lib/vines/stanza/pubsub/unsubscribe.rb +30 -0
- data/lib/vines/stanza/pubsub.rb +22 -0
- data/lib/vines/stanza.rb +72 -22
- data/lib/vines/storage/couchdb.rb +46 -65
- data/lib/vines/storage/local.rb +20 -14
- data/lib/vines/storage/mongodb.rb +132 -0
- data/lib/vines/storage/null.rb +39 -0
- data/lib/vines/storage/redis.rb +61 -68
- data/lib/vines/storage/sql.rb +73 -69
- data/lib/vines/storage.rb +1 -1
- data/lib/vines/stream/client/bind.rb +2 -2
- data/lib/vines/stream/client/session.rb +71 -16
- data/lib/vines/stream/component/handshake.rb +1 -0
- data/lib/vines/stream/component/ready.rb +2 -2
- data/lib/vines/stream/http/session.rb +2 -0
- data/lib/vines/stream/http.rb +0 -6
- data/lib/vines/stream/server/final_restart.rb +1 -0
- data/lib/vines/stream/server/outbound/final_features.rb +1 -0
- data/lib/vines/stream/server/ready.rb +6 -2
- data/lib/vines/stream/server.rb +4 -3
- data/lib/vines/stream.rb +10 -6
- data/lib/vines/version.rb +1 -1
- data/lib/vines.rb +48 -22
- data/test/cluster/publisher_test.rb +45 -0
- data/test/cluster/sessions_test.rb +54 -0
- data/test/cluster/subscriber_test.rb +94 -0
- data/test/config/host_test.rb +100 -21
- data/test/config/pubsub_test.rb +181 -0
- data/test/config_test.rb +225 -43
- data/test/jid_test.rb +7 -0
- data/test/router_test.rb +181 -9
- data/test/stanza/iq/disco_info_test.rb +8 -6
- data/test/stanza/iq/disco_items_test.rb +3 -3
- data/test/stanza/iq/private_storage_test.rb +8 -19
- data/test/stanza/iq/roster_test.rb +1 -1
- data/test/stanza/iq/session_test.rb +3 -6
- data/test/stanza/iq/vcard_test.rb +6 -2
- data/test/stanza/iq/version_test.rb +3 -2
- data/test/stanza/iq_test.rb +5 -5
- data/test/stanza/message_test.rb +3 -2
- data/test/stanza/presence/probe_test.rb +2 -1
- data/test/stanza/pubsub/create_test.rb +138 -0
- data/test/stanza/pubsub/delete_test.rb +142 -0
- data/test/stanza/pubsub/publish_test.rb +373 -0
- data/test/stanza/pubsub/subscribe_test.rb +186 -0
- data/test/stanza/pubsub/unsubscribe_test.rb +179 -0
- data/test/stanza_test.rb +2 -1
- data/test/storage/local_test.rb +26 -25
- data/test/storage/mock_mongo.rb +40 -0
- data/test/storage/mock_redis.rb +98 -0
- data/test/storage/mongodb_test.rb +81 -0
- data/test/storage/null_test.rb +30 -0
- data/test/storage/redis_test.rb +3 -36
- data/test/stream/component/handshake_test.rb +4 -0
- data/test/stream/component/ready_test.rb +2 -1
- data/test/stream/server/ready_test.rb +7 -1
- data/web/404.html +5 -3
- data/web/chat/coffeescripts/chat.coffee +9 -5
- data/web/chat/javascripts/app.js +1 -1
- data/web/chat/javascripts/chat.js +14 -8
- data/web/chat/stylesheets/chat.css +4 -1
- data/web/lib/coffeescripts/button.coffee +9 -5
- data/web/lib/coffeescripts/filter.coffee +1 -1
- data/web/lib/coffeescripts/login.coffee +14 -1
- data/web/lib/coffeescripts/session.coffee +8 -11
- data/web/lib/images/dark-gray.png +0 -0
- data/web/lib/images/light-gray.png +0 -0
- data/web/lib/images/logo-large.png +0 -0
- data/web/lib/images/logo-small.png +0 -0
- data/web/lib/images/white.png +0 -0
- data/web/lib/javascripts/base.js +9 -8
- data/web/lib/javascripts/button.js +20 -12
- data/web/lib/javascripts/filter.js +1 -1
- data/web/lib/javascripts/icons.js +7 -1
- data/web/lib/javascripts/jquery.js +4 -4
- data/web/lib/javascripts/login.js +16 -2
- data/web/lib/javascripts/raphael.js +5 -7
- data/web/lib/javascripts/session.js +10 -14
- data/web/lib/stylesheets/base.css +7 -11
- data/web/lib/stylesheets/login.css +31 -27
- metadata +100 -34
@@ -0,0 +1,30 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'vines'
|
4
|
+
require 'minitest/autorun'
|
5
|
+
|
6
|
+
class NullStorageTest < MiniTest::Unit::TestCase
|
7
|
+
def setup
|
8
|
+
@storage = Vines::Storage::Null.new
|
9
|
+
@user = Vines::User.new(jid: 'alice@wonderland.lit')
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_find_user_returns_nil
|
13
|
+
assert_nil @storage.find_user(@user.jid)
|
14
|
+
@storage.save_user(@user)
|
15
|
+
assert_nil @storage.find_user(@user.jid)
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_find_vcard_returns_nil
|
19
|
+
assert_nil @storage.find_vcard(@user.jid)
|
20
|
+
@storage.save_vcard(@user.jid, 'card')
|
21
|
+
assert_nil @storage.find_vcard(@user.jid)
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_find_fragment_returns_nil
|
25
|
+
assert_nil @storage.find_fragment(@user.jid, 'node')
|
26
|
+
@storage.save_fragment(@user.jid, 'node')
|
27
|
+
assert_nil @storage.find_fragment(@user.jid, 'node')
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
end
|
data/test/storage/redis_test.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# encoding: UTF-8
|
2
2
|
|
3
|
+
require 'mock_redis'
|
3
4
|
require 'storage_tests'
|
4
5
|
require 'vines'
|
5
6
|
require 'minitest/autorun'
|
@@ -7,41 +8,7 @@ require 'minitest/autorun'
|
|
7
8
|
class RedisTest < MiniTest::Unit::TestCase
|
8
9
|
include StorageTests
|
9
10
|
|
10
|
-
MOCK_REDIS =
|
11
|
-
def initialize
|
12
|
-
@db = {}
|
13
|
-
end
|
14
|
-
def del(key)
|
15
|
-
@db.delete(key)
|
16
|
-
EM.next_tick { yield if block_given? }
|
17
|
-
end
|
18
|
-
def get(key)
|
19
|
-
EM.next_tick { yield @db[key] }
|
20
|
-
end
|
21
|
-
def set(key, value)
|
22
|
-
@db[key] = value
|
23
|
-
EM.next_tick { yield if block_given? }
|
24
|
-
end
|
25
|
-
def hget(key, field)
|
26
|
-
EM.next_tick { yield @db[key][field] rescue nil }
|
27
|
-
end
|
28
|
-
def hgetall(key)
|
29
|
-
EM.next_tick { yield @db[key] || {} }
|
30
|
-
end
|
31
|
-
def hset(key, field, value)
|
32
|
-
@db[key] ||= {}
|
33
|
-
@db[key][field] = value
|
34
|
-
EM.next_tick { yield if block_given? }
|
35
|
-
end
|
36
|
-
def hmset(key, *args)
|
37
|
-
@db[key] = Hash[*args]
|
38
|
-
EM.next_tick { yield if block_given? }
|
39
|
-
end
|
40
|
-
def flushdb
|
41
|
-
@db.clear
|
42
|
-
EM.next_tick { yield if block_given? }
|
43
|
-
end
|
44
|
-
end.new
|
11
|
+
MOCK_REDIS = MockRedis.new
|
45
12
|
|
46
13
|
def setup
|
47
14
|
EMLoop.new do
|
@@ -54,7 +21,7 @@ class RedisTest < MiniTest::Unit::TestCase
|
|
54
21
|
'password' => BCrypt::Password.create('secret'),
|
55
22
|
'name' => 'Tester'
|
56
23
|
}.to_json)
|
57
|
-
db.hmset('roster:full@wonderland.lit',
|
24
|
+
db.hmset('roster:full@wonderland.lit',
|
58
25
|
'contact1@wonderland.lit',
|
59
26
|
{'name' => 'Contact1', 'groups' => %w[Group1 Group2]}.to_json,
|
60
27
|
'contact2@wonderland.lit',
|
@@ -29,12 +29,16 @@ class HandshakeTest < MiniTest::Unit::TestCase
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def test_valid_secret
|
32
|
+
router = MiniTest::Mock.new
|
33
|
+
router.expect(:<<, nil, [@stream])
|
34
|
+
@stream.expect(:router, router)
|
32
35
|
@stream.expect(:secret, 'secr3t')
|
33
36
|
@stream.expect(:write, nil, ['<handshake/>'])
|
34
37
|
@stream.expect(:advance, nil, [Vines::Stream::Component::Ready.new(@stream)])
|
35
38
|
node = node('<handshake>secr3t</handshake>')
|
36
39
|
@state.node(node)
|
37
40
|
assert @stream.verify
|
41
|
+
assert router.verify
|
38
42
|
end
|
39
43
|
|
40
44
|
private
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# encoding: UTF-8
|
2
2
|
|
3
|
+
require 'tmpdir'
|
3
4
|
require 'vines'
|
4
5
|
require 'ext/nokogiri'
|
5
6
|
require 'minitest/autorun'
|
@@ -10,7 +11,7 @@ class ComponentReadyTest < MiniTest::Unit::TestCase
|
|
10
11
|
@state = Vines::Stream::Component::Ready.new(@stream, nil)
|
11
12
|
@config = Vines::Config.new do
|
12
13
|
host 'wonderland.lit' do
|
13
|
-
storage(:fs) { dir
|
14
|
+
storage(:fs) { dir Dir.tmpdir }
|
14
15
|
end
|
15
16
|
end
|
16
17
|
end
|
@@ -23,13 +23,19 @@ class ServerReadyTest < MiniTest::Unit::TestCase
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def test_good_node_processes
|
26
|
+
config = MiniTest::Mock.new
|
27
|
+
config.expect(:local_jid?, true, [Vines::JID.new('romeo@verona.lit')])
|
28
|
+
|
29
|
+
@stream.expect(:config, config)
|
26
30
|
@stream.expect(:remote_domain, 'wonderland.lit')
|
27
31
|
@stream.expect(:domain, 'verona.lit')
|
28
|
-
@stream.expect(:user=, nil, [Vines::User.new(:
|
32
|
+
@stream.expect(:user=, nil, [Vines::User.new(jid: 'alice@wonderland.lit')])
|
33
|
+
|
29
34
|
node = node(%Q{<message from="alice@wonderland.lit" to="romeo@verona.lit"/>})
|
30
35
|
@state.node(node)
|
31
36
|
assert_equal 1, STANZAS.size
|
32
37
|
assert @stream.verify
|
38
|
+
assert config.verify
|
33
39
|
end
|
34
40
|
|
35
41
|
def test_unsupported_stanza_type
|
data/web/404.html
CHANGED
@@ -8,8 +8,10 @@
|
|
8
8
|
<link rel="stylesheet" href="/lib/stylesheets/base.css"/>
|
9
9
|
<style type="text/css">
|
10
10
|
body {
|
11
|
-
background: -moz-radial-gradient(
|
12
|
-
background: -
|
11
|
+
background: -moz-radial-gradient(rgba(26, 55, 98, 0.8), rgba(12, 26, 45, 0.8)), url(/lib/images/dark-gray.png);
|
12
|
+
background: -ms-radial-gradient(center, 500px 500px, rgba(26, 55, 98, 0.8), rgba(12, 26, 45, 0.8)), url(/lib/images/dark-gray.png);
|
13
|
+
background: -o-radial-gradient(rgba(26, 55, 98, 0.8), rgba(12, 26, 45, 0.8)), url(/lib/images/dark-gray.png);
|
14
|
+
background: -webkit-radial-gradient(center, 500px 500px, rgba(26, 55, 98, 0.8), rgba(12, 26, 45, 0.8)), url(/lib/images/dark-gray.png);
|
13
15
|
display: table;
|
14
16
|
text-align: center;
|
15
17
|
width: 100%;
|
@@ -22,7 +24,7 @@
|
|
22
24
|
h1 {
|
23
25
|
background: url(/lib/images/logo-large.png) no-repeat center;
|
24
26
|
color: transparent;
|
25
|
-
height:
|
27
|
+
height: 82px;
|
26
28
|
text-shadow: none;
|
27
29
|
width: 100%;
|
28
30
|
}
|
@@ -6,6 +6,7 @@ class ChatPage
|
|
6
6
|
@session.onPresence (p) => this.presence(p)
|
7
7
|
@chats = {}
|
8
8
|
@currentContact = null
|
9
|
+
@layout = null
|
9
10
|
|
10
11
|
datef: (millis) ->
|
11
12
|
d = new Date(millis)
|
@@ -47,6 +48,7 @@ class ChatPage
|
|
47
48
|
node.click (event) => this.selectContact(event)
|
48
49
|
|
49
50
|
message: (message) ->
|
51
|
+
return unless message.type == 'chat' && message.text
|
50
52
|
this.queueMessage message
|
51
53
|
me = message.from == @session.jid()
|
52
54
|
from = message.from.split('/')[0]
|
@@ -193,6 +195,7 @@ class ChatPage
|
|
193
195
|
from: @session.jid()
|
194
196
|
text: text
|
195
197
|
to: jid
|
198
|
+
type: 'chat'
|
196
199
|
received: new Date()
|
197
200
|
@session.sendMessage jid, text
|
198
201
|
input.val ''
|
@@ -241,8 +244,9 @@ class ChatPage
|
|
241
244
|
fn() if fn
|
242
245
|
form.fadeIn 100
|
243
246
|
else
|
244
|
-
form.fadeOut 100,
|
247
|
+
form.fadeOut 100, =>
|
245
248
|
form[0].reset()
|
249
|
+
@layout.resize()
|
246
250
|
fn() if fn
|
247
251
|
|
248
252
|
draw: ->
|
@@ -333,11 +337,11 @@ class ChatPage
|
|
333
337
|
$('#edit-contact-form').submit => this.updateContact()
|
334
338
|
|
335
339
|
$('#container').fadeIn 200
|
336
|
-
layout = this.resize()
|
340
|
+
@layout = this.resize()
|
337
341
|
|
338
|
-
fn =
|
339
|
-
layout.resize()
|
340
|
-
layout.resize() # not sure why two are needed
|
342
|
+
fn = =>
|
343
|
+
@layout.resize()
|
344
|
+
@layout.resize() # not sure why two are needed
|
341
345
|
|
342
346
|
new Filter
|
343
347
|
list: '#roster'
|
data/web/chat/javascripts/app.js
CHANGED
@@ -1 +1 @@
|
|
1
|
-
var ChatPage,__bind=function(a,b){return function(){return a.apply(b,arguments)}};ChatPage=function(){function a(a){this.session=a,this.session.onRoster(__bind(function(){return this.roster()},this)),this.session.onCard(__bind(function(a){return this.card(a)},this)),this.session.onMessage(__bind(function(a){return this.message(a)},this)),this.session.onPresence(__bind(function(a){return this.presence(a)},this)),this.chats={},this.currentContact=null}return a.prototype.datef=function(a){var b,c,d,e;return b=new Date(a),d=b.getHours()<12?" am":" pm",c=b.getHours()>12?b.getHours()-12:b.getHours(),c===0&&(c=12),e=b.getMinutes()+"",e.length===1&&(e="0"+e),c+":"+e+d},a.prototype.card=function(a){return this.eachContact(a.jid,__bind(function(b){return $(".vcard-img",b).attr("src",this.session.avatar(a.jid))},this))},a.prototype.roster=function(){var a,b,c,d,e,f,g,h;e=$("#roster"),$("li",e).each(__bind(function(a,b){var c;c=$(b).attr("data-jid");if(!this.session.roster[c])return $(b).remove()},this)),f=function(a,b){return $(".text",a).text(b.name||b.jid),a.attr("data-name",b.name||"")},g=this.session.roster,h=[];for(c in g)a=g[c],b=$("#roster li[data-jid='"+c+"']"),f(b,a),h.push(b.length===0?(d=$('<li data-jid="'+c+'" data-name="" class="offline">\n <span class="text"></span>\n <span class="status-msg">Offline</span>\n <span class="unread" style="display:none;"></span>\n <img class="vcard-img" alt="'+c+'" src="'+this.session.avatar(c)+'"/>\n</li>').appendTo(e),f(d,a),d.click(__bind(function(a){return this.selectContact(a)},this))):void 0);return h},a.prototype.message=function(a){var b,c,d,e;this.queueMessage(a),e=a.from===this.session.jid(),d=a.from.split("/")[0];if(!e&&d!==this.currentContact)return c=this.chat(a.from),c.unread++,this.eachContact(d,function(a){return $(".unread",a).text(c.unread).show()});b=this.atBottom(),this.appendMessage(a);if(b)return this.scroll()},a.prototype.eachContact=function(a,b){var c,d,e,f,g;f=$("#roster li[data-jid='"+a+"']").get(),g=[];for(d=0,e=f.length;d<e;d++)c=f[d],g.push(b($(c)));return g},a.prototype.appendMessage=function(a){var b,c,d,e;return c=a.from.split("/")[0],b=this.session.roster[c],d=b?b.name||c:c,a.from===this.session.jid()&&(d="Me"),e=$('<li data-jid="'+c+'" style="display:none;">\n <p></p>\n <img alt="'+c+'" src="'+this.session.avatar(c)+'"/>\n <footer>\n <span class="author"></span>\n <span class="time">'+this.datef(a.received)+"</span>\n </footer>\n</li>").appendTo("#messages"),$("p",e).text(a.text),$(".author",e).text(d),e.fadeIn(200)},a.prototype.queueMessage=function(a){var b,c,d;return d=a.from===this.session.jid(),c=a[d?"to":"from"],b=this.chat(c),b.jid=c,b.messages.push(a)},a.prototype.chat=function(a){var b,c;return b=a.split("/")[0],c=this.chats[b],c||(c={jid:a,messages:[],unread:0},this.chats[b]=c),c},a.prototype.presence=function(a){var b,c,d;c=a.from.split("/")[0];if(c===this.session.bareJid())return;if(!a.type||a.offline)b=this.session.roster[c],this.eachContact(c,function(a){return $(".status-msg",a).text(b.status()),b.offline()?a.addClass("offline"):a.removeClass("offline")});a.offline&&(this.chat(c).jid=c);if(a.type==="subscribe")return d=$('<li data-jid="'+a.from+'" style="display:none;">\n <form class="inset">\n <h2>Buddy Approval</h2>\n <p>'+a.from+' wants to add you as a buddy.</p>\n <fieldset class="buttons">\n <input type="button" value="Decline"/>\n <input type="submit" value="Accept"/>\n </fieldset>\n </form>\n</li>').appendTo("#notifications"),d.fadeIn(200),$("form",d).submit(__bind(function(){return this.acceptContact(d,a.from)},this)),$('input[type="button"]',d).click(__bind(function(){return this.rejectContact(d,a.from)},this))},a.prototype.acceptContact=function(a,b){return a.fadeOut(200,function(){return a.remove()}),this.session.sendSubscribed(b),this.session.sendSubscribe(b),!1},a.prototype.rejectContact=function(a,b){return a.fadeOut(200,function(){return a.remove()}),this.session.sendUnsubscribed(b)},a.prototype.selectContact=function(a){var b,c,d,e,f,g,h;d=$(a.currentTarget).attr("data-jid"),c=this.session.roster[d];if(this.currentContact===d)return;this.currentContact=d,$("#roster li").removeClass("selected"),$(a.currentTarget).addClass("selected"),$("#chat-title").text("Chat with "+(c.name||c.jid)),$("#messages").empty(),b=this.chats[d],e=[],b&&(e=b.messages,b.unread=0,this.eachContact(d,function(a){return $(".unread",a).text("").hide()}));for(g=0,h=e.length;g<h;g++)f=e[g],this.appendMessage(f);return this.scroll(),$("#remove-contact-msg").html("Are you sure you want to remove "+("<strong>"+this.currentContact+"</strong> from your buddy list?")),$("#remove-contact-form .buttons").fadeIn(200),$("#edit-contact-jid").text(this.currentContact),$("#edit-contact-name").val(this.session.roster[this.currentContact].name),$("#edit-contact-form input").fadeIn(200),$("#edit-contact-form .buttons").fadeIn(200)},a.prototype.scroll=function(){var a;return a=$("#messages"),a.animate({scrollTop:a.prop("scrollHeight")},400)},a.prototype.atBottom=function(){var a,b;return b=$("#messages"),a=b.prop("scrollHeight")-b.outerHeight(),b.scrollTop()>=a},a.prototype.send=function(){var a,b,c,d;return this.currentContact?(b=$("#message"),d=b.val().trim(),d&&(a=this.chats[this.currentContact],c=a?a.jid:this.currentContact,this.message({from:this.session.jid(),text:d,to:c,received:new Date}),this.session.sendMessage(c,d)),b.val(""),!1):!1},a.prototype.addContact=function(){var a;return this.toggleForm("#add-contact-form"),a={jid:$("#add-contact-jid").val(),name:$("#add-contact-name").val(),groups:["Buddies"]},a.jid&&this.session.updateContact(a,!0),!1},a.prototype.removeContact=function(){return this.toggleForm("#remove-contact-form"),this.session.removeContact(this.currentContact),this.currentContact=null,$("#chat-title").text("Select a buddy to chat"),$("#messages").empty(),$("#remove-contact-msg").html("Select a buddy in the list above to remove."),$("#remove-contact-form .buttons").hide(),$("#edit-contact-jid").text("Select a buddy in the list above to update."),$("#edit-contact-name").val(""),$("#edit-contact-form input").hide(),$("#edit-contact-form .buttons").hide(),!1},a.prototype.updateContact=function(){var a;return this.toggleForm("#edit-contact-form"),a={jid:this.currentContact,name:$("#edit-contact-name").val(),groups:this.session.roster[this.currentContact].groups},this.session.updateContact(a),!1},a.prototype.toggleForm=function(a,b){return a=$(a),$("form.overlay").each(function(){if(this.id!==a.attr("id"))return $(this).hide()}),a.is(":hidden")?(b&&b(),a.fadeIn(100)):a.fadeOut(100,function(){a[0].reset();if(b)return b()})},a.prototype.draw=function(){var a,b;if(!this.session.connected()){window.location.hash="";return}return $("body").attr("id","chat-page"),$("#container").hide().empty(),$('<div id="alpha" class="sidebar column y-fill">\n <h2>Buddies <div id="search-roster-icon"></div></h2>\n <div id="search-roster-form"></div>\n <ul id="roster" class="selectable scroll y-fill"></ul>\n <div id="alpha-controls" class="controls">\n <div id="add-contact"></div>\n <div id="remove-contact"></div>\n <div id="edit-contact"></div>\n </div>\n <form id="add-contact-form" class="overlay" style="display:none;">\n <h2>Add Buddy</h2>\n <input id="add-contact-jid" type="email" maxlength="1024" placeholder="Account name"/>\n <input id="add-contact-name" type="text" maxlength="1024" placeholder="Real name"/>\n <fieldset class="buttons">\n <input id="add-contact-cancel" type="button" value="Cancel"/>\n <input id="add-contact-ok" type="submit" value="Add"/>\n </fieldset>\n </form>\n <form id="remove-contact-form" class="overlay" style="display:none;">\n <h2>Remove Buddy</h2>\n <p id="remove-contact-msg">Select a buddy in the list above to remove.</p>\n <fieldset class="buttons" style="display:none;">\n <input id="remove-contact-cancel" type="button" value="Cancel"/>\n <input id="remove-contact-ok" type="submit" value="Remove"/>\n </fieldset>\n </form>\n <form id="edit-contact-form" class="overlay" style="display:none;">\n <h2>Update Profile</h2>\n <p id="edit-contact-jid">Select a buddy in the list above to update.</p>\n <input id="edit-contact-name" type="text" maxlength="1024" placeholder="Real name" style="display:none;"/>\n <fieldset class="buttons" style="display:none;">\n <input id="edit-contact-cancel" type="button" value="Cancel"/>\n <input id="edit-contact-ok" type="submit" value="Save"/>\n </fieldset>\n </form>\n</div>\n<div id="beta" class="primary column x-fill y-fill">\n <h2 id="chat-title">Select a buddy to chat</h2>\n <ul id="messages" class="scroll y-fill"></ul>\n <form id="message-form">\n <input id="message" name="message" type="text" maxlength="1024" placeholder="Type a message and press enter to send"/>\n </form>\n</div>\n<div id="charlie" class="sidebar column y-fill">\n <h2>Notifications</h2>\n <ul id="notifications" class="scroll y-fill"></ul>\n <div id="charlie-controls" class="controls">\n <div id="clear-notices"></div>\n </div>\n</div>').appendTo("#container"),this.roster(),new Button("#clear-notices",ICONS.no),new Button("#add-contact",ICONS.plus),new Button("#remove-contact",ICONS.minus),new Button("#edit-contact",ICONS.user),$("#message").focus(function(){return $("form.overlay").fadeOut()}),$("#message-form").submit(__bind(function(){return this.send()},this)),$("#clear-notices").click(function(){return $("#notifications li").fadeOut(200)}),$("#add-contact").click(__bind(function(){return this.toggleForm("#add-contact-form")},this)),$("#remove-contact").click(__bind(function(){return this.toggleForm("#remove-contact-form")},this)),$("#edit-contact").click(__bind(function(){return this.toggleForm("#edit-contact-form",__bind(function(){if(this.currentContact)return $("#edit-contact-jid").text(this.currentContact),$("#edit-contact-name").val(this.session.roster[this.currentContact].name)},this))},this)),$("#add-contact-cancel").click(__bind(function(){return this.toggleForm("#add-contact-form")},this)),$("#remove-contact-cancel").click(__bind(function(){return this.toggleForm("#remove-contact-form")},this)),$("#edit-contact-cancel").click(__bind(function(){return this.toggleForm("#edit-contact-form")},this)),$("#add-contact-form").submit(__bind(function(){return this.addContact()},this)),$("#remove-contact-form").submit(__bind(function(){return this.removeContact()},this)),$("#edit-contact-form").submit(__bind(function(){return this.updateContact()},this)),$("#container").fadeIn(200),b=this.resize(),a=function(){return b.resize(),b.resize()},new Filter({list:"#roster",icon:"#search-roster-icon",form:"#search-roster-form",attrs:["data-jid","data-name"],open:a,close:a})},a.prototype.resize=function(){var a,b,c,d,e;return a=$("#alpha"),b=$("#beta"),c=$("#charlie"),e=$("#message"),d=$("#message-form"),new Layout(function(){return c.css("left",a.width()+b.width()),e.width(d.width()-32)})},a}(),$(function(){var a,b,c,d,e,f;f=new Session,d=new NavBar(f),d.draw(),a={Messages:ICONS.chat,Logout:ICONS.power};for(c in a)b=a[c],d.addButton(c,b);return e={"/messages":new ChatPage(f),"/logout":new LogoutPage(f),"default":new LoginPage(f,"/messages/")},(new Router(e)).draw(),d.select($("#nav-link-messages").parent())})
|
1
|
+
var ChatPage,__bind=function(a,b){return function(){return a.apply(b,arguments)}};ChatPage=function(){function a(a){this.session=a,this.session.onRoster(__bind(function(){return this.roster()},this)),this.session.onCard(__bind(function(a){return this.card(a)},this)),this.session.onMessage(__bind(function(a){return this.message(a)},this)),this.session.onPresence(__bind(function(a){return this.presence(a)},this)),this.chats={},this.currentContact=null,this.layout=null}return a.prototype.datef=function(a){var b,c,d,e;return b=new Date(a),d=b.getHours()<12?" am":" pm",c=b.getHours()>12?b.getHours()-12:b.getHours(),c===0&&(c=12),e=b.getMinutes()+"",e.length===1&&(e="0"+e),c+":"+e+d},a.prototype.card=function(a){return this.eachContact(a.jid,__bind(function(b){return $(".vcard-img",b).attr("src",this.session.avatar(a.jid))},this))},a.prototype.roster=function(){var a,b,c,d,e,f,g,h;e=$("#roster"),$("li",e).each(__bind(function(a,b){var c;c=$(b).attr("data-jid");if(!this.session.roster[c])return $(b).remove()},this)),f=function(a,b){return $(".text",a).text(b.name||b.jid),a.attr("data-name",b.name||"")},g=this.session.roster,h=[];for(c in g)a=g[c],b=$("#roster li[data-jid='"+c+"']"),f(b,a),h.push(b.length===0?(d=$('<li data-jid="'+c+'" data-name="" class="offline">\n <span class="text"></span>\n <span class="status-msg">Offline</span>\n <span class="unread" style="display:none;"></span>\n <img class="vcard-img" alt="'+c+'" src="'+this.session.avatar(c)+'"/>\n</li>').appendTo(e),f(d,a),d.click(__bind(function(a){return this.selectContact(a)},this))):void 0);return h},a.prototype.message=function(a){var b,c,d,e;if(a.type!=="chat"||!a.text)return;this.queueMessage(a),e=a.from===this.session.jid(),d=a.from.split("/")[0];if(!e&&d!==this.currentContact)return c=this.chat(a.from),c.unread++,this.eachContact(d,function(a){return $(".unread",a).text(c.unread).show()});b=this.atBottom(),this.appendMessage(a);if(b)return this.scroll()},a.prototype.eachContact=function(a,b){var c,d,e,f,g;f=$("#roster li[data-jid='"+a+"']").get(),g=[];for(d=0,e=f.length;d<e;d++)c=f[d],g.push(b($(c)));return g},a.prototype.appendMessage=function(a){var b,c,d,e;return c=a.from.split("/")[0],b=this.session.roster[c],d=b?b.name||c:c,a.from===this.session.jid()&&(d="Me"),e=$('<li data-jid="'+c+'" style="display:none;">\n <p></p>\n <img alt="'+c+'" src="'+this.session.avatar(c)+'"/>\n <footer>\n <span class="author"></span>\n <span class="time">'+this.datef(a.received)+"</span>\n </footer>\n</li>").appendTo("#messages"),$("p",e).text(a.text),$(".author",e).text(d),e.fadeIn(200)},a.prototype.queueMessage=function(a){var b,c,d;return d=a.from===this.session.jid(),c=a[d?"to":"from"],b=this.chat(c),b.jid=c,b.messages.push(a)},a.prototype.chat=function(a){var b,c;return b=a.split("/")[0],c=this.chats[b],c||(c={jid:a,messages:[],unread:0},this.chats[b]=c),c},a.prototype.presence=function(a){var b,c,d;c=a.from.split("/")[0];if(c===this.session.bareJid())return;if(!a.type||a.offline)b=this.session.roster[c],this.eachContact(c,function(a){return $(".status-msg",a).text(b.status()),b.offline()?a.addClass("offline"):a.removeClass("offline")});a.offline&&(this.chat(c).jid=c);if(a.type==="subscribe")return d=$('<li data-jid="'+a.from+'" style="display:none;">\n <form class="inset">\n <h2>Buddy Approval</h2>\n <p>'+a.from+' wants to add you as a buddy.</p>\n <fieldset class="buttons">\n <input type="button" value="Decline"/>\n <input type="submit" value="Accept"/>\n </fieldset>\n </form>\n</li>').appendTo("#notifications"),d.fadeIn(200),$("form",d).submit(__bind(function(){return this.acceptContact(d,a.from)},this)),$('input[type="button"]',d).click(__bind(function(){return this.rejectContact(d,a.from)},this))},a.prototype.acceptContact=function(a,b){return a.fadeOut(200,function(){return a.remove()}),this.session.sendSubscribed(b),this.session.sendSubscribe(b),!1},a.prototype.rejectContact=function(a,b){return a.fadeOut(200,function(){return a.remove()}),this.session.sendUnsubscribed(b)},a.prototype.selectContact=function(a){var b,c,d,e,f,g,h;d=$(a.currentTarget).attr("data-jid"),c=this.session.roster[d];if(this.currentContact===d)return;this.currentContact=d,$("#roster li").removeClass("selected"),$(a.currentTarget).addClass("selected"),$("#chat-title").text("Chat with "+(c.name||c.jid)),$("#messages").empty(),b=this.chats[d],e=[],b&&(e=b.messages,b.unread=0,this.eachContact(d,function(a){return $(".unread",a).text("").hide()}));for(g=0,h=e.length;g<h;g++)f=e[g],this.appendMessage(f);return this.scroll(),$("#remove-contact-msg").html("Are you sure you want to remove "+("<strong>"+this.currentContact+"</strong> from your buddy list?")),$("#remove-contact-form .buttons").fadeIn(200),$("#edit-contact-jid").text(this.currentContact),$("#edit-contact-name").val(this.session.roster[this.currentContact].name),$("#edit-contact-form input").fadeIn(200),$("#edit-contact-form .buttons").fadeIn(200)},a.prototype.scroll=function(){var a;return a=$("#messages"),a.animate({scrollTop:a.prop("scrollHeight")},400)},a.prototype.atBottom=function(){var a,b;return b=$("#messages"),a=b.prop("scrollHeight")-b.outerHeight(),b.scrollTop()>=a},a.prototype.send=function(){var a,b,c,d;return this.currentContact?(b=$("#message"),d=b.val().trim(),d&&(a=this.chats[this.currentContact],c=a?a.jid:this.currentContact,this.message({from:this.session.jid(),text:d,to:c,type:"chat",received:new Date}),this.session.sendMessage(c,d)),b.val(""),!1):!1},a.prototype.addContact=function(){var a;return this.toggleForm("#add-contact-form"),a={jid:$("#add-contact-jid").val(),name:$("#add-contact-name").val(),groups:["Buddies"]},a.jid&&this.session.updateContact(a,!0),!1},a.prototype.removeContact=function(){return this.toggleForm("#remove-contact-form"),this.session.removeContact(this.currentContact),this.currentContact=null,$("#chat-title").text("Select a buddy to chat"),$("#messages").empty(),$("#remove-contact-msg").html("Select a buddy in the list above to remove."),$("#remove-contact-form .buttons").hide(),$("#edit-contact-jid").text("Select a buddy in the list above to update."),$("#edit-contact-name").val(""),$("#edit-contact-form input").hide(),$("#edit-contact-form .buttons").hide(),!1},a.prototype.updateContact=function(){var a;return this.toggleForm("#edit-contact-form"),a={jid:this.currentContact,name:$("#edit-contact-name").val(),groups:this.session.roster[this.currentContact].groups},this.session.updateContact(a),!1},a.prototype.toggleForm=function(a,b){return a=$(a),$("form.overlay").each(function(){if(this.id!==a.attr("id"))return $(this).hide()}),a.is(":hidden")?(b&&b(),a.fadeIn(100)):a.fadeOut(100,__bind(function(){a[0].reset(),this.layout.resize();if(b)return b()},this))},a.prototype.draw=function(){var a;if(!this.session.connected()){window.location.hash="";return}return $("body").attr("id","chat-page"),$("#container").hide().empty(),$('<div id="alpha" class="sidebar column y-fill">\n <h2>Buddies <div id="search-roster-icon"></div></h2>\n <div id="search-roster-form"></div>\n <ul id="roster" class="selectable scroll y-fill"></ul>\n <div id="alpha-controls" class="controls">\n <div id="add-contact"></div>\n <div id="remove-contact"></div>\n <div id="edit-contact"></div>\n </div>\n <form id="add-contact-form" class="overlay" style="display:none;">\n <h2>Add Buddy</h2>\n <input id="add-contact-jid" type="email" maxlength="1024" placeholder="Account name"/>\n <input id="add-contact-name" type="text" maxlength="1024" placeholder="Real name"/>\n <fieldset class="buttons">\n <input id="add-contact-cancel" type="button" value="Cancel"/>\n <input id="add-contact-ok" type="submit" value="Add"/>\n </fieldset>\n </form>\n <form id="remove-contact-form" class="overlay" style="display:none;">\n <h2>Remove Buddy</h2>\n <p id="remove-contact-msg">Select a buddy in the list above to remove.</p>\n <fieldset class="buttons" style="display:none;">\n <input id="remove-contact-cancel" type="button" value="Cancel"/>\n <input id="remove-contact-ok" type="submit" value="Remove"/>\n </fieldset>\n </form>\n <form id="edit-contact-form" class="overlay" style="display:none;">\n <h2>Update Profile</h2>\n <p id="edit-contact-jid">Select a buddy in the list above to update.</p>\n <input id="edit-contact-name" type="text" maxlength="1024" placeholder="Real name" style="display:none;"/>\n <fieldset class="buttons" style="display:none;">\n <input id="edit-contact-cancel" type="button" value="Cancel"/>\n <input id="edit-contact-ok" type="submit" value="Save"/>\n </fieldset>\n </form>\n</div>\n<div id="beta" class="primary column x-fill y-fill">\n <h2 id="chat-title">Select a buddy to chat</h2>\n <ul id="messages" class="scroll y-fill"></ul>\n <form id="message-form">\n <input id="message" name="message" type="text" maxlength="1024" placeholder="Type a message and press enter to send"/>\n </form>\n</div>\n<div id="charlie" class="sidebar column y-fill">\n <h2>Notifications</h2>\n <ul id="notifications" class="scroll y-fill"></ul>\n <div id="charlie-controls" class="controls">\n <div id="clear-notices"></div>\n </div>\n</div>').appendTo("#container"),this.roster(),new Button("#clear-notices",ICONS.no),new Button("#add-contact",ICONS.plus),new Button("#remove-contact",ICONS.minus),new Button("#edit-contact",ICONS.user),$("#message").focus(function(){return $("form.overlay").fadeOut()}),$("#message-form").submit(__bind(function(){return this.send()},this)),$("#clear-notices").click(function(){return $("#notifications li").fadeOut(200)}),$("#add-contact").click(__bind(function(){return this.toggleForm("#add-contact-form")},this)),$("#remove-contact").click(__bind(function(){return this.toggleForm("#remove-contact-form")},this)),$("#edit-contact").click(__bind(function(){return this.toggleForm("#edit-contact-form",__bind(function(){if(this.currentContact)return $("#edit-contact-jid").text(this.currentContact),$("#edit-contact-name").val(this.session.roster[this.currentContact].name)},this))},this)),$("#add-contact-cancel").click(__bind(function(){return this.toggleForm("#add-contact-form")},this)),$("#remove-contact-cancel").click(__bind(function(){return this.toggleForm("#remove-contact-form")},this)),$("#edit-contact-cancel").click(__bind(function(){return this.toggleForm("#edit-contact-form")},this)),$("#add-contact-form").submit(__bind(function(){return this.addContact()},this)),$("#remove-contact-form").submit(__bind(function(){return this.removeContact()},this)),$("#edit-contact-form").submit(__bind(function(){return this.updateContact()},this)),$("#container").fadeIn(200),this.layout=this.resize(),a=__bind(function(){return this.layout.resize(),this.layout.resize()},this),new Filter({list:"#roster",icon:"#search-roster-icon",form:"#search-roster-form",attrs:["data-jid","data-name"],open:a,close:a})},a.prototype.resize=function(){var a,b,c,d,e;return a=$("#alpha"),b=$("#beta"),c=$("#charlie"),e=$("#message"),d=$("#message-form"),new Layout(function(){return c.css("left",a.width()+b.width()),e.width(d.width()-32)})},a}(),$(function(){var a,b,c,d,e,f;f=new Session,d=new NavBar(f),d.draw(),a={Messages:ICONS.chat,Logout:ICONS.power};for(c in a)b=a[c],d.addButton(c,b);return e={"/messages":new ChatPage(f),"/logout":new LogoutPage(f),"default":new LoginPage(f,"/messages/")},(new Router(e)).draw(),d.select($("#nav-link-messages").parent())})
|
@@ -17,6 +17,7 @@ ChatPage = (function() {
|
|
17
17
|
}, this));
|
18
18
|
this.chats = {};
|
19
19
|
this.currentContact = null;
|
20
|
+
this.layout = null;
|
20
21
|
}
|
21
22
|
ChatPage.prototype.datef = function(millis) {
|
22
23
|
var d, hour, meridian, minutes;
|
@@ -65,6 +66,9 @@ ChatPage = (function() {
|
|
65
66
|
};
|
66
67
|
ChatPage.prototype.message = function(message) {
|
67
68
|
var bottom, chat, from, me;
|
69
|
+
if (!(message.type === 'chat' && message.text)) {
|
70
|
+
return;
|
71
|
+
}
|
68
72
|
this.queueMessage(message);
|
69
73
|
me = message.from === this.session.jid();
|
70
74
|
from = message.from.split('/')[0];
|
@@ -232,6 +236,7 @@ ChatPage = (function() {
|
|
232
236
|
from: this.session.jid(),
|
233
237
|
text: text,
|
234
238
|
to: jid,
|
239
|
+
type: 'chat',
|
235
240
|
received: new Date()
|
236
241
|
});
|
237
242
|
this.session.sendMessage(jid, text);
|
@@ -290,16 +295,17 @@ ChatPage = (function() {
|
|
290
295
|
}
|
291
296
|
return form.fadeIn(100);
|
292
297
|
} else {
|
293
|
-
return form.fadeOut(100, function() {
|
298
|
+
return form.fadeOut(100, __bind(function() {
|
294
299
|
form[0].reset();
|
300
|
+
this.layout.resize();
|
295
301
|
if (fn) {
|
296
302
|
return fn();
|
297
303
|
}
|
298
|
-
});
|
304
|
+
}, this));
|
299
305
|
}
|
300
306
|
};
|
301
307
|
ChatPage.prototype.draw = function() {
|
302
|
-
var fn
|
308
|
+
var fn;
|
303
309
|
if (!this.session.connected()) {
|
304
310
|
window.location.hash = '';
|
305
311
|
return;
|
@@ -354,11 +360,11 @@ ChatPage = (function() {
|
|
354
360
|
return this.updateContact();
|
355
361
|
}, this));
|
356
362
|
$('#container').fadeIn(200);
|
357
|
-
layout = this.resize();
|
358
|
-
fn = function() {
|
359
|
-
layout.resize();
|
360
|
-
return layout.resize();
|
361
|
-
};
|
363
|
+
this.layout = this.resize();
|
364
|
+
fn = __bind(function() {
|
365
|
+
this.layout.resize();
|
366
|
+
return this.layout.resize();
|
367
|
+
}, this);
|
362
368
|
return new Filter({
|
363
369
|
list: '#roster',
|
364
370
|
icon: '#search-roster-icon',
|
@@ -47,7 +47,10 @@
|
|
47
47
|
content: '\2014 ';
|
48
48
|
}
|
49
49
|
#chat-page #message-form {
|
50
|
-
background: #f8f8f8;
|
50
|
+
background: #f8f8f8 -moz-linear-gradient(rgba(255, 255, 255, 0.1), rgba(0, 0, 0, 0.05));
|
51
|
+
background: #f8f8f8 -ms-linear-gradient(rgba(255, 255, 255, 0.1), rgba(0, 0, 0, 0.05));
|
52
|
+
background: #f8f8f8 -o-linear-gradient(rgba(255, 255, 255, 0.1), rgba(0, 0, 0, 0.05));
|
53
|
+
background: #f8f8f8 -webkit-linear-gradient(rgba(255, 255, 255, 0.1), rgba(0, 0, 0, 0.05));
|
51
54
|
border-top: 1px solid #dfdfdf;
|
52
55
|
height: 50px;
|
53
56
|
position: absolute;
|
@@ -3,19 +3,23 @@ class Button
|
|
3
3
|
@node = $ node
|
4
4
|
@path = path
|
5
5
|
@options = options || {}
|
6
|
+
@options.animate = true unless @options.animate?
|
6
7
|
this.draw()
|
7
8
|
|
8
9
|
draw: ->
|
9
10
|
paper = Raphael @node.get(0)
|
10
11
|
|
12
|
+
transform = "s#{@options.scale || 0.85}"
|
13
|
+
transform += ",t#{@options.translation}" if @options.translation
|
14
|
+
|
11
15
|
icon = paper.path(@path).attr
|
12
16
|
fill: @options.fill || '#000'
|
13
17
|
stroke: @options.stroke || '#fff'
|
14
18
|
'stroke-width': @options['stroke-width'] || 0.3
|
15
19
|
opacity: @options.opacity || 0.6
|
16
|
-
|
17
|
-
translation: @options.translation || ''
|
20
|
+
transform: transform
|
18
21
|
|
19
|
-
@
|
20
|
-
|
21
|
-
|
22
|
+
if @options.animate
|
23
|
+
@node.hover(
|
24
|
+
-> icon.animate(opacity: 1.0, 200),
|
25
|
+
-> icon.animate(opacity: 0.6, 200))
|
@@ -32,7 +32,8 @@ class LoginPage
|
|
32
32
|
$('#container').hide().empty()
|
33
33
|
$("""
|
34
34
|
<form id="login-form">
|
35
|
-
<
|
35
|
+
<div id="icon"></div>
|
36
|
+
<h1>vines</h1>
|
36
37
|
<fieldset id="login-form-controls">
|
37
38
|
<input id="jid" name="jid" type="email" maxlength="1024" value="#{jid}" placeholder="Your user name"/>
|
38
39
|
<input id="password" name="password" type="password" maxlength="1024" placeholder="Your password"/>
|
@@ -46,6 +47,18 @@ class LoginPage
|
|
46
47
|
$('#jid').keydown -> $('#error').fadeOut()
|
47
48
|
$('#password').keydown -> $('#error').fadeOut()
|
48
49
|
this.resize()
|
50
|
+
this.icon()
|
51
|
+
|
52
|
+
icon: ->
|
53
|
+
opts =
|
54
|
+
fill: '90-#ccc-#fff'
|
55
|
+
stroke: '#fff'
|
56
|
+
'stroke-width': 1.1
|
57
|
+
opacity: 0.95
|
58
|
+
scale: 3.0
|
59
|
+
translation: '10,8'
|
60
|
+
animate: false
|
61
|
+
new Button('#icon', ICONS.chat, opts)
|
49
62
|
|
50
63
|
resize: ->
|
51
64
|
win = $ window
|
@@ -191,17 +191,14 @@ class Session
|
|
191
191
|
type = node.attr 'type'
|
192
192
|
thread = node.find('thread').first()
|
193
193
|
body = node.find('body').first()
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
html: html.text()
|
203
|
-
received: new Date()
|
204
|
-
node: node
|
194
|
+
this.notify 'message',
|
195
|
+
to: to
|
196
|
+
from: from
|
197
|
+
type: type
|
198
|
+
thread: thread.text()
|
199
|
+
text: body.text()
|
200
|
+
received: new Date()
|
201
|
+
node: node
|
205
202
|
true # keep handler alive
|
206
203
|
|
207
204
|
handlePresence: (node) ->
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|