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
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2010-2013 Negative Code
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# Welcome to Vines
|
2
|
+
|
3
|
+
Vines is a scalable XMPP chat server, using EventMachine for asynchronous IO.
|
4
|
+
This gem provides a web chat client, used to test the server's BOSH support.
|
5
|
+
|
6
|
+
Additional documentation can be found at [getvines.org](http://www.getvines.org/).
|
7
|
+
|
8
|
+
## Usage
|
9
|
+
|
10
|
+
```
|
11
|
+
$ gem install vines vines-web
|
12
|
+
$ vines-web init wonderland.lit
|
13
|
+
$ cd wonderland.lit && vines start
|
14
|
+
$ open http://localhost:5280
|
15
|
+
```
|
16
|
+
|
17
|
+
## Dependencies
|
18
|
+
|
19
|
+
Vines requires Ruby 1.9.3 or better. Instructions for installing the
|
20
|
+
needed OS packages, as well as Ruby itself, are available at
|
21
|
+
[getvines.org/ruby](http://www.getvines.org/ruby).
|
22
|
+
|
23
|
+
## Development
|
24
|
+
|
25
|
+
```
|
26
|
+
$ script/bootstrap
|
27
|
+
$ script/tests
|
28
|
+
$ script/server
|
29
|
+
```
|
30
|
+
|
31
|
+
## Contact
|
32
|
+
|
33
|
+
* David Graham <david@negativecode.com>
|
34
|
+
|
35
|
+
## License
|
36
|
+
|
37
|
+
Vines is released under the MIT license. Check the LICENSE file for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/clean'
|
3
|
+
|
4
|
+
CLOBBER.include('pkg', 'public/assets')
|
5
|
+
|
6
|
+
directory 'pkg'
|
7
|
+
|
8
|
+
desc 'Build distributable packages'
|
9
|
+
task :build => [:assets, :pkg] do
|
10
|
+
system 'gem build vines-web.gemspec && mv vines-*.gem pkg/'
|
11
|
+
end
|
12
|
+
|
13
|
+
desc 'Compile web assets'
|
14
|
+
task :assets do
|
15
|
+
require 'sprockets'
|
16
|
+
env = Sprockets::Environment.new
|
17
|
+
env.cache = Sprockets::Cache::FileStore.new(Dir.tmpdir)
|
18
|
+
env.append_path 'app/assets/javascripts'
|
19
|
+
env.append_path 'app/assets/stylesheets'
|
20
|
+
env.js_compressor = :uglifier
|
21
|
+
|
22
|
+
assets = %w[application.js vendor.js lib.js application.css]
|
23
|
+
assets.each do |asset|
|
24
|
+
env[asset].write_to "public/assets/#{asset}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
task :default => [:clobber, :build]
|
@@ -0,0 +1,19 @@
|
|
1
|
+
#= require vendor
|
2
|
+
#= require lib
|
3
|
+
#= require chat
|
4
|
+
|
5
|
+
$ ->
|
6
|
+
session = new Session()
|
7
|
+
nav = new NavBar(session)
|
8
|
+
nav.draw()
|
9
|
+
buttons =
|
10
|
+
Messages: ICONS.chat
|
11
|
+
Logout: ICONS.power
|
12
|
+
nav.addButton(label, icon) for label, icon of buttons
|
13
|
+
|
14
|
+
pages =
|
15
|
+
'/messages': new ChatPage(session)
|
16
|
+
'/logout': new LogoutPage(session)
|
17
|
+
'default': new LoginPage(session, '/messages/')
|
18
|
+
new Router(pages).draw()
|
19
|
+
nav.select $('#nav-link-messages').parent()
|
@@ -0,0 +1,362 @@
|
|
1
|
+
class @ChatPage
|
2
|
+
constructor: (@session) ->
|
3
|
+
@session.onRoster ( ) => this.roster()
|
4
|
+
@session.onCard (c) => this.card(c)
|
5
|
+
@session.onMessage (m) => this.message(m)
|
6
|
+
@session.onPresence (p) => this.presence(p)
|
7
|
+
@chats = {}
|
8
|
+
@currentContact = null
|
9
|
+
@layout = null
|
10
|
+
|
11
|
+
datef: (millis) ->
|
12
|
+
d = new Date(millis)
|
13
|
+
meridian = if d.getHours() >= 12 then ' pm' else ' am'
|
14
|
+
hour = if d.getHours() > 12 then d.getHours() - 12 else d.getHours()
|
15
|
+
hour = 12 if hour == 0
|
16
|
+
minutes = d.getMinutes() + ''
|
17
|
+
minutes = '0' + minutes if minutes.length == 1
|
18
|
+
hour + ':' + minutes + meridian
|
19
|
+
|
20
|
+
card: (card) ->
|
21
|
+
this.eachContact card.jid, (node) =>
|
22
|
+
$('.vcard-img', node).attr 'src', @session.avatar card.jid
|
23
|
+
|
24
|
+
roster: ->
|
25
|
+
roster = $('#roster')
|
26
|
+
|
27
|
+
$('li', roster).each (ix, node) =>
|
28
|
+
jid = $(node).attr('data-jid')
|
29
|
+
$(node).remove() unless @session.roster[jid]
|
30
|
+
|
31
|
+
setName = (node, contact) ->
|
32
|
+
$('.text', node).text contact.name || contact.jid
|
33
|
+
node.attr 'data-name', contact.name || ''
|
34
|
+
|
35
|
+
for jid, contact of @session.roster
|
36
|
+
found = $("#roster li[data-jid='#{jid}']")
|
37
|
+
setName(found, contact)
|
38
|
+
if found.length == 0
|
39
|
+
node = $("""
|
40
|
+
<li data-jid="#{jid}" data-name="" class="offline">
|
41
|
+
<span class="text"></span>
|
42
|
+
<span class="status-msg">Offline</span>
|
43
|
+
<span class="unread" style="display:none;"></span>
|
44
|
+
<img class="vcard-img" alt="#{jid}" src="#{@session.avatar jid}"/>
|
45
|
+
</li>
|
46
|
+
""").appendTo roster
|
47
|
+
setName(node, contact)
|
48
|
+
node.click (event) => this.selectContact(event)
|
49
|
+
|
50
|
+
message: (message) ->
|
51
|
+
return unless message.type == 'chat' && message.text
|
52
|
+
this.queueMessage message
|
53
|
+
me = message.from == @session.jid()
|
54
|
+
from = message.from.split('/')[0]
|
55
|
+
|
56
|
+
if me || from == @currentContact
|
57
|
+
bottom = this.atBottom()
|
58
|
+
this.appendMessage message
|
59
|
+
this.scroll() if bottom
|
60
|
+
else
|
61
|
+
chat = this.chat message.from
|
62
|
+
chat.unread++
|
63
|
+
this.eachContact from, (node) ->
|
64
|
+
$('.unread', node).text(chat.unread).show()
|
65
|
+
|
66
|
+
eachContact: (jid, callback) ->
|
67
|
+
for node in $("#roster li[data-jid='#{jid}']").get()
|
68
|
+
callback $(node)
|
69
|
+
|
70
|
+
appendMessage: (message) ->
|
71
|
+
from = message.from.split('/')[0]
|
72
|
+
contact = @session.roster[from]
|
73
|
+
name = if contact then (contact.name || from) else from
|
74
|
+
name = 'Me' if message.from == @session.jid()
|
75
|
+
node = $("""
|
76
|
+
<li data-jid="#{from}" style="display:none;">
|
77
|
+
<p></p>
|
78
|
+
<img alt="#{from}" src="#{@session.avatar from}"/>
|
79
|
+
<footer>
|
80
|
+
<span class="author"></span>
|
81
|
+
<span class="time">#{this.datef message.received}</span>
|
82
|
+
</footer>
|
83
|
+
</li>
|
84
|
+
""").appendTo '#messages'
|
85
|
+
|
86
|
+
$('p', node).text message.text
|
87
|
+
$('.author', node).text name
|
88
|
+
node.fadeIn 200
|
89
|
+
|
90
|
+
queueMessage: (message) ->
|
91
|
+
me = message.from == @session.jid()
|
92
|
+
full = message[if me then 'to' else 'from']
|
93
|
+
chat = this.chat full
|
94
|
+
chat.jid = full
|
95
|
+
chat.messages.push message
|
96
|
+
|
97
|
+
chat: (jid) ->
|
98
|
+
bare = jid.split('/')[0]
|
99
|
+
chat = @chats[bare]
|
100
|
+
unless chat
|
101
|
+
chat = jid: jid, messages: [], unread: 0
|
102
|
+
@chats[bare] = chat
|
103
|
+
chat
|
104
|
+
|
105
|
+
presence: (presence) ->
|
106
|
+
from = presence.from.split('/')[0]
|
107
|
+
return if from == @session.bareJid()
|
108
|
+
if !presence.type || presence.offline
|
109
|
+
contact = @session.roster[from]
|
110
|
+
this.eachContact from, (node) ->
|
111
|
+
$('.status-msg', node).text contact.status()
|
112
|
+
if contact.offline()
|
113
|
+
node.addClass 'offline'
|
114
|
+
else
|
115
|
+
node.removeClass 'offline'
|
116
|
+
|
117
|
+
if presence.offline
|
118
|
+
this.chat(from).jid = from
|
119
|
+
|
120
|
+
if presence.type == 'subscribe'
|
121
|
+
node = $("""
|
122
|
+
<li data-jid="#{presence.from}" style="display:none;">
|
123
|
+
<form class="inset">
|
124
|
+
<h2>Buddy Approval</h2>
|
125
|
+
<p>#{presence.from} wants to add you as a buddy.</p>
|
126
|
+
<fieldset class="buttons">
|
127
|
+
<input type="button" value="Decline"/>
|
128
|
+
<input type="submit" value="Accept"/>
|
129
|
+
</fieldset>
|
130
|
+
</form>
|
131
|
+
</li>
|
132
|
+
""").appendTo '#notifications'
|
133
|
+
node.fadeIn 200
|
134
|
+
$('form', node).submit => this.acceptContact node, presence.from
|
135
|
+
$('input[type="button"]', node).click => this.rejectContact node, presence.from
|
136
|
+
|
137
|
+
acceptContact: (node, jid) ->
|
138
|
+
node.fadeOut 200, -> node.remove()
|
139
|
+
@session.sendSubscribed jid
|
140
|
+
@session.sendSubscribe jid
|
141
|
+
false
|
142
|
+
|
143
|
+
rejectContact: (node, jid) ->
|
144
|
+
node.fadeOut 200, -> node.remove()
|
145
|
+
@session.sendUnsubscribed jid
|
146
|
+
|
147
|
+
selectContact: (event) ->
|
148
|
+
jid = $(event.currentTarget).attr 'data-jid'
|
149
|
+
contact = @session.roster[jid]
|
150
|
+
return if @currentContact == jid
|
151
|
+
@currentContact = jid
|
152
|
+
|
153
|
+
$('#roster li').removeClass 'selected'
|
154
|
+
$(event.currentTarget).addClass 'selected'
|
155
|
+
$('#chat-title').text('Chat with ' + (contact.name || contact.jid))
|
156
|
+
$('#messages').empty()
|
157
|
+
|
158
|
+
chat = @chats[jid]
|
159
|
+
messages = []
|
160
|
+
if chat
|
161
|
+
messages = chat.messages
|
162
|
+
chat.unread = 0
|
163
|
+
this.eachContact jid, (node) ->
|
164
|
+
$('.unread', node).text('').hide()
|
165
|
+
|
166
|
+
this.appendMessage msg for msg in messages
|
167
|
+
this.scroll()
|
168
|
+
|
169
|
+
$('#remove-contact-msg').html "Are you sure you want to remove " +
|
170
|
+
"<strong>#{@currentContact}</strong> from your buddy list?"
|
171
|
+
$('#remove-contact-form .buttons').fadeIn 200
|
172
|
+
|
173
|
+
$('#edit-contact-jid').text @currentContact
|
174
|
+
$('#edit-contact-name').val @session.roster[@currentContact].name
|
175
|
+
$('#edit-contact-form input').fadeIn 200
|
176
|
+
$('#edit-contact-form .buttons').fadeIn 200
|
177
|
+
|
178
|
+
scroll: ->
|
179
|
+
msgs = $ '#messages'
|
180
|
+
msgs.animate(scrollTop: msgs.prop('scrollHeight'), 400)
|
181
|
+
|
182
|
+
atBottom: ->
|
183
|
+
msgs = $('#messages')
|
184
|
+
bottom = msgs.prop('scrollHeight') - msgs.outerHeight()
|
185
|
+
msgs.scrollTop() >= bottom
|
186
|
+
|
187
|
+
send: ->
|
188
|
+
return false unless @currentContact
|
189
|
+
input = $('#message')
|
190
|
+
text = input.val().trim()
|
191
|
+
if text
|
192
|
+
chat = @chats[@currentContact]
|
193
|
+
jid = if chat then chat.jid else @currentContact
|
194
|
+
this.message
|
195
|
+
from: @session.jid()
|
196
|
+
text: text
|
197
|
+
to: jid
|
198
|
+
type: 'chat'
|
199
|
+
received: new Date()
|
200
|
+
@session.sendMessage jid, text
|
201
|
+
input.val ''
|
202
|
+
false
|
203
|
+
|
204
|
+
addContact: ->
|
205
|
+
this.toggleForm '#add-contact-form'
|
206
|
+
contact =
|
207
|
+
jid: $('#add-contact-jid').val()
|
208
|
+
name: $('#add-contact-name').val()
|
209
|
+
groups: ['Buddies']
|
210
|
+
@session.updateContact contact, true if contact.jid
|
211
|
+
false
|
212
|
+
|
213
|
+
removeContact: ->
|
214
|
+
this.toggleForm '#remove-contact-form'
|
215
|
+
@session.removeContact @currentContact
|
216
|
+
@currentContact = null
|
217
|
+
|
218
|
+
$('#chat-title').text 'Select a buddy to chat'
|
219
|
+
$('#messages').empty()
|
220
|
+
|
221
|
+
$('#remove-contact-msg').html "Select a buddy in the list above to remove."
|
222
|
+
$('#remove-contact-form .buttons').hide()
|
223
|
+
|
224
|
+
$('#edit-contact-jid').text "Select a buddy in the list above to update."
|
225
|
+
$('#edit-contact-name').val ''
|
226
|
+
$('#edit-contact-form input').hide()
|
227
|
+
$('#edit-contact-form .buttons').hide()
|
228
|
+
false
|
229
|
+
|
230
|
+
updateContact: ->
|
231
|
+
this.toggleForm '#edit-contact-form'
|
232
|
+
contact =
|
233
|
+
jid: @currentContact
|
234
|
+
name: $('#edit-contact-name').val()
|
235
|
+
groups: @session.roster[@currentContact].groups
|
236
|
+
@session.updateContact contact
|
237
|
+
false
|
238
|
+
|
239
|
+
toggleForm: (form, fn) ->
|
240
|
+
form = $(form)
|
241
|
+
$('form.overlay').each ->
|
242
|
+
$(this).hide() unless this.id == form.attr 'id'
|
243
|
+
if form.is ':hidden'
|
244
|
+
fn() if fn
|
245
|
+
form.fadeIn 100
|
246
|
+
else
|
247
|
+
form.fadeOut 100, =>
|
248
|
+
form[0].reset()
|
249
|
+
@layout.resize()
|
250
|
+
fn() if fn
|
251
|
+
|
252
|
+
draw: ->
|
253
|
+
unless @session.connected()
|
254
|
+
window.location.hash = ''
|
255
|
+
return
|
256
|
+
|
257
|
+
$('body').attr 'id', 'chat-page'
|
258
|
+
$('#container').hide().empty()
|
259
|
+
$("""
|
260
|
+
<div id="alpha" class="sidebar column y-fill">
|
261
|
+
<h2>Buddies <div id="search-roster-icon"></div></h2>
|
262
|
+
<div id="search-roster-form"></div>
|
263
|
+
<ul id="roster" class="selectable scroll y-fill"></ul>
|
264
|
+
<div id="alpha-controls" class="controls">
|
265
|
+
<div id="add-contact"></div>
|
266
|
+
<div id="remove-contact"></div>
|
267
|
+
<div id="edit-contact"></div>
|
268
|
+
</div>
|
269
|
+
<form id="add-contact-form" class="overlay" style="display:none;">
|
270
|
+
<h2>Add Buddy</h2>
|
271
|
+
<input id="add-contact-jid" type="email" maxlength="1024" placeholder="Account name"/>
|
272
|
+
<input id="add-contact-name" type="text" maxlength="1024" placeholder="Real name"/>
|
273
|
+
<fieldset class="buttons">
|
274
|
+
<input id="add-contact-cancel" type="button" value="Cancel"/>
|
275
|
+
<input id="add-contact-ok" type="submit" value="Add"/>
|
276
|
+
</fieldset>
|
277
|
+
</form>
|
278
|
+
<form id="remove-contact-form" class="overlay" style="display:none;">
|
279
|
+
<h2>Remove Buddy</h2>
|
280
|
+
<p id="remove-contact-msg">Select a buddy in the list above to remove.</p>
|
281
|
+
<fieldset class="buttons" style="display:none;">
|
282
|
+
<input id="remove-contact-cancel" type="button" value="Cancel"/>
|
283
|
+
<input id="remove-contact-ok" type="submit" value="Remove"/>
|
284
|
+
</fieldset>
|
285
|
+
</form>
|
286
|
+
<form id="edit-contact-form" class="overlay" style="display:none;">
|
287
|
+
<h2>Update Profile</h2>
|
288
|
+
<p id="edit-contact-jid">Select a buddy in the list above to update.</p>
|
289
|
+
<input id="edit-contact-name" type="text" maxlength="1024" placeholder="Real name" style="display:none;"/>
|
290
|
+
<fieldset class="buttons" style="display:none;">
|
291
|
+
<input id="edit-contact-cancel" type="button" value="Cancel"/>
|
292
|
+
<input id="edit-contact-ok" type="submit" value="Save"/>
|
293
|
+
</fieldset>
|
294
|
+
</form>
|
295
|
+
</div>
|
296
|
+
<div id="beta" class="primary column x-fill y-fill">
|
297
|
+
<h2 id="chat-title">Select a buddy to chat</h2>
|
298
|
+
<ul id="messages" class="scroll y-fill"></ul>
|
299
|
+
<form id="message-form">
|
300
|
+
<input id="message" name="message" type="text" maxlength="1024" placeholder="Type a message and press enter to send"/>
|
301
|
+
</form>
|
302
|
+
</div>
|
303
|
+
<div id="charlie" class="sidebar column y-fill">
|
304
|
+
<h2>Notifications</h2>
|
305
|
+
<ul id="notifications" class="scroll y-fill"></ul>
|
306
|
+
<div id="charlie-controls" class="controls">
|
307
|
+
<div id="clear-notices"></div>
|
308
|
+
</div>
|
309
|
+
</div>
|
310
|
+
""").appendTo '#container'
|
311
|
+
|
312
|
+
this.roster()
|
313
|
+
|
314
|
+
new Button '#clear-notices', ICONS.no
|
315
|
+
new Button '#add-contact', ICONS.plus
|
316
|
+
new Button '#remove-contact', ICONS.minus
|
317
|
+
new Button '#edit-contact', ICONS.user
|
318
|
+
|
319
|
+
$('#message').focus -> $('form.overlay').fadeOut()
|
320
|
+
$('#message-form').submit => this.send()
|
321
|
+
|
322
|
+
$('#clear-notices').click -> $('#notifications li').fadeOut 200
|
323
|
+
|
324
|
+
$('#add-contact').click => this.toggleForm '#add-contact-form'
|
325
|
+
$('#remove-contact').click => this.toggleForm '#remove-contact-form'
|
326
|
+
$('#edit-contact').click => this.toggleForm '#edit-contact-form', =>
|
327
|
+
if @currentContact
|
328
|
+
$('#edit-contact-jid').text @currentContact
|
329
|
+
$('#edit-contact-name').val @session.roster[@currentContact].name
|
330
|
+
|
331
|
+
$('#add-contact-cancel').click => this.toggleForm '#add-contact-form'
|
332
|
+
$('#remove-contact-cancel').click => this.toggleForm '#remove-contact-form'
|
333
|
+
$('#edit-contact-cancel').click => this.toggleForm '#edit-contact-form'
|
334
|
+
|
335
|
+
$('#add-contact-form').submit => this.addContact()
|
336
|
+
$('#remove-contact-form').submit => this.removeContact()
|
337
|
+
$('#edit-contact-form').submit => this.updateContact()
|
338
|
+
|
339
|
+
$('#container').fadeIn 200
|
340
|
+
@layout = this.resize()
|
341
|
+
|
342
|
+
fn = =>
|
343
|
+
@layout.resize()
|
344
|
+
@layout.resize() # not sure why two are needed
|
345
|
+
|
346
|
+
new Filter
|
347
|
+
list: '#roster'
|
348
|
+
icon: '#search-roster-icon'
|
349
|
+
form: '#search-roster-form'
|
350
|
+
attrs: ['data-jid', 'data-name']
|
351
|
+
open: fn
|
352
|
+
close: fn
|
353
|
+
|
354
|
+
resize: ->
|
355
|
+
a = $ '#alpha'
|
356
|
+
b = $ '#beta'
|
357
|
+
c = $ '#charlie'
|
358
|
+
msg = $ '#message'
|
359
|
+
form = $ '#message-form'
|
360
|
+
new Layout ->
|
361
|
+
c.css 'left', a.width() + b.width()
|
362
|
+
msg.width form.width() - 32
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class @Button
|
2
|
+
constructor: (node, path, options) ->
|
3
|
+
@node = $ node
|
4
|
+
@path = path
|
5
|
+
@options = options || {}
|
6
|
+
@options.animate = true unless @options.animate?
|
7
|
+
this.draw()
|
8
|
+
|
9
|
+
draw: ->
|
10
|
+
paper = Raphael @node.get(0)
|
11
|
+
|
12
|
+
transform = "s#{@options.scale || 0.85}"
|
13
|
+
transform += ",t#{@options.translation}" if @options.translation
|
14
|
+
|
15
|
+
icon = paper.path(@path).attr
|
16
|
+
fill: @options.fill || '#000'
|
17
|
+
stroke: @options.stroke || '#fff'
|
18
|
+
'stroke-width': @options['stroke-width'] || 0.3
|
19
|
+
opacity: @options.opacity || 0.6
|
20
|
+
transform: transform
|
21
|
+
|
22
|
+
if @options.animate
|
23
|
+
@node.hover(
|
24
|
+
-> icon.animate(opacity: 1.0, 200),
|
25
|
+
-> icon.animate(opacity: 0.6, 200))
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class @Contact
|
2
|
+
constructor: (node) ->
|
3
|
+
node = $(node)
|
4
|
+
@jid = node.attr 'jid'
|
5
|
+
@name = node.attr 'name'
|
6
|
+
@ask = node.attr 'ask'
|
7
|
+
@subscription = node.attr 'subscription'
|
8
|
+
@groups = $('group', node).map(-> $(this).text()).get()
|
9
|
+
@presence = []
|
10
|
+
|
11
|
+
online: ->
|
12
|
+
@presence.length > 0
|
13
|
+
|
14
|
+
offline: ->
|
15
|
+
@presence.length == 0
|
16
|
+
|
17
|
+
available: ->
|
18
|
+
this.online() && (p for p in @presence when !p.away).length > 0
|
19
|
+
|
20
|
+
away: -> !this.available()
|
21
|
+
|
22
|
+
status: ->
|
23
|
+
available = (p.status for p in @presence when p.status && !p.away)[0] || 'Available'
|
24
|
+
away = (p.status for p in @presence when p.status && p.away)[0] || 'Away'
|
25
|
+
if this.offline() then 'Offline'
|
26
|
+
else if this.away() then away
|
27
|
+
else available
|
28
|
+
|
29
|
+
update: (presence) ->
|
30
|
+
@presence = (p for p in @presence when p.from != presence.from)
|
31
|
+
@presence.push presence unless presence.type
|
32
|
+
@presence = [] if presence.type == 'unsubscribed'
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class @Filter
|
2
|
+
constructor: (options) ->
|
3
|
+
@list = options.list
|
4
|
+
@icon = options.icon
|
5
|
+
@form = options.form
|
6
|
+
@attrs = options.attrs
|
7
|
+
@open = options.open
|
8
|
+
@close = options.close
|
9
|
+
this.draw()
|
10
|
+
|
11
|
+
draw: ->
|
12
|
+
$(@icon).addClass 'filter-button'
|
13
|
+
form = $('<form class="filter-form" style="display:none;"></form>').appendTo @form
|
14
|
+
text = $('<input class="filter-text" type="search" placeholder="Filter" results="5"/>').appendTo form
|
15
|
+
|
16
|
+
if @icon
|
17
|
+
new Button @icon, ICONS.search,
|
18
|
+
scale: 0.5
|
19
|
+
translation: '-16,-16'
|
20
|
+
|
21
|
+
form.submit -> false
|
22
|
+
text.keyup => this.filter(text)
|
23
|
+
text.change => this.filter(text)
|
24
|
+
text.click => this.filter(text)
|
25
|
+
$(@icon).click =>
|
26
|
+
if form.is ':hidden'
|
27
|
+
this.filter(text)
|
28
|
+
form.show()
|
29
|
+
this.open() if this.open
|
30
|
+
else
|
31
|
+
form.hide()
|
32
|
+
form[0].reset()
|
33
|
+
this.filter(text)
|
34
|
+
this.close() if this.close
|
35
|
+
|
36
|
+
filter: (input) ->
|
37
|
+
text = input.val().toLowerCase()
|
38
|
+
if text == ''
|
39
|
+
$('li', @list).show()
|
40
|
+
return
|
41
|
+
|
42
|
+
test = (node, attr) ->
|
43
|
+
val = (node.attr(attr) || '').toLowerCase()
|
44
|
+
val.indexOf(text) != -1
|
45
|
+
|
46
|
+
$('> li', @list).each (ix, node) =>
|
47
|
+
node = $ node
|
48
|
+
matches = (true for attr in @attrs when test node, attr)
|
49
|
+
if matches.length > 0 then node.show() else node.hide()
|
@@ -0,0 +1 @@
|
|
1
|
+
#= require_tree .
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class @Layout
|
2
|
+
constructor: (@fn) ->
|
3
|
+
this.resize()
|
4
|
+
this.listen()
|
5
|
+
setTimeout (=> this.resize()), 250
|
6
|
+
|
7
|
+
resize: ->
|
8
|
+
this.fill '.x-fill', 'outerWidth', 'width'
|
9
|
+
this.fill '.y-fill', 'outerHeight', 'height'
|
10
|
+
this.fn()
|
11
|
+
|
12
|
+
fill: (selector, get, set) ->
|
13
|
+
$(selector).filter(':visible').each (ix, node) =>
|
14
|
+
node = $(node)
|
15
|
+
getter = node[get]
|
16
|
+
parent = getter.call node.parent(), true
|
17
|
+
fixed = this.fixed node, selector, (n) -> getter.call(n, true)
|
18
|
+
node[set].call node, parent - fixed
|
19
|
+
|
20
|
+
fixed: (node, selector, fn) ->
|
21
|
+
node.siblings().not(selector).not('.float').filter(':visible')
|
22
|
+
.map(-> fn $ this).get()
|
23
|
+
.reduce ((sum, num) -> sum + num), 0
|
24
|
+
|
25
|
+
listen: ->
|
26
|
+
id = null
|
27
|
+
$(window).resize =>
|
28
|
+
clearTimeout id
|
29
|
+
id = setTimeout (=> this.resize()), 10
|
30
|
+
this.resize()
|
@@ -0,0 +1,68 @@
|
|
1
|
+
class @LoginPage
|
2
|
+
constructor: (@session, @startPage) ->
|
3
|
+
|
4
|
+
start: ->
|
5
|
+
$('#error').hide()
|
6
|
+
|
7
|
+
[jid, password] = ($(id).val().trim() for id in ['#jid', '#password'])
|
8
|
+
if jid.length == 0 || password.length == 0 || jid.indexOf('@') == -1
|
9
|
+
$('#error').show()
|
10
|
+
return
|
11
|
+
|
12
|
+
@session.connect jid, password, (success) =>
|
13
|
+
unless success
|
14
|
+
@session.disconnect()
|
15
|
+
$('#error').show()
|
16
|
+
$('#password').val('').focus()
|
17
|
+
return
|
18
|
+
|
19
|
+
localStorage['jid'] = jid
|
20
|
+
$('#current-user-name').text @session.bareJid()
|
21
|
+
$('#current-user-avatar').attr 'src', @session.avatar @session.jid()
|
22
|
+
$('#current-user-avatar').attr 'alt', @session.bareJid()
|
23
|
+
$('#container').fadeOut 200, =>
|
24
|
+
$('#navbar').show()
|
25
|
+
window.location.hash = @startPage
|
26
|
+
|
27
|
+
draw: ->
|
28
|
+
@session.disconnect()
|
29
|
+
jid = localStorage['jid'] || ''
|
30
|
+
$('#navbar').hide()
|
31
|
+
$('body').attr 'id', 'login-page'
|
32
|
+
$('#container').hide().empty()
|
33
|
+
$("""
|
34
|
+
<form id="login-form">
|
35
|
+
<div id="icon"></div>
|
36
|
+
<h1>vines</h1>
|
37
|
+
<fieldset id="login-form-controls">
|
38
|
+
<input id="jid" name="jid" type="email" maxlength="1024" value="#{jid}" placeholder="Your user name"/>
|
39
|
+
<input id="password" name="password" type="password" maxlength="1024" placeholder="Your password"/>
|
40
|
+
<input id="start" type="submit" value="Sign in"/>
|
41
|
+
</fieldset>
|
42
|
+
<p id="error" style="display:none;">User name and password not found.</p>
|
43
|
+
</form>
|
44
|
+
""").appendTo '#container'
|
45
|
+
$('#container').fadeIn 1000
|
46
|
+
$('#login-form').submit => this.start(); false
|
47
|
+
$('#jid').keydown -> $('#error').fadeOut()
|
48
|
+
$('#password').keydown -> $('#error').fadeOut()
|
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)
|
62
|
+
|
63
|
+
resize: ->
|
64
|
+
win = $ window
|
65
|
+
form = $ '#login-form'
|
66
|
+
sizer = -> form.css 'top', win.height() / 2 - form.height() / 2
|
67
|
+
win.resize sizer
|
68
|
+
sizer()
|