vines-web 0.1.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 (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