vines 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|