vines-web 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +3 -0
- data/LICENSE +19 -0
- data/README.md +37 -0
- data/Rakefile +28 -0
- data/app/assets/javascripts/application.coffee +19 -0
- data/app/assets/javascripts/chat.coffee +362 -0
- data/app/assets/javascripts/lib/button.coffee +25 -0
- data/app/assets/javascripts/lib/contact.coffee +32 -0
- data/app/assets/javascripts/lib/filter.coffee +49 -0
- data/app/assets/javascripts/lib/index.coffee +1 -0
- data/app/assets/javascripts/lib/layout.coffee +30 -0
- data/app/assets/javascripts/lib/login.coffee +68 -0
- data/app/assets/javascripts/lib/logout.coffee +5 -0
- data/app/assets/javascripts/lib/navbar.coffee +84 -0
- data/app/assets/javascripts/lib/notification.coffee +14 -0
- data/app/assets/javascripts/lib/router.coffee +40 -0
- data/app/assets/javascripts/lib/session.coffee +229 -0
- data/app/assets/javascripts/lib/transfer.coffee +106 -0
- data/app/assets/javascripts/vendor/icons.js +110 -0
- data/app/assets/javascripts/vendor/index.js +1 -0
- data/app/assets/javascripts/vendor/jquery.js +4 -0
- data/app/assets/javascripts/vendor/raphael.js +6 -0
- data/app/assets/javascripts/vendor/strophe.js +1 -0
- data/app/assets/stylesheets/application.css +5 -0
- data/app/assets/stylesheets/base.scss +385 -0
- data/app/assets/stylesheets/chat.scss +144 -0
- data/app/assets/stylesheets/login.scss +68 -0
- data/bin/vines-web +63 -0
- data/config.ru +10 -0
- data/lib/vines/web/command/init.rb +34 -0
- data/lib/vines/web/command/install.rb +19 -0
- data/lib/vines/web/version.rb +5 -0
- data/lib/vines/web.rb +4 -0
- data/public/assets/application.css +598 -0
- data/public/assets/application.js +9 -0
- data/public/assets/lib.js +1 -0
- data/public/assets/vendor.js +8 -0
- data/public/images/dark-gray.png +0 -0
- data/public/images/default-user.png +0 -0
- data/public/images/light-gray.png +0 -0
- data/public/images/logo-large.png +0 -0
- data/public/images/logo-small.png +0 -0
- data/public/images/white.png +0 -0
- data/public/index.html +13 -0
- data/vines-web.gemspec +27 -0
- metadata +207 -0
@@ -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></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
|