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.
Files changed (115) hide show
  1. data/README +5 -9
  2. data/Rakefile +11 -9
  3. data/conf/config.rb +30 -4
  4. data/lib/vines/cluster/connection.rb +26 -0
  5. data/lib/vines/cluster/publisher.rb +55 -0
  6. data/lib/vines/cluster/pubsub.rb +92 -0
  7. data/lib/vines/cluster/sessions.rb +125 -0
  8. data/lib/vines/cluster/subscriber.rb +108 -0
  9. data/lib/vines/cluster.rb +246 -0
  10. data/lib/vines/command/init.rb +21 -24
  11. data/lib/vines/config/host.rb +48 -8
  12. data/lib/vines/config/port.rb +5 -0
  13. data/lib/vines/config/pubsub.rb +108 -0
  14. data/lib/vines/config.rb +74 -20
  15. data/lib/vines/jid.rb +14 -0
  16. data/lib/vines/router.rb +69 -55
  17. data/lib/vines/stanza/iq/disco_info.rb +22 -9
  18. data/lib/vines/stanza/iq/disco_items.rb +6 -3
  19. data/lib/vines/stanza/iq/ping.rb +1 -1
  20. data/lib/vines/stanza/iq/private_storage.rb +4 -8
  21. data/lib/vines/stanza/iq/roster.rb +6 -14
  22. data/lib/vines/stanza/iq/session.rb +2 -7
  23. data/lib/vines/stanza/iq/vcard.rb +4 -6
  24. data/lib/vines/stanza/iq/version.rb +1 -1
  25. data/lib/vines/stanza/iq.rb +8 -10
  26. data/lib/vines/stanza/presence/subscribe.rb +3 -11
  27. data/lib/vines/stanza/presence/subscribed.rb +16 -29
  28. data/lib/vines/stanza/presence/unsubscribe.rb +3 -15
  29. data/lib/vines/stanza/presence/unsubscribed.rb +3 -16
  30. data/lib/vines/stanza/presence.rb +30 -0
  31. data/lib/vines/stanza/pubsub/create.rb +39 -0
  32. data/lib/vines/stanza/pubsub/delete.rb +41 -0
  33. data/lib/vines/stanza/pubsub/publish.rb +66 -0
  34. data/lib/vines/stanza/pubsub/subscribe.rb +44 -0
  35. data/lib/vines/stanza/pubsub/unsubscribe.rb +30 -0
  36. data/lib/vines/stanza/pubsub.rb +22 -0
  37. data/lib/vines/stanza.rb +72 -22
  38. data/lib/vines/storage/couchdb.rb +46 -65
  39. data/lib/vines/storage/local.rb +20 -14
  40. data/lib/vines/storage/mongodb.rb +132 -0
  41. data/lib/vines/storage/null.rb +39 -0
  42. data/lib/vines/storage/redis.rb +61 -68
  43. data/lib/vines/storage/sql.rb +73 -69
  44. data/lib/vines/storage.rb +1 -1
  45. data/lib/vines/stream/client/bind.rb +2 -2
  46. data/lib/vines/stream/client/session.rb +71 -16
  47. data/lib/vines/stream/component/handshake.rb +1 -0
  48. data/lib/vines/stream/component/ready.rb +2 -2
  49. data/lib/vines/stream/http/session.rb +2 -0
  50. data/lib/vines/stream/http.rb +0 -6
  51. data/lib/vines/stream/server/final_restart.rb +1 -0
  52. data/lib/vines/stream/server/outbound/final_features.rb +1 -0
  53. data/lib/vines/stream/server/ready.rb +6 -2
  54. data/lib/vines/stream/server.rb +4 -3
  55. data/lib/vines/stream.rb +10 -6
  56. data/lib/vines/version.rb +1 -1
  57. data/lib/vines.rb +48 -22
  58. data/test/cluster/publisher_test.rb +45 -0
  59. data/test/cluster/sessions_test.rb +54 -0
  60. data/test/cluster/subscriber_test.rb +94 -0
  61. data/test/config/host_test.rb +100 -21
  62. data/test/config/pubsub_test.rb +181 -0
  63. data/test/config_test.rb +225 -43
  64. data/test/jid_test.rb +7 -0
  65. data/test/router_test.rb +181 -9
  66. data/test/stanza/iq/disco_info_test.rb +8 -6
  67. data/test/stanza/iq/disco_items_test.rb +3 -3
  68. data/test/stanza/iq/private_storage_test.rb +8 -19
  69. data/test/stanza/iq/roster_test.rb +1 -1
  70. data/test/stanza/iq/session_test.rb +3 -6
  71. data/test/stanza/iq/vcard_test.rb +6 -2
  72. data/test/stanza/iq/version_test.rb +3 -2
  73. data/test/stanza/iq_test.rb +5 -5
  74. data/test/stanza/message_test.rb +3 -2
  75. data/test/stanza/presence/probe_test.rb +2 -1
  76. data/test/stanza/pubsub/create_test.rb +138 -0
  77. data/test/stanza/pubsub/delete_test.rb +142 -0
  78. data/test/stanza/pubsub/publish_test.rb +373 -0
  79. data/test/stanza/pubsub/subscribe_test.rb +186 -0
  80. data/test/stanza/pubsub/unsubscribe_test.rb +179 -0
  81. data/test/stanza_test.rb +2 -1
  82. data/test/storage/local_test.rb +26 -25
  83. data/test/storage/mock_mongo.rb +40 -0
  84. data/test/storage/mock_redis.rb +98 -0
  85. data/test/storage/mongodb_test.rb +81 -0
  86. data/test/storage/null_test.rb +30 -0
  87. data/test/storage/redis_test.rb +3 -36
  88. data/test/stream/component/handshake_test.rb +4 -0
  89. data/test/stream/component/ready_test.rb +2 -1
  90. data/test/stream/server/ready_test.rb +7 -1
  91. data/web/404.html +5 -3
  92. data/web/chat/coffeescripts/chat.coffee +9 -5
  93. data/web/chat/javascripts/app.js +1 -1
  94. data/web/chat/javascripts/chat.js +14 -8
  95. data/web/chat/stylesheets/chat.css +4 -1
  96. data/web/lib/coffeescripts/button.coffee +9 -5
  97. data/web/lib/coffeescripts/filter.coffee +1 -1
  98. data/web/lib/coffeescripts/login.coffee +14 -1
  99. data/web/lib/coffeescripts/session.coffee +8 -11
  100. data/web/lib/images/dark-gray.png +0 -0
  101. data/web/lib/images/light-gray.png +0 -0
  102. data/web/lib/images/logo-large.png +0 -0
  103. data/web/lib/images/logo-small.png +0 -0
  104. data/web/lib/images/white.png +0 -0
  105. data/web/lib/javascripts/base.js +9 -8
  106. data/web/lib/javascripts/button.js +20 -12
  107. data/web/lib/javascripts/filter.js +1 -1
  108. data/web/lib/javascripts/icons.js +7 -1
  109. data/web/lib/javascripts/jquery.js +4 -4
  110. data/web/lib/javascripts/login.js +16 -2
  111. data/web/lib/javascripts/raphael.js +5 -7
  112. data/web/lib/javascripts/session.js +10 -14
  113. data/web/lib/stylesheets/base.css +7 -11
  114. data/web/lib/stylesheets/login.css +31 -27
  115. 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
@@ -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 = Class.new do
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(:jid => 'alice@wonderland.lit')])
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(#1a3762, #0c1a2d);
12
- background: -webkit-gradient(radial, 50% 50%, 0, 50% 50%, 500, from(#1a3762), to(#0c1a2d));
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: 64px;
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'
@@ -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, layout;
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
- scale: @options.scale || 0.85
17
- translation: @options.translation || ''
20
+ transform: transform
18
21
 
19
- @node.hover(
20
- -> icon.animate(opacity: 1.0, 200),
21
- -> icon.animate(opacity: 0.6, 200))
22
+ if @options.animate
23
+ @node.hover(
24
+ -> icon.animate(opacity: 1.0, 200),
25
+ -> icon.animate(opacity: 0.6, 200))
@@ -16,7 +16,7 @@ class Filter
16
16
  if @icon
17
17
  new Button @icon, ICONS.search,
18
18
  scale: 0.5
19
- translation: '-8 -8'
19
+ translation: '-16,-16'
20
20
 
21
21
  form.submit -> false
22
22
  text.keyup => this.filter(text)
@@ -32,7 +32,8 @@ class LoginPage
32
32
  $('#container').hide().empty()
33
33
  $("""
34
34
  <form id="login-form">
35
- <h1>vines&gt;</h1>
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
- html = node.find('span').first()
195
- if type == 'chat' && body.size() > 0
196
- this.notify 'message',
197
- to: to
198
- from: from
199
- type: type
200
- thread: thread.text()
201
- text: body.text()
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