vines-web 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/Gemfile +3 -0
  2. data/LICENSE +19 -0
  3. data/README.md +37 -0
  4. data/Rakefile +28 -0
  5. data/app/assets/javascripts/application.coffee +19 -0
  6. data/app/assets/javascripts/chat.coffee +362 -0
  7. data/app/assets/javascripts/lib/button.coffee +25 -0
  8. data/app/assets/javascripts/lib/contact.coffee +32 -0
  9. data/app/assets/javascripts/lib/filter.coffee +49 -0
  10. data/app/assets/javascripts/lib/index.coffee +1 -0
  11. data/app/assets/javascripts/lib/layout.coffee +30 -0
  12. data/app/assets/javascripts/lib/login.coffee +68 -0
  13. data/app/assets/javascripts/lib/logout.coffee +5 -0
  14. data/app/assets/javascripts/lib/navbar.coffee +84 -0
  15. data/app/assets/javascripts/lib/notification.coffee +14 -0
  16. data/app/assets/javascripts/lib/router.coffee +40 -0
  17. data/app/assets/javascripts/lib/session.coffee +229 -0
  18. data/app/assets/javascripts/lib/transfer.coffee +106 -0
  19. data/app/assets/javascripts/vendor/icons.js +110 -0
  20. data/app/assets/javascripts/vendor/index.js +1 -0
  21. data/app/assets/javascripts/vendor/jquery.js +4 -0
  22. data/app/assets/javascripts/vendor/raphael.js +6 -0
  23. data/app/assets/javascripts/vendor/strophe.js +1 -0
  24. data/app/assets/stylesheets/application.css +5 -0
  25. data/app/assets/stylesheets/base.scss +385 -0
  26. data/app/assets/stylesheets/chat.scss +144 -0
  27. data/app/assets/stylesheets/login.scss +68 -0
  28. data/bin/vines-web +63 -0
  29. data/config.ru +10 -0
  30. data/lib/vines/web/command/init.rb +34 -0
  31. data/lib/vines/web/command/install.rb +19 -0
  32. data/lib/vines/web/version.rb +5 -0
  33. data/lib/vines/web.rb +4 -0
  34. data/public/assets/application.css +598 -0
  35. data/public/assets/application.js +9 -0
  36. data/public/assets/lib.js +1 -0
  37. data/public/assets/vendor.js +8 -0
  38. data/public/images/dark-gray.png +0 -0
  39. data/public/images/default-user.png +0 -0
  40. data/public/images/light-gray.png +0 -0
  41. data/public/images/logo-large.png +0 -0
  42. data/public/images/logo-small.png +0 -0
  43. data/public/images/white.png +0 -0
  44. data/public/index.html +13 -0
  45. data/vines-web.gemspec +27 -0
  46. metadata +207 -0
@@ -0,0 +1,5 @@
1
+ class @LogoutPage
2
+ constructor: (@session) ->
3
+ draw: ->
4
+ window.location.hash = ''
5
+ window.location.reload()
@@ -0,0 +1,84 @@
1
+ class @NavBar
2
+ constructor: (@session) ->
3
+ @session.onCard (card) =>
4
+ if card.jid == @session.bareJid()
5
+ $('#current-user-avatar').attr 'src', @session.avatar card.jid
6
+
7
+ draw: ->
8
+ $("""
9
+ <header id="navbar" class="x-fill">
10
+ <h1 id="logo">vines&gt;</h1>
11
+ <div id="current-user">
12
+ <img id="current-user-avatar" alt="#{@session.bareJid()}" src="#{@session.avatar(@session.jid())}"/>
13
+ <div id="current-user-info">
14
+ <h1 id="current-user-name">#{@session.bareJid()}</h1>
15
+ <form id="current-user-presence-form">
16
+ <span class="select">
17
+ <span class="text">Available</span>
18
+ <select id="current-user-presence">
19
+ <optgroup label="Available">
20
+ <option>Available</option>
21
+ <option>Surfing the web</option>
22
+ <option>Reading email</option>
23
+ </optgroup>
24
+ <optgroup label="Away">
25
+ <option value="xa">Away</option>
26
+ <option value="xa">Out to lunch</option>
27
+ <option value="xa">On the phone</option>
28
+ <option value="xa">In a meeting</option>
29
+ </optgroup>
30
+ </select>
31
+ </span>
32
+ </form>
33
+ </div>
34
+ </div>
35
+ <nav id="app-nav" class="x-fill">
36
+ <ul id="nav-links"></ul>
37
+ </nav>
38
+ </header>
39
+ """).appendTo 'body'
40
+ $('<div id="container" class="x-fill y-fill"></div>').appendTo 'body'
41
+
42
+ $('#current-user-presence').change (event) =>
43
+ selected = $ 'option:selected', event.currentTarget
44
+ $('#current-user-presence-form .text').text selected.text()
45
+ @session.sendPresence selected.val() == 'xa', selected.text()
46
+
47
+ addButton: (label, icon) ->
48
+ id = "nav-link-#{label.toLowerCase()}"
49
+ node = $("""
50
+ <li>
51
+ <a id="#{id}" href="#/#{label.toLowerCase()}">
52
+ <span>#{label}</span>
53
+ </a>
54
+ </li>
55
+ """).appendTo '#nav-links'
56
+ this.button(id, icon)
57
+ node.click (event) => this.select(event.currentTarget)
58
+
59
+ select: (button) ->
60
+ button = $(button)
61
+ $('#nav-links li').removeClass('selected')
62
+ $('#nav-links li a').removeClass('selected')
63
+ button.addClass('selected')
64
+ $('a', button).addClass('selected')
65
+ dark = $('#nav-links svg path')
66
+ dark.attr 'opacity', '0.6'
67
+ dark.css 'opacity', '0.6'
68
+ light = $('svg path', button)
69
+ light.attr 'opacity', '1.0'
70
+ light.css 'opacity', '1.0'
71
+
72
+ button: (id, path) ->
73
+ paper = Raphael(id)
74
+ icon = paper.path(path).attr
75
+ fill: '#fff'
76
+ stroke: '#000'
77
+ 'stroke-width': 0.3
78
+ opacity: 0.6
79
+
80
+ node = $('#' + id)
81
+ node.hover(
82
+ -> icon.animate(opacity: 1.0, 200),
83
+ -> icon.animate(opacity: 0.6, 200) unless node.hasClass('selected'))
84
+ node.get 0
@@ -0,0 +1,14 @@
1
+ class @Notification
2
+ constructor: (@text) ->
3
+ this.draw()
4
+
5
+ draw: ->
6
+ node = $('<div class="notification float" style="display:none;"></div>').appendTo 'body'
7
+ node.text @text
8
+ top = node.outerHeight() / 2
9
+ left = node.outerWidth() / 2
10
+ node.css {marginTop: "-#{top}px", marginLeft: "-#{left}px"}
11
+ node.fadeIn 200
12
+ fn = ->
13
+ node.fadeOut 200, -> node.remove()
14
+ setTimeout fn, 1500
@@ -0,0 +1,40 @@
1
+ class @Router
2
+ constructor: (@pages) ->
3
+ @routes = this.build()
4
+ $(window).bind 'hashchange', => this.draw()
5
+
6
+ build: ->
7
+ routes = []
8
+ for pattern, page of @pages
9
+ routes.push route = args: [], page: page, re: null
10
+ if pattern == 'default'
11
+ route.re = pattern
12
+ continue
13
+
14
+ fragments = (f for f in pattern.split '/' when f.length > 0)
15
+ map = (fragment) ->
16
+ if fragment[0] == ':'
17
+ route.args.push fragment.replace ':', ''
18
+ '(/[^/]+)?'
19
+ else '/' + fragment
20
+ route.re = new RegExp '#' + (map f for f in fragments).join ''
21
+ routes
22
+
23
+ draw: ->
24
+ [route, args] = this.match()
25
+ route ||= this.defaultRoute()
26
+ return unless route
27
+ [opts, ix] = [{}, 0]
28
+ opts[name] = args[ix++] for name in route.args
29
+ route.page.draw(opts)
30
+
31
+ match: ->
32
+ for route in @routes
33
+ if match = window.location.hash.match route.re
34
+ args = (arg.replace '/', '' for arg in match[1..-1])
35
+ return [route, args]
36
+ []
37
+
38
+ defaultRoute: ->
39
+ for route in @routes
40
+ return route if route.re == 'default'
@@ -0,0 +1,229 @@
1
+ class @Session
2
+ constructor: ->
3
+ @xmpp = new Strophe.Connection '/xmpp'
4
+ @roster = {}
5
+ @listeners =
6
+ card: []
7
+ message: []
8
+ presence: []
9
+ roster: []
10
+
11
+ connect: (jid, password, callback) ->
12
+ @xmpp.connect jid, password, (status) =>
13
+ switch status
14
+ when Strophe.Status.AUTHFAIL, Strophe.Status.CONNFAIL
15
+ callback false
16
+ when Strophe.Status.CONNECTED
17
+ @xmpp.addHandler ((el) => this.handleIq(el)), null, 'iq'
18
+ @xmpp.addHandler ((el) => this.handleMessage(el)), null, 'message'
19
+ @xmpp.addHandler ((el) => this.handlePresence(el)), null, 'presence'
20
+ callback true
21
+ this.findRoster =>
22
+ this.notify('roster')
23
+ @xmpp.send this.xml '<presence/>'
24
+ this.findCards()
25
+
26
+ disconnect: -> @xmpp.disconnect()
27
+
28
+ onCard: (callback) ->
29
+ @listeners['card'].push callback
30
+
31
+ onRoster: (callback) ->
32
+ @listeners['roster'].push callback
33
+
34
+ onMessage: (callback) ->
35
+ @listeners['message'].push callback
36
+
37
+ onPresence: (callback) ->
38
+ @listeners['presence'].push callback
39
+
40
+ connected: ->
41
+ @xmpp.jid && @xmpp.jid.length > 0
42
+
43
+ jid: -> @xmpp.jid
44
+
45
+ bareJid: -> @xmpp.jid.split('/')[0]
46
+
47
+ uniqueId: -> @xmpp.getUniqueId()
48
+
49
+ avatar: (jid) ->
50
+ card = this.loadCard(jid)
51
+ if card && card.photo
52
+ "data:#{card.photo.type};base64,#{card.photo.binval}"
53
+ else
54
+ '/lib/images/default-user.png'
55
+
56
+ loadCard: (jid) ->
57
+ jid = jid.split('/')[0]
58
+ found = localStorage['vcard:' + jid]
59
+ JSON.parse found if found
60
+
61
+ storeCard: (card) ->
62
+ localStorage['vcard:' + card.jid] = JSON.stringify card
63
+
64
+ findCards: ->
65
+ jids = (jid for jid, contacts of @roster when !this.loadCard jid)
66
+ jids.push this.bareJid() if !this.loadCard(this.bareJid())
67
+
68
+ success = (card) =>
69
+ this.findCard jids.shift(), success
70
+ if card
71
+ this.storeCard card
72
+ this.notify 'card', card
73
+
74
+ this.findCard jids.shift(), success
75
+
76
+ findCard: (jid, callback) ->
77
+ return unless jid
78
+ node = this.xml """
79
+ <iq id="#{this.uniqueId()}" to="#{jid}" type="get">
80
+ <vCard xmlns="vcard-temp"/>
81
+ </iq>
82
+ """
83
+ this.sendIQ node, (result) ->
84
+ card = $('vCard', result)
85
+ photo = $('PHOTO', card)
86
+ type = $('TYPE', photo).text()
87
+ bin = $('BINVAL', photo).text()
88
+ photo =
89
+ if type && bin
90
+ type: type, binval: bin.replace(/\n/g, '')
91
+ else null
92
+ vcard = jid: jid, photo: photo, retrieved: new Date()
93
+ callback if card.size() > 0 then vcard else null
94
+
95
+ parseRoster: (node) ->
96
+ $('item', node).map(-> new Contact this ).get()
97
+
98
+ findRoster: (callback) ->
99
+ node = this.xml """
100
+ <iq id='#{this.uniqueId()}' type="get">
101
+ <query xmlns="jabber:iq:roster"/>
102
+ </iq>
103
+ """
104
+ this.sendIQ node, (result) =>
105
+ contacts = this.parseRoster(result)
106
+ @roster[contact.jid] = contact for contact in contacts
107
+ callback()
108
+
109
+ sendMessage: (jid, message) ->
110
+ node = this.xml """
111
+ <message id="#{this.uniqueId()}" to="#{jid}" type="chat">
112
+ <body></body>
113
+ </message>
114
+ """
115
+ $('body', node).text message
116
+ @xmpp.send node
117
+
118
+ sendPresence: (away, status) ->
119
+ node = $ this.xml '<presence/>'
120
+ if away
121
+ node.append $(this.xml '<show>xa</show>')
122
+ node.append $(this.xml '<status/>').text status if status != 'Away'
123
+ else
124
+ node.append $(this.xml '<status/>').text status if status != 'Available'
125
+ @xmpp.send node
126
+
127
+ sendIQ: (node, callback) ->
128
+ @xmpp.sendIQ node, callback, callback, 5000
129
+
130
+ updateContact: (contact, add) ->
131
+ node = this.xml """
132
+ <iq id="#{this.uniqueId()}" type="set">
133
+ <query xmlns="jabber:iq:roster">
134
+ <item name="" jid="#{contact.jid}"/>
135
+ </query>
136
+ </iq>
137
+ """
138
+ $('item', node).attr 'name', contact.name
139
+ for group in contact.groups
140
+ $('item', node).append $(this.xml '<group></group>').text group
141
+ @xmpp.send node
142
+ this.sendSubscribe(contact.jid) if add
143
+
144
+ removeContact: (jid) ->
145
+ node = this.xml """
146
+ <iq id="#{this.uniqueId()}" type="set">
147
+ <query xmlns="jabber:iq:roster">
148
+ <item jid="#{jid}" subscription="remove"/>
149
+ </query>
150
+ </iq>
151
+ """
152
+ @xmpp.send node
153
+
154
+ sendSubscribe: (jid) ->
155
+ @xmpp.send this.presence jid, 'subscribe'
156
+
157
+ sendSubscribed: (jid) ->
158
+ @xmpp.send this.presence jid, 'subscribed'
159
+
160
+ sendUnsubscribed: (jid) ->
161
+ @xmpp.send this.presence jid, 'unsubscribed'
162
+
163
+ presence: (to, type) ->
164
+ this.xml """
165
+ <presence
166
+ id="#{this.uniqueId()}"
167
+ to="#{to}"
168
+ type="#{type}"/>
169
+ """
170
+
171
+ handleIq: (node) ->
172
+ node = $(node)
173
+ type = node.attr 'type'
174
+ ns = node.find('query').attr 'xmlns'
175
+ if type == 'set' && ns == 'jabber:iq:roster'
176
+ contacts = this.parseRoster(node)
177
+ for contact in contacts
178
+ if contact.subscription == 'remove'
179
+ delete @roster[contact.jid]
180
+ else
181
+ old = @roster[contact.jid]
182
+ contact.presence = old.presence if old
183
+ @roster[contact.jid] = contact
184
+ this.notify('roster')
185
+ true # keep handler alive
186
+
187
+ handleMessage: (node) ->
188
+ node = $(node)
189
+ to = node.attr 'to'
190
+ from = node.attr 'from'
191
+ type = node.attr 'type'
192
+ thread = node.find('thread').first()
193
+ body = node.find('body').first()
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
202
+ true # keep handler alive
203
+
204
+ handlePresence: (node) ->
205
+ node = $(node)
206
+ to = node.attr 'to'
207
+ from = node.attr 'from'
208
+ type = node.attr 'type'
209
+ show = node.find('show').first()
210
+ status = node.find('status').first()
211
+ presence =
212
+ to: to
213
+ from: from
214
+ status: status.text()
215
+ show: show.text()
216
+ type: type
217
+ offline: type == 'unavailable' || type == 'error'
218
+ away: show.text() == 'away' || show.text() == 'xa'
219
+ dnd: show.text() == 'dnd'
220
+ node: node
221
+ contact = @roster[from.split('/')[0]]
222
+ contact.update presence if contact
223
+ this.notify 'presence', presence
224
+ true # keep handler alive
225
+
226
+ notify: (type, obj) ->
227
+ callback(obj) for callback in (@listeners[type] || [])
228
+
229
+ xml: (xml) -> $.parseXML(xml).documentElement
@@ -0,0 +1,106 @@
1
+ class @Transfer
2
+ constructor: (options) ->
3
+ @session = options.session
4
+ @file = options.file
5
+ @to = options.to
6
+ @progress = options.progress
7
+ @complete = options.complete
8
+ @chunks = new Chunks @file
9
+ @opened = false
10
+ @closed = false
11
+ @sid = @session.uniqueId()
12
+ @seq = 0
13
+ @sent = 0
14
+
15
+ start: ->
16
+ node = $("""
17
+ <iq id="#{@session.uniqueId()}" to="#{@to}" type="set">
18
+ <si xmlns="http://jabber.org/protocol/si" id="#{@session.uniqueId()}" profile="http://jabber.org/protocol/si/profile/file-transfer">
19
+ <file xmlns="http://jabber.org/protocol/si/profile/file-transfer" name="" size="#{@file.size}"/>
20
+ <feature xmlns="http://jabber.org/protocol/feature-neg">
21
+ <x xmlns="jabber:x:data" type="form">
22
+ <field var="stream-method" type="list-single">
23
+ <option><value>http://jabber.org/protocol/ibb</value></option>
24
+ </field>
25
+ </x>
26
+ </feature>
27
+ </si>
28
+ </iq>
29
+ """)
30
+ $('file', node).attr 'name', @file.name
31
+
32
+ @session.sendIQ node.get(0), (result) =>
33
+ methods = $('si feature x field[var="stream-method"] value', result)
34
+ ok = (true for m in methods when $(m).text() == 'http://jabber.org/protocol/ibb').length > 0
35
+ this.open() if ok
36
+
37
+ open: ->
38
+ node = $("""
39
+ <iq id="#{@session.uniqueId()}" to="#{@to}" type="set">
40
+ <open xmlns="http://jabber.org/protocol/ibb" sid="#{@sid}" block-size="4096"/>
41
+ </iq>
42
+ """)
43
+ @session.sendIQ node.get(0), (result) =>
44
+ if this.ok result
45
+ @opened = true
46
+ this.sendChunk()
47
+
48
+ sendChunk: ->
49
+ return if @closed
50
+ @chunks.chunk (chunk) =>
51
+ unless chunk
52
+ this.close()
53
+ return
54
+
55
+ node = $("""
56
+ <iq id="#{@session.uniqueId()}" to="#{@to}" type="set">
57
+ <data xmlns="http://jabber.org/protocol/ibb" sid="#{@sid}" seq="#{@seq++}">#{chunk}</data>
58
+ </iq>
59
+ """)
60
+ @seq = 0 if @seq > 65535
61
+
62
+ @session.sendIQ node.get(0), (result) =>
63
+ return unless this.ok result
64
+ pct = Math.ceil ++@sent / @chunks.total * 100
65
+ this.progress pct
66
+ this.sendChunk()
67
+
68
+ close: ->
69
+ return if @closed
70
+ @closed = true
71
+ node = $("""
72
+ <iq id="#{@session.uniqueId()}" to="#{@to}" type="set">
73
+ <close xmlns="http://jabber.org/protocol/ibb" sid="#{@sid}"/>
74
+ </iq>
75
+ """)
76
+ @session.sendIQ node.get(0), ->
77
+ this.complete()
78
+
79
+ stop: ->
80
+ if @opened
81
+ this.close()
82
+ else
83
+ this.complete()
84
+
85
+ ok: (result) -> $(result).attr('type') == 'result'
86
+
87
+ class Chunks
88
+ CHUNK_SIZE = 3 / 4 * 4096
89
+
90
+ constructor: (@file) ->
91
+ @total = Math.ceil @file.size / CHUNK_SIZE
92
+ @slice = @file.slice || @file.webkitSlice || @file.mozSlice
93
+ @pos = 0
94
+
95
+ chunk: (callback) ->
96
+ start = @pos
97
+ end = @pos + CHUNK_SIZE
98
+ @pos = end
99
+ if start > @file.size
100
+ callback null
101
+ else
102
+ chunk = @slice.call @file, start, end
103
+ reader = new FileReader()
104
+ reader.onload = (event) ->
105
+ callback btoa event.target.result
106
+ reader.readAsBinaryString chunk