Browse code

Merge branch 'sending-messages'

Cinan Rakosnik authored on 06/05/2013 at 14:02:22
Showing 24 changed files
... ...
@@ -16,14 +16,14 @@ this.App =
16 16
       App._dispatcher = new WebSocketRails('www.xmpp.dev:3000/websocket')
17 17
       App._dispatcher.on_open = =>
18 18
         @_setupBackboneComponents()
19
-        @_bindEvents()
19
+        @_bindListeners()
20 20
         callback?()
21 21
 
22 22
     trigger: (options) ->
23 23
       _.defaults(options, {data: {} })
24 24
       App._dispatcher.trigger(options.event, options.data, options.success, options.error)
25 25
 
26
-    initRoster: ->
26
+    initRoster: (callback) ->
27 27
       App.Com.trigger(
28 28
         event: 'app.roster.initRoster'
29 29
         success: (data) ->
... ...
@@ -35,6 +35,8 @@ this.App =
35 35
             )
36 36
             App.Collections.contacts.add(newContact, merge: true)
37 37
           )
38
+
39
+          callback?()
38 40
       )
39 41
 
40 42
     startFetchingVcards: ->
... ...
@@ -43,6 +45,12 @@ this.App =
43 43
     startPollingRoster: ->
44 44
       App.Com.trigger(event: 'app.roster.startPolling')
45 45
 
46
+    startPollingMessages: ->
47
+      App.Com.trigger(event: 'app.chat.startPollingMessages')
48
+
49
+    whoUseThisApp: ->
50
+      App.Com.trigger(event: 'app.roster.whoUseThisApp')
51
+
46 52
     setPresence: ->
47 53
       App.Com.trigger(event: 'app.roster.setPresence')
48 54
 
... ...
@@ -56,6 +64,39 @@ this.App =
56 56
         )
57 57
       )
58 58
 
59
+    sendMessage: (message, to, from, callbackOk, callbackFail) ->
60
+      App.Com.trigger(event: 'app.chat.sendMessage', data: {message: message, to: to, from: from}, success: callbackOk, error: callbackFail)
61
+
62
+    sendMultiMessage: (message, chatId, from, callbackOk, callbackFail) ->
63
+      App.Com.trigger(event: 'app.chat.sendMessage', data: {message: message, chatId: chatId, from: from}, success: callbackOk, error: callbackFail)
64
+
65
+    openNewMultiChat: (chatOwner, attendant, chat) ->
66
+      @trigger(event: 'app.chat.newMultiChat', data: {chatOwner: chatOwner.get('jid')}, success: (response) =>
67
+        chat.setChatId(response.id)
68
+        chat.appendWithWhom(attendant)
69
+        @trigger(event: 'app.chat.addToMultiChat', data: {chatOwner: chatOwner.get('jid'), chatId: response.id, jid: attendant.get('jid')}, success: ->
70
+          Backbone.Events.trigger('openChat', chat)
71
+        )
72
+      )
73
+
74
+    inviteToChat: (chat, toAdd, me) ->
75
+      @trigger(event: 'app.chat.addToMultiChat', data: {chatOwner: me, chatId: chat.get('chatId'), jid: toAdd.get('jid')}, success: ->
76
+        chat.appendWithWhom(toAdd)
77
+        Backbone.Events.trigger('openChat', chat)
78
+      )
79
+
80
+    iClosedMultichat: (chatId, jid) ->
81
+      @trigger(event: 'app.chat.iClosedMultichat', data: {chatId: chatId, me: jid})
82
+
83
+    kickFromMultichat: (chatId, me, jidToKick) ->
84
+      @trigger(event: 'app.chat.kickFromMultichat', data: {chatId: chatId, me: me, kick: jidToKick})
85
+
86
+    syncMultiChatContacts: (me, chatId) ->
87
+      @trigger(event: 'app.chat.syncMultiChatContacts', data: {me: me, chatId: chatId})
88
+
89
+    syncedContacts: () ->
90
+
91
+
59 92
     updateMyStatus: (message, state)->
60 93
       App.Com.trigger(event: 'app.roster.updateMyStatus', data: {message: message, state: state})
61 94
 
... ...
@@ -68,7 +109,7 @@ this.App =
68 68
 
69 69
       App.Views.tabbar = new Xmpp.Views.Tabbar.TabbarView()
70 70
 
71
-    _bindEvents: ->
71
+    _bindListeners: ->
72 72
       App._dispatcher.bind('app.roster.statusChanged', (result) ->
73 73
         App.debug 'change contact state'
74 74
         App.Collections.contacts.updateStatus(result)
... ...
@@ -84,6 +125,91 @@ this.App =
84 84
         App.Collections.contacts.subscriptionChanged(subscription)
85 85
       )
86 86
 
87
+      App._dispatcher.bind('app.roster.using_this_app', (person) ->
88
+        App.debug ['is using this app', person.jid]
89
+        contact = App.Collections.contacts.get(person.jid)
90
+        if (contact)
91
+          contact.setUsingMyApp()
92
+      )
93
+
94
+      App._dispatcher.bind('app.chat.destroyMultichat', (data) ->
95
+        chatId = data.chat_id
96
+        chat = App.Collections.chats.findById(chatId)
97
+        if chat
98
+          App.debug 'destroying multicat from outside'
99
+          Backbone.Events.trigger('closeChat', chat, true)
100
+      )
101
+
102
+      App._dispatcher.bind('app.chat.importChat', (chatData) ->
103
+        chatId = chatData.chat_id
104
+        chat = App.Collections.chats.findById(chatId)
105
+
106
+        if not chat
107
+          chat = new Xmpp.Models.Chat(
108
+            chatId: chatId,
109
+            isMultiChat: true
110
+          )
111
+
112
+          App.Collections.chats.add(chat);
113
+
114
+        chat.syncContacts(chatData.contacts, chatData.owner)
115
+        Backbone.Events.trigger('openChat', chat)
116
+      )
117
+
118
+      App._dispatcher.bind('app.chat.updateSyncedContacts', (syncData) ->
119
+        App.debug ['new multichat contacts arrived', syncData.contacts]
120
+
121
+        chat = App.Collections.chats.findById(syncData.chat_id)
122
+        if chat
123
+          chat.syncContacts(syncData.contacts, syncData.owner)
124
+          Backbone.Events.trigger('openChat', chat)
125
+      )
126
+
127
+      App._dispatcher.bind('app.chat.messageReceived', (result) ->
128
+        App.debug ['message received', result]
129
+
130
+        if (result.chat_id)
131
+          _.each(App.Collections.chats.models, (chat) ->
132
+            App.debug chat.get('chatId')
133
+          )
134
+          chat = App.Collections.chats.findById(result.chat_id)
135
+
136
+          Backbone.Events.trigger('openChat', chat)
137
+
138
+          tab = _.find(App.Views.tabbar.tabs, (tab) ->
139
+            tab.getChatId() == result.chat_id
140
+          )
141
+
142
+          tab.showChat()
143
+          contact = App.Collections.contacts.findByJid(result.from)
144
+          tab.chatWindow.appendMessage(contact, new Date(), result.message)
145
+        else
146
+          #TODO: prepisat tuto hrozu
147
+
148
+
149
+          contact = App.Collections.contacts.findByJid(result.from)
150
+
151
+          if contact
152
+            contactView = App.Collections.contacts.friendsList.hasContact(contact) ||
153
+              App.Collections.contacts.activeList.hasContact(contact)
154
+          else
155
+            contact = new Xmpp.Models.Contact(
156
+              id: result.from
157
+              jid: result.from
158
+              belongsTo: [result.to]
159
+            )
160
+            App.Collections.contacts.add(contact, merge: true)
161
+            contactView = App.Collections.contacts.friendsList.hasContact(contact)
162
+
163
+          contactView.startChat()
164
+
165
+          tab = _.find(App.Views.tabbar.tabs, (tab) ->
166
+            tab.hasParticipants(App.Models.me, contact)
167
+          )
168
+
169
+          tab.chatWindow.appendMessage(contact, new Date(), result.message)
170
+      )
171
+
87 172
   Models:
88 173
     me: null
89 174
 
... ...
@@ -11,10 +11,13 @@
11 11
 #= require chat
12 12
 
13 13
 App.Com.connect(->
14
-  App.Com.initRoster()
15
-  App.Com.startFetchingVcards()
16
-  App.Com.startPollingRoster()
17
-  App.Com.setPresence()
14
+  App.Com.initRoster(->
15
+    App.Com.startFetchingVcards()
16
+    App.Com.startPollingRoster()
17
+    App.Com.whoUseThisApp()
18
+    App.Com.setPresence()
18 19
 
19
-  App.Com.getMe()
20
+    App.Com.getMe()
21
+    App.Com.startPollingMessages()
22
+  )
20 23
 )
... ...
@@ -4,24 +4,102 @@ class Xmpp.Models.Chat extends Xmpp.Models.Model
4 4
   defaults:
5 5
     who: null
6 6
     withWhom: null
7
+    chatId: null
8
+    isMultiChat: false
7 9
 
8 10
   initialize: ->
9 11
     _.bindAll(this)
10 12
 
13
+    if (@get('isMultiChat'))
14
+      @set('withWhom', [])
15
+
16
+  appendWithWhom: (newPerson) ->
17
+    allWithWhom = @get('withWhom');
18
+
19
+    exists = _.find(allWithWhom, (withWhom) ->
20
+      withWhom == newPerson
21
+    )?
22
+
23
+    if not exists
24
+      allWithWhom.push(newPerson)
25
+
26
+  setChatId: (id) ->
27
+    App.debug ['multichat has id', id]
28
+    @set('chatId', id)
29
+
30
+  isMeOwner: ->
31
+    @get('who') == App.Models.me
32
+
33
+  isAttending: (contact) ->
34
+    attendants = @get('withWhom')
35
+    if _.isArray(attendants)
36
+      return _.find(attendants, (somebody) -> somebody == contact)
37
+    else
38
+      return contact == attendants
39
+
40
+  syncContacts: (contacts, owner) ->
41
+    contactsWithoutMe = _.filter(contacts, (jid) ->
42
+      jid != App.Models.me.get('jid')
43
+    )
44
+
45
+    if owner and owner != App.Models.me.get('jid')
46
+      contactsWithoutMe = contactsWithoutMe.concat([owner])
47
+
48
+    attendants = _.map(contactsWithoutMe, (jid) =>
49
+      contact = App.Collections.contacts.findByJid(jid)
50
+      if not contact
51
+        newTempContact = Xmpp.Collections.ChatsCollection.createTempContact(jid)
52
+        App.Collections.contacts.add(newTempContact)
53
+        contact = newTempContact
54
+
55
+      contact
56
+    )
57
+
58
+    @set('withWhom', attendants);
59
+
60
+    if owner
61
+      ownerContact = App.Collections.contacts.findByJid(owner)
62
+      if not ownerContact
63
+        ownerContact = Xmpp.Collections.ChatsCollection.createTempContact(owner)
64
+        App.Collections.contacts.add(ownerContact)
65
+
66
+      @set('who', ownerContact)
67
+
68
+    App.debug ['importing contacts to chat', this, this.get('chatId')]
69
+
70
+
11 71
 class Xmpp.Collections.ChatsCollection extends Backbone.Collection
12 72
   model: Xmpp.Models.Chat
73
+  activeChat: null
13 74
 
14 75
   initialize: ->
15 76
     _.bindAll(this)
16 77
 
17
-    Backbone.Events.on('closeChat', (tab, chat) =>
78
+    Backbone.Events.on('closeChat', (chat, ignoreCheckIfMultichat) =>
18 79
       @removeChat(chat)
80
+      if not ignoreCheckIfMultichat
81
+        if (chat.get('isMultiChat'))
82
+          App.Com.iClosedMultichat(chat.get('chatId'), App.Models.me.get('jid'))
83
+    )
84
+
85
+    Backbone.Events.on('openChat', (chat) =>
86
+      @activeChat = chat
19 87
     )
20 88
 
21 89
   find: (who, withWhom) ->
22 90
     _.find(@models, (chat) ->
23
-      chat.get('who') == who && chat.get('withWhom') == withWhom
91
+      chat.get('who') == who and chat.get('withWhom') == withWhom and not chat.get('isMultiChat')
92
+    )
93
+
94
+  findById: (id) ->
95
+    _.find(@models, (chat) ->
96
+      chat.get('chatId') == id
24 97
     )
25 98
 
26 99
   removeChat: (chat) ->
27
-    @models = _.without(@openedChats, chat)
28 100
\ No newline at end of file
101
+    @activeChat = null
102
+    @models = _.without(@openedChats, chat)
103
+
104
+  createTempContact: (jid) ->
105
+    newTempContact = new Xmpp.Models.Contact(jid: jid, belongsTo: [App.Models.me])
106
+    newTempContact
29 107
\ No newline at end of file
... ...
@@ -6,8 +6,9 @@ class Xmpp.Models.Contact extends Xmpp.Models.Model
6 6
     name: ''
7 7
     status: 'offline'
8 8
     message: ''
9
-    avatar: ''
9
+    avatar: 'assets/avatar.png'
10 10
     belongsTo: []
11
+    usingMyApp: false
11 12
 
12 13
   initialize: ->
13 14
     _.bindAll(this)
... ...
@@ -15,9 +16,14 @@ class Xmpp.Models.Contact extends Xmpp.Models.Model
15 15
     if ! @get('name')
16 16
       @set(name: @get('jid'))
17 17
 
18
+    @set('id', @get('jid'))
19
+
18 20
   firstname: ->
19 21
     @get('name').split(' ')[0]
20 22
 
23
+  setUsingMyApp: ->
24
+    @set('usingMyApp', true)
25
+
21 26
 class Xmpp.Collections.ContactsCollection extends Backbone.Collection
22 27
   model: Xmpp.Models.Contact
23 28
 
... ...
@@ -35,11 +41,12 @@ class Xmpp.Collections.ContactsCollection extends Backbone.Collection
35 35
 
36 36
     Backbone.Events.on('openChat', (chat) =>
37 37
       @moveToInactiveList('all')
38
+#      App.debug chat
38 39
       @moveToActiveList(chat.get('withWhom'))
39 40
     )
40 41
 
41
-    Backbone.Events.on('closeChat', (tab, chat) =>
42
-      @moveToInactiveList(chat.get('withWhom'))
42
+    Backbone.Events.on('closeChat', (chat) =>
43
+      @moveToInactiveList('all')
43 44
     )
44 45
 
45 46
   appendContact: (contact) ->
... ...
@@ -58,8 +65,13 @@ class Xmpp.Collections.ContactsCollection extends Backbone.Collection
58 58
     else
59 59
       @add(contact)
60 60
 
61
-  moveToActiveList: (contact) ->
62
-    @_switchContactBelongingList(@get(contact), @friendsList, @activeList) && @activeList.reOrder()
61
+  moveToActiveList: (contacts) ->
62
+    App.debug ['move to active list', contacts];
63
+    contacts = [contacts] unless _.isArray(contacts)
64
+
65
+    _.each(contacts, (contact) =>
66
+      @_switchContactBelongingList(@get(contact), @friendsList, @activeList) && @activeList.reOrder()
67
+    )
63 68
 
64 69
   moveToInactiveList: (contact) ->
65 70
     if contact == 'all'
... ...
@@ -100,10 +112,14 @@ class Xmpp.Collections.ContactsCollection extends Backbone.Collection
100 100
 
101 101
     # ignore 'unsubscribe' type of action
102 102
 
103
+  # toto sem asi nepatri, ale asi do views. Nic to v skutocnosti nerobi s modelmi.
103 104
   filter: (searchTerm) ->
104 105
     @friendsList.filter(searchTerm)
105 106
     @activeList.filter(searchTerm)
106 107
 
108
+  findByJid: (jid) ->
109
+    @get(jid)
110
+
107 111
   _switchContactBelongingList: (contact, fromList, toList) ->
108 112
     if !contact?
109 113
       false
... ...
@@ -11,5 +11,16 @@ class Xmpp.Models.Me extends Xmpp.Models.Contact
11 11
   initialize: ->
12 12
     _.bindAll(this)
13 13
 
14
+    if ! @get('name')
15
+      @set(name: @get('jid'))
16
+
14 17
     @view = new Xmpp.Views.Contacts.MeView(model: this)
15
-    @view.render()
16 18
\ No newline at end of file
19
+    @view.render()
20
+
21
+  sendMessage: (message, chatId, to, callbackOk, callbackFail) ->
22
+    App.debug ['sending from:', @get('jid'), message]
23
+
24
+    if (to)
25
+      App.Com.sendMessage(message, to, @get('jid'), callbackOk, callbackFail)
26
+    else
27
+      App.Com.sendMultiMessage(message, chatId, @get('jid'), callbackOk, callbackFail)
17 28
\ No newline at end of file
... ...
@@ -1,9 +1,6 @@
1
-.message-box
2
-  .user
3
-    .avatar
4
-      %img{src: 'assets/avatar.png', alt: I18n.t("chat.roster.avatar_alt")}
5
-    -#todo: user name missing in design
6
-  .date 3 hours ago
7
-  %p Lorem ipsum dolor sit amet enim. Etiam ullamcorper. Supendisse a pellentesque dui, non felis. Maecenas malesuada elit lectus felis, malesuada ultricies. Curabitur et ligula...
8
-  -#%hr
9
-  %p Lorem ipsum dolor sit amet enim. Etiam ullamcorper. Supendisse a pellentesque dui, non felis. Maecenas malesuada elit lectus felis, malesuada ultricies. Curabitur et ligula...
10 1
\ No newline at end of file
2
+.user
3
+  .avatar
4
+    %img{src: "#{@avatar}", alt: "#{@username}"}
5
+  -#todo: user name missing in design
6
+.date #{@date}
7
+%p #{@message}
11 8
\ No newline at end of file
... ...
@@ -1,3 +1,3 @@
1
-.message-writer#msg-writer
2
-  %input{ type: "text", placeholder: "Type your message here" }
3
-  %input{ type: "submit", value: "Send" }
4 1
\ No newline at end of file
2
+%form.message-writer#msg-writer
3
+  %input{ type: 'text', placeholder: "#{I18n.t('chat.messages.new_msg_placeholder')}" }
4
+  %input{ type: 'submit', value: "#{I18n.t('chat.messages.send')}" }
5 5
\ No newline at end of file
... ...
@@ -1,4 +1,5 @@
1 1
 .conversation.clear.border
2
-  #{@date}
2
+  -for view in @history
3
+    #{view}
3 4
   -if @showWriter
4 5
     != JST['backbone/templates/chat/message_writer']()
5 6
\ No newline at end of file
... ...
@@ -5,8 +5,18 @@
5 5
   %span.status.icon-record{class: "#{@status}"}
6 6
 %h2 #{@message}  
7 7
 %span.action
8
-  -#%span.icon-bubbles
8
+  -#-if @usingMyApp
9
+  %span.icon-bubbles.multichat
10
+    -# ak nemam otvoreny multichat
11
+    %span multi
12
+  %span.icon-bubbles.invite
13
+    -# ak nie je v multichate a mam otvoreny multichat a som admin toho multichatu
9 14
     %span invite
15
+  %span.icon-x-full.kick
16
+    -# ak som admin otvoreneho multichatu
17
+    %span kick
18
+  -#%span.icon-x-full.remove
19
+    %span remove
10 20
 
11 21
 -#  .avatar
12 22
 -#    %img{src: 'assets/avatar.png', alt: t("chat.avatar_alt")}
... ...
@@ -14,9 +24,6 @@
14 14
 -#    Bob Awesome
15 15
 -#    %span.status.dnd.icon-record
16 16
 -#  %h2 sales manager
17
--#  %span.action
18
--#    %span.icon-x-full
19
--#      %span kick
20 17
 -#%li.clear.user
21 18
 -#  .avatar
22 19
 -#    %img{src: 'assets/avatar.png', alt: t("chat.avatar_alt")}
... ...
@@ -1,4 +1,9 @@
1
-%a{ href: "#" }
2
-  %div{ title: "#{@withWhom} #{I18n.t('chat.tabbar.and')} #{@who}"}
3
-    #{@withWhom} #{I18n.t('chat.tabbar.and')} #{@who}
4
-  %span.right.icon.icon-x.js-close
5 1
\ No newline at end of file
2
+%a{ href: '#' }
3
+  -if @multiChat
4
+    %div{ title: 'multichat'}
5
+      MultiChat
6
+    %span.right.icon.icon-x.js-close
7
+  -else
8
+    %div{ title: "#{@withWhom} #{I18n.t('chat.tabbar.and')} #{@who}"}
9
+      #{@withWhom} #{I18n.t('chat.tabbar.and')} #{@who}
10
+    %span.right.icon.icon-x.js-close
6 11
\ No newline at end of file
7 12
new file mode 100644
... ...
@@ -0,0 +1,32 @@
0
+Xmpp.Views.Chat ||= {}
1
+
2
+class Xmpp.Views.Chat.MessageView extends Backbone.View
3
+  template: JST["backbone/templates/chat/message"]
4
+  className: 'message-box'
5
+
6
+  initialize: (parts) ->
7
+    _.bindAll(this)
8
+
9
+    @user = parts.user
10
+    @dateSent = parts.date
11
+    @message = parts.message
12
+
13
+  render: ->
14
+    $(@el).html(@template(username: @user.get('name'), avatar: @user.get('avatar'), date: @formatTime(), message: @message))
15
+    return this
16
+
17
+  formatTime:  ->
18
+    nowDate = new Date()
19
+    if (nowDate.getDate() != @dateSent.getDate() &&
20
+        nowDate.getMonth() != @dateSent.getMonth() &&
21
+        nowDate.getFullYear() != @dateSent.getFullYear())
22
+      return @dateSent.getFullYear() + '/' + @_padDate((@dateSent.getMonth()+1)) + '/' + @_padDate(@dateSent.getDate()) +
23
+        ' @ ' + @_padDate(@dateSent.getHours()) + ':' + @_padDate(@dateSent.getMinutes())
24
+
25
+    return @_padDate(@dateSent.getHours()) + ':' + @_padDate(@dateSent.getMinutes())
26
+
27
+  _padDate: (number) ->
28
+    if number < 10
29
+      '0' + number
30
+    else
31
+      number
0 32
\ No newline at end of file
... ...
@@ -3,27 +3,81 @@ Xmpp.Views.Chat ||= {}
3 3
 class Xmpp.Views.Chat.WindowView extends Backbone.View
4 4
   template: JST["backbone/templates/chat/window"]
5 5
   el: $('#conversation-js')
6
+  inputSelector: 'input[type=text]'
7
+
8
+  maxHistoryLength: 20
9
+
10
+  events:
11
+    'submit #msg-writer': (e) -> @sendMessage(e)
6 12
 
7 13
   initialize: () ->
8 14
     _.bindAll(this)
9 15
     @tab = @attributes['tab']
10
-    @date = new Date() #pre testovacie ucely
16
+    @historyStack = []
11 17
 
12 18
   render: ->
13
-    $(@el).html(@template(date: @date.getTime(), showWriter: true))
19
+    historyStackHtml = _.map(@historyStack, (view) ->
20
+      view.render().el.outerHTML
21
+    )
22
+    $(@el).html(@template(showWriter: true, history: historyStackHtml))
14 23
     @show()
15 24
     return this
16 25
 
17 26
   hide: ->
18 27
     $(@el).addClass('hidden')
28
+    @undelegateEvents()
19 29
     this
20 30
 
21 31
   show: ->
22 32
     $(@el).removeClass('hidden')
33
+    @delegateEvents()
34
+    $(@el).find(@inputSelector).focus()
23 35
     this
24 36
 
25 37
   remove: ->
26 38
     @undelegateEvents()
27 39
     $(@el).empty()
28
-    # @stopListening() TODO: po upgrade na 1.0 uz bude pristupne
29
-    this
30 40
\ No newline at end of file
41
+    @stopListening()
42
+    _.each(@historyStack, (item)->
43
+      item.remove()
44
+    )
45
+    return this
46
+
47
+  sendMessage: (e) ->
48
+    e.preventDefault()
49
+    $this = $(e.currentTarget)
50
+    input = $this.find(@inputSelector)
51
+    message = input.val()
52
+    input.val('').focus()
53
+
54
+    if (message.trim())
55
+      chatId = @tab.getChatId()
56
+      attendant = if not chatId then @tab.getAttendant().get('jid') else null
57
+
58
+      App.Models.me.sendMessage(message, chatId, attendant
59
+      , (message) =>
60
+        @sendSuccess(@tab.getOwner(), message)
61
+      , (message) =>
62
+        @sendFail(message))
63
+
64
+  sendSuccess: (me, msg) ->
65
+    @appendMessage(me, new Date(), msg)
66
+
67
+  sendFail: ->
68
+    @appendEvent(I18n.t('chat.window.sendFailed'), false)
69
+
70
+  appendEvent: (msg, logMe) ->
71
+    App.debug ['append event', msg]
72
+#    @log(eventView) if logMe == true
73
+
74
+  appendMessage: (user, date, msg) ->
75
+    messageView = new Xmpp.Views.Chat.MessageView(user: user, date: date, message: msg)
76
+    $(@el).find('#msg-writer').before(messageView.render().el)
77
+    @log(messageView)
78
+
79
+  log: (view) ->
80
+    if (@historyStack.length + 1 >= @maxHistoryLength)
81
+      @historyStack = @historyStack.slice(@historyStack.length - @maxHistoryLength + 1)
82
+
83
+    @historyStack.push(view)
84
+#    App.debug @historyStack
... ...
@@ -6,7 +6,12 @@ class Xmpp.Views.Contacts.ContactView extends Backbone.View
6 6
   className: 'clear user'
7 7
 
8 8
   events:
9
+    'click .invite': 'inviteToMultichat'
10
+    'click .kick': 'kickFromMultiChat'
11
+    'click .multichat': 'newMultiChat'
9 12
     click: 'startChat'
13
+    mouseover: 'showIcons'
14
+    mouseleave: 'hideIcons'
10 15
 
11 16
   initialize: () ->
12 17
     _.bindAll(this)
... ...
@@ -36,9 +41,6 @@ class Xmpp.Views.Contacts.ContactView extends Backbone.View
36 36
     @parentList.activeGroup == true
37 37
 
38 38
   startChat: ->
39
-    if @belongsToActiveList()
40
-      return
41
-
42 39
     who      = App.Models.me
43 40
     withWhom = @model
44 41
 
... ...
@@ -52,3 +54,54 @@ class Xmpp.Views.Contacts.ContactView extends Backbone.View
52 52
 
53 53
     Backbone.Events.trigger('openChat', chat)
54 54
 
55
+  inviteToMultichat: (e) ->
56
+    e.stopPropagation();
57
+    App.debug 'iniviting to multichat'
58
+    multichat = App.Collections.chats.activeChat
59
+    if not multichat or not multichat.get('isMultiChat')
60
+      return
61
+
62
+    App.Com.inviteToChat(multichat, @model, App.Models.me.get('jid'))
63
+
64
+  newMultiChat: (e) ->
65
+    e.stopPropagation();
66
+    App.debug 'new multichat'
67
+
68
+    newChat = new Xmpp.Models.Chat(isMultiChat: true)
69
+    newChat.set('who', App.Models.me)
70
+
71
+    App.Collections.chats.add(newChat)
72
+    App.Com.openNewMultiChat(App.Models.me, @model, newChat)
73
+
74
+  kickFromMultiChat: (e) ->
75
+    e.stopPropagation();
76
+    App.debug 'kick him from multichat!'
77
+
78
+    multichat = App.Collections.chats.activeChat
79
+    if not multichat or not multichat.get('isMultiChat')
80
+      return
81
+
82
+    App.Com.kickFromMultichat(multichat.get('chatId'), App.Models.me.get('jid'), @model.get('jid'))
83
+    App.Collections.contacts.moveToInactiveList(@model)
84
+
85
+  openChatById: (chatId) ->
86
+    App.Collections.chats.findById(chatId)
87
+
88
+  showIcons: ->
89
+    activeChat = App.Collections.chats.activeChat
90
+
91
+    $(@el).find('.action .multichat, .action .invite, .action .kick').hide()
92
+
93
+    if (@model.get('usingMyApp'))
94
+      if not activeChat or not activeChat.get('isMultiChat')
95
+        $(@el).find('.action .multichat').show()
96
+      else if activeChat.get('isMultiChat') and activeChat.isMeOwner() and not activeChat.isAttending(@model)
97
+        $(@el).find('.action .invite').show()
98
+      else if activeChat.get('isMultiChat') and activeChat.isMeOwner()
99
+        $(@el).find('.action .kick').show()
100
+
101
+
102
+    $(@el).find('.action').show()
103
+
104
+  hideIcons: ->
105
+    $(@el).find('.action').hide()
55 106
\ No newline at end of file
... ...
@@ -6,13 +6,21 @@ class Xmpp.Views.Tabbar.TabView extends Backbone.View
6 6
   className: 'tab'
7 7
 
8 8
   events:
9
-    'click .js-close': 'closeChat'
9
+    'click .js-close': 'fireCloseChatTrigger'
10 10
     'click a div': 'switchChatWindow'
11 11
 
12 12
   initialize: () ->
13 13
     _.bindAll(this)
14 14
     @active = false
15 15
 
16
+    Backbone.Events.on('closeChat', (chat) =>
17
+      if chat == @model
18
+        @destroy()
19
+    )
20
+
21
+  fireCloseChatTrigger: ->
22
+    Backbone.Events.trigger('closeChat', @model)
23
+
16 24
   setActive: ->
17 25
     $(@el).addClass('active')
18 26
     @active = true
... ...
@@ -24,10 +32,17 @@ class Xmpp.Views.Tabbar.TabView extends Backbone.View
24 24
     @render()
25 25
 
26 26
   render: ->
27
-    $(@el).html(@template(
28
-      who:      @model.get('who').firstname(),
29
-      withWhom: @model.get('withWhom').firstname()
30
-    ))
27
+    if (@model.get('isMultiChat'))
28
+      $(@el).html(@template(
29
+        who:       @model.get('who').firstname(),
30
+        multiChat: true
31
+      ))
32
+    else
33
+      $(@el).html(@template(
34
+        who:       @model.get('who').firstname(),
35
+        withWhom:  @model.get('withWhom').firstname(),
36
+        multiChat: false
37
+      ))
31 38
     return this
32 39
 
33 40
   showChat: ->
... ...
@@ -49,12 +64,17 @@ class Xmpp.Views.Tabbar.TabView extends Backbone.View
49 49
     if @chatWindow
50 50
       @chatWindow.hide();
51 51
 
52
-  closeChat: ->
53
-    @destroy()
54
-    Backbone.Events.trigger('closeChat', this, @model)
55
-
56 52
   hasParticipants: (who, withWhom) ->
57 53
     @model && @model.get('who') == who && @model.get('withWhom') == withWhom
58 54
 
59 55
   switchChatWindow: ->
60
-    Backbone.Events.trigger('openChat', @model)
61 56
\ No newline at end of file
57
+    Backbone.Events.trigger('openChat', @model)
58
+
59
+  getOwner: ->
60
+    @model.get('who')
61
+
62
+  getChatId: ->
63
+    @model.get('chatId')
64
+
65
+  getAttendant: ->
66
+    @model.get('withWhom')
62 67
\ No newline at end of file
... ...
@@ -11,15 +11,13 @@ class Xmpp.Views.Tabbar.TabbarView extends Backbone.View
11 11
       @hideCurrentChatWindow()
12 12
       @addOrSelect(chat)
13 13
     )
14
-    Backbone.Events.on('closeChat', (tab, chatModel) =>
15
-      @removeTab(tab)
16
-
17
-      if tab == @activeTab
18
-        firstTab = @selectFirstTab()
19
-        if firstTab
20
-          App.debug 'fire openChat'
21
-          Backbone.Events.trigger('openChat', firstTab.model)
22
-        #TODO: vybrat naposledy pouzity tab
14
+
15
+    Backbone.Events.on('closeChat', (chat) =>
16
+      tab = _.find(@tabs, (_tab) =>
17
+        _tab.model == chat
18
+      )
19
+
20
+      @closeTab(tab) if tab
23 21
     )
24 22
 
25 23
     @tabs = []
... ...
@@ -52,6 +50,16 @@ class Xmpp.Views.Tabbar.TabbarView extends Backbone.View
52 52
       App.debug 'zakryvam chat window'
53 53
       @activeTab.hideChat()
54 54
 
55
+  closeTab: (tab) ->
56
+    @removeTab(tab)
57
+
58
+    if tab == @activeTab
59
+      firstTab = @selectFirstTab()
60
+      if firstTab
61
+        App.debug 'fire openChat'
62
+        Backbone.Events.trigger('openChat', firstTab.model)
63
+    #TODO: vybrat naposledy pouzity tab
64
+
55 65
   removeTab: (tab) ->
56 66
     @tabs = _.without(@tabs, tab)
57 67
 
... ...
@@ -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/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","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"}}}};
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","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","date":{"now":"A few moments ago"}}}}};
4 4
\ No newline at end of file
... ...
@@ -201,6 +201,7 @@
201 201
   }
202 202
 
203 203
   .action {
204
+    display: none;
204 205
     margin-right: 2px;
205 206
     margin-top: -31px;
206 207
     float: right;
207 208
new file mode 100644
... ...
@@ -0,0 +1,175 @@
0
+class WsChatController < WsController
1
+
2
+    def start_polling_messages
3
+        connection_store[:clients].each do |client|
4
+            client.add_message_callback do |message|
5
+                from = message.from.strip.to_s
6
+                to   = message.to.strip.to_s
7
+                chat_id = message.attribute('chat_id').to_s
8
+
9
+                #noinspection RubyAssignmentExpressionInConditionalInspection
10
+                if message.body
11
+                    process_incoming_message(from, to, message.body, chat_id)
12
+                elsif request = message.first_element('sync_contacts_request')
13
+                    # toto mozem prijat len ako admin multichatu
14
+                    send_contacts(from, to, request.attribute('chat_id').to_s)
15
+                elsif answer = message.first_element('synced_contacts')
16
+                    # toto mozem prijat len ako ucastnik multichatu (nie admin)
17
+                    contacts = xml_contacts_to_array(answer)
18
+                    sync_contacts_frontend(from, to, answer.attribute('chat_id').to_s, contacts)
19
+                elsif answer = message.first_element('exported_chat')
20
+                    contacts = xml_contacts_to_array(message.first_element('exported_chat'))
21
+                    import_people_in_chat(from, to, answer.attribute('chat_id').to_s, contacts)
22
+                elsif message.attribute('destroy_multichat')
23
+                    destroy_multichat(to, chat_id)
24
+                elsif message.attribute('req_update_contacts')
25
+                    added   = xml_contacts_to_array(message.first_element('added'))
26
+                    removed = xml_contacts_to_array(message.first_element('removed'))
27
+                    update_attendants_in_multichat(from, to, chat_id, removed, added)
28
+                end
29
+                #TODO: upozornit na pisanie spravy
30
+                #TODO: odoslat informaciu o tom, ze pisem spravu
31
+                #else
32
+                #    send_message 'app.chat.messageState',
33
+                #                 state: message.chat_state,
34
+                #                 from: message.from.strip.to_s,
35
+                #                 to: message.to.strip.to_s,
36
+                #                 message: message.body,
37
+                #                 chat_id: if message.attribute(:is_simulating) then message.attribute(:chat_id) end
38
+            end
39
+        end
40
+    end
41
+
42
+    def import_people_in_chat(from, to, chat_id, contacts)
43
+        #Rails.logger.debug ['imported chat arrived', message.to_s, chat_id]
44
+        client = find_client(to)
45
+        create_opened_chat(client, chat_id, from, contacts)
46
+
47
+        send_message 'app.chat.importChat',
48
+                     owner:    from,
49
+                     chat_id:  chat_id,
50
+                     contacts: contacts
51
+    end
52
+
53
+    # Owner vytvori najprv u seba novy multichat
54
+    def new_multichat
55
+        me = message[:chatOwner]
56
+        hash = Digest::SHA2.hexdigest(me)
57
+        chat_id = hash + Time.now.to_f.to_s
58
+        client = find_client(me)
59
+
60
+        create_opened_chat(client, chat_id, me)
61
+
62
+        trigger_success id: chat_id
63
+    end
64
+
65
+    # Owner posle novemu cloveku informaciu o chat_id a kontaktoch
66
+    def add_to_multichat
67
+        client = find_client(message[:chatOwner])
68
+
69
+        chat_id  = message[:chatId]
70
+        add      = message[:jid]
71
+
72
+        connection_store[:opened_chats][client][chat_id][:attendants] << add
73
+
74
+        contacts = connection_store[:opened_chats][client][chat_id][:attendants]
75
+
76
+        contacts.each do |contact|
77
+            client.send(MessageBuilder::export_multichat(client.jid.strip.to_s, contact, chat_id, contacts))
78
+        end
79
+
80
+        trigger_success
81
+    end
82
+
83
+    def send_chat_message
84
+        me = message[:from]
85
+        client = find_client(me)
86
+
87
+        if client
88
+            chat_id = message[:chatId]
89
+            if chat_id
90
+                me = client.jid.strip.to_s
91
+                attendants = connection_store[:opened_chats][client][chat_id][:attendants] + [connection_store[:opened_chats][client][chat_id][:owner]]
92
+                attendants -= [me]
93
+
94
+                messages = MessageBuilder::build_multi_messages(message[:message], me, attendants, chat_id)
95
+                #Rails.logger.debug messages
96
+            else
97
+                messages = [MessageBuilder::build_message(message[:message], me, message[:to])]
98
+            end
99
+
100
+            # Xmpp4r doesn't support XEP-0033 (multicast messages)
101
+            messages.each do |message|
102
+                client.send(message)
103
+            end
104
+
105
+            trigger_success message[:message]
106
+        else
107
+            trigger_failure
108
+        end
109
+    end
110
+
111
+    def request_sync_chat_contacts
112
+        client = find_client(message[:me])
113
+        chat_id = message[:chatId]
114
+
115
+        owner = connection_store[:opened_chats][client][chat_id][:owner]
116
+        client.send(MessageBuilder::ask_for_multichat_contacts(client.jid.strip.to_s, owner, chat_id))
117
+    end
118
+
119
+    def i_closed_multichat
120
+        chat_id = message[:chatId]
121
+        client  = find_client(message[:me])
122
+        me      = client.jid.strip.to_s
123
+        owner   = connection_store[:opened_chats][client][chat_id][:owner]
124
+
125
+        if owner == me
126
+            attendants = connection_store[:opened_chats][client][chat_id][:attendants]
127
+            attendants.each do |attendant|
128
+                client.send(MessageBuilder::kick_from_multichat(me, attendant, chat_id))
129
+            end
130
+        else
131
+            changes = {removed: [me]}
132
+            client.send(MessageBuilder::req_update_multichat_contacts(me, owner, chat_id, changes))
133
+        end
134
+
135
+        connection_store[:opened_chats][client].delete(chat_id)
136
+    end
137
+
138
+    private
139
+
140
+    def process_incoming_message(from, to, body, chat_id = nil)
141
+        #Rails.logger.debug [message, message.to.strip.to_s]
142
+        send_message 'app.chat.messageReceived',
143
+                     from: from,
144
+                     to: to,
145
+                     message: body,
146
+                     chat_id: chat_id
147
+    end
148
+
149
+    def send_contacts(from, to, chat_id)
150
+        client = find_client(to)
151
+        contacts = connection_store[:opened_chats][client][chat_id][:attendants]
152
+        client.send(MessageBuilder::send_multichat_contacts(client.jid.strip.to_s, from, chat_id, contacts))
153
+    end
154
+
155
+    def update_attendants_in_multichat(from, to, chat_id, removed, added)
156
+        client = find_client(to)
157
+
158
+        contacts = connection_store[:opened_chats][client][chat_id][:attendants]
159
+        contacts -= removed
160
+        contacts += added
161
+
162
+        connection_store[:opened_chats][client][chat_id][:attendants] = contacts
163
+
164
+        if contacts.empty?
165
+            destroy_multichat(to, chat_id)
166
+        else
167
+            contacts.each do |contact|
168
+                client.send(MessageBuilder::send_multichat_contacts(to, contact, chat_id, contacts))
169
+            end
170
+
171
+            sync_contacts_frontend(from, to, chat_id, contacts)
172
+        end
173
+    end
174
+end
0 175
\ No newline at end of file
1 176
new file mode 100644
... ...
@@ -0,0 +1,73 @@
0
+class WsController < WebsocketRails::BaseController
1
+
2
+    protected
3
+
4
+    def create_opened_chat(client, chat_id, owner = '', attendants = [])
5
+        connection_store[:opened_chats] = {} if ! connection_store[:opened_chats]
6
+        connection_store[:opened_chats][client] = {} if ! connection_store[:opened_chats][client]
7
+        connection_store[:opened_chats][client][chat_id] = {owner: owner, attendants: attendants}
8
+    end
9
+
10
+    def find_client(client_jid)
11
+        connection_store[:clients].find do |client|
12
+            client.jid.strip.to_s == client_jid
13
+        end
14
+    end
15
+
16
+    def find_client_jid(client_jid)
17
+        client = find_client(client_jid)
18
+        client.jid.strip.to_s
19
+    end
20
+
21
+    def where_i_am_multichat_owner
22
+        chats_owner = {}
23
+        connection_store[:clients].each do |client|
24
+            connection_store[:opened_chats][client].each do |chat_id, contacts|
25
+                if contacts[:owner] == client.jid.strip.to_s
26
+                    chats_owner[client] = chat_id
27
+                end
28
+            end
29
+        end
30
+
31
+        chats_owner
32
+    end
33
+
34
+    def kick_from_all_multichats(kick)
35
+        chats = where_i_am_multichat_owner()
36
+        chats.each do |client, chat_id|
37
+            contacts = connection_store[:opened_chats][client][chat_id][:attendants]
38
+            contacts -= [kick]
39
+
40
+            if contacts.empty?
41
+                destroy_multichat(client.jid.strip.to_s, chat_id)
42
+            else
43
+                contacts.each do |contact|
44
+                    client.send(MessageBuilder::send_multichat_contacts(client.jid.strip.to_s, contact, chat_id, contacts))
45
+                end
46
+
47
+                sync_contacts_frontend('', client.jid.strip.to_s, chat_id, contacts)
48
+            end
49
+        end
50
+    end
51
+
52
+    def sync_contacts_frontend(from, to, chat_id, contacts)
53
+        send_message 'app.chat.updateSyncedContacts',
54
+                     me: to,
55
+                     contacts: contacts,
56
+                     owner: from,
57
+                     chat_id: chat_id
58
+    end
59
+
60
+    def destroy_multichat(to, chat_id)
61
+        client = find_client(to)
62
+        connection_store[:opened_chats][client].delete(chat_id)
63
+
64
+        send_message 'app.chat.destroyMultichat', chat_id: chat_id
65
+    end
66
+
67
+    def xml_contacts_to_array(xml_contacts)
68
+        xml_contacts.get_elements('contact').map do |contact|
69
+            contact.text
70
+        end
71
+    end
72
+end
0 73
\ No newline at end of file
... ...
@@ -1,4 +1,4 @@
1
-class WsRosterController < WebsocketRails::BaseController
1
+class WsRosterController < WsController
2 2
     require 'xmpp4r/roster'
3 3
     require 'xmpp4r/vcard'
4 4
 
... ...
@@ -27,6 +27,7 @@ class WsRosterController < WebsocketRails::BaseController
27 27
         # TODO: Pouzit najprv:
28 28
         # clients = Token.fing_user_accounts_having_to_token(session[:token])
29 29
         # ale toto, az ked budem mat dokonceny multiaccount (settings a popup)
30
+        # TODO: skusit zrychlit
30 31
         cookies = env['rack.request.cookie_hash'] # TODO: nahlasit bug na websocket-rails, lebo sa neda pristupit ku `cookies'
31 32
         cipher_key = cookies['key']
32 33
         cipher_iv = cookies['iv']
... ...
@@ -107,6 +108,8 @@ class WsRosterController < WebsocketRails::BaseController
107 107
             roster.add_presence_callback do |roster_item, old_presence, new_presence|
108 108
                 if new_presence.type == :unavailable
109 109
                     result = {status: :offline, message: ''}
110
+                    # mozno treba vyhodit cloveka z multichatu, ak som jeho owner
111
+                    kick_from_all_multichats(roster_item.jid.strip.to_s)
110 112
                 else
111 113
                     status = uniform_presence(new_presence.show)
112 114
                     result = { status: status, message: new_presence.status.to_s }
... ...
@@ -200,6 +203,18 @@ class WsRosterController < WebsocketRails::BaseController
200 200
         end
201 201
     end
202 202
 
203
+    def ask_if_using_this_app
204
+        connection_store[:rosters].each do |roster|
205
+            client = connection_store[:link_roster_client][roster]
206
+
207
+            roster.items.each do |jid, contact|
208
+                client.send(MessageBuilder::control_question(client.jid.strip.to_s, jid))
209
+            end
210
+        end
211
+
212
+        start_polling_control_answer()
213
+    end
214
+
203 215
     ##
204 216
     # Pridaj noveho priatela do zoznamu
205 217
     def add_friend(data)
... ...
@@ -227,4 +242,17 @@ class WsRosterController < WebsocketRails::BaseController
227 227
             else                 :online
228 228
         end
229 229
     end
230
+
231
+    def start_polling_control_answer
232
+        connection_store[:clients].each do |client|
233
+            client.add_message_callback do |message|
234
+                if message.attribute('i_am_using_same_app')
235
+                    send_message 'app.roster.using_this_app',
236
+                                 jid: message.from.node + '@' + message.from.domain
237
+                elsif message.attribute('are_you_using_my_app')
238
+                    client.send(MessageBuilder::control_answer(client.jid.strip.to_s, message.from))
239
+                end
240
+            end
241
+        end
242
+    end
230 243
 end
231 244
new file mode 100644
... ...
@@ -0,0 +1,104 @@
0
+module MessageBuilder
1
+    def self.build_multi_messages(message, from, attendants, chat_id)
2
+        attendants.map do |person|
3
+            new_message = Jabber::Message.new(person, message)
4
+            new_message.from = from
5
+            new_message.add_attribute('chat_id', chat_id)
6
+            new_message
7
+        end
8
+    end
9
+
10
+    def self.build_message(message, from, to)
11
+        message = Jabber::Message.new(to, message)
12
+        message.from = from
13
+
14
+        message
15
+    end
16
+
17
+    def self.ask_for_multichat_contacts(from, to, chat_id)
18
+        message = Jabber::Message.new(to)
19
+        message.from = from
20
+        sync_request = REXML::Element.new('sync_contacts_request')
21
+        sync_request.add_attributes('chat_id' => chat_id)
22
+        message.add_element sync_request
23
+
24
+        message
25
+    end
26
+
27
+    def self.send_multichat_contacts(from, to, chat_id, contacts)
28
+        message = Jabber::Message.new(to)
29
+        message.from = from
30
+        sync_answer = REXML::Element.new('synced_contacts')
31
+        collect_contacts(sync_answer, contacts)
32
+        sync_answer.add_attributes('chat_id' => chat_id)
33
+        message.add_element sync_answer
34
+
35
+        message
36
+    end
37
+
38
+    def self.export_multichat(from, to, chat_id, contacts)
39
+        message = Jabber::Message.new(to)
40
+        message.from = from
41
+        chat = REXML::Element.new('exported_chat')
42
+        collect_contacts(chat, contacts)
43
+        chat.add_attributes('chat_id' => chat_id)
44
+        message.add_element chat
45
+
46
+        message
47
+    end
48
+
49
+    def self.control_answer(from, to)
50
+        message = Jabber::Message.new(to)
51
+        message.from = from
52
+        message.add_attributes('i_am_using_same_app' => 'true')
53
+
54
+        message
55
+    end
56
+
57
+    def self.control_question(from, to)
58
+        message = Jabber::Message.new(to)
59
+        message.from = from
60
+        message.add_attributes('are_you_using_my_app' => '?')
61
+
62
+        message
63
+    end
64
+
65
+    def self.req_update_multichat_contacts(from, to, chat_id, changes)
66
+        message = Jabber::Message.new(to)
67
+        message.from = from
68
+        message.add_attributes('req_update_contacts' => 'true')
69
+        message.add_attributes('chat_id' => chat_id)
70
+
71
+        added   = REXML::Element.new('added')
72
+        removed = REXML::Element.new('removed')
73
+
74
+        collect_contacts(added, changes[:added])
75
+        collect_contacts(removed, changes[:removed])
76
+
77
+        message.add_element added
78
+        message.add_element removed
79
+
80
+        message
81
+    end
82
+
83
+    def self.kick_from_multichat(from, to, chat_id)
84
+        message = Jabber::Message.new(to)
85
+        message.from = from
86
+        message.add_attributes('destroy_multichat' => 'true')
87
+        message.add_attributes('chat_id' => chat_id)
88
+
89
+        message
90
+    end
91
+
92
+    private
93
+
94
+    def self.collect_contacts(to_element, contacts)
95
+        if contacts.kind_of?(Array)
96
+            contacts.each do |contact|
97
+                contact_xml = REXML::Element.new('contact')
98
+                contact_xml.text = contact
99
+                to_element.add_element contact_xml
100
+            end
101
+        end
102
+    end
103
+end
0 104
\ No newline at end of file
... ...
@@ -18,7 +18,7 @@
18 18
         #js-active-friends
19 19
         #js-inactive-friends
20 20
       .toolbox
21
-        %input#js-search-contacts{type: 'search', placeholder: "#{I18n.t('chat.roster.search')}"}
21
+        %input#js-search-contacts{type: 'search', placeholder: "#{I18n.t('chat.roster.search')}", name: 'contact'}
22 22
         %a{ href: '#'}
23 23
           %span.icon-cog
24 24
   .chatting-window.left.rightside
... ...
@@ -43,7 +43,7 @@ WebsocketRails::EventMap.describe do
43 43
   # The above will handle an event triggered on the client like `product.new`.
44 44
 
45 45
     #toto asi poide do nadtriedy spajajucej roster a chat a ine veci
46
-    subscribe :client_connected, to: WsRosterController, with_method: :connect
46
+    subscribe :client_connected,    to: WsRosterController, with_method: :connect
47 47
     subscribe :client_disconnected, to: WsRosterController, with_method: :disconnect
48 48
 
49 49
     namespace :app do
... ...
@@ -57,7 +57,17 @@ WebsocketRails::EventMap.describe do
57 57
             subscribe :updateMyStatus,      to: WsRosterController, with_method: :me_update_status
58 58
             subscribe :updateMyVcard,       to: WsRosterController, with_method: :me_update_vcard
59 59
             subscribe :removeContact,       to: WsRosterController, with_method: :remove_contact
60
+            subscribe :whoUseThisApp,       to: WsRosterController, with_method: :ask_if_using_this_app
60 61
         end
61 62
 
63
+        namespace :chat do
64
+            subscribe :newMultiChat,          to: WsChatController, with_method: :new_multichat
65
+            subscribe :addToMultiChat,        to: WsChatController, with_method: :add_to_multichat
66
+            subscribe :sendMessage,           to: WsChatController, with_method: :send_chat_message
67
+            subscribe :startPollingMessages,  to: WsChatController, with_method: :start_polling_messages
68
+            subscribe :syncMultiChatContacts, to: WsChatController, with_method: :request_sync_chat_contacts
69
+            subscribe :iClosedMultichat,      to: WsChatController, with_method: :i_closed_multichat
70
+            subscribe :kickFromMultichat,     to: WsChatController, with_method: :kick_from_multichat
71
+        end
62 72
     end
63 73
 end
... ...
@@ -24,4 +24,9 @@ en:
24 24
       search: "Search contacts"
25 25
       group-empty: "Empty"
26 26
     tabbar:
27
-      and: 'and'
28 27
\ No newline at end of file
28
+      and: 'and'
29
+    messages:
30
+      new_msg_placeholder: "Type your message here"
31
+      send: "Send"
32
+    window:
33
+      sendFailed: "An error occured while sending your message"
29 34
\ No newline at end of file