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