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
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
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()