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
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()