Browse code

Merge branch 'master' into history

Conflicts:
app/assets/javascripts/i18n/translations.js

Cinan Rakosnik authored on 25/05/2013 at 12:09:35
Showing 43 changed files
... ...
@@ -10,6 +10,7 @@ gem 'xmpp4r', '~> 0.5'
10 10
 gem 'thin', '~> 1.5.0'
11 11
 gem 'websocket-rails', '~> 0.4.3'
12 12
 gem 'bcrypt-ruby'
13
+gem 'rmagick', '~> 2.13.2'
13 14
 
14 15
 # Gems used only for assets and not required
15 16
 # in production environments by default.
... ...
@@ -18,9 +19,8 @@ group :assets do
18 18
   gem 'coffee-rails', '~> 3.2.1'
19 19
   gem 'normalize-rails'
20 20
   gem 'compass-rails'
21
-  gem 'rails-backbone'
21
+  gem 'backbone-rails'
22 22
   gem 'haml_coffee_assets'
23
-  #gem "marionette-rails", "~> 0.10.2"
24 23
 
25 24
   # See https://github.com/sstephenson/execjs#readme for more supported runtimes
26 25
   # gem 'therubyracer', :platforms => :ruby
... ...
@@ -52,7 +52,7 @@ group :test do
52 52
     gem 'casperjs'
53 53
 end
54 54
 
55
-gem 'jquery-rails'
55
+gem 'jquery-rails', '~> 2.2.1'
56 56
 gem 'haml', '~> 3.1.7'
57 57
 gem 'haml-rails', '~> 0.3.5'
58 58
 gem 'i18n-js'
... ...
@@ -31,6 +31,8 @@ GEM
31 31
     addressable (2.3.3)
32 32
     ansi (1.4.3)
33 33
     arel (3.0.2)
34
+    backbone-rails (1.0.0)
35
+      rails (>= 3.0.0)
34 36
     bcrypt-ruby (3.0.1)
35 37
     better_errors (0.7.2)
36 38
       coderay (>= 1.0.0)
... ...
@@ -72,7 +74,6 @@ GEM
72 72
     daemons (1.1.9)
73 73
     debug_inspector (0.0.2)
74 74
     diff-lcs (1.2.2)
75
-    ejs (1.1.1)
76 75
     em-synchrony (1.0.3)
77 76
       eventmachine (>= 1.0.0.beta.1)
78 77
     erubis (2.7.0)
... ...
@@ -111,16 +112,16 @@ GEM
111 111
       tilt (>= 1.3.3)
112 112
     hashie (2.0.3)
113 113
     highline (1.6.16)
114
-    hike (1.2.1)
114
+    hike (1.2.2)
115 115
     hiredis (0.4.5)
116 116
     i18n (0.6.1)
117 117
     i18n-js (2.1.2)
118 118
       i18n
119 119
     journey (1.0.4)
120
-    jquery-rails (2.1.4)
120
+    jquery-rails (2.2.1)
121 121
       railties (>= 3.0, < 5.0)
122 122
       thor (>= 0.14, < 2.0)
123
-    json (1.7.7)
123
+    json (1.8.0)
124 124
     launchy (2.2.0)
125 125
       addressable (~> 2.3)
126 126
     listen (0.7.3)
... ...
@@ -146,7 +147,7 @@ GEM
146 146
       origin (~> 1.0)
147 147
       tzinfo (~> 0.3.22)
148 148
     moped (1.4.5)
149
-    multi_json (1.7.2)
149
+    multi_json (1.7.3)
150 150
     net-scp (1.1.0)
151 151
       net-ssh (>= 2.6.5)
152 152
     net-sftp (2.1.1)
... ...
@@ -184,11 +185,6 @@ GEM
184 184
       activesupport (= 3.2.13)
185 185
       bundler (~> 1.0)
186 186
       railties (= 3.2.13)
187
-    rails-backbone (0.9.10)
188
-      coffee-script (~> 2.2.0)
189
-      ejs (~> 1.1.1)
190
-      jquery-rails (~> 2.1.3)
191
-      railties (>= 3.1.0)
192 187
     railties (3.2.13)
193 188
       actionpack (= 3.2.13)
194 189
       activesupport (= 3.2.13)
... ...
@@ -200,6 +196,7 @@ GEM
200 200
     rdoc (3.12.2)
201 201
       json (~> 1.4)
202 202
     redis (3.0.3)
203
+    rmagick (2.13.2)
203 204
     rspec-core (2.13.1)
204 205
     rspec-expectations (2.13.0)
205 206
       diff-lcs (>= 1.1.3, < 2.0)
... ...
@@ -237,7 +234,7 @@ GEM
237 237
       eventmachine (>= 0.12.6)
238 238
       rack (>= 1.0.0)
239 239
     thor (0.18.1)
240
-    tilt (1.3.6)
240
+    tilt (1.4.1)
241 241
     timecop (0.6.1)
242 242
     treetop (1.4.12)
243 243
       polyglot
... ...
@@ -263,6 +260,7 @@ PLATFORMS
263 263
   ruby
264 264
 
265 265
 DEPENDENCIES
266
+  backbone-rails
266 267
   bcrypt-ruby
267 268
   better_errors
268 269
   binding_of_caller
... ...
@@ -277,7 +275,7 @@ DEPENDENCIES
277 277
   haml-rails (~> 0.3.5)
278 278
   haml_coffee_assets
279 279
   i18n-js
280
-  jquery-rails
280
+  jquery-rails (~> 2.2.1)
281 281
   launchy
282 282
   meta_request
283 283
   minitest
... ...
@@ -286,7 +284,7 @@ DEPENDENCIES
286 286
   normalize-rails
287 287
   phantomjs-binaries
288 288
   rails (~> 3.2.11)
289
-  rails-backbone
289
+  rmagick (~> 2.13.2)
290 290
   rspec-rails
291 291
   sass-rails (~> 3.2.3)
292 292
   spork
... ...
@@ -12,9 +12,24 @@ this.App =
12 12
 
13 13
   UI:
14 14
     setAutoHeight: ->
15
-      height = $(window).height()
16
-      $("#height-setter-1").css({height: height - 50})
17
-      $("#height-setter-2").css({height: height - $('#tabbar').outerHeight() - $('#msg-writer').outerHeight() - 57})
15
+      App.debug 'resizing'
16
+      App.UI.resizeRoster()
17
+      App.UI.resizeMessages()
18
+
19
+    resizeRoster: ->
20
+      if (windowHeight < $('.roster').css('min-height').replace(/[^-\d\.]/g, ''))
21
+        return
22
+
23
+      windowHeight = $(window).height()
24
+      toolboxHeight = $("#height-setter-1 .toolbox").height()
25
+      $(".friends").css({height: windowHeight - toolboxHeight - $('#js-me').height() - 40})
26
+      $("#height-setter-1").css({height: windowHeight - toolboxHeight - $('#js-active-friends').height() - 120})
27
+
28
+    resizeMessages: ->
29
+      windowHeight = $(window).height()
30
+      $("#height-setter-2").css({height: windowHeight - $('#tabbar').outerHeight() - $('#msg-writer').outerHeight() - 62})
31
+      messagesContainer = $('.messages')
32
+      messagesContainer.animate(scrollTop: messagesContainer.height(), 'fast')
18 33
 
19 34
     filterContacts: (searchTerm) ->
20 35
       App.Collections.contacts.filter(searchTerm)
... ...
@@ -107,12 +122,21 @@ this.App =
107 107
     updateMyStatus: (message, state)->
108 108
       App.Com.trigger(event: 'app.roster.updateMyStatus', data: {message: message, state: state})
109 109
 
110
-    updateMyVcard: (name, avatar) ->
110
+    updateMyVcardName: (name) ->
111 111
       App.Com.trigger(event: 'app.roster.updateMyVcard', data: {name: name})
112 112
 
113
+    updateMyVcardAvatar: (imageBase64) ->
114
+      App.Com.trigger(event: 'app.roster.updateMyVcard', data: {avatar: imageBase64})
115
+
113 116
     removeContactRemote: (contact, client) ->
114 117
       App.Com.trigger(event: 'app.roster.removeContact', data: {jid: contact, client: client})
115 118
 
119
+    addContact: (jid) ->
120
+      App.Com.trigger(event: 'app.roster.addContact', data: {jid: jid})
121
+
122
+    answerFriendRequest: (jid, answer) ->
123
+      App.Com.trigger(event: 'app.roster.answerFriendRequest', data: { jid: jid, answer: answer})
124
+
116 125
     _setupBackboneComponents: ->
117 126
       App.Collections.contacts = new Xmpp.Collections.ContactsCollection()
118 127
       App.Collections.chats = new Xmpp.Collections.ChatsCollection()
... ...
@@ -173,6 +197,12 @@ this.App =
173 173
           chat.set('withWhom', attendants)
174 174
       )
175 175
 
176
+      App._dispatcher.bind('app.roster.friendRequest', (request) ->
177
+        App.debug ['receiving friend request', request]
178
+        popup = new Xmpp.Views.Popup.FriendRequestView(jid: request.jid, name: request.name)
179
+        popup.render()
180
+      )
181
+
176 182
       App._dispatcher.bind('app.chat.importChat', (chatData) ->
177 183
         chatId = chatData.chat_id
178 184
         chat = App.Collections.chats.findById(chatId)
... ...
@@ -2,15 +2,13 @@
2 2
 #= require hamlcoffee
3 3
 #= require underscore
4 4
 #= require backbone
5
-#= require backbone_rails_sync
6
-#= require backbone_datalink
7 5
 #= require backbone/xmpp
8 6
 #= require i18n
9 7
 #= require i18n/translations
10 8
 #= require _main
11 9
 #= require chat
12 10
 
13
-App.Com.connect( (connected_jid) ->
11
+App.Com.connect( ->
14 12
   App.Com.initRoster(->
15 13
     App.Com.startFetchingVcards()
16 14
     App.Com.startPollingRoster()
... ...
@@ -50,15 +50,13 @@ class Xmpp.Collections.ContactsCollection extends Backbone.Collection
50 50
       @moveToInactiveList('all')
51 51
 #      App.debug chat
52 52
       @moveToActiveList(chat.get('withWhom'))
53
-    )
54
-
55
-    Backbone.Events.on('closeChat', (chat) =>
56
-      @moveToInactiveList('all')
53
+      Backbone.Events.trigger('resizeWorkspace')
57 54
     )
58 55
 
59 56
   appendContact: (contact) ->
60 57
     App.debug ['apppend', contact]
61 58
     @friendsList.appendContact(contact)
59
+    Backbone.Events.trigger('resizeWorkspace')
62 60
 
63 61
   mergeContacts: (contact) ->
64 62
     App.debug ['merging contacts', contact]
... ...
@@ -123,11 +121,13 @@ class Xmpp.Collections.ContactsCollection extends Backbone.Collection
123 123
         @remove(contact)
124 124
 
125 125
     # ignore 'unsubscribe' type of action
126
+    Backbone.Events.trigger('resizeWorkspace')
126 127
 
127 128
   # toto sem asi nepatri, ale asi do views. Nic to v skutocnosti nerobi s modelmi.
128 129
   filter: (searchTerm) ->
129 130
     @friendsList.filter(searchTerm)
130 131
     @activeList.filter(searchTerm)
132
+    Backbone.Events.trigger('resizeWorkspace')
131 133
 
132 134
   findByJid: (jid) ->
133 135
     @get(jid)
... ...
@@ -1,6 +1,5 @@
1 1
 .user
2 2
   .avatar
3 3
     %img{src: "#{@avatar || 'assets/avatar.png'}", alt: "#{@username}"}
4
-  -#todo: user name missing in design
5
-.date #{@date}
4
+.date #{@username} @ #{@date}
6 5
 %p #{@message}
7 6
\ No newline at end of file
... ...
@@ -1,5 +1,6 @@
1 1
 .conversation.clear.border
2
-  -for view in @history
3
-    #{view}
2
+  .messages#height-setter-2
3
+    -for view in @history
4
+      #{view}
4 5
   -if @showWriter
5 6
     != JST['backbone/templates/chat/message_writer']()
6 7
\ No newline at end of file
... ...
@@ -1,42 +1,24 @@
1 1
 .avatar
2
-  %img{src: "#{@avatar || 'assets/avatar.png'}", alt: I18n.t("chat.roster.avatar_alt") }
2
+  %img{src: "#{@avatar || 'assets/avatar.png'}", alt: I18n.t("chat.roster.avatar_alt") , title: "#{@jid}" }
3 3
 %h1
4
-  #{@name}
4
+  %span.name{ title: "#{@jid}" } #{@name}
5 5
   %span.status.icon-record{class: "#{@status}"}
6 6
 %h2 #{@message} &nbsp;
7 7
 %span.action
8
-  -#-if @usingMyApp
9
-  %span.icon-bubbles.multichat
8
+  %span.icon-bubbles.multichat{ title: "#{I18n.t('chat.roster.new-multichat', user: @name )}" }
10 9
     -# ak nemam otvoreny multichat
11
-    %span multi
12
-  %span.icon-bubbles.invite
10
+    %span #{I18n.t('chat.roster.icons.multi')}
11
+  %span.icon-bubbles.invite{ title: "#{I18n.t('chat.roster.invite', user: @name)}" }
13 12
     -# ak nie je v multichate a mam otvoreny multichat a som admin toho multichatu
14
-    %span invite
15
-  %span.icon-x-full.kick
13
+    %span #{I18n.t('chat.roster.icons.invite')}
14
+  %span.icon-x-full.kick{ title: "#{I18n.t('chat.roster.kick', user: @name)}" }
16 15
     -# ak som admin otvoreneho multichatu
17
-    %span kick
18
-  %span.icon-owner.owner
16
+    %span #{I18n.t('chat.roster.icons.kick')}
17
+  %span.icon-owner.owner{ title: "#{I18n.t('chat.roster.is-owner', user: @name)}"}
19 18
     -# ak niesom admin otvoreneho multichatu
20
-    %span owner
21
-  %span.icon-make-owner.make-owner
19
+    %span #{I18n.t('chat.roster.icons.owner')}
20
+  %span.icon-make-owner.make-owner{ title: "#{I18n.t('chat.roster.make-owner', user: @name)}" }
22 21
     -# ak som admin otvoreneho multichatu
23
-    %span owner
24
-  -#%span.icon-x-full.remove
25
-    %span remove
26
-
27
--#  .avatar
28
--#    %img{src: 'assets/avatar.png', alt: t("chat.avatar_alt")}
29
--#  %h1
30
--#    Bob Awesome
31
--#    %span.status.dnd.icon-record
32
--#  %h2 sales manager
33
--#%li.clear.user
34
--#  .avatar
35
--#    %img{src: 'assets/avatar.png', alt: t("chat.avatar_alt")}
36
--#  %h1
37
--#    Bob Awesome
38
--#    %span.status.dnd.icon-record
39
--#  %h2 sales manager
40
--#  %span.action
41
--#    %span.icon-bubble
42
--#      %span chat
22
+    %span #{I18n.t('chat.roster.icons.owner')}
23
+  %span.icon-x-full.remove{ title: "#{I18n.t('chat.roster.remove', user: @name )}" }
24
+    %span #{I18n.t('chat.roster.icons.remove')}
... ...
@@ -1,24 +1,29 @@
1 1
 .user
2 2
   .avatar.bigger{ title: "#{@jid}" }
3 3
     %img{ src: "#{@avatar || 'assets/avatar.png'}", alt: I18n.t('chat.roster.avatar_alt') }
4
+    %input.hidden{ type: 'file', name: 'avatar', accept: 'image/png,image/jpeg,image/pjpeg,image/gif,image/webp'}
5
+    .change-avatar.hidden
6
+      .background
7
+      %p change
4 8
   %h1
5 9
     %input{ type: 'text', title: "#{@jid}", class: 'js-edit-name', value: "#{@name}"}
6
-  %h2 #{@message || '<input type="text" class="js-edit-status empty" value="'+I18n.t('chat.roster.change_status_msg')+'">' }
7
-  -#todo: nastylovat placeholder
10
+  %h2{style: 'margin-top: -5px'} #{@message || '<input type="text" class="js-edit-status empty" value="'+I18n.t('chat.roster.change_status_msg')+'">' }
8 11
   %h3
9 12
     .rolldown.js-change-state
10
-      .js-state-clickable{ 'data-state': 'online' }
13
+      .js-state-clickable{ 'data-state' => 'online' }
11 14
         %span.status.icon-record.online
12 15
         %a.active{ href: '#' }
13 16
           Online
14
-          %br
15
-      .hidden.js-state-clickable{ 'data-state': 'away' }
17
+          %span.icon-arrow-down.drop-down
18
+      .hidden.js-state-clickable{ 'data-state' => 'away' }
16 19
         %span.status.icon-record.away
17 20
         %a{ href: '#' }
18 21
           Away
19
-          %br
20
-      .hidden.js-state-clickable{ 'data-state': 'dnd' }
22
+          %span.icon-arrow-down.drop-down
23
+      .hidden.js-state-clickable{ 'data-state' => 'dnd' }
21 24
         %span.status.icon-record.dnd
22 25
         %a{ href: '#' }
23 26
           DND
24
-    %span.icon-arrow-down.drop-down
25 27
\ No newline at end of file
28
+          %span.icon-arrow-down.drop-down
29
+    %a.logout.icon-exit.icon-after{ href: '/logout' }
30
+      Logout
26 31
\ No newline at end of file
27 32
new file mode 100644
... ...
@@ -0,0 +1,7 @@
0
+%h1 #{I18n.t('popup.headers.add_contact')}
1
+%form{ action: '#' }
2
+  %label{for: 'jid'}
3
+    %strong Jabber ID
4
+  %input{type: 'text', name: 'jid', class: 'text', placeholder: 'nick@example.com'}
5
+
6
+  %input{type: 'submit', value: "Send friend request", class: 'button'}
0 7
\ No newline at end of file
1 8
new file mode 100644
... ...
@@ -0,0 +1,7 @@
0
+%h1 #{I18n.t('popup.headers.friend_request')}
1
+%form{ action: '#' }
2
+  %strong #{@new_contact}
3
+  #{I18n.t('popup.friend_request.question')}
4
+  %br
5
+  %input.left{type: 'submit', value: "#{I18n.t('popup.friend_request.answer_yes')}", name: '1', class: 'button', style: 'width: 60%'}
6
+  %input.close.inactive{type: 'button', value: "#{I18n.t('popup.friend_request.answer_no')}", style: 'width: 30%; padding: 0'}
0 7
\ No newline at end of file
1 8
new file mode 100644
... ...
@@ -0,0 +1,3 @@
0
+.popup{ class: "#{@class}" }
1
+  %a.close.icon-x{ alt: "#{I18n.t('chat.close')}", href: '#' }
2
+  #{@inside}
0 3
\ No newline at end of file
... ...
@@ -9,10 +9,14 @@ class Xmpp.Views.Chat.MessageView extends Backbone.View
9 9
 
10 10
     @user = parts.user
11 11
     @dateSent = parts.date
12
+    @fromMe = parts.fromMe
12 13
     @message = parts.message
13 14
 
14 15
   render: ->
15 16
     $(@el).html(@template(username: @user.get('name'), avatar: @user.get('avatar'), date: @formatTime(), message: @message))
17
+    if @fromMe
18
+      $(@el).addClass('me')
19
+
16 20
     return this
17 21
 
18 22
   formatTime:  ->
... ...
@@ -56,7 +56,7 @@ class Xmpp.Views.Chat.WindowView extends Backbone.View
56 56
 
57 57
       App.Models.me.sendMessage(message, chatId, attendant
58 58
       , (message) =>
59
-        @sendSuccess(@tab.getOwner(), message)
59
+        @sendSuccess(App.Models.me, message)
60 60
       , (message) =>
61 61
         @sendFail(message))
62 62
 
... ...
@@ -71,13 +71,13 @@ class Xmpp.Views.Chat.WindowView extends Backbone.View
71 71
 #    @log(eventView) if logMe == true
72 72
 
73 73
   appendMessage: (user, date, msg) ->
74
-    messageView = new Xmpp.Views.Chat.MessageView(user: user, date: date, message: msg)
75
-    $(@el).find('#msg-writer').before(messageView.render().el)
74
+    messageView = new Xmpp.Views.Chat.MessageView(user: user, date: date, message: msg, fromMe: user.get('jid') == App.Models.me.get('jid'))
75
+    $(@el).find('.messages').append(messageView.render().el)
76 76
     @log(messageView)
77
+    Backbone.Events.trigger('resizeWorkspace')
77 78
 
78 79
   log: (view) ->
79 80
     if (@historyStack.length + 1 >= @maxHistoryLength)
80 81
       @historyStack = @historyStack.slice(@historyStack.length - @maxHistoryLength + 1)
81 82
 
82
-    @historyStack.push(view)
83
-#    App.debug @historyStack
83
+    @historyStack.push(view)
84 84
\ No newline at end of file
... ...
@@ -10,10 +10,13 @@ class Xmpp.Views.Contacts.ContactView extends Backbone.View
10 10
     'click .kick': 'kickFromMultiChat'
11 11
     'click .multichat': 'newMultiChat'
12 12
     'click .make-owner': 'switchOwner'
13
+    'click .remove': 'removeContact'
13 14
     click: 'startChat'
14 15
     mouseover: 'showIcons'
15 16
     mouseleave: 'hideIcons'
16 17
 
18
+  @iconClasses = ['multichat', 'invite', 'kick', 'make-owner', 'remove']
19
+
17 20
   initialize: () ->
18 21
     _.bindAll(this)
19 22
 
... ...
@@ -54,6 +57,7 @@ class Xmpp.Views.Contacts.ContactView extends Backbone.View
54 54
       App.Collections.chats.add(chat)
55 55
 
56 56
     Backbone.Events.trigger('openChat', chat)
57
+    Backbone.Events.trigger('resizeWorkspace')
57 58
 
58 59
   inviteToMultichat: (e) ->
59 60
     e.stopPropagation()
... ...
@@ -73,6 +77,7 @@ class Xmpp.Views.Contacts.ContactView extends Backbone.View
73 73
 
74 74
     App.Collections.chats.add(newChat)
75 75
     App.Com.openNewMultiChat(App.Models.me, @model, newChat)
76
+    Backbone.Events.trigger('resizeWorkspace')
76 77
 
77 78
   kickFromMultiChat: (e) ->
78 79
     e.stopPropagation()
... ...
@@ -94,15 +99,21 @@ class Xmpp.Views.Contacts.ContactView extends Backbone.View
94 94
 
95 95
     App.Com.switchMultiChatOwner(App.Models.me.get('jid'), @model.get('jid'), multichat.get('chatId'))
96 96
 
97
+  removeContact: (e) ->
98
+    e.stopPropagation()
99
+    App.debug 'remove from roster'
100
+    App.Com.removeContactRemote(@model.get('jid'))
101
+    Backbone.Events.trigger('removeContact', @model)
102
+
97 103
   openChatById: (chatId) ->
98 104
     App.Collections.chats.findById(chatId)
99 105
 
100 106
   showIcons: ->
101 107
     activeChat = App.Collections.chats.activeChat
102 108
 
103
-    $(@el).find('.action .multichat, .action .invite, .action .kick, .action .make-owner').hide()
109
+    @hideIcons()
104 110
 
105
-    if (@model.get('usingMyApp'))
111
+    if @model.get('usingMyApp') and @model.get('status') != 'offline'
106 112
       if not activeChat or not activeChat.get('isMultiChat')
107 113
         $(@el).find('.action .multichat').show()
108 114
       else if activeChat.get('isMultiChat') and activeChat.isMeOwner() and not activeChat.isAttending(@model)
... ...
@@ -111,5 +122,9 @@ class Xmpp.Views.Contacts.ContactView extends Backbone.View
111 111
         $(@el).find('.action .kick').show()
112 112
         $(@el).find('.action .make-owner').show()
113 113
 
114
+    if not activeChat or not activeChat.get('isMultiChat')
115
+      $(@el).find('.action .remove').show()
116
+
114 117
   hideIcons: ->
115
-    $(@el).find('.action .multichat, .action .invite, .action .kick, .action .make-owner').hide()
118
+    all_icons = '.action .' + ContactView.iconClasses.join(', .action .')
119
+    $(@el).find(all_icons).hide()
... ...
@@ -14,6 +14,12 @@ class Xmpp.Views.Contacts.MeView extends Backbone.View
14 14
     'click .js-change-state': (e) -> @openChangeState(e)
15 15
     'click .js-state-clickable': (e) -> @confirmChangeState(e)
16 16
 
17
+    #avatar change events:
18
+    'mouseover .avatar': -> $('.change-avatar').show()
19
+    'mouseout .avatar': -> $('.change-avatar').hide()
20
+    'click .change-avatar': -> $('input[name=avatar]').click()
21
+    'change input[name=avatar]': 'uploadAvatar'
22
+
17 23
   initialize: () ->
18 24
     _.bindAll(this)
19 25
 
... ...
@@ -67,7 +73,7 @@ class Xmpp.Views.Contacts.MeView extends Backbone.View
67 67
       $this.removeClass('editing')
68 68
 
69 69
     $this.blur()
70
-    App.Com.updateMyVcard(@_getName())
70
+    App.Com.updateMyVcardName(@_getName())
71 71
     @model.set('name', @_getName())
72 72
 
73 73
   openChangeState: (e) ->
... ...
@@ -77,6 +83,7 @@ class Xmpp.Views.Contacts.MeView extends Backbone.View
77 77
          .find('.hidden').removeClass('hidden')
78 78
 
79 79
     $('.rolldown').removeClass('js-change-state')
80
+                  .find('.drop-down').addClass('hidden')
80 81
 
81 82
   confirmChangeState: (e) ->
82 83
     $this = $(e.currentTarget)
... ...
@@ -86,12 +93,21 @@ class Xmpp.Views.Contacts.MeView extends Backbone.View
86 86
       .children().addClass('hidden')
87 87
 
88 88
     $('.rolldown').find('a').removeClass('active')
89
+                  .find('.drop-down').removeClass('hidden')
89 90
 
90 91
     $this.removeClass('hidden')
91 92
       .find('a').addClass('active')
92 93
 
93 94
     App.Com.updateMyStatus(@_getStatusMessage(), @_getState())
94 95
 
96
+  uploadAvatar: (e) ->
97
+    fr = new FileReader;
98
+    fr.onloadend = ->
99
+      App.Models.me.set(avatar: fr.result)
100
+      App.Com.updateMyVcardAvatar(fr.result)
101
+
102
+    fr.readAsDataURL(e.target.files[0]);
103
+
95 104
   _getStatusMessage: ->
96 105
     if $('.js-edit-status').hasClass('empty')
97 106
       return ''
98 107
new file mode 100644
... ...
@@ -0,0 +1,43 @@
0
+Xmpp.Views.Popup ||= {}
1
+
2
+class Xmpp.Views.Popup.PopupView extends Backbone.View
3
+  superTemplate: JST["backbone/templates/popup/popup"]
4
+  el: $('#js-popups')
5
+
6
+  @popups = {}
7
+
8
+  constructor: ->
9
+    super
10
+    PopupView.popups[@cid] = this
11
+
12
+  superEvents: ->
13
+    result = {}
14
+    result["click .#{@className} .close"] = 'close'
15
+    result
16
+
17
+  render: (childHtml) ->
18
+    $(@el).find('.popup').hide()
19
+    _.each(PopupView.popups, (view) ->
20
+      view.undelegateEvents()
21
+    )
22
+
23
+    thisView = PopupView.popups[@cid]
24
+    thisView.delegateEvents()
25
+
26
+    $(@el).append(@superTemplate(inside: childHtml, class: @className))
27
+
28
+  close: (e) ->
29
+    e.preventDefault()
30
+    @remove(e)
31
+
32
+  remove: (e) ->
33
+    @stopListening()
34
+    @undelegateEvents()
35
+    $(e.currentTarget).closest('.popup').remove()
36
+
37
+    delete PopupView.popups[@cid]
38
+
39
+    firstFoundView = _.find(PopupView.popups, (view) -> view) # najde prvy view
40
+    if firstFoundView
41
+      $(@el).find('.popup').first().show()
42
+      firstFoundView.delegateEvents()
0 43
\ No newline at end of file
1 44
new file mode 100644
... ...
@@ -0,0 +1,26 @@
0
+Xmpp.Views.Popup ||= {}
1
+
2
+class Xmpp.Views.Popup.AddContactView extends Xmpp.Views.Popup.PopupView
3
+  template: JST["backbone/templates/popup/add_contact"]
4
+  className: 'add-contact'
5
+
6
+  events:
7
+    'submit form': 'addContact'
8
+
9
+  initialize: ->
10
+    @events = _.extend(@events, @superEvents())
11
+
12
+  render: ->
13
+    super(@template())
14
+    $(@el).css(width: '20%', 'z-index': 999)
15
+    $(@el).find('input[type=text]').focus()
16
+    this
17
+
18
+  addContact: (e) ->
19
+    e.preventDefault()
20
+    jid = $(@el).find('input[name=jid]').val()
21
+    r = new RegExp('^.+@.+\..+$')
22
+    match = r.exec(jid)
23
+    if match and (match.length > 0)
24
+      jid = App.stripJid(match[0])
25
+      App.Com.addContact(jid)
0 26
new file mode 100644
... ...
@@ -0,0 +1,31 @@
0
+Xmpp.Views.Popup ||= {}
1
+
2
+class Xmpp.Views.Popup.FriendRequestView extends Xmpp.Views.Popup.PopupView
3
+  template: JST["backbone/templates/popup/friend_request"]
4
+  className: 'friend-reqeust'
5
+
6
+  events:
7
+    'submit form': 'approve'
8
+
9
+  initialize: (options) ->
10
+    @name = options.name
11
+    @jid  = options.jid
12
+
13
+    @events = _.extend(@events, @superEvents())
14
+
15
+  render: ->
16
+    super(@template(new_contact: @name))
17
+    $(@el).css(width: '20%', 'z-index': 999)
18
+    this
19
+
20
+  approve: (e) ->
21
+    e.preventDefault()
22
+    App.debug ['approve request', @jid]
23
+    App.Com.answerFriendRequest(@jid, true)
24
+    @remove(e)
25
+
26
+  close: (e) ->
27
+    e.preventDefault()
28
+    App.debug ['cancel request', @jid]
29
+    App.Com.answerFriendRequest(@jid, false)
30
+    @remove(e)
0 31
\ No newline at end of file
... ...
@@ -6,21 +6,28 @@ class Xmpp.Views.Tabbar.TabView extends Backbone.View
6 6
   className: 'tab'
7 7
 
8 8
   events:
9
-    'click .js-close': 'fireCloseChatTrigger'
9
+    'click .js-close': 'closeChat'
10 10
     'click a div': 'switchChatWindow'
11 11
 
12 12
   initialize: () ->
13 13
     _.bindAll(this)
14 14
     @active = false
15
+    @isClosingTab = false
15 16
 
16 17
     Backbone.Events.on('closeChat', (chat) =>
17
-      if chat == @model
18
-        @destroy()
18
+      if not @isClosingTab
19
+        if chat == @model
20
+          @destroy()
19 21
     )
20 22
 
21 23
     @model.get('who').on('change:name', @render)
22 24
 
23
-  fireCloseChatTrigger: ->
25
+  closeChat: ->
26
+    # zatvorenie musi prebehnut ako prva akcia,
27
+    # lebo zavisi na poradi a eventy mi nezarucia spravne poradie
28
+    @destroy()
29
+
30
+    @isClosingTab = true
24 31
     Backbone.Events.trigger('closeChat', @model)
25 32
 
26 33
   setActive: ->
... ...
@@ -55,11 +62,13 @@ class Xmpp.Views.Tabbar.TabView extends Backbone.View
55 55
       App.debug 'zobrazujem EXISTUJUCI chat window'
56 56
 
57 57
     @chatWindow.render()
58
+    Backbone.Events.trigger('resizeWorkspace')
58 59
 
59 60
   destroy: ->
60
-    App.debug 'destroy chatWindow and tab'
61 61
     if @active
62
+      App.debug 'destroy chatWindow'
62 63
       @chatWindow.remove()
64
+    App.debug 'destroy tab'
63 65
     @remove()
64 66
 
65 67
   hideChat: ->
... ...
@@ -71,6 +80,7 @@ class Xmpp.Views.Tabbar.TabView extends Backbone.View
71 71
 
72 72
   switchChatWindow: ->
73 73
     Backbone.Events.trigger('openChat', @model)
74
+    Backbone.Events.trigger('resizeWorkspace')
74 75
 
75 76
   getOwner: ->
76 77
     @model.get('who')
... ...
@@ -17,7 +17,20 @@ class Xmpp.Views.Tabbar.TabbarView extends Backbone.View
17 17
         _tab.model == chat
18 18
       )
19 19
 
20
-      @closeTab(tab) if tab
20
+      if tab
21
+        @removeTab(tab)
22
+
23
+        if tab == @activeTab
24
+          App.Collections.contacts.moveToInactiveList('all')
25
+          @openFirstTab()
26
+    )
27
+
28
+    Backbone.Events.on('removeContact', (contact) =>
29
+      tab = _.find(@tabs, (_tab) =>
30
+        _tab.model.get('withWhom') == contact
31
+      )
32
+
33
+      Backbone.Events.trigger('closeChat', tab.model) if tab
21 34
     )
22 35
 
23 36
     @tabs = []
... ...
@@ -50,15 +63,9 @@ class Xmpp.Views.Tabbar.TabbarView extends Backbone.View
50 50
       App.debug 'zakryvam chat window'
51 51
       @activeTab.hideChat()
52 52
 
53
-  closeTab: (tab) ->
54
-    @removeTab(tab)
55
-
56
-    if tab == @activeTab
57
-      firstTab = @selectFirstTab()
58
-      if firstTab
59
-        App.debug 'fire openChat'
60
-        Backbone.Events.trigger('openChat', firstTab.model)
61
-    #TODO: vybrat naposledy pouzity tab
53
+  openFirstTab: ->
54
+    firstTab = @selectFirstTab()
55
+    Backbone.Events.trigger('openChat', firstTab.model) if firstTab
62 56
 
63 57
   removeTab: (tab) ->
64 58
     @tabs = _.without(@tabs, tab)
... ...
@@ -68,4 +75,7 @@ class Xmpp.Views.Tabbar.TabbarView extends Backbone.View
68 68
     return this
69 69
 
70 70
   selectFirstTab: ->
71
-    _.first(@tabs)
72 71
\ No newline at end of file
72
+    _.first(@tabs)
73
+
74
+  isEmpty: ->
75
+    _.isEmpty(@tabs)
73 76
\ No newline at end of file
... ...
@@ -3,8 +3,17 @@ $().ready(->
3 3
     App.UI.setAutoHeight()
4 4
   ).trigger('resize')
5 5
 
6
-  $('#js-search-contacts').keyup( (e) ->
6
+  Backbone.Events.on('resizeWorkspace', -> App.UI.setAutoHeight())
7
+
8
+  $('#js-search-contacts').keyup( ->
7 9
     searchTerm = $(this).val()
8 10
     App.UI.filterContacts(searchTerm)
9 11
   )
12
+
13
+  $('#add-contact').click((e) ->
14
+    e.preventDefault()
15
+    $('#js-popup .close').click()
16
+    popup = new Xmpp.Views.Popup.AddContactView()
17
+    popup.render()
18
+  )
10 19
 )
11 20
\ No newline at end of file
... ...
@@ -1,2 +1,2 @@
1 1
 var I18n = I18n || {};
2
-I18n.translations = {"en":{"date":{"formats":{"default":"%Y-%m-%d","short":"%b %d","long":"%B %d, %Y"},"day_names":["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],"abbr_day_names":["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],"month_names":[null,"January","February","March","April","May","June","July","August","September","October","November","December"],"abbr_month_names":[null,"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],"order":["year","month","day"]},"time":{"formats":{"default":"%a, %d %b %Y %H:%M:%S %z","short":"%d %b %H:%M","long":"%B %d, %Y %H:%M"},"am":"am","pm":"pm"},"support":{"array":{"words_connector":", ","two_words_connector":" and ","last_word_connector":", and "}},"errors":{"format":"%{attribute} %{message}","messages":{"inclusion":"is not included in the list","exclusion":"is reserved","invalid":"is invalid","confirmation":"doesn't match confirmation","accepted":"must be accepted","empty":"can't be empty","blank":"can't be blank","too_long":"is too long (maximum is %{count} characters)","too_short":"is too short (minimum is %{count} characters)","wrong_length":"is the wrong length (should be %{count} characters)","not_a_number":"is not a number","not_an_integer":"must be an integer","greater_than":"must be greater than %{count}","greater_than_or_equal_to":"must be greater than or equal to %{count}","equal_to":"must be equal to %{count}","less_than":"must be less than %{count}","less_than_or_equal_to":"must be less than or equal to %{count}","odd":"must be odd","even":"must be even"}},"number":{"format":{"separator":".","delimiter":",","precision":3,"significant":false,"strip_insignificant_zeros":false},"currency":{"format":{"format":"%u%n","unit":"$","separator":".","delimiter":",","precision":2,"significant":false,"strip_insignificant_zeros":false}},"percentage":{"format":{"delimiter":""}},"precision":{"format":{"delimiter":""}},"human":{"format":{"delimiter":"","precision":3,"significant":true,"strip_insignificant_zeros":true},"storage_units":{"format":"%n %u","units":{"byte":{"one":"Byte","other":"Bytes"},"kb":"KB","mb":"MB","gb":"GB","tb":"TB"}},"decimal_units":{"format":"%n %u","units":{"unit":"","thousand":"Thousand","million":"Million","billion":"Billion","trillion":"Trillion","quadrillion":"Quadrillion"}}}},"datetime":{"distance_in_words":{"half_a_minute":"half a minute","less_than_x_seconds":{"one":"less than 1 second","other":"less than %{count} seconds"},"x_seconds":{"one":"1 second","other":"%{count} seconds"},"less_than_x_minutes":{"one":"less than a minute","other":"less than %{count} minutes"},"x_minutes":{"one":"1 minute","other":"%{count} minutes"},"about_x_hours":{"one":"about 1 hour","other":"about %{count} hours"},"x_days":{"one":"1 day","other":"%{count} days"},"about_x_months":{"one":"about 1 month","other":"about %{count} months"},"x_months":{"one":"1 month","other":"%{count} months"},"about_x_years":{"one":"about 1 year","other":"about %{count} years"},"over_x_years":{"one":"over 1 year","other":"over %{count} years"},"almost_x_years":{"one":"almost 1 year","other":"almost %{count} years"}},"prompts":{"year":"Year","month":"Month","day":"Day","hour":"Hour","minute":"Minute","second":"Seconds"}},"helpers":{"select":{"prompt":"Please select"},"submit":{"create":"Create %{model}","update":"Update %{model}","submit":"Save %{model}"},"button":{"create":"Create %{model}","update":"Update %{model}","submit":"Save %{model}"}},"mongoid":{"errors":{"messages":{"blank_in_locale":"can't be blank in %{location}","ambiguous_relationship":{"message":"Ambiguous relations %{candidates} defined on %{klass}.","summary":"When Mongoid attempts to set an inverse document of a relation in memory, it needs to know which relation it belongs to. When setting %{name}, Mongoid looked on the class %{inverse} for a matching relation, but multiples were found that could potentially match: %{candidates}.","resolution":"On the %{name} relation on %{inverse} you must add an :inverse_of option to specify the exact relationship on %{klass} that is the opposite of %{name}."},"callbacks":{"message":"Calling %{method} on %{klass} resulted in a false return from a callback.","summary":"If a before callback returns false when using Document.create!, Document#save!, or Documnet#update_attributes! this error will get raised since the document did not actually get saved.","resolution":"Double check all before callbacks to make sure they are not unintentionally returning false."},"calling_document_find_with_nil_is_invalid":{"message":"Calling Document.find with nil is invalid.","summary":"Document.find expects the parameters to be 1 or more ids, and will return a single document if 1 id is provided, otherwise an array of documents if multiple ids are provided.","resolution":"Most likely this is caused by passing parameters directly through to the find, and the parameter either is not present or the key from which it is accessed is incorrect."},"document_not_found":{"message":"Document(s) not found for class %{klass} with id(s) %{missing}.","summary":"When calling %{klass}.find with an id or array of ids, each parameter must match a document in the database or this error will be raised. The search was for the id(s): %{searched} (%{total} total) and the following ids were not found: %{missing}.","resolution":"Search for an id that is in the database or set the Mongoid.raise_not_found_error configuration option to false, which will cause a nil to be returned instead of raising this error when searching for a single id, or only the matched documents when searching for multiples."},"document_with_attributes_not_found":{"message":"Document not found for class %{klass} with attributes %{attributes}.","summary":"When calling %{klass}.find_by with a hash of attributes, all attributes provided must match a document in the database or this error will be raised.","resolution":"Search for attributes that are in the database or set the Mongoid.raise_not_found_error configuration option to false, which will cause a nil to be returned instead of raising this error."},"eager_load":{"message":"Eager loading :%{name} is not supported since it is a polymorphic belongs_to relation.","summary":"Mongoid cannot currently determine the classes it needs to eager load when the relation is polymorphic. The parents reside in different collections so a simple id lookup is not sufficient enough.","resolution":"Don't attempt to perform this action and have patience, maybe this will be supported in the future."},"invalid_collection":{"message":"Access to the collection for %{klass} is not allowed.","summary":"%{klass}.collection was called, and %{klass} is an embedded document - it resides within the collection of the root document of the hierarchy.","resolution":"For access to the collection that the embedded document is in, use %{klass}#_root.collection, or do not attempt to persist an embedded document without a parent set."},"invalid_config_option":{"message":"Invalid configuration option: %{name}.","summary":"A invalid configuration option was provided in your mongoid.yml, or a typo is potentially present. The valid configuration options are: %{options}.","resolution":"Remove the invalid option or fix the typo. If you were expecting the option to be there, please consult the following page with repect to Mongoid's configuration:\n\n   http://mongoid.org/en/mongoid/docs/installation.html"},"invalid_database":{"message":"Database should be a Mongo::DB, not %{name}.","summary":"When setting a master database in the Mongoid configuration it must be an actual instance of a Mongo::DB, and not just a name of the database. This check is performed when calling Mongoid.master = object.","resolution":"Make sure that when setting the configuration programatically that you are passing an actual db instance."},"invalid_field":{"message":"Defining a field named '%{name}' is not allowed.","summary":"Defining this field would override the method '%{name}', which would cause issues with expectations around the original method and cause extremely hard to debug issues. The original method was defined in:\n   Object: %{origin}\n   File: %{file}\n   Line: %{line}","resolution":"Use Mongoid.destructive_fields to see what names are not allowed, and don't use these names. These include names that also conflict with core Ruby methods on Object, Module, Enumerable, or included gems that inject methods into these or Mongoid internals."},"invalid_field_option":{"message":"Invalid option :%{option} provided for field :%{name}.","summary":"Mongoid requires that you only provide valid options on each field definition in order to prevent unexpected behaviour later on.","resolution":"When defining the field :%{name} on '%{klass}', please provide valid options for the field. These are currently: %{valid}. If you meant to define a custom field option, please do so first like so:\n\n   Mongoid::Fields.option :%{option} do |model, field, value|\n     # Your logic here...\n   end\n   class %{klass}\n     include Mongoid::Document\n     field :%{name}, %{option}: true\n   end\n\n"},"invalid_includes":{"message":"Invalid includes directive: %{klass}.includes(%{args})","summary":"Eager loading in Mongoid only supports providing arguments to %{klass}.includes that are the names of relations on the %{klass} model, and only supports one level of eager loading. (ie, eager loading associations not on the %{klass} but one step away via another relation is not allowed.","resolution":"Ensure that each parameter passed to %{klass}.includes is a valid name of a relation on the %{klass} model. These are: %{relations}."},"invalid_index":{"message":"Invalid index specification on %{klass}: %{spec}, %{options}","summary":"Indexes in Mongoid are defined as a hash of field name and direction/2d pairs, with a hash for any additional options.","resolution":"Ensure that the index conforms to the correct syntax and has the correct options.\n\n Valid options are:\n   background: true|false\n   drop_dups: true|false\n   name: 'index_name'\n   sparse: true|false\n   unique: true|false\n   min: 1\n   max: 1\n   bits: 26\n   bucket_size : 1\n   weights: { content: 1, title: 2 }\n Valid types are: 1, -1, '2d', '2dsphere', 'geoHaystack', 'text', 'hashed'\n\n Example:\n   class Band\n     include Mongoid::Document\n     index({ name: 1, label: -1 }, { sparse: true })\n     index({ location: '2d' }, { background: true })\n   end\n\n"},"invalid_options":{"message":"Invalid option :%{invalid} provided to relation :%{name}.","summary":"Mongoid checks the options that are passed to the relation macros to ensure that no ill side effects occur by letting something slip by.","resolution":"Valid options are: %{valid}, make sure these are the ones you are using."},"invalid_path":{"message":"Having a root path assigned for %{klass} is invalid.","summary":"Mongoid has two different path objects for determining the location of a document in the database, Root and Embedded. This error is raised when an embedded document somehow gets a root path assigned.","resolution":"Most likely your embedded model, %{klass} is also referenced via a has_many from a root document in another collection. Double check the relation definitions and fix any instances where embedded documents are improperly referenced from other collections."},"invalid_scope":{"message":"Defining a scope of value %{value} on %{klass} is not allowed.","summary":"Scopes in Mongoid must be either criteria objects or procs that wrap criteria objects.","resolution":"Change the scope to be a criteria or proc wrapped critera.\n\n Example:\n   class Band\n     include Mongoid::Document\n     field :active, type: Boolean, default: true\n     scope :active, where(active: true)\n     scope :inactive, ->{ where(active: false) }\n   end\n\n"},"invalid_storage_options":{"message":"Invalid options passed to %{klass}.store_in: %{options}.","summary":"The :store_in macro takes only a hash of parameters with the keys :database, :collection, or :session.","resolution":"Change the options passed to store_in to match the documented API, and ensure all keys in the options hash are symbols.\n\n Example:\n   class Band\n     include Mongoid::Document\n     store_in collection: 'artists', database: 'secondary'\n   end\n\n"},"invalid_time":{"message":"'%{value}' is not a valid Time.","summary":"Mongoid tries to serialize the values for Date, DateTime, and Time into proper UTC times to store in the database. The provided value could not be parsed.","resolution":"Make sure to pass parsable values to the field setter for Date, DateTime, and Time objects. When this is a String it needs to be valid for Time.parse. Other objects must be valid to pass to Time.local."},"inverse_not_found":{"message":"When adding a(n) %{klass} to %{base}#%{name}, Mongoid could not determine the inverse foreign key to set. The attempted key was '%{inverse}'.","summary":"When adding a document to a relation, Mongoid attempts to link the newly added document to the base of the relation in memory, as well as set the foreign key to link them on the database side. In this case Mongoid could not determine what the inverse foreign key was.","resolution":"If an inverse is not required, like a belongs_to or has_and_belongs_to_many, ensure that :inverse_of => nil is set on the relation. If the inverse is needed, most likely the inverse cannot be figured out from the names of the relations and you will need to explicitly tell Mongoid on the relation what the inverse is.\n\n Example:\n   class Lush\n     include Mongoid::Document\n     has_one :whiskey, class_name: \"Drink\", inverse_of: :alcoholic\n   end\n\n   class Drink\n     include Mongoid::Document\n     belongs_to :alcoholic, class_name: \"Lush\", inverse_of: :whiskey\n   end"},"invalid_set_polymorphic_relation":{"message":"The %{name} attribute can't be set to an instance of %{other_klass} as %{other_klass} has multiple relations referencing %{klass} as %{name}.","summary":"If the parent class of a polymorphic relation has multiple definitions for the same relation, the values must be set from the parent side and not the child side since Mongoid cannot determine from the child side which relation to go in.","resolution":"Set the values from the parent, or redefine the relation with only a single definition in the parent."},"mixed_relations":{"message":"Referencing a(n) %{embedded} document from the %{root} document via a relational association is not allowed since the %{embedded} is embedded.","summary":"In order to properly access a(n) %{embedded} from %{root} the reference would need to go through the root document of %{embedded}. In a simple case this would require Mongoid to store an extra foreign key for the root, in more complex cases where %{embedded} is multiple levels deep a key would need to be stored for each parent up the hierarchy.","resolution":"Consider not embedding %{embedded}, or do the key storage and access in a custom manner in the application code."},"mixed_session_configuration":{"message":"Both uri and standard configuration options defined for session: '%{name}'.","summary":"Instead of simply giving uri or standard options a preference order, Mongoid assumes that you have made a mistake in your configuration and requires that you provide one or the other, but not both. The options that were provided were: %{config}.","resolution":"Provide either only a uri as configuration or only standard options."},"nested_attributes_metadata_not_found":{"message":"Could not find metadata for relation '%{name}' on model: %{klass}.","summary":"When defining nested attributes for a relation, Mongoid needs to access the metadata for the relation '%{name}' in order if add autosave functionality to it, if applicable. Either no relation named '%{name}' could be found, or the relation had not been defined yet.","resolution":"Make sure that there is a relation defined named '%{name}' on %{klass} or that the relation definition comes before the accepts_nested_attributes_for macro in the model - order matters so that Mongoid has access to the metadata.\n\n Example:\n   class Band\n     include Mongoid::Document\n     has_many :albums\n     accepts_nested_attributes_for :albums\n   end\n\n"},"no_default_session":{"message":"No default session configuration is defined.","summary":"The configuration provided settings for: %{keys}, but Mongoid requires a :default to be defined at minimum.","resolution":"If configuring via a mongoid.yml, ensure that within your :sessions section a :default session is defined.\n\n Example:\n   development:\n     sessions:\n       default:\n         hosts:\n           - localhost:27017\n\n"},"no_environment":{"message":"Could not load the configuration since no environment was defined.","summary":"Mongoid attempted to find the appropriate environment but no Rails.env, Sinatra::Base.environment, RACK_ENV, or MONGOID_ENV could be found.","resolution":"Make sure some environment is set from the mentioned options. Mongoid cannot load configuration from the yaml without knowing which environment it is in, and we have considered defaulting to development an undesireable side effect of this not being defined."},"no_map_reduce_output":{"message":"No output location was specified for the map/reduce operation.","summary":"When executing a map/reduce, you must provide the output location of the results. The attempted command was: %{command}.","resolution":"Provide the location that the output of the operation is to go by chaining an #out call to the map/reduce.\n\n Example:\n   Band.map_reduce(map, reduce).out(inline: 1)\n\n Valid options for the out function are:\n   inline:  1\n   merge:   'collection-name'\n   replace: 'collection-name'\n   reduce:  'collection-name'\n\n"},"no_metadata":{"message":"Metadata not found for document of type %{klass}.","summary":"Mongoid sets the metadata of a relation on the document when it is either loaded from within the relation, or added to one. The presence of the metadata is required in order to provide various functionality around relations. Most likely you are getting this error because the document is embedded and was attempted to be persisted without being associated with a parent, or the relation was not properly defined.","resolution":"Ensure that your relations on the %{klass} model are all properly defined, and that the inverse relations are also properly defined. Embedded relations must have both the parent (embeds_one/embeds_many) and the inverse (embedded_in) present in order to work properly."},"no_parent":{"message":"Cannot persist embedded document %{klass} without a parent document.","summary":"If the document is embedded, in order to be persisted it must always have a reference to its parent document. This is most likely caused by either calling %{klass}.create or %{klass}.create! without setting the parent document as an attribute.","resolution":"Ensure that you've set the parent relation if instantiating the embedded document direcly, or always create new embedded documents via the parent relation."},"no_session_config":{"message":"No configuration could be found for a session named '%{name}'.","summary":"When attempting to create the new session, Mongoid could not find a session configuration for the name: '%{name}'. This is necessary in order to know the host, port, and options needed to connect.","resolution":"Double check your mongoid.yml to make sure under the sessions key that a configuration exists for '%{name}'. If you have set the configuration programatically, ensure that '%{name}' exists in the configuration hash."},"no_sessions_config":{"message":"No sessions configuration provided.","summary":"Mongoid's configuration requires that you provide details about each session that can be connected to, and requires in the sessions config at least 1 default session to exist.","resolution":"Double check your mongoid.yml to make sure that you have a top-level sessions key with at least 1 default session configuration for it. You can regenerate a new mongoid.yml for assistance via `rails g mongoid:config`.\n\n Example:\n   development:\n     sessions:\n       default:\n         database: mongoid_dev\n         hosts:\n           - localhost:27017\n\n"},"no_session_database":{"message":"No database provided for session configuration: :%{name}.","summary":"Each session configuration must provide a database so Mongoid knows where the default database to persist to. What was provided was: %{config}.","resolution":"If configuring via a mongoid.yml, ensure that within your :%{name} section a :database value for the session's default database is defined.\n\n Example:\n   development:\n     sessions:\n       %{name}:\n         database: my_app_db\n         hosts:\n           - localhost:27017\n\n"},"no_session_hosts":{"message":"No hosts provided for session configuration: :%{name}.","summary":"Each session configuration must provide hosts so Mongoid knows where the database server is located. What was provided was: %{config}.","resolution":"If configuring via a mongoid.yml, ensure that within your :%{name} section a :hosts value for the session hosts is defined.\n\n Example:\n   development:\n     sessions:\n       %{name}:\n         database: my_app_db\n         hosts:\n           - localhost:27017\n\n"},"readonly_attribute":{"message":"Attempted to set the readonly attribute '%{name}' with the value: %{value}.","summary":"Attributes flagged as readonly via Model.attr_readonly can only have values set when the document is a new record.","resolution":"Don't define '%{name}' as readonly, or do not attempt to update its value after the document is persisted."},"scope_overwrite":{"message":"Cannot create scope :%{scope_name}, because of existing method %{model_name}.%{scope_name}.","summary":"When defining a scope that conflicts with a method that already exists on the model, this error will get raised if Mongoid.scope_overwrite_exception is set to true.","resolution":"Change the name of the scope so it does not conflict with the already defined method %{model_name}, or set the configuration option Mongoid.scope_overwrite_exception to false, which is its default. In this case a warning will be logged."},"taken":"is already taken","too_many_nested_attribute_records":{"message":"Accepting nested attributes for %{association} is limited to %{limit} records.","summary":"More documents were sent to be processed than the allowed limit.","resolution":"The limit is set as an option to the macro, for example: accepts_nested_attributes_for :%{association}, limit: %{limit}. Consider raising this limit or making sure no more are sent than the set value."},"unknown_attribute":{"message":"Attempted to set a value for '%{name}' which is not allowed on the model %{klass}.","summary":"When setting Mongoid.allow_dynamic_fields to false and the attribute does not already exist in the attributes hash, attempting to call %{klass}#%{name}= for it is not allowed. This is also triggered by passing the attribute to any method that accepts an attributes hash, and is raised instead of getting a NoMethodError.","resolution":"You can set Mongoid.allow_dynamic_fields to true if you expect to be writing values for undefined fields often."},"unsaved_document":{"message":"Attempted to save %{document} before the parent %{base}.","summary":"You cannot call create or create! through the relation (%{document}) who's parent (%{base}) is not already saved. This would case the database to be out of sync since the child could potentially reference a nonexistant parent.","resolution":"Make sure to only use create or create! when the parent document %{base} is persisted."},"unsupported_javascript":{"message":"Executing Javascript $where selector on an embedded criteria is not supported.","summary":"Mongoid only supports providing a hash of arguments to #where criterion on embedded documents. Since %{klass} is embedded, the expression %{javascript} is not allowed.","resolution":"Please provide a standard hash to #where when the criteria is for an embedded relation."},"validations":{"message":"Validation of %{document} failed.","summary":"The following errors were found: %{errors}","resolution":"Try persisting the document with valid data or remove the validations."},"versioning_not_on_root":{"message":"Versioning not allowed on embedded document: %{klass}.","summary":"Mongoid::Versioning behaviour is only allowed on documents that are the root document in the hierarchy.","resolution":"Remove the versioning from the embedded %{klass} or consider moving it to a root location in the hierarchy if versioning is needed."},"delete_restriction":{"message":"Cannot delete %{document} because of dependent '%{relation}'.","summary":"When defining '%{relation}' with a :dependent => :restrict, Mongoid will raise an error when attempting to delete the %{document} when the child '%{relation}' still has documents in it.","resolution":"Don't attempt to delete the parent %{document} when it has children, or change the dependent option on the relation."}}}},"login":{"error":"Invalid email/password combination","success":"Successfully logged in","access-denied":"Access denied. Please, sign in first."},"sessions":{"title":"Login","new":{"form-header":"Login form","form-send":"Login"}},"chat":{"title":"Chat","roster":{"friends":"Friends","chat-group":"Chat group","avatar_alt":"Avatar","change_status_msg":"Change your status message","search":"Search contacts","group-empty":"Empty"},"tabbar":{"and":"and"},"messages":{"new_msg_placeholder":"Type your message here","send":"Send"},"window":{"sendFailed":"An error occured while sending your message"}}}};
3 2
\ No newline at end of file
3
+I18n.translations = {"en":{"date":{"formats":{"default":"%Y-%m-%d","short":"%b %d","long":"%B %d, %Y"},"day_names":["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],"abbr_day_names":["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],"month_names":[null,"January","February","March","April","May","June","July","August","September","October","November","December"],"abbr_month_names":[null,"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],"order":["year","month","day"]},"time":{"formats":{"default":"%a, %d %b %Y %H:%M:%S %z","short":"%d %b %H:%M","long":"%B %d, %Y %H:%M"},"am":"am","pm":"pm"},"support":{"array":{"words_connector":", ","two_words_connector":" and ","last_word_connector":", and "}},"errors":{"format":"%{attribute} %{message}","messages":{"inclusion":"is not included in the list","exclusion":"is reserved","invalid":"is invalid","confirmation":"doesn't match confirmation","accepted":"must be accepted","empty":"can't be empty","blank":"can't be blank","too_long":"is too long (maximum is %{count} characters)","too_short":"is too short (minimum is %{count} characters)","wrong_length":"is the wrong length (should be %{count} characters)","not_a_number":"is not a number","not_an_integer":"must be an integer","greater_than":"must be greater than %{count}","greater_than_or_equal_to":"must be greater than or equal to %{count}","equal_to":"must be equal to %{count}","less_than":"must be less than %{count}","less_than_or_equal_to":"must be less than or equal to %{count}","odd":"must be odd","even":"must be even"}},"number":{"format":{"separator":".","delimiter":",","precision":3,"significant":false,"strip_insignificant_zeros":false},"currency":{"format":{"format":"%u%n","unit":"$","separator":".","delimiter":",","precision":2,"significant":false,"strip_insignificant_zeros":false}},"percentage":{"format":{"delimiter":""}},"precision":{"format":{"delimiter":""}},"human":{"format":{"delimiter":"","precision":3,"significant":true,"strip_insignificant_zeros":true},"storage_units":{"format":"%n %u","units":{"byte":{"one":"Byte","other":"Bytes"},"kb":"KB","mb":"MB","gb":"GB","tb":"TB"}},"decimal_units":{"format":"%n %u","units":{"unit":"","thousand":"Thousand","million":"Million","billion":"Billion","trillion":"Trillion","quadrillion":"Quadrillion"}}}},"datetime":{"distance_in_words":{"half_a_minute":"half a minute","less_than_x_seconds":{"one":"less than 1 second","other":"less than %{count} seconds"},"x_seconds":{"one":"1 second","other":"%{count} seconds"},"less_than_x_minutes":{"one":"less than a minute","other":"less than %{count} minutes"},"x_minutes":{"one":"1 minute","other":"%{count} minutes"},"about_x_hours":{"one":"about 1 hour","other":"about %{count} hours"},"x_days":{"one":"1 day","other":"%{count} days"},"about_x_months":{"one":"about 1 month","other":"about %{count} months"},"x_months":{"one":"1 month","other":"%{count} months"},"about_x_years":{"one":"about 1 year","other":"about %{count} years"},"over_x_years":{"one":"over 1 year","other":"over %{count} years"},"almost_x_years":{"one":"almost 1 year","other":"almost %{count} years"}},"prompts":{"year":"Year","month":"Month","day":"Day","hour":"Hour","minute":"Minute","second":"Seconds"}},"helpers":{"select":{"prompt":"Please select"},"submit":{"create":"Create %{model}","update":"Update %{model}","submit":"Save %{model}"},"button":{"create":"Create %{model}","update":"Update %{model}","submit":"Save %{model}"}},"mongoid":{"errors":{"messages":{"blank_in_locale":"can't be blank in %{location}","ambiguous_relationship":{"message":"Ambiguous relations %{candidates} defined on %{klass}.","summary":"When Mongoid attempts to set an inverse document of a relation in memory, it needs to know which relation it belongs to. When setting %{name}, Mongoid looked on the class %{inverse} for a matching relation, but multiples were found that could potentially match: %{candidates}.","resolution":"On the %{name} relation on %{inverse} you must add an :inverse_of option to specify the exact relationship on %{klass} that is the opposite of %{name}."},"callbacks":{"message":"Calling %{method} on %{klass} resulted in a false return from a callback.","summary":"If a before callback returns false when using Document.create!, Document#save!, or Documnet#update_attributes! this error will get raised since the document did not actually get saved.","resolution":"Double check all before callbacks to make sure they are not unintentionally returning false."},"calling_document_find_with_nil_is_invalid":{"message":"Calling Document.find with nil is invalid.","summary":"Document.find expects the parameters to be 1 or more ids, and will return a single document if 1 id is provided, otherwise an array of documents if multiple ids are provided.","resolution":"Most likely this is caused by passing parameters directly through to the find, and the parameter either is not present or the key from which it is accessed is incorrect."},"document_not_found":{"message":"Document(s) not found for class %{klass} with id(s) %{missing}.","summary":"When calling %{klass}.find with an id or array of ids, each parameter must match a document in the database or this error will be raised. The search was for the id(s): %{searched} (%{total} total) and the following ids were not found: %{missing}.","resolution":"Search for an id that is in the database or set the Mongoid.raise_not_found_error configuration option to false, which will cause a nil to be returned instead of raising this error when searching for a single id, or only the matched documents when searching for multiples."},"document_with_attributes_not_found":{"message":"Document not found for class %{klass} with attributes %{attributes}.","summary":"When calling %{klass}.find_by with a hash of attributes, all attributes provided must match a document in the database or this error will be raised.","resolution":"Search for attributes that are in the database or set the Mongoid.raise_not_found_error configuration option to false, which will cause a nil to be returned instead of raising this error."},"eager_load":{"message":"Eager loading :%{name} is not supported since it is a polymorphic belongs_to relation.","summary":"Mongoid cannot currently determine the classes it needs to eager load when the relation is polymorphic. The parents reside in different collections so a simple id lookup is not sufficient enough.","resolution":"Don't attempt to perform this action and have patience, maybe this will be supported in the future."},"invalid_collection":{"message":"Access to the collection for %{klass} is not allowed.","summary":"%{klass}.collection was called, and %{klass} is an embedded document - it resides within the collection of the root document of the hierarchy.","resolution":"For access to the collection that the embedded document is in, use %{klass}#_root.collection, or do not attempt to persist an embedded document without a parent set."},"invalid_config_option":{"message":"Invalid configuration option: %{name}.","summary":"A invalid configuration option was provided in your mongoid.yml, or a typo is potentially present. The valid configuration options are: %{options}.","resolution":"Remove the invalid option or fix the typo. If you were expecting the option to be there, please consult the following page with repect to Mongoid's configuration:\n\n   http://mongoid.org/docs/installation.html"},"invalid_database":{"message":"Database should be a Mongo::DB, not %{name}.","summary":"When setting a master database in the Mongoid configuration it must be an actual instance of a Mongo::DB, and not just a name of the database. This check is performed when calling Mongoid.master = object.","resolution":"Make sure that when setting the configuration programatically that you are passing an actual db instance."},"invalid_field":{"message":"Defining a field named '%{name}' is not allowed.","summary":"Defining this field would override the method '%{name}', which would cause issues with expectations around the original method and cause extremely hard to debug issues. The original method was defined in:\n   Object: %{origin}\n   File: %{file}\n   Line: %{line}","resolution":"Use Mongoid.destructive_fields to see what names are not allowed, and don't use these names. These include names that also conflict with core Ruby methods on Object, Module, Enumerable, or included gems that inject methods into these or Mongoid internals."},"invalid_field_option":{"message":"Invalid option :%{option} provided for field :%{name}.","summary":"Mongoid requires that you only provide valid options on each field definition in order to prevent unexpected behaviour later on.","resolution":"When defining the field :%{name} on '%{klass}', please provide valid options for the field. These are currently: %{valid}. If you meant to define a custom field option, please do so first like so:\n\n   Mongoid::Fields.option :%{option} do |model, field, value|\n     # Your logic here...\n   end\n   class %{klass}\n     include Mongoid::Document\n     field :%{name}, %{option}: true\n   end\n\n"},"invalid_includes":{"message":"Invalid includes directive: %{klass}.includes(%{args})","summary":"Eager loading in Mongoid only supports providing arguments to %{klass}.includes that are the names of relations on the %{klass} model, and only supports one level of eager loading. (ie, eager loading associations not on the %{klass} but one step away via another relation is not allowed.","resolution":"Ensure that each parameter passed to %{klass}.includes is a valid name of a relation on the %{klass} model. These are: %{relations}."},"invalid_index":{"message":"Invalid index specification on %{klass}: %{spec}, %{options}","summary":"Indexes in Mongoid are defined as a hash of field name and direction/2d pairs, with a hash for any additional options.","resolution":"Ensure that the index conforms to the correct syntax and has the correct options.\n\n Valid options are:\n   background: true|false\n   drop_dups: true|false\n   name: 'index_name'\n   sparse: true|false\n   unique: true|false\n   min: 1\n   max: 1\n   bits: 26\n   bucket_size : 1\n Valid types are: 1, -1, '2d', 'geoHaystack'\n\n Example:\n   class Band\n     include Mongoid::Document\n     index({ name: 1, label: -1 }, { sparse: true })\n     index({ location: '2d' }, { background: true })\n   end\n\n"},"invalid_options":{"message":"Invalid option :%{invalid} provided to relation :%{name}.","summary":"Mongoid checks the options that are passed to the relation macros to ensure that no ill side effects occur by letting something slip by.","resolution":"Valid options are: %{valid}, make sure these are the ones you are using."},"invalid_path":{"message":"Having a root path assigned for %{klass} is invalid.","summary":"Mongoid has two different path objects for determining the location of a document in the database, Root and Embedded. This error is raised when an embedded document somehow gets a root path assigned.","resolution":"Most likely your embedded model, %{klass} is also referenced via a has_many from a root document in another collection. Double check the relation definitions and fix any instances where embedded documents are improperly referenced from other collections."},"invalid_scope":{"message":"Defining a scope of value %{value} on %{klass} is not allowed.","summary":"Scopes in Mongoid must be either criteria objects or procs that wrap criteria objects.","resolution":"Change the scope to be a criteria or proc wrapped critera.\n\n Example:\n   class Band\n     include Mongoid::Document\n     field :active, type: Boolean, default: true\n     scope :active, where(active: true)\n     scope :inactive, ->{ where(active: false) }\n   end\n\n"},"invalid_storage_options":{"message":"Invalid options passed to %{klass}.store_in: %{options}.","summary":"The :store_in macro takes only a hash of parameters with the keys :database, :collection, or :session.","resolution":"Change the options passed to store_in to match the documented API, and ensure all keys in the options hash are symbols.\n\n Example:\n   class Band\n     include Mongoid::Document\n     store_in collection: 'artists', database: 'secondary'\n   end\n\n"},"invalid_time":{"message":"'%{value}' is not a valid Time.","summary":"Mongoid tries to serialize the values for Date, DateTime, and Time into proper UTC times to store in the database. The provided value could not be parsed.","resolution":"Make sure to pass parsable values to the field setter for Date, DateTime, and Time objects. When this is a String it needs to be valid for Time.parse. Other objects must be valid to pass to Time.local."},"inverse_not_found":{"message":"When adding a(n) %{klass} to %{base}#%{name}, Mongoid could not determine the inverse foreign key to set. The attempted key was '%{inverse}'.","summary":"When adding a document to a relation, Mongoid attempts to link the newly added document to the base of the relation in memory, as well as set the foreign key to link them on the database side. In this case Mongoid could not determine what the inverse foreign key was.","resolution":"If an inverse is not required, like a belongs_to or has_and_belongs_to_many, ensure that :inverse_of => nil is set on the relation. If the inverse is needed, most likely the inverse cannot be figured out from the names of the relations and you will need to explicitly tell Mongoid on the relation what the inverse is.\n\n Example:\n   class Lush\n     include Mongoid::Document\n     has_one :whiskey, class_name: \"Drink\", inverse_of: :alcoholic\n   end\n\n   class Drink\n     include Mongoid::Document\n     belongs_to :alcoholic, class_name: \"Lush\", inverse_of: :whiskey\n   end"},"invalid_set_polymorphic_relation":{"message":"The %{name} attribute can't be set to an instance of %{other_klass} as %{other_klass} has multiple relations referencing %{klass} as %{name}.","summary":"If the parent class of a polymorphic relation has multiple definitions for the same relation, the values must be set from the parent side and not the child side since Mongoid cannot determine from the child side which relation to go in.","resolution":"Set the values from the parent, or redefine the relation with only a single definition in the parent."},"mixed_relations":{"message":"Referencing a(n) %{embedded} document from the %{root} document via a relational association is not allowed since the %{embedded} is embedded.","summary":"In order to properly access a(n) %{embedded} from %{root} the reference would need to go through the root document of %{embedded}. In a simple case this would require Mongoid to store an extra foreign key for the root, in more complex cases where %{embedded} is multiple levels deep a key would need to be stored for each parent up the hierarchy.","resolution":"Consider not embedding %{embedded}, or do the key storage and access in a custom manner in the application code."},"mixed_session_configuration":{"message":"Both uri and standard configuration options defined for session: '%{name}'.","summary":"Instead of simply giving uri or standard options a preference order, Mongoid assumes that you have made a mistake in your configuration and requires that you provide one or the other, but not both. The options that were provided were: %{config}.","resolution":"Provide either only a uri as configuration or only standard options."},"nested_attributes_metadata_not_found":{"message":"Could not find metadata for relation '%{name}' on model: %{klass}.","summary":"When defining nested attributes for a relation, Mongoid needs to access the metadata for the relation '%{name}' in order if add autosave functionality to it, if applicable. Either no relation named '%{name}' could be found, or the relation had not been defined yet.","resolution":"Make sure that there is a relation defined named '%{name}' on %{klass} or that the relation definition comes before the accepts_nested_attributes_for macro in the model - order matters so that Mongoid has access to the metadata.\n\n Example:\n   class Band\n     include Mongoid::Document\n     has_many :albums\n     accepts_nested_attributes_for :albums\n   end\n\n"},"no_default_session":{"message":"No default session configuration is defined.","summary":"The configuration provided settings for: %{keys}, but Mongoid requires a :default to be defined at minimum.","resolution":"If configuring via a mongoid.yml, ensure that within your :sessions section a :default session is defined.\n\n Example:\n   development:\n     sessions:\n       default:\n         hosts:\n           - localhost:27017\n\n"},"no_environment":{"message":"Could not load the configuration since no environment was defined.","summary":"Mongoid attempted to find the appropriate environment but no Rails.env, Sinatra::Base.environment, RACK_ENV, or MONGOID_ENV could be found.","resolution":"Make sure some environment is set from the mentioned options. Mongoid cannot load configuration from the yaml without knowing which environment it is in, and we have considered defaulting to development an undesireable side effect of this not being defined."},"no_map_reduce_output":{"message":"No output location was specified for the map/reduce operation.","summary":"When executing a map/reduce, you must provide the output location of the results. The attempted command was: %{command}.","resolution":"Provide the location that the output of the operation is to go by chaining an #out call to the map/reduce.\n\n Example:\n   Band.map_reduce(map, reduce).out(inline: 1)\n\n Valid options for the out function are:\n   inline:  1\n   merge:   'collection-name'\n   replace: 'collection-name'\n   reduce:  'collection-name'\n\n"},"no_metadata":{"message":"Metadata not found for document of type %{klass}.","summary":"Mongoid sets the metadata of a relation on the document when it is either loaded from within the relation, or added to one. The presence of the metadata is required in order to provide various functionality around relations. Most likely you are getting this error because the document is embedded and was attempted to be persisted without being associated with a parent, or the relation was not properly defined.","resolution":"Ensure that your relations on the %{klass} model are all properly defined, and that the inverse relations are also properly defined. Embedded relations must have both the parent (embeds_one/embeds_many) and the inverse (embedded_in) present in order to work properly."},"no_parent":{"message":"Cannot persist embedded document %{klass} without a parent document.","summary":"If the document is embedded, in order to be persisted it must always have a reference to it's parent document. This is most likely cause by either calling %{klass}.create or %{klass}.create! without setting the parent document as an attribute.","resolution":"Ensure that you've set the parent relation if instantiating the embedded document direcly, or always create new embedded documents via the parent relation."},"no_session_config":{"message":"No configuration could be found for a session named '%{name}'.","summary":"When attempting to create the new session, Mongoid could not find a session configuration for the name: '%{name}'. This is necessary in order to know the host, port, and options needed to connect.","resolution":"Double check your mongoid.yml to make sure under the sessions key that a configuration exists for '%{name}'. If you have set the configuration programatically, ensure that '%{name}' exists in the configuration hash."},"no_sessions_config":{"message":"No sessions configuration provided.","summary":"Mongoid's configuration requires that you provide details about each session that can be connected to, and requires in the sessions config at least 1 default session to exist.","resolution":"Double check your mongoid.yml to make sure that you have a top-level sessions key with at least 1 default session configuration for it. You can regenerate a new mongoid.yml for assistance via `rails g mongoid:config`.\n\n Example:\n   development:\n     sessions:\n       default:\n         database: mongoid_dev\n         hosts:\n           - localhost:27017\n\n"},"no_session_database":{"message":"No database provided for session configuration: :%{name}.","summary":"Each session configuration must provide a database so Mongoid knows where the default database to persist to. What was provided was: %{config}.","resolution":"If configuring via a mongoid.yml, ensure that within your :%{name} section a :database value for the session's default database is defined.\n\n Example:\n   development:\n     sessions:\n       %{name}:\n         database: my_app_db\n         hosts:\n           - localhost:27017\n\n"},"no_session_hosts":{"message":"No hosts provided for session configuration: :%{name}.","summary":"Each session configuration must provide hosts so Mongoid knows where the database server is located. What was provided was: %{config}.","resolution":"If configuring via a mongoid.yml, ensure that within your :%{name} section a :hosts value for the session hosts is defined.\n\n Example:\n   development:\n     sessions:\n       %{name}:\n         database: my_app_db\n         hosts:\n           - localhost:27017\n\n"},"readonly_attribute":{"message":"Attempted to set the readonly attribute '%{name}' with the value: %{value}.","summary":"Attributes flagged as readonly via Model.attr_readonly can only have values set when the document is a new record.","resolution":"Don't define '%{name}' as readonly, or do not attempt to update it's value after the document is persisted."},"scope_overwrite":{"message":"Cannot create scope :%{scope_name}, because of existing method %{model_name}.%{scope_name}.","summary":"When defining a scope that conflicts with a method that already exists on the model, this error will get raised if Mongoid.scope_overwrite_exception is set to true.","resolution":"Change the name of the scope so it does not conflict with the already defined method %{model_name}, or set the configuration option Mongoid.scope_overwrite_exception to false, which is it's default. In this case a warning will be logged."},"taken":"is already taken","too_many_nested_attribute_records":{"message":"Accepting nested attributes for %{association} is limited to %{limit} records.","summary":"More documents were sent to be processed than the allowed limit.","resolution":"The limit is set as an option to the macro, for example: accepts_nested_attributes_for :%{association}, limit: %{limit}. Consider raising this limit or making sure no more are sent than the set value."},"unknown_attribute":{"message":"Attempted to set a value for '%{name}' which is not allowed on the model %{klass}.","summary":"When setting Mongoid.allow_dynamic_fields to false and the attribute does not already exist in the attributes hash, attempting to call %{klass}#%{name}= for it is not allowed. This is also triggered by passing the attribute to any method that accepts an attributes hash, and is raised instead of getting a NoMethodError.","resolution":"You can set Mongoid.allow_dynamic_fields to true if you expect to be writing values for undefined fields often."},"unsaved_document":{"message":"Attempted to save %{document} before the parent %{base}.","summary":"You cannot call create or create! through the relation (%{document}) who's parent (%{base}) is not already saved. This would case the database to be out of sync since the child could potentially reference a nonexistant parent.","resolution":"Make sure to only use create or create! when the parent document %{base} is persisted."},"unsupported_javascript":{"message":"Executing Javascript $where selector on an embedded criteria is not supported.","summary":"Mongoid only supports providing a hash of arguments to #where criterion on embedded documents. Since %{klass} is embedded, the expression %{javascript} is not allowed.","resolution":"Please provide a standard hash to #where when the criteria is for an embedded relation."},"validations":{"message":"Validation of %{document} failed.","summary":"The following errors were found: %{errors}","resolution":"Try persisting the document with valid data or remove the validations."},"versioning_not_on_root":{"message":"Versioning not allowed on embedded document: %{klass}.","summary":"Mongoid::Versioning behaviour is only allowed on documents that are the root document in the hierarchy.","resolution":"Remove the versioning from the embedded %{klass} or consider moving it to a root location in the hierarchy if versioning is needed."},"delete_restriction":{"message":"Cannot delete %{document} because of dependent '%{relation}'.","summary":"When defining '%{relation}' with a :dependent => :restrict, Mongoid will raise an error when attempting to delete the %{document} when the child '%{relation}' still has documents in it.","resolution":"Don't attempt to delete the parent %{document} when it has children, or change the dependent option on the relation."}}}},"login":{"error":"Invalid email/password combination","success":"Successfully logged in","access-denied":"Access denied. Please, sign in first."},"sessions":{"title":"Login","new":{"form-header":"Login form","form-send":"Login"}},"chat":{"title":"Chat","close":"Close","roster":{"friends":"Friends","chat-group":"Chat group","avatar_alt":"Avatar","change_status_msg":"Change your status message","search":"Search contacts","group-empty":"Empty","new-multichat":"Create new multichat with %{user}","invite":"Invite %{user} to the opened multichat","kick":"Kick %{user} from the opened multichat","is-owner":"%{user} is the owner of the opened multichat","make-owner":"Make %{user} a new owner of the opened multichat","remove":"Remove %{user} from my roster","icons":{"multi":"multi","invite":"invite","kick":"kick","owner":"owner","remove":"rem"}},"tabbar":{"and":"and"},"messages":{"new_msg_placeholder":"Type your message here","send":"Send"},"window":{"sendFailed":"An error occured while sending your message"}},"popup":{"headers":{"add_contact":"Add a new contact","friend_request":"Friend request"},"friend_request":{"question":"wants to add you to his contacts. Allow?","answer_yes":"Yes","answer_no":"No"}}}};
4 4
\ No newline at end of file
... ...
@@ -1,6 +1,7 @@
1 1
 $bg-color: #DEDBDB;
2 2
 $app-color: #24a396;
3 3
 $bg-roster: #F4F4F4;
4
+$inactive-color: #658489;
4 5
 
5 6
 $white: #fff;
6 7
 $gray: #b0b7b9;
... ...
@@ -5,6 +5,8 @@
5 5
 */
6 6
 
7 7
 @import "compass/css3/box-sizing";
8
+@import "compass/css3/opacity";
9
+@import "compass/css3/box-shadow";
8 10
 
9 11
 @import "config";
10 12
 @import "chat.css.scss";
... ...
@@ -14,7 +16,7 @@
14 14
 
15 15
 body {
16 16
   background: $bg-color;
17
-  border-top: #658489 7px solid;
17
+  border-top: $inactive-color 7px solid;
18 18
   font-size: 12px;
19 19
   font-family: 'Source Sans Pro', 'Helvetica', 'Arial', sans-serif;
20 20
 }
... ...
@@ -36,15 +38,28 @@ input {
36 36
   height: 30px;
37 37
   @include box-sizing(border-box);
38 38
 
39
-  &[type=submit] {
39
+  &[type=submit], &[type=button] {
40 40
     text-transform:lowercase;
41 41
     font-variant: small-caps;
42 42
     font-weight: bold;
43 43
     font-size: 18px;
44
+    margin-top: 16px;
45
+  }
46
+
47
+  &[type=submit] {
44 48
     background: $app-color;
45 49
     color: #f9f9f9;
46 50
     border: none 0px #000000;
47
-    margin-top: 16px;
51
+  }
52
+
53
+  &[type=button] {
54
+    background: $app-color;
55
+    color: #f9f9f9;
56
+    border: none 0px #000000;
57
+  }
58
+
59
+  &.inactive {
60
+    background-color: $inactive-color;
48 61
   }
49 62
 
50 63
   &:focus {
... ...
@@ -74,17 +89,6 @@ input {
74 74
   border-left: 0;
75 75
 }
76 76
 
77
-.alert {
78
-  font-size: 12px;
79
-  &.alert-notice {
80
-
81
-  }
82
-
83
-  &.alert-error {
84
-    color: $error;
85
-  }
86
-}
87
-
88 77
 .left {
89 78
   float: left;
90 79
 }
... ...
@@ -3,6 +3,26 @@
3 3
   padding: 10px 17px;
4 4
 }
5 5
 
6
+.chat-alert {
7
+  width: 400px;
8
+  left: 490px;
9
+  background: white;
10
+  margin-bottom: 58px;
11
+  padding: 0 11px;
12
+  position: absolute;
13
+  @include box-shadow(rgba(33,87,81, 0.4) 0 1px 2px);
14
+
15
+  h1 {
16
+    font-size: 1.4em;
17
+  }
18
+
19
+  .close {
20
+    font-size: 18px;
21
+    float: right;
22
+    padding: 11px 0 0;
23
+  }
24
+}
25
+
6 26
 .leftside {
7 27
   width: 23%;
8 28
 }
... ...
@@ -20,6 +40,12 @@
20 20
   &:hover, &:active {
21 21
     cursor: default;
22 22
   }
23
+
24
+  .wrap {
25
+    overflow-y: auto;
26
+    overflow-x: hidden;
27
+    width: 210px;
28
+  }
23 29
 }
24 30
 
25 31
 .my-info {
... ...
@@ -46,39 +72,77 @@
46 46
     font-size: 12px;
47 47
     color: $black;
48 48
     text-shadow: white 1px 1px;
49
+
50
+    .name {
51
+      overflow: hidden;
52
+      text-overflow: ellipsis;
53
+      white-space: nowrap;
54
+      display: inline-block;
55
+      max-width: 66%;
56
+      float: left;
57
+    }
58
+  }
59
+
60
+  h2 {
61
+    overflow: hidden;
62
+    text-overflow: ellipsis;
63
+    white-space: nowrap;
64
+    max-width: 66%;
49 65
   }
50 66
 
51 67
   h3 {
52 68
     font-size: 12px;
53 69
     color: $status-color;
54
-    margin-top: -3px;
70
+    margin-top: -2px;
55 71
 
56 72
     .rolldown {
57 73
       clear: both;
58
-      left: 67px;
59
-      position: absolute;
60
-      width: 67px;
74
+      left: 59px;
75
+      width: 72px;
61 76
       font-weight: normal;
62 77
       float: left;
78
+      z-index: 10;
79
+      margin: 2px;
80
+      position: absolute;
81
+      top: 60px;
82
+
63 83
       a {
64 84
         color: $status-color;
65 85
       }
66 86
 
67 87
       &.editing {
68 88
         background: white;
89
+        @include box-shadow(rgba(33,87,81, 0.4) 0 0 2px);
69 90
       }
70 91
 
71 92
       .active {
72 93
         font-weight: bold;
73 94
       }
95
+
96
+      div {
97
+        clear: both;
98
+      }
74 99
     }
75 100
 
76 101
     .drop-down {
77 102
       position: relative;
78
-      top: 1px;
79
-      left: 54px;
103
+      top: 2px;
80 104
       font-size: 13px;
81 105
     }
106
+
107
+    .logout {
108
+      left: 158px;
109
+      font-weight: normal;
110
+      float: left;
111
+      margin: 2px;
112
+      position: absolute;
113
+      top: 61px;
114
+      color: $status-color;
115
+
116
+      &:after {
117
+        padding-left: 4px;
118
+      }
119
+    }
82 120
   }
83 121
 
84 122
   h1 input, h2 input {
... ...
@@ -117,31 +181,64 @@
117 117
 
118 118
   .avatar {
119 119
     float: left;
120
-
121 120
     height: 32px;
122 121
     width: 32px;
123
-
124 122
     margin-right: 8px;
125 123
 
126
-    img {
127
-      height: 32px;
128
-
129
-      margin: 0 auto;
130
-      display: block;
131
-    }
132
-
133 124
     &.bigger {
134 125
       height: 46px;
135 126
       width: 46px;
127
+      position: relative;
136 128
 
137 129
       img {
138 130
         height: 46px;
131
+        max-width: 46px;
132
+      }
133
+
134
+      .change-avatar {
135
+        &:hover {
136
+          cursor: pointer;
137
+        }
138
+
139
+        .background {
140
+          position: absolute;
141
+          top: 0;
142
+          left: 0;
143
+          height: 46px;
144
+          width: 46px;
145
+          background: $inactive-color;
146
+          @include opacity(0.8);
147
+        }
148
+
149
+
150
+        p {
151
+          position: absolute;
152
+          top: 3px;
153
+          left: 4px;
154
+          color: $gray-light;
155
+          font-weight: bold;
156
+
157
+          &:hover {
158
+            text-decoration: underline;
159
+          }
160
+        }
139 161
       }
140 162
     }
163
+
164
+    img {
165
+      height: 32px;
166
+      border-radius: 5px;
167
+      margin: 0 auto;
168
+      display: block;
169
+    }
141 170
   }
142 171
 }
143 172
 
144 173
 .friends {
174
+  height: inherit;
175
+  //overflow-x: hidden;
176
+  //overflow-y: auto;
177
+
145 178
   .group-header {
146 179
     margin: 16px 3px 3px;
147 180
     font-size: 12px;
... ...
@@ -193,6 +290,7 @@
193 193
     padding: 8px 10px;
194 194
     background: white;
195 195
     border-right: 0 !important;
196
+    z-index: 1;
196 197
 
197 198
     li {
198 199
       background: white;
... ...
@@ -213,14 +311,22 @@
213 213
         background: $bg-roster;
214 214
       }
215 215
     }
216
+
217
+    .action {
218
+      background: $bg-roster;
219
+      margin-right: 0;
220
+    }
216 221
   }
217 222
 
218 223
   .action {
219
-    margin-right: 2px;
220
-    margin-top: -31px;
224
+    margin-right: 10px;
225
+    margin-top: -33px;
221 226
     float: right;
222 227
     width: 67px;
223 228
     color: #7e8b90;
229
+    position: relative;
230
+    width: auto;
231
+    background: #fbfbfb;
224 232
 
225 233
     .icon-bubble, .icon-bubbles, .icon-x-full, .icon-owner, .icon-make-owner {
226 234
       display: none;
... ...
@@ -230,6 +336,10 @@
230 230
       padding-right: 10px;
231 231
       padding-top: 1px;
232 232
 
233
+      &:hover {
234
+        cursor: pointer;
235
+      }
236
+
233 237
       &:before {
234 238
         margin-left: 6px;
235 239
       }
... ...
@@ -249,11 +359,23 @@
249 249
       }
250 250
     }
251 251
 
252
-    .icon-bubbles, .icon-x-full {
252
+    .icon-bubbles {
253 253
       span {
254 254
         left: 1px;
255 255
       }
256 256
     }
257
+
258
+    .icon-x-full {
259
+      span {
260
+        left: 7px;
261
+      }
262
+    }
263
+
264
+    .icon-x-full {
265
+      &:before {
266
+        margin-left: 9px;
267
+      }
268
+    }
257 269
   }
258 270
 }
259 271
 
... ...
@@ -264,7 +386,8 @@
264 264
 .status {
265 265
   font-size: 15px;
266 266
   position: relative;
267
-  top: 2px;
267
+  top: 1px;
268
+  margin-left: 5px;
268 269
 
269 270
   &.online, &.away, &.dnd, &.offline {
270 271
     font-size: 11px;
... ...
@@ -307,10 +430,10 @@
307 307
     color: $app-color;
308 308
     border: 1px solid #dde0e3;
309 309
     border-radius: 12px;
310
-    padding: 5px 4px 4px 5px;
310
+    padding: 4px 4px 4px 5px;
311 311
     margin-left: 0;
312 312
     width: 13px;
313
-    height: 13px;
313
+    height: 14px;
314 314
     background: white;
315 315
     display: inline-block;
316 316
   }
... ...
@@ -323,6 +446,7 @@
323 323
 .tabbar {
324 324
   display: inline-block;
325 325
   margin-bottom: -3px;
326
+  width: 750px;
326 327
 
327 328
   .tab {
328 329
     background: #f7f6f5;
... ...
@@ -374,6 +498,8 @@
374 374
   background: white;
375 375
   padding: 9px;
376 376
   min-height: 367px;
377
+  width: 730px;
378
+  position: relative;
377 379
 
378 380
   .event {
379 381
     color: $status-color;
... ...
@@ -390,7 +516,7 @@
390 390
   }
391 391
 
392 392
   .message-box {
393
-    width: 75%;
393
+    width: 580px;
394 394
     color: #393939;
395 395
     background: $bg-roster;
396 396
     border: 1px solid $gray-light;
... ...
@@ -400,6 +526,11 @@
400 400
     margin-left: 45px;
401 401
     position: relative;
402 402
 
403
+    &.me {
404
+      border-right: 4px solid $app-color;
405
+      width: 577px;
406
+    }
407
+
403 408
     &::after, &::before {
404 409
       content: '';
405 410
       display: block;
... ...
@@ -23,4 +23,15 @@ $form-wrapper-padding: 14px;
23 23
 
24 24
 .login-form-header {
25 25
   margin: 0 -1*$form-wrapper-padding;
26
+}
27
+
28
+.alert {
29
+  font-size: 12px;
30
+  &.alert-notice {
31
+
32
+  }
33
+
34
+  &.alert-error {
35
+    color: $error;
36
+  }
26 37
 }
27 38
\ No newline at end of file
... ...
@@ -6,7 +6,7 @@ class ApplicationController < ActionController::Base
6 6
 		if authenticated?
7 7
 			create_new_authentication()
8 8
 
9
-            unless controller_name == 'chat'
9
+            if controller_name != 'chat' and (controller_name != 'sessions' and action_name != 'destroy')
10 10
                 redirect_to chat_path
11 11
             end
12 12
 		else
... ...
@@ -35,6 +35,12 @@ class ApplicationController < ActionController::Base
35 35
         save_session(user_id, user_credentials)
36 36
     end
37 37
 
38
+    def remove_session
39
+        reset_session()
40
+        cookies.delete :iv
41
+        cookies.delete :key
42
+    end
43
+
38 44
     private
39 45
 
40 46
     def save_session(user_id, user_credentials)
... ...
@@ -44,6 +50,7 @@ class ApplicationController < ActionController::Base
44 44
 
45 45
         if user_credentials
46 46
             encrypted_pass = Security::encrypt(user_credentials[:password])
47
+            #TODO: pridat do sifry aj nieco z konfigov
47 48
             cookies[:key] = Security::cipher_key
48 49
             cookies[:iv]  = Security::cipher_iv
49 50
 
... ...
@@ -20,6 +20,9 @@ class SessionsController < ApplicationController
20 20
 	end
21 21
 
22 22
 	def destroy
23
-		self.remove_old_session(session[:token])
23
+        Token.remove_token(session[:token])
24
+        remove_session()
25
+
26
+        redirect_to root_path
24 27
 	end
25 28
 end
... ...
@@ -32,8 +32,12 @@ class WsController < WebsocketRails::BaseController
32 32
         chats = where_i_am_multichat_owner(client)
33 33
         chats.each do |chat_id|
34 34
             contacts = connection_store[:opened_chats][client][chat_id][:attendants]
35
-            #TODO: kontrola, ak ten clovek sa nenasiel v mutlichate - tak nic nerob
35
+
36
+            size_before = contacts.size
36 37
             contacts -= [somebody_to_kick.to_s]
38
+            size_after = contacts.size
39
+
40
+            return if size_after == size_before
37 41
 
38 42
             if contacts.empty?
39 43
                 destroy_multichat(client.jid, chat_id)
... ...
@@ -1,6 +1,7 @@
1 1
 class WsRosterController < WsController
2 2
     require 'xmpp4r/roster'
3 3
     require 'xmpp4r/vcard'
4
+    require 'RMagick'
4 5
 
5 6
     def initialize
6 7
         super
... ...
@@ -84,7 +85,7 @@ class WsRosterController < WsController
84 84
 
85 85
             roster.items.each do |jid, contact|
86 86
                 Thread.new do
87
-                    vcard = get_vcard(client, jid.to_s)
87
+                    vcard = get_vcard_info(client, jid.to_s)
88 88
                     send_message 'app.roster.vcard', jid: jid.to_s, vcard: vcard
89 89
                 end
90 90
             end
... ...
@@ -99,6 +100,7 @@ class WsRosterController < WsController
99 99
         connection_store[:rosters].each do |roster|
100 100
             start_polling_subscription(roster)
101 101
             start_polling_presence(roster)
102
+            start_polling_friend_requests(roster)
102 103
         end
103 104
     end
104 105
 
... ...
@@ -119,16 +121,24 @@ class WsRosterController < WsController
119 119
         jid = message[:jid]
120 120
         client_jid = message[:client]
121 121
 
122
-        found_roster_item = nil
122
+        found_roster_items = []
123 123
         connection_store[:rosters].each do |roster|
124 124
             roster_item = roster.find(jid)
125
-            if roster_item.first[1] && connection_store[:link_roster_client][roster].jid.strip.to_s == client_jid
126
-                found_roster_item = roster_item.first[1]
127
-                break
125
+            contact_obj = roster_item.first[1]
126
+
127
+            if contact_obj
128
+                if client_jid && connection_store[:link_roster_client][roster].jid.strip.to_s == client_jid
129
+                    found_roster_items = [ contact_obj ]
130
+                    break
131
+                else
132
+                    found_roster_items.push(contact_obj)
133
+                end
128 134
             end
129 135
         end
130 136
 
131
-        found_roster_item && found_roster_item.remove()
137
+        found_roster_items.each do |contact|
138
+            contact.remove()
139
+        end
132 140
     end
133 141
 
134 142
     ##
... ...
@@ -139,7 +149,7 @@ class WsRosterController < WsController
139 139
         jid = presence = ''
140 140
 
141 141
         connection_store[:clients].each do |client|
142
-            vcard = get_vcard(client)
142
+            vcard = get_vcard_info(client)
143 143
             jid = client.jid.strip.to_s
144 144
             presence = uniform_presence(connection_store[:my_presences][client].show)
145 145
         end
... ...
@@ -168,9 +178,33 @@ class WsRosterController < WsController
168 168
 
169 169
     def me_update_vcard
170 170
         connection_store[:clients].each do |client|
171
-            my_vcard = Jabber::Vcard::Helper.get(client)
172
-            my_vcard['FN'] = my_vcard['NICKNAME'] = message[:name]
173
-            Jabber::Vcard::Helper.set(client, my_vcard)
171
+            Thread.new do