Commit 1c125da767ba502b800bd58896ed1aaac828a092

Authored by Rodrigo Souto
Committed by Joenio Costa
1 parent c9fb1096

chat fixes and new features

* try to reconnect if connection fails
* fix ordering
* reconnect if connection is lost but user status is not offline
* remove some anchors
* keep track of buddies order on the client
* tweak notification to work on chromium
* respect async request order
* focus on window when notification clicked
* deal with window visibility
* add chat-buttons
* fix recent_conversations
* fix sorting and join room on opening
* remove obsolete code
* adjusting indentation (sorry but I needed too...)
* fix load_conversation
* return identifier as key on avatars action
* fix typo
* add timeout to notification
* deal properly with offline messages
* fix notification sound
* clear unread after loading conversation on background
* ignore message from room
* add unread messages counter on chat-label
* as for notification permission as soon as the chat connects
* add profile_info_action to open chat
* using jid to save message instead of identifier
* add profile_info_action to open chat
* as for notification permission as soon as the chat connects
* add unread messages counter on chat-label

Squashed and Signed-off-by: Joenio Costa <joenio@colivre.coop.br>
app/controllers/public/chat_controller.rb
@@ -2,6 +2,7 @@ class ChatController &lt; PublicController @@ -2,6 +2,7 @@ class ChatController &lt; PublicController
2 2
3 before_filter :login_required 3 before_filter :login_required
4 before_filter :check_environment_feature 4 before_filter :check_environment_feature
  5 + before_filter :can_send_message, :only => :register_message
5 6
6 def start_session 7 def start_session
7 login = user.jid 8 login = user.jid
@@ -54,6 +55,16 @@ class ChatController &lt; PublicController @@ -54,6 +55,16 @@ class ChatController &lt; PublicController
54 end 55 end
55 end 56 end
56 57
  58 + def avatars
  59 + profiles = environment.profiles.where(:identifier => params[:profiles])
  60 + avatar_map = profiles.inject({}) do |result, profile|
  61 + result[profile.identifier] = profile_icon(profile, :minor)
  62 + result
  63 + end
  64 +
  65 + render_json avatar_map
  66 + end
  67 +
57 def update_presence_status 68 def update_presence_status
58 if request.xhr? 69 if request.xhr?
59 current_user.update_attributes({:chat_status_at => DateTime.now}.merge(params[:status] || {})) 70 current_user.update_attributes({:chat_status_at => DateTime.now}.merge(params[:status] || {}))
@@ -62,11 +73,17 @@ class ChatController &lt; PublicController @@ -62,11 +73,17 @@ class ChatController &lt; PublicController
62 end 73 end
63 74
64 def save_message 75 def save_message
65 - to = environment.profiles.find_by_identifier(params[:to])  
66 - body = params[:body]  
67 -  
68 - ChatMessage.create!(:to => to, :from => user, :body => body)  
69 - render :text => 'ok' 76 + if request.post?
  77 + to = environment.profiles.where(:identifier => params[:to]).first
  78 + body = params[:body]
  79 +
  80 + begin
  81 + ChatMessage.create!(:to => to, :from => user, :body => body)
  82 + return render_json({:status => 0})
  83 + rescue Exception => exception
  84 + return render_json({:status => 3, :message => exception.to_s, :backtrace => exception.backtrace})
  85 + end
  86 + end
70 end 87 end
71 88
72 def recent_messages 89 def recent_messages
@@ -90,8 +107,9 @@ class ChatController &lt; PublicController @@ -90,8 +107,9 @@ class ChatController &lt; PublicController
90 end 107 end
91 108
92 def recent_conversations 109 def recent_conversations
93 - conversations_order = ActiveRecord::Base.connection.execute("select profiles.identifier from profiles inner join (select distinct r.id as id, MAX(r.created_at) as created_at from (select from_id, to_id, created_at, (case when from_id=#{user.id} then to_id else from_id end) as id from chat_messages where from_id=#{user.id} or to_id=#{user.id}) as r group by id order by created_at desc, id) as t on profiles.id=t.id order by t.created_at desc").entries.map {|e| e['identifier']}  
94 - render :json => {:order => conversations_order.reverse, :domain => environment.default_hostname.gsub('.','-')}.to_json 110 + profiles = Profile.find_by_sql("select profiles.* from profiles inner join (select distinct r.id as id, MAX(r.created_at) as created_at from (select from_id, to_id, created_at, (case when from_id=#{user.id} then to_id else from_id end) as id from chat_messages where from_id=#{user.id} or to_id=#{user.id}) as r group by id order by created_at desc, id) as t on profiles.id=t.id order by t.created_at desc")
  111 + jids = profiles.map(&:jid).reverse
  112 + render :json => jids.to_json
95 end 113 end
96 114
97 #TODO Ideally this is done through roster table on ejabberd. 115 #TODO Ideally this is done through roster table on ejabberd.
@@ -108,4 +126,14 @@ class ChatController &lt; PublicController @@ -108,4 +126,14 @@ class ChatController &lt; PublicController
108 end 126 end
109 end 127 end
110 128
  129 + def can_send_message
  130 + return render_json({:status => 1, :message => 'Missing parameters!'}) if params[:from].nil? || params[:to].nil? || params[:message].nil?
  131 + return render_json({:status => 2, :message => 'You can not send message as another user!'}) if params[:from] != user.jid
  132 + # TODO Maybe register the jid in a table someday to avoid this below
  133 + return render_json({:status => 3, :messsage => 'You can not send messages to strangers!'}) if user.friends.where(:identifier => params[:to].split('@').first).blank?
  134 + end
  135 +
  136 + def render_json(result)
  137 + render :text => result.to_json
  138 + end
111 end 139 end
app/helpers/chat_helper.rb
@@ -9,12 +9,12 @@ module ChatHelper @@ -9,12 +9,12 @@ module ChatHelper
9 avatar = profile_image(user, :portrait, :class => 'avatar') 9 avatar = profile_image(user, :portrait, :class => 'avatar')
10 content_tag('span', 10 content_tag('span',
11 link_to(avatar + content_tag('span', user.name) + ui_icon('ui-icon-triangle-1-s'), 11 link_to(avatar + content_tag('span', user.name) + ui_icon('ui-icon-triangle-1-s'),
12 - '#', 12 + '',
13 :onclick => 'toggleMenu(this); return false', 13 :onclick => 'toggleMenu(this); return false',
14 :class => icon_class + ' simplemenu-trigger' 14 :class => icon_class + ' simplemenu-trigger'
15 ) + 15 ) +
16 content_tag('ul', 16 content_tag('ul',
17 - links.map{|link| content_tag('li', link_to(link[1], '#', :class => link[0], :id => link[2], 'data-jid' => user.jid), :class => 'simplemenu-item') }.join("\n"), 17 + links.map{|link| content_tag('li', link_to(link[1], '', :class => link[0], :id => link[2], 'data-jid' => user.jid), :class => 'simplemenu-item') }.join("\n"),
18 :style => 'display: none; z-index: 100', 18 :style => 'display: none; z-index: 100',
19 :class => 'simplemenu-submenu' 19 :class => 'simplemenu-submenu'
20 ), 20 ),
app/models/chat_message.rb
@@ -3,5 +3,4 @@ class ChatMessage &lt; ActiveRecord::Base @@ -3,5 +3,4 @@ class ChatMessage &lt; ActiveRecord::Base
3 3
4 belongs_to :to, :class_name => 'Profile' 4 belongs_to :to, :class_name => 'Profile'
5 belongs_to :from, :class_name => 'Profile' 5 belongs_to :from, :class_name => 'Profile'
6 -  
7 end 6 end
app/views/blocks/profile_info_actions/_common.html.erb 0 → 100644
@@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
  1 +<li><%= report_abuse(profile, :button) %></li>
  2 +<%= render_environment_features(:profile_actions) %></li>
app/views/blocks/profile_info_actions/_community.html.erb
@@ -13,8 +13,6 @@ @@ -13,8 +13,6 @@
13 </li> 13 </li>
14 <% end %> 14 <% end %>
15 15
16 - <li><%= report_abuse(profile, :button) %></li>  
17 -  
18 - <%= render_environment_features(:profile_actions) %> 16 + <%= render :partial => 'blocks/profile_info_actions/common' %>
19 <% end %> 17 <% end %>
20 </ul> 18 </ul>
app/views/blocks/profile_info_actions/_enterprise.html.erb
@@ -8,5 +8,5 @@ @@ -8,5 +8,5 @@
8 <li><%= button(:'menu-mail', _('Send an e-mail'), {:profile => profile.identifier, :controller => 'contact', :action => 'new'}, {:id => 'enterprise-contact-button'} ) %></li> 8 <li><%= button(:'menu-mail', _('Send an e-mail'), {:profile => profile.identifier, :controller => 'contact', :action => 'new'}, {:id => 'enterprise-contact-button'} ) %></li>
9 <% end %> 9 <% end %>
10 10
11 - <li><%= report_abuse(profile, :button) %></li> 11 + <%= render :partial => 'blocks/profile_info_actions/common' %>
12 </ul> 12 </ul>
app/views/blocks/profile_info_actions/_person.html.erb
@@ -11,6 +11,6 @@ @@ -11,6 +11,6 @@
11 <li><%= button(:back, _('Send an e-mail'), {:profile => profile.identifier, :controller => 'contact', :action => 'new'}) %></li> 11 <li><%= button(:back, _('Send an e-mail'), {:profile => profile.identifier, :controller => 'contact', :action => 'new'}) %></li>
12 <% end %> 12 <% end %>
13 13
14 - <li><%= report_abuse(profile, :button) %></li> 14 + <%= render :partial => 'blocks/profile_info_actions/common' %>
15 <% end %> 15 <% end %>
16 </ul> 16 </ul>
app/views/chat/start_session_error.html.erb
1 <p> 1 <p>
2 <%= ui_icon('ui-icon-alert') %> 2 <%= ui_icon('ui-icon-alert') %>
3 -<%= _('Could not connect to chat') %>, <a id='chat-retry' href='#' data-jid='<%= user.jid %>'><%= _('try again') %></a>. 3 +<%= _('Could not connect to chat') %>, <a id='chat-retry' href='' data-jid='<%= user.jid %>'><%= _('try again') %></a>.
4 </p> 4 </p>
app/views/shared/logged_in/xmpp_chat.html.erb
@@ -7,13 +7,13 @@ @@ -7,13 +7,13 @@
7 var $own_name = '<%= user.name %>'; 7 var $own_name = '<%= user.name %>';
8 var $muc_domain = '<%= "conference.#{environment.default_hostname}" %>'; 8 var $muc_domain = '<%= "conference.#{environment.default_hostname}" %>';
9 var $bosh_service = '//<%= environment.default_hostname %>/http-bind'; 9 var $bosh_service = '//<%= environment.default_hostname %>/http-bind';
10 - var $user_unavailable_error = '<%= _("<strong>ooops!</strong> The message could not be sent because the user is not online") %>'; 10 + var $user_unavailable_error = '<%= _("The user is not online now. He/She will receive these messages as soon as he/she gets online.") %>';
11 var $update_presence_status_every = <%= User.expires_chat_status_every.minutes %>; 11 var $update_presence_status_every = <%= User.expires_chat_status_every.minutes %>;
12 var $presence = '<%= current_user.last_chat_status %>'; 12 var $presence = '<%= current_user.last_chat_status %>';
13 </script> 13 </script>
14 14
15 -  
16 <div id="chat-label"> 15 <div id="chat-label">
  16 + <span id="unread-messages"></span>
17 <span class="right-arrow">&#9654;</span> 17 <span class="right-arrow">&#9654;</span>
18 <span class="title"><%= _('Chat') %></span> 18 <span class="title"><%= _('Chat') %></span>
19 </div> 19 </div>
@@ -98,10 +98,5 @@ @@ -98,10 +98,5 @@
98 </div> 98 </div>
99 </div> 99 </div>
100 </div> 100 </div>
101 -  
102 - <div class="error-message">  
103 - <span class='error'>%{text}</span>  
104 - </div>  
105 -  
106 </div> 101 </div>
107 </div> 102 </div>
app/views/shared/profile_actions/xmpp_chat.html.erb 0 → 100644
@@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
  1 +<% label_name = profile.person? ? _('Open chat') : _('Join chat room') %>
  2 +<% display = profile.person? ? profile.friends.include?(user) : profile.members.include?(user) %>
  3 +
  4 +<% if display %>
  5 + <li>
  6 + <%= button(:chat, label_name , {}, :class => 'open-conversation', 'data-jid' => profile.jid) %>
  7 + </li>
  8 +<% end %>
db/migrate/20140820173129_create_chat_messages.rb
@@ -1,11 +0,0 @@ @@ -1,11 +0,0 @@
1 -class CreateChatMessages < ActiveRecord::Migration  
2 - def change  
3 - create_table :chat_messages do |t|  
4 - t.integer :to_id  
5 - t.integer :from_id  
6 - t.string :body  
7 -  
8 - t.timestamps  
9 - end  
10 - end  
11 -end  
db/migrate/20141014205254_create_chat_messages.rb 0 → 100644
@@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
  1 +class CreateChatMessages < ActiveRecord::Migration
  2 + def up
  3 + create_table :chat_messages do |t|
  4 + t.references :from
  5 + t.references :to
  6 + t.text :body
  7 + t.timestamps
  8 + end
  9 + add_index :chat_messages, :from_id
  10 + add_index :chat_messages, :to_id
  11 + add_index :chat_messages, :created_at
  12 + end
  13 +
  14 + def down
  15 + drop_table :chat_messages
  16 + remove_index :chat_messages, :from
  17 + remove_index :chat_messages, :to
  18 + remove_index :chat_messages, :created_at
  19 + end
  20 +end
@@ -245,13 +245,17 @@ ActiveRecord::Schema.define(:version =&gt; 20150513213939) do @@ -245,13 +245,17 @@ ActiveRecord::Schema.define(:version =&gt; 20150513213939) do
245 end 245 end
246 246
247 create_table "chat_messages", :force => true do |t| 247 create_table "chat_messages", :force => true do |t|
248 - t.integer "to_id"  
249 t.integer "from_id" 248 t.integer "from_id"
250 - t.string "body" 249 + t.integer "to_id"
  250 + t.text "body"
251 t.datetime "created_at", :null => false 251 t.datetime "created_at", :null => false
252 t.datetime "updated_at", :null => false 252 t.datetime "updated_at", :null => false
253 end 253 end
254 254
  255 + add_index "chat_messages", ["created_at"], :name => "index_chat_messages_on_created_at"
  256 + add_index "chat_messages", ["from_id"], :name => "index_chat_messages_on_from_id"
  257 + add_index "chat_messages", ["to_id"], :name => "index_chat_messages_on_to_id"
  258 +
255 create_table "comments", :force => true do |t| 259 create_table "comments", :force => true do |t|
256 t.string "title" 260 t.string "title"
257 t.text "body" 261 t.text "body"
debian/noosfero.install
@@ -34,3 +34,4 @@ public usr/share/noosfero @@ -34,3 +34,4 @@ public usr/share/noosfero
34 script usr/share/noosfero 34 script usr/share/noosfero
35 util usr/share/noosfero 35 util usr/share/noosfero
36 vendor usr/share/noosfero 36 vendor usr/share/noosfero
  37 +
debian/update-noosfero-apache
@@ -18,9 +18,20 @@ if test -x /usr/share/noosfero/script/apacheconf; then @@ -18,9 +18,20 @@ if test -x /usr/share/noosfero/script/apacheconf; then
18 fi 18 fi
19 19
20 apache_site='/etc/apache2/sites-available/noosfero' 20 apache_site='/etc/apache2/sites-available/noosfero'
  21 + apache_site_configs='/etc/noosfero/apache.d'
21 if ! test -e "$apache_site"; then 22 if ! test -e "$apache_site"; then
22 echo "Generating apache virtual host ..." 23 echo "Generating apache virtual host ..."
23 cd /usr/share/noosfero && su noosfero -c "RAILS_ENV=production ./script/apacheconf virtualhosts" > "$apache_site" 24 cd /usr/share/noosfero && su noosfero -c "RAILS_ENV=production ./script/apacheconf virtualhosts" > "$apache_site"
  25 + if ! test -d "$apache_site_configs"; then
  26 + echo "Creating noosfero site config folder ..."
  27 + mkdir $apache_site_configs
  28 + fi
  29 + else
  30 + pattern="Include \/etc\/noosfero\/apache\/virtualhost.conf"
  31 + include="Include \/etc\/noosfero\/apache.d\/*"
  32 + if ! cat $apache_site | grep "^ *$include" > /dev/null ; then
  33 + sed -i "s/.*$pattern.*/ $include\n&/" $apache_site
  34 + fi
24 fi 35 fi
25 36
26 echo 'Noosfero Apache configuration updated.' 37 echo 'Noosfero Apache configuration updated.'
etc/noosfero/apache.d/noosfero-chat 0 → 100644
@@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
  1 +RewriteEngine On
  2 +Include /usr/share/noosfero/util/chat/apache/xmpp.conf
public/javascripts/application.js
@@ -1140,10 +1140,20 @@ function notifyMe(title, options) { @@ -1140,10 +1140,20 @@ function notifyMe(title, options) {
1140 1140
1141 // If the user is okay, let's create a notification 1141 // If the user is okay, let's create a notification
1142 if (permission === "granted") { 1142 if (permission === "granted") {
1143 - notification = new Notification(title, options); 1143 + notification = new Notification(title, options);
1144 } 1144 }
1145 }); 1145 });
1146 } 1146 }
  1147 +
  1148 + setTimeout(function() {notification.close()}, 5000);
  1149 + notification.onclick = function(){
  1150 + notification.close();
  1151 + // Chromium tweak
  1152 + window.open().close();
  1153 + window.focus();
  1154 + this.cancel();
  1155 + };
  1156 +
1147 return notification; 1157 return notification;
1148 // At last, if the user already denied any notification, and you 1158 // At last, if the user already denied any notification, and you
1149 // want to be respectful there is no need to bother them any more. 1159 // want to be respectful there is no need to bother them any more.
public/javascripts/chat.js
1 /* XMPP/Jabber Noosfero's client 1 /* XMPP/Jabber Noosfero's client
2 2
3 - XMPP Core:  
4 - http://xmpp.org/rfcs/rfc3920.html 3 +XMPP Core:
  4 +http://xmpp.org/rfcs/rfc3920.html
5 5
6 - MUC support:  
7 - http://xmpp.org/extensions/xep-0045.html 6 +MUC support:
  7 +http://xmpp.org/extensions/xep-0045.html
8 8
9 - Messages and presence:  
10 - http://xmpp.org/rfcs/rfc3921.html 9 +Messages and presence:
  10 +http://xmpp.org/rfcs/rfc3921.html
11 */ 11 */
12 12
13 jQuery(function($) { 13 jQuery(function($) {
14 - // extending the current namespaces in Strophe.NS  
15 - Strophe.addNamespace('MUC_USER', 'http://jabber.org/protocol/muc#user');  
16 - Strophe.addNamespace('MUC_OWNER', 'http://jabber.org/protocol/muc#owner');  
17 - Strophe.addNamespace('CHAT_STATES', 'http://jabber.org/protocol/chatstates');  
18 - Strophe.addNamespace('DATA_FORMS', 'jabber:x:data');  
19 -  
20 - var Jabber = {  
21 - debug: true,  
22 - connection: null,  
23 - bosh_service: $bosh_service,  
24 - muc_domain: $muc_domain,  
25 - muc_supported: false,  
26 - presence_status: '',  
27 - conversation_prefix: 'conversation-',  
28 - jids: {},  
29 - rooms: {},  
30 - no_more_messages: {},  
31 -  
32 - template: function(selector) {  
33 - return $('#chat #chat-templates '+selector).clone().html();  
34 - },  
35 -  
36 - jid_to_id: function (jid) {  
37 - return Strophe.getBareJidFromJid(jid).replace(/@/g, "-").replace(/\./g, "-");  
38 - },  
39 -  
40 - jid_of: function(jid_id) {  
41 - return Jabber.jids[jid_id].jid;  
42 - },  
43 - name_of: function(jid_id) {  
44 - return Jabber.jids[jid_id].name;  
45 - },  
46 - type_of: function(jid_id) {  
47 - return Jabber.jids[jid_id].type;  
48 - },  
49 - unread_messages_of: function(jid_id, value) {  
50 - Jabber.jids[jid_id].unread_messages = (value == undefined ? Jabber.jids[jid_id].unread_messages : value);  
51 - return Jabber.jids[jid_id].unread_messages;  
52 - },  
53 -  
54 - insert_or_update_user: function (list, item, jid, name, presence, template, type, remove_on_offline) {  
55 - var jid_id = Jabber.jid_to_id(jid);  
56 - var identifier = Strophe.getNodeFromJid(jid);  
57 - var html = template  
58 - .replace('%{jid_id}', jid_id)  
59 - .replace(/%{presence_status}/g, presence)  
60 - .replace('%{avatar}', getAvatar(identifier))  
61 - .replace('%{name}', name);  
62 -  
63 - $(item).parent().remove();  
64 - if(presence != 'offline' || !remove_on_offline)  
65 - $(list).append(html);  
66 - Jabber.jids[jid_id] = {jid: jid, name: name, type: type, presence: presence};  
67 - },  
68 - insert_or_update_group: function (jid, presence) {  
69 - var jid_id = Jabber.jid_to_id(jid);  
70 - var list = $('#buddy-list .buddies ul.'+presence);  
71 - var item = $('#' + jid_id);  
72 - presence = presence || ($(item).length > 0 ? $(item).parent('li').attr('class') : 'offline');  
73 - log('adding or updating contact ' + jid + ' as ' + presence);  
74 - Jabber.insert_or_update_user(list, item, jid, Jabber.name_of(jid_id), presence, Jabber.template('.buddy-item'), 'groupchat');  
75 - $("#chat-window .tab a[href='#"+ Jabber.conversation_prefix + jid_id +"']")  
76 - .removeClass()  
77 - .addClass('icon-menu-' + presence + '-11');  
78 - },  
79 - insert_or_update_contact: function (jid, name, presence) {  
80 - var jid_id = Jabber.jid_to_id(jid);  
81 - var item = $('#' + jid_id);  
82 - presence = presence || ($(item).length > 0 ? $(item).parent('li').attr('class') : 'offline');  
83 - var list = $('#buddy-list .buddies ul' + (presence=='offline' ? '.offline' : '.online'));  
84 -  
85 - log('adding or updating contact ' + jid + ' as ' + presence);  
86 - Jabber.insert_or_update_user(list, item, jid, name, presence, Jabber.template('.buddy-item'), 'chat');  
87 - $("#chat-window .tab a[href='#"+ Jabber.conversation_prefix + jid_id +"']")  
88 - .removeClass()  
89 - .addClass('icon-menu-' + presence + '-11');  
90 - },  
91 - insert_or_update_occupant: function (jid, name, presence, room_jid) {  
92 - log('adding or updating occupant ' + jid + ' as ' + presence);  
93 - var jid_id = Jabber.jid_to_id(jid);  
94 - var room_jid_id = Jabber.jid_to_id(room_jid);  
95 - var list = $('#' + Jabber.conversation_prefix + room_jid_id + ' .occupants ul');  
96 - var item = $(list).find('a[data-id='+ jid_id +']');  
97 - Jabber.insert_or_update_user(list, item, jid, name, presence, Jabber.template('.occupant-item'), 'chat', true);  
98 - if (Jabber.rooms[room_jid_id] === undefined)  
99 - Jabber.rooms[room_jid_id] = {};  
100 -  
101 - var room = Jabber.rooms[room_jid_id];  
102 - if(presence == 'offline') {  
103 - delete Jabber.rooms[room_jid_id][name];  
104 - }  
105 - else {  
106 - Jabber.rooms[room_jid_id][name] = jid;  
107 - } 14 + // extending the current namespaces in Strophe.NS
  15 + Strophe.addNamespace('MUC_USER', 'http://jabber.org/protocol/muc#user');
  16 + Strophe.addNamespace('MUC_OWNER', 'http://jabber.org/protocol/muc#owner');
  17 + Strophe.addNamespace('CHAT_STATES', 'http://jabber.org/protocol/chatstates');
  18 + Strophe.addNamespace('DATA_FORMS', 'jabber:x:data');
  19 +
  20 + var Jabber = {
  21 + debug: true,
  22 + connection: null,
  23 + bosh_service: $bosh_service,
  24 + muc_domain: $muc_domain,
  25 + muc_supported: false,
  26 + presence_status: '',
  27 + conversation_prefix: 'conversation-',
  28 + conversations_order: null,
  29 + notification_sound: new Audio('/sounds/receive.wav'),
  30 + window_visibility: null,
  31 + jids: {},
  32 + rooms: {},
  33 + no_more_messages: {},
  34 + avatars: {},
  35 +
  36 + template: function(selector) {
  37 + return $('#chat #chat-templates '+selector).clone().html();
  38 + },
  39 +
  40 + jid_to_id: function (jid) {
  41 + return Strophe.getBareJidFromJid(jid).replace(/@/g, "-").replace(/\./g, "-");
  42 + },
  43 +
  44 + jid_of: function(jid_id) {
  45 + return Jabber.jids[jid_id].jid;
  46 + },
  47 + name_of: function(jid_id) {
  48 + return Jabber.jids[jid_id].name;
  49 + },
  50 + type_of: function(jid_id) {
  51 + return Jabber.jids[jid_id].type;
  52 + },
  53 + presence_of: function(jid_id) {
  54 + return Jabber.jids[jid_id].presence;
  55 + },
  56 + unread_messages_of: function(jid_id, value) {
  57 + Jabber.jids[jid_id].unread_messages = (value == undefined ? Jabber.jids[jid_id].unread_messages : value);
  58 + return Jabber.jids[jid_id].unread_messages;
  59 + },
  60 +
  61 + insert_or_update_user: function (list, item, jid, name, presence, template, type, remove_on_offline) {
  62 + var jid_id = Jabber.jid_to_id(jid);
  63 + var identifier = Strophe.getNodeFromJid(jid);
  64 + var html = template
  65 + .replace('%{jid_id}', jid_id)
  66 + .replace(/%{presence_status}/g, presence)
  67 + .replace('%{avatar}', getAvatar(identifier))
  68 + .replace('%{name}', name);
  69 +
  70 + $(item).parent().remove();
  71 + if(presence != 'offline' || !remove_on_offline){
  72 + $(list).append(html);
  73 + sort_conversations();
  74 + }
  75 + Jabber.jids[jid_id] = {jid: jid, name: name, type: type, presence: presence};
  76 + },
  77 + insert_or_update_group: function (jid, presence) {
  78 + var jid_id = Jabber.jid_to_id(jid);
  79 + var list = $('#buddy-list .buddies ul.'+presence);
  80 + var item = $('#' + jid_id);
  81 + presence = presence || ($(item).length > 0 ? $(item).parent('li').attr('class') : 'offline');
  82 + log('adding or updating contact ' + jid + ' as ' + presence);
  83 + Jabber.insert_or_update_user(list, item, jid, Jabber.name_of(jid_id), presence, Jabber.template('.buddy-item'), 'groupchat');
  84 + $("#chat-window .tab a[href='#"+ Jabber.conversation_prefix + jid_id +"']")
  85 + .removeClass()
  86 + .addClass('icon-menu-' + presence + '-11');
  87 + },
  88 + insert_or_update_contact: function (jid, name, presence) {
  89 + var jid_id = Jabber.jid_to_id(jid);
  90 + var item = $('#' + jid_id);
  91 + presence = presence || ($(item).length > 0 ? $(item).parent('li').attr('class') : 'offline');
  92 + var list = $('#buddy-list .buddies ul' + (presence=='offline' ? '.offline' : '.online'));
  93 +
  94 + log('adding or updating contact ' + jid + ' as ' + presence);
  95 + Jabber.insert_or_update_user(list, item, jid, name, presence, Jabber.template('.buddy-item'), 'chat');
  96 + $("#chat-window .tab a[href='#"+ Jabber.conversation_prefix + jid_id +"']")
  97 + .removeClass()
  98 + .addClass('icon-menu-' + presence + '-11');
  99 + },
  100 + insert_or_update_occupant: function (jid, name, presence, room_jid) {
  101 + log('adding or updating occupant ' + jid + ' as ' + presence);
  102 + var jid_id = Jabber.jid_to_id(jid);
  103 + var room_jid_id = Jabber.jid_to_id(room_jid);
  104 + var list = $('#' + Jabber.conversation_prefix + room_jid_id + ' .occupants ul');
  105 + var item = $(list).find('a[data-id='+ jid_id +']');
  106 + Jabber.insert_or_update_user(list, item, jid, name, presence, Jabber.template('.occupant-item'), 'chat', true);
  107 + if (Jabber.rooms[room_jid_id] === undefined)
  108 + Jabber.rooms[room_jid_id] = {};
  109 +
  110 + var room = Jabber.rooms[room_jid_id];
  111 + if(presence == 'offline') {
  112 + delete Jabber.rooms[room_jid_id][name];
  113 + }
  114 + else {
  115 + Jabber.rooms[room_jid_id][name] = jid;
  116 + }
108 117
109 - list.parents('.occupants').find('.occupants-online').text(Object.keys(Jabber.rooms[room_jid_id]).length);  
110 - },  
111 -  
112 - remove_contact: function(jid) {  
113 - var jid_id = Jabber.jid_to_id(jid)  
114 - log('Removing contact ' + jid);  
115 - $('#' + jid_id).parent('li').remove();  
116 - },  
117 -  
118 - render_body_message: function(body) {  
119 - body = body.replace(/\r?\n/g, '<br>');  
120 - body = $().emoticon(body);  
121 - body = linkify(body, {  
122 - callback: function(text, href) {  
123 - return href ? '<a href="' + href + '" title="' + href + '" target="_blank">' + text + '</a>' : text;  
124 - }  
125 - });  
126 - return body;  
127 - }, 118 + list.parents('.occupants').find('.occupants-online').text(Object.keys(Jabber.rooms[room_jid_id]).length);
  119 + },
  120 +
  121 + remove_contact: function(jid) {
  122 + var jid_id = Jabber.jid_to_id(jid)
  123 + log('Removing contact ' + jid);
  124 + $('#' + jid_id).parent('li').remove();
  125 + },
  126 +
  127 + render_body_message: function(body) {
  128 + body = body.replace(/\r?\n/g, '<br>');
  129 + body = $().emoticon(body);
  130 + body = linkify(body, {
  131 + callback: function(text, href) {
  132 + return href ? '<a href="' + href + '" title="' + href + '" target="_blank">' + text + '</a>' : text;
  133 + }
  134 + });
  135 + return body;
  136 + },
128 137
129 - show_message: function (jid, name, body, who, identifier, time, offset) {  
130 - if(!offset) offset = 0;  
131 - if (body) {  
132 - body = Jabber.render_body_message(body);  
133 - var jid_id = Jabber.jid_to_id(jid);  
134 - var tab_id = '#' + Jabber.conversation_prefix + jid_id;  
135 - var history = $(tab_id).find('.history'); 138 + show_message: function (jid, name, body, who, identifier, time, offset) {
  139 + if(!offset) offset = 0;
  140 + if (body) {
  141 + body = Jabber.render_body_message(body);
  142 + var jid_id = Jabber.jid_to_id(jid);
  143 + var tab_id = '#' + Jabber.conversation_prefix + jid_id;
  144 + var history = $(tab_id).find('.history');
136 145
137 - var offset_container = history.find('.chat-offset-container-'+offset);  
138 - if(offset_container.length == 0)  
139 - offset_container = $('<div class="chat-offset-container-'+offset+'"></div>').prependTo(history); 146 + var offset_container = history.find('.chat-offset-container-'+offset);
  147 + if(offset_container.length == 0)
  148 + offset_container = $('<div class="chat-offset-container-'+offset+'"></div>').prependTo(history);
140 149
141 - if (offset_container.find('.message:last').attr('data-who') == who) {  
142 - offset_container.find('.message:last .content').append('<p>' + body + '</p>');  
143 - }  
144 - else {  
145 - if (time==undefined) {  
146 - time = new Date().toISOString();  
147 - }  
148 - var message_html = Jabber.template('.message')  
149 - .replace('%{message}', body)  
150 - .replace(/%{who}/g, who)  
151 - .replace('%{time}', time)  
152 - .replace('%{name}', name)  
153 - .replace('%{avatar}', getAvatar(identifier));  
154 - offset_container.append(message_html);  
155 - $(".message span.time").timeago();  
156 - }  
157 - if(offset == 0) history.scrollTo({top:'100%', left:'0%'});  
158 - else history.scrollTo(offset_container.height());  
159 - if (who != "self") {  
160 - if ($(tab_id).find('.history:visible').length == 0) {  
161 - count_unread_messages(jid_id);  
162 - }  
163 - document.alert_title = name;  
164 - }  
165 - }  
166 - },  
167 -  
168 - show_status: function(presence) {  
169 - log('changing my status to ' + presence);  
170 - $('#buddy-list .user-status .simplemenu-trigger')  
171 - .removeClass('icon-menu-chat')  
172 - .removeClass('icon-menu-offline')  
173 - .removeClass('icon-menu-dnd')  
174 - .addClass('icon-menu-' + (presence || 'offline'));  
175 - $('#buddy-list #user-status img.avatar').replaceWith(getMyAvatar());  
176 - $.get('/chat/update_presence_status', { status: {chat_status: presence, last_chat_status: presence} });  
177 - },  
178 -  
179 - send_availability_status: function(presence) {  
180 - log('send availability status ' + presence);  
181 - Jabber.connection.send($pres().c('show').t(presence).up());  
182 - Jabber.show_status(presence);  
183 - },  
184 -  
185 - enter_room: function(jid, push) {  
186 - if(push == undefined)  
187 - push = true  
188 - var jid_id = Jabber.jid_to_id(jid);  
189 - var conversation_id = Jabber.conversation_prefix + jid_id;  
190 - var button = $('#' + conversation_id + ' .join');  
191 - button.hide();  
192 - button.siblings('.leave').show();  
193 - Jabber.connection.send(  
194 - $pres({to: jid + '/' + $own_name}).c('x', {xmlns: Strophe.NS.MUC}).c('history', {maxchars: 0})  
195 - );  
196 - Jabber.insert_or_update_group(jid, 'online'); 150 + if (offset_container.find('.message:last').attr('data-who') == who) {
  151 + offset_container.find('.message:last .content').append('<p>' + body + '</p>');
  152 + }
  153 + else {
  154 + if (time==undefined) {
  155 + time = new Date().toISOString();
  156 + }
  157 + var message_html = Jabber.template('.message')
  158 + .replace('%{message}', body)
  159 + .replace(/%{who}/g, who)
  160 + .replace('%{time}', time)
  161 + .replace('%{name}', name)
  162 + .replace('%{avatar}', getAvatar(identifier));
  163 + offset_container.append(message_html);
  164 + $(".message span.time").timeago();
  165 + }
  166 + if(offset == 0) history.scrollTo({top:'100%', left:'0%'});
  167 + else history.scrollTo(offset_container.height());
  168 + if (who != "self") {
  169 + if ($(tab_id).find('.history:visible').length == 0) {
  170 + count_unread_messages(jid_id);
  171 + }
  172 + document.alert_title = name;
  173 + }
  174 + }
  175 + },
  176 +
  177 + show_status: function(presence) {
  178 + log('changing my status to ' + presence);
  179 + $('#buddy-list .user-status .simplemenu-trigger')
  180 + .removeClass('icon-menu-chat')
  181 + .removeClass('icon-menu-offline')
  182 + .removeClass('icon-menu-dnd')
  183 + .addClass('icon-menu-' + (presence || 'offline'));
  184 + $('#buddy-list #user-status img.avatar').replaceWith(getMyAvatar());
  185 + $.get('/chat/update_presence_status', { status: {chat_status: presence, last_chat_status: presence} });
  186 + },
  187 +
  188 + send_availability_status: function(presence) {
  189 + log('send availability status ' + presence);
  190 + Jabber.connection.send($pres().c('show').t(presence).up());
  191 + Jabber.show_status(presence);
  192 + },
  193 +
  194 + enter_room: function(jid, push) {
  195 + if(push == undefined)
  196 + push = true
  197 + var jid_id = Jabber.jid_to_id(jid);
  198 + var conversation_id = Jabber.conversation_prefix + jid_id;
  199 + var button = $('#' + conversation_id + ' .join');
  200 + button.hide();
  201 + button.siblings('.leave').show();
  202 + Jabber.connection.send(
  203 + $pres({to: jid + '/' + $own_name}).c('x', {xmlns: Strophe.NS.MUC}).c('history', {maxchars: 0})
  204 + );
  205 + Jabber.insert_or_update_group(jid, 'online');
  206 + Jabber.update_chat_title();
  207 + sort_conversations();
  208 + if(push)
  209 + $.post('/chat/join', {room_id: jid});
  210 + },
  211 +
  212 + leave_room: function(jid, push) {
  213 + if(push == undefined)
  214 + push = true
  215 + var jid_id = Jabber.jid_to_id(jid);
  216 + var conversation_id = Jabber.conversation_prefix + jid_id;
  217 + var button = $('#' + conversation_id + ' .leave');
  218 + button.hide();
  219 + button.siblings('.join').show();
  220 + Jabber.connection.send($pres({from: Jabber.connection.jid, to: jid + '/' + $own_name, type: 'unavailable'}))
  221 + Jabber.insert_or_update_group(jid, 'offline');
  222 + sort_conversations();
  223 + if(push)
  224 + $.post('/chat/leave', {room_id: jid});
  225 + },
  226 +
  227 + update_chat_title: function () {
  228 + var friends_online = $('#buddy-list #friends .buddy-list.online li').length;
  229 + $('#friends-online').text(friends_online);
  230 + var friends_offline = $('#buddy-list #friends .buddy-list.offline li').length;
  231 + $('#friends-offline').text(friends_offline);
  232 + var groups_online = $('#buddy-list #rooms .buddy-list li').length;
  233 + $('#groups-online').text(groups_online);
  234 + },
  235 +
  236 + on_connect: function (status) {
  237 + switch (status) {
  238 + case Strophe.Status.CONNECTING:
  239 + log('connecting...');
  240 + break;
  241 + case Strophe.Status.CONNFAIL:
  242 + log('failed to connect');
  243 + setTimeout(function(){Jabber.connect()}, 10000);
  244 + break;
  245 + case Strophe.Status.DISCONNECTING:
  246 + log('disconnecting...');
  247 + $('#buddy-list .toolbar').addClass('small-loading-dark');
  248 + break;
  249 + case Strophe.Status.DISCONNECTED:
  250 + log('disconnected');
  251 + $('#buddy-list ul.buddy-list, .occupants ul.occupant-list').html('');
197 Jabber.update_chat_title(); 252 Jabber.update_chat_title();
198 - sort_conversations();  
199 - if(push)  
200 - $.post('/chat/join', {room_id: jid});  
201 - },  
202 -  
203 - leave_room: function(jid, push) {  
204 - if(push == undefined)  
205 - push = true 253 + $('#buddy-list .toolbar').removeClass('small-loading-dark');
  254 + $('textarea').prop('disabled', 'disabled');
  255 + if(Jabber.presence_status != 'offline')
  256 + Jabber.connect();
  257 + break;
  258 + case Strophe.Status.CONNECTED:
  259 + log('connected');
  260 + case Strophe.Status.ATTACHED:
  261 + log('XMPP/BOSH session attached');
  262 + $('#buddy-list .toolbar').removeClass('small-loading-dark');
  263 + $('textarea').prop('disabled', '');
  264 + break;
  265 + }
  266 + },
  267 +
  268 + on_roster: function (iq) {
  269 + log('receiving roster');
  270 + var profiles = [];
  271 + var contacts_to_insert = {};
  272 + var groups_to_insert = [];
  273 +
  274 + $(iq).find('item').each(function () {
  275 + var jid = $(this).attr('jid');
  276 + profiles.push(getIdentifier(jid));
  277 + var name = $(this).attr('name') || jid;
206 var jid_id = Jabber.jid_to_id(jid); 278 var jid_id = Jabber.jid_to_id(jid);
207 - var conversation_id = Jabber.conversation_prefix + jid_id;  
208 - var button = $('#' + conversation_id + ' .leave');  
209 - button.hide();  
210 - button.siblings('.join').show();  
211 - Jabber.connection.send($pres({from: Jabber.connection.jid, to: jid + '/' + $own_name, type: 'unavailable'}))  
212 - Jabber.insert_or_update_group(jid, 'offline');  
213 - sort_conversations();  
214 - if(push)  
215 - $.post('/chat/leave', {room_id: jid});  
216 - },  
217 -  
218 - update_chat_title: function () {  
219 - var friends_online = $('#buddy-list #friends .buddy-list.online li').length;  
220 - $('#friends-online').text(friends_online);  
221 - var friends_offline = $('#buddy-list #friends .buddy-list.offline li').length;  
222 - $('#friends-offline').text(friends_offline);  
223 - var groups_online = $('#buddy-list #rooms .buddy-list li').length;  
224 - $('#groups-online').text(groups_online);  
225 - },  
226 -  
227 - on_connect: function (status) {  
228 - switch (status) {  
229 - case Strophe.Status.CONNECTING:  
230 - log('connecting...');  
231 - break;  
232 - case Strophe.Status.CONNFAIL:  
233 - log('failed to connect');  
234 - break;  
235 - case Strophe.Status.DISCONNECTING:  
236 - log('disconnecting...');  
237 - $('#buddy-list .toolbar').addClass('small-loading-dark');  
238 - break;  
239 - case Strophe.Status.DISCONNECTED:  
240 - log('disconnected');  
241 - $('#buddy-list ul.buddy-list, .occupants ul.occupant-list').html('');  
242 - Jabber.update_chat_title();  
243 - $('#buddy-list .toolbar').removeClass('small-loading-dark');  
244 - $('textarea').prop('disabled', 'disabled');  
245 - break;  
246 - case Strophe.Status.CONNECTED:  
247 - log('connected');  
248 - case Strophe.Status.ATTACHED:  
249 - log('XMPP/BOSH session attached');  
250 - $('#buddy-list .toolbar').removeClass('small-loading-dark');  
251 - $('textarea').prop('disabled', '');  
252 - break;  
253 - }  
254 - },  
255 -  
256 - on_roster: function (iq) {  
257 - log('receiving roster');  
258 - $(iq).find('item').each(function () {  
259 - var jid = $(this).attr('jid');  
260 - var name = $(this).attr('name') || jid;  
261 - var jid_id = Jabber.jid_to_id(jid);  
262 - Jabber.insert_or_update_contact(jid, name);  
263 - });  
264 - //TODO Add groups through roster too...  
265 - $.ajax({  
266 - url: '/chat/roster_groups',  
267 - dataType: 'json',  
268 - success: function(data){  
269 - data.each(function(room){  
270 - var jid_id = Jabber.jid_to_id(room.jid);  
271 - Jabber.jids[jid_id] = {jid: room.jid, name: room.name, type: 'groupchat'};  
272 - //FIXME This must check on session if the user is inside the room...  
273 - Jabber.insert_or_update_group(room.jid, 'offline'); 279 + contacts_to_insert[jid] = name;
  280 + });
  281 +
  282 + //TODO Add groups through roster too...
  283 + $.ajax({
  284 + url: '/chat/roster_groups',
  285 + dataType: 'json',
  286 + success: function(data){
  287 + $(data).each(function(index, room){
  288 + profiles.push(getIdentifier(room.jid));
  289 + var jid_id = Jabber.jid_to_id(room.jid);
  290 + Jabber.jids[jid_id] = {jid: room.jid, name: room.name, type: 'groupchat'};
  291 + //FIXME This must check on session if the user is inside the room...
  292 + groups_to_insert.push(room.jid);
  293 +
  294 + });
  295 + $.getJSON('/chat/avatars', {profiles: profiles}, function(data) {
  296 + for(identifier in data)
  297 + Jabber.avatars[identifier] = data[identifier];
  298 +
  299 + // Insert contacts
  300 + for(contact_jid in contacts_to_insert)
  301 + Jabber.insert_or_update_contact(contact_jid, contacts_to_insert[contact_jid]);
  302 +
  303 + // Insert groups
  304 + for (var i = 0; i < groups_to_insert.length; i++)
  305 + Jabber.insert_or_update_group(groups_to_insert[i], 'offline');
  306 +
  307 + $.getJSON('/chat/recent_conversations', {}, function(data) {
  308 + Jabber.conversations_order = data;
  309 + sort_conversations();
274 }); 310 });
275 - },  
276 - error: function(data, textStatus, jqXHR){  
277 - console.log(data);  
278 - },  
279 - });  
280 - sort_conversations();  
281 - // set up presence handler and send initial presence  
282 - Jabber.connection.addHandler(Jabber.on_presence, null, "presence");  
283 - Jabber.send_availability_status(Jabber.presence_status);  
284 - load_defaults();  
285 - },  
286 -  
287 - // NOTE: cause Noosfero store's rosters in database based on friendship relation between people  
288 - // these event never occurs cause jabber service (ejabberd) didn't know when a roster was changed  
289 - on_roster_changed: function (iq) {  
290 - log('roster changed');  
291 - $(iq).find('item').each(function () {  
292 - var sub = $(this).attr('subscription');  
293 - var jid = $(this).attr('jid');  
294 - var name = $(this).attr('name') || jid;  
295 - if (sub == 'remove') {  
296 - // contact is being removed  
297 - Jabber.remove_contact(jid);  
298 - } else {  
299 - // contact is being added or modified  
300 - Jabber.insert_or_update_contact(jid, name);  
301 - }  
302 - });  
303 - return true;  
304 - },  
305 -  
306 - parse: function (stanza) {  
307 - var result = {};  
308 - if (Strophe.isTagEqual(stanza, 'presence')) {  
309 - result.from = $(stanza).attr('from');  
310 - result.type = $(stanza).attr('type');  
311 - if (result.type == 'unavailable') {  
312 - result.show = 'offline';  
313 - } else {  
314 - var show = $(stanza).find("show").text();  
315 - if (show === "" || show == "chat") {  
316 - result.show = 'chat';  
317 - }  
318 - else if (show == "dnd" || show == "xa") {  
319 - result.show = 'dnd';  
320 - }  
321 - else {  
322 - result.show = 'away';  
323 - }  
324 - }  
325 - if ($(stanza).find('x[xmlns="'+ Strophe.NS.MUC_USER +'"]').length > 0) {  
326 - result.is_from_room = true;  
327 - result.from_user = $(stanza).find('x item').attr('jid');  
328 - if ($(stanza).find('x item').attr('affiliation') == 'owner') {  
329 - result.awaiting_configuration = ($(stanza).find('x status').attr('code') == '201');  
330 - }  
331 - } 311 +
  312 + // set up presence handler and send initial presence
  313 + Jabber.connection.addHandler(Jabber.on_presence, null, "presence");
  314 + Jabber.send_availability_status(Jabber.presence_status);
  315 + load_defaults();
  316 + });
  317 + },
  318 + error: function(data, textStatus, jqXHR){
  319 + console.log(data);
  320 + },
  321 + });
  322 +
  323 + },
  324 +
  325 + // NOTE: cause Noosfero store's rosters in database based on friendship relation between people
  326 + // these event never occurs cause jabber service (ejabberd) didn't know when a roster was changed
  327 + on_roster_changed: function (iq) {
  328 + log('roster changed');
  329 + $(iq).find('item').each(function () {
  330 + var sub = $(this).attr('subscription');
  331 + var jid = $(this).attr('jid');
  332 + var name = $(this).attr('name') || jid;
  333 + if (sub == 'remove') {
  334 + // contact is being removed
  335 + Jabber.remove_contact(jid);
  336 + } else {
  337 + // contact is being added or modified
  338 + Jabber.insert_or_update_contact(jid, name);
332 } 339 }
333 - else if (Strophe.isTagEqual(stanza, 'message')) {  
334 - result.from = $(stanza).attr('from');  
335 - result.body = $(stanza).find('body').text();  
336 - if ($(stanza).find('error').length > 0) {  
337 - result.error = $(stanza).find('error text').text();  
338 - if (!result.error && $(stanza).find('error').find('service-unavailable').length > 0) {  
339 - result.error = $user_unavailable_error;  
340 - }  
341 - } 340 + });
  341 + return true;
  342 + },
  343 +
  344 + parse: function (stanza) {
  345 + var result = {};
  346 + if (Strophe.isTagEqual(stanza, 'presence')) {
  347 + result.from = $(stanza).attr('from');
  348 + result.type = $(stanza).attr('type');
  349 + if (result.type == 'unavailable') {
  350 + result.show = 'offline';
  351 + } else {
  352 + var show = $(stanza).find("show").text();
  353 + if (show === "" || show == "chat") {
  354 + result.show = 'chat';
  355 + }
  356 + else if (show == "dnd" || show == "xa") {
  357 + result.show = 'dnd';
  358 + }
  359 + else {
  360 + result.show = 'away';
  361 + }
342 } 362 }
343 - return result;  
344 - },  
345 -  
346 - on_presence: function (presence) {  
347 - presence = Jabber.parse(presence);  
348 - if (presence.type != 'error') {  
349 - if (presence.is_from_room) {  
350 - log('receiving room presence from ' + presence.from + ' as ' + presence.show);  
351 - var name = Strophe.getResourceFromJid(presence.from);  
352 - if (presence.from_user) {  
353 - Jabber.insert_or_update_occupant(presence.from_user, name, presence.show, presence.from);  
354 - }  
355 - else {  
356 - log('ooops! user jid not found in presence stanza');  
357 - }  
358 - if (presence.awaiting_configuration) {  
359 - log('sending instant room configuration to ' + Strophe.getBareJidFromJid(presence.from));  
360 - Jabber.connection.sendIQ(  
361 - $iq({type: 'set', to: Strophe.getBareJidFromJid(presence.from)})  
362 - .c('query', {xmlns: Strophe.NS.MUC_OWNER})  
363 - .c('x', {xmlns: Strophe.NS.DATA_FORMS, type: 'submit'})  
364 - );  
365 - }  
366 - }  
367 - else {  
368 - log('receiving contact presence from ' + presence.from + ' as ' + presence.show);  
369 - var jid = Strophe.getBareJidFromJid(presence.from);  
370 - if (jid != Jabber.connection.jid) {  
371 - var name = Jabber.name_of(Jabber.jid_to_id(jid));  
372 - Jabber.insert_or_update_contact(jid, name, presence.show);  
373 - Jabber.update_chat_title();  
374 - }  
375 - else {  
376 - // why server sends presence from myself to me?  
377 - log('ignoring presence from myself');  
378 - if(presence.show=='offline') {  
379 - console.log(Jabber.presence_status);  
380 - Jabber.send_availability_status(Jabber.presence_status);  
381 - }  
382 - }  
383 - } 363 + if ($(stanza).find('x[xmlns="'+ Strophe.NS.MUC_USER +'"]').length > 0) {
  364 + result.is_from_room = true;
  365 + result.from_user = $(stanza).find('x item').attr('jid');
  366 + if ($(stanza).find('x item').attr('affiliation') == 'owner') {
  367 + result.awaiting_configuration = ($(stanza).find('x status').attr('code') == '201');
  368 + }
384 } 369 }
385 - return true;  
386 - },  
387 -  
388 - on_private_message: function (message) {  
389 - message = Jabber.parse(message);  
390 - log('receiving message from ' + message.from);  
391 - var jid = Strophe.getBareJidFromJid(message.from);  
392 - var jid_id = Jabber.jid_to_id(jid);  
393 - var name = Jabber.name_of(jid_id);  
394 - create_conversation_tab(name, jid_id);  
395 - Jabber.show_message(jid, name, escape_html(message.body), 'other', Strophe.getNodeFromJid(jid));  
396 - notifyMessage(message);  
397 - return true;  
398 - },  
399 -  
400 - on_public_message: function (message) {  
401 - message = Jabber.parse(message);  
402 - log('receiving message from ' + message.from);  
403 - var name = Strophe.getResourceFromJid(message.from);  
404 - // is a message from the room itself  
405 - if (! name) {  
406 - Jabber.show_notice(Jabber.jid_to_id(message.from), message.body);  
407 - }  
408 - // is a message from another user, not mine  
409 - else if ($own_name != name) {  
410 - var jid = Jabber.rooms[Jabber.jid_to_id(message.from)][name];  
411 - Jabber.show_message(message.from, name, escape_html(message.body), name, Strophe.getNodeFromJid(jid)); 370 + }
  371 + else if (Strophe.isTagEqual(stanza, 'message')) {
  372 + result.from = $(stanza).attr('from');
  373 + result.body = $(stanza).find('body').text();
  374 + if ($(stanza).find('error').length > 0) {
  375 + result.error = $(stanza).find('error text').text();
  376 + if (!result.error && $(stanza).find('error').find('service-unavailable').length > 0) {
  377 + result.error = $user_unavailable_error;
  378 + }
412 } 379 }
413 - notifyMessage(message);  
414 - return true;  
415 - },  
416 -  
417 - on_message_error: function (message) {  
418 - message = Jabber.parse(message)  
419 - var jid = Strophe.getBareJidFromJid(message.from);  
420 - log('Receiving error message from ' + jid);  
421 - var body = Jabber.template('.error-message').replace('%{text}', message.error);  
422 - Jabber.show_message(jid, Jabber.name_of(Jabber.jid_to_id(jid)), body, 'other', Strophe.getNodeFromJid(jid));  
423 - return true;  
424 - },  
425 -  
426 - on_muc_support: function(iq) {  
427 - if ($(iq).find('identity[category=conference]').length > 0 && $(iq).find('feature[var="'+ Strophe.NS.MUC +'"]').length > 0) {  
428 - var name = $(iq).find('identity[category=conference]').attr('name');  
429 - log('muc support found with identity '+ name);  
430 - Jabber.muc_supported = true; 380 + }
  381 + return result;
  382 + },
  383 +
  384 + on_presence: function (presence) {
  385 + presence = Jabber.parse(presence);
  386 + if (presence.type != 'error') {
  387 + if (presence.is_from_room) {
  388 + log('receiving room presence from ' + presence.from + ' as ' + presence.show);
  389 + var name = Strophe.getResourceFromJid(presence.from);
  390 + if (presence.from_user) {
  391 + Jabber.insert_or_update_occupant(presence.from_user, name, presence.show, presence.from);
  392 + }
  393 + else {
  394 + log('ooops! user jid not found in presence stanza');
  395 + }
  396 + if (presence.awaiting_configuration) {
  397 + log('sending instant room configuration to ' + Strophe.getBareJidFromJid(presence.from));
  398 + Jabber.connection.sendIQ(
  399 + $iq({type: 'set', to: Strophe.getBareJidFromJid(presence.from)})
  400 + .c('query', {xmlns: Strophe.NS.MUC_OWNER})
  401 + .c('x', {xmlns: Strophe.NS.DATA_FORMS, type: 'submit'})
  402 + );
  403 + }
431 } 404 }
432 else { 405 else {
433 - log('muc support not found'); 406 + log('receiving contact presence from ' + presence.from + ' as ' + presence.show);
  407 + var jid = Strophe.getBareJidFromJid(presence.from);
  408 + if (jid != Jabber.connection.jid) {
  409 + var jid_id = Jabber.jid_to_id(jid);
  410 + var name = Jabber.name_of(jid_id);
  411 + if(presence.show == 'chat')
  412 + Jabber.remove_notice(jid_id);
  413 + Jabber.insert_or_update_contact(jid, name, presence.show);
  414 + Jabber.update_chat_title();
  415 + }
  416 + else {
  417 + // why server sends presence from myself to me?
  418 + log('ignoring presence from myself');
  419 + if(presence.show=='offline') {
  420 + Jabber.send_availability_status(Jabber.presence_status);
  421 + }
  422 + }
434 } 423 }
435 - }, 424 + }
  425 + return true;
  426 + },
436 427
437 - attach_connection: function(data) {  
438 - // create the connection and attach it  
439 - Jabber.connection = new Strophe.Connection(Jabber.bosh_service);  
440 - Jabber.connection.attach(data.jid, data.sid, data.rid, Jabber.on_connect); 428 + on_private_message: function (message) {
  429 + message = Jabber.parse(message);
  430 + log('receiving message from ' + message.from);
  431 + var jid = Strophe.getBareJidFromJid(message.from);
  432 + var jid_id = Jabber.jid_to_id(jid);
  433 + var name = Jabber.name_of(jid_id);
  434 + create_conversation_tab(name, jid_id);
  435 + Jabber.show_message(jid, name, escape_html(message.body), 'other', Strophe.getNodeFromJid(jid));
  436 + renew_conversation_order(jid);
  437 + notifyMessage(message);
  438 + return true;
  439 + },
  440 +
  441 + on_public_message: function (message) {
  442 + message = Jabber.parse(message);
  443 + log('receiving message from ' + message.from);
  444 + var name = Strophe.getResourceFromJid(message.from);
  445 + // is a message from the room itself
  446 + if (! name) {
  447 + // FIXME Ignoring message from room for now.
  448 + // Jabber.show_notice(Jabber.jid_to_id(message.from), message.body);
  449 + }
  450 + // is a message from another user, not mine
  451 + else if ($own_name != name) {
  452 + var jid = Jabber.rooms[Jabber.jid_to_id(message.from)][name];
  453 + Jabber.show_message(message.from, name, escape_html(message.body), name, Strophe.getNodeFromJid(jid));
  454 + renew_conversation_order(jid);
  455 + notifyMessage(message);
  456 + }
  457 + return true;
  458 + },
441 459
442 - // handle get roster list (buddy list)  
443 - Jabber.connection.sendIQ($iq({type: 'get'}).c('query', {xmlns: Strophe.NS.ROSTER}), Jabber.on_roster); 460 + on_message_error: function (message) {
  461 + },
444 462
445 - // handle presence updates in roster list  
446 - Jabber.connection.addHandler(Jabber.on_roster_changed, 'jabber:iq:roster', 'iq', 'set'); 463 + on_muc_support: function(iq) {
  464 + if ($(iq).find('identity[category=conference]').length > 0 && $(iq).find('feature[var="'+ Strophe.NS.MUC +'"]').length > 0) {
  465 + var name = $(iq).find('identity[category=conference]').attr('name');
  466 + log('muc support found with identity '+ name);
  467 + Jabber.muc_supported = true;
  468 + }
  469 + else {
  470 + log('muc support not found');
  471 + }
  472 + },
447 473
448 - // Handle messages  
449 - Jabber.connection.addHandler(Jabber.on_private_message, null, "message", "chat"); 474 + attach_connection: function(data) {
  475 + // create the connection and attach it
  476 + Jabber.connection = new Strophe.Connection(Jabber.bosh_service);
  477 + Jabber.connection.attach(data.jid, data.sid, data.rid, Jabber.on_connect);
450 478
451 - // Handle conference messages  
452 - Jabber.connection.addHandler(Jabber.on_public_message, null, "message", "groupchat"); 479 + // handle get roster list (buddy list)
  480 + Jabber.connection.sendIQ($iq({type: 'get'}).c('query', {xmlns: Strophe.NS.ROSTER}), Jabber.on_roster);
453 481
454 - // Handle message errors  
455 - Jabber.connection.addHandler(Jabber.on_message_error, null, "message", "error"); 482 + // handle presence updates in roster list
  483 + Jabber.connection.addHandler(Jabber.on_roster_changed, 'jabber:iq:roster', 'iq', 'set');
456 484
457 - // discovering MUC support  
458 - Jabber.connection.sendIQ(  
459 - $iq({type: 'get', from: Jabber.connection.jid, to: Jabber.muc_domain})  
460 - .c('query', {xmlns: Strophe.NS.DISCO_INFO}),  
461 - Jabber.on_muc_support  
462 - ); 485 + // Handle messages
  486 + Jabber.connection.addHandler(Jabber.on_private_message, null, "message", "chat");
463 487
464 - // uncomment for extra debugging  
465 - //Strophe.log = function (lvl, msg) { log(msg); };  
466 - }, 488 + // Handle conference messages
  489 + Jabber.connection.addHandler(Jabber.on_public_message, null, "message", "groupchat");
467 490
468 - connect: function() {  
469 - if (Jabber.connection && Jabber.connection.connected) {  
470 - Jabber.send_availability_status(Jabber.presence_status);  
471 - }  
472 - else {  
473 - log('starting XMPP/BOSH session...');  
474 - $('#buddy-list .toolbar').removeClass('small-loading-dark').addClass('small-loading-dark');  
475 - $('.dialog-error').hide();  
476 - $.ajax({  
477 - url: '/chat/start_session',  
478 - dataType: 'json',  
479 - success: function(data) {  
480 - Jabber.attach_connection(data)  
481 - },  
482 - error: function(error) {  
483 - $('#buddy-list .toolbar').removeClass('small-loading-dark');  
484 - $('#buddy-list .dialog-error')  
485 - .html(error.responseText)  
486 - .show('highlight')  
487 - .unbind('click')  
488 - .click(function() { $(this).hide('highlight'); });  
489 - }  
490 - });  
491 - }  
492 - },  
493 -  
494 - deliver_message: function(jid, body) {  
495 - var type = Jabber.type_of(Jabber.jid_to_id(jid));  
496 - var message = $msg({to: jid, from: Jabber.connection.jid, "type": type})  
497 - .c('body').t(body).up()  
498 - .c('active', {xmlns: Strophe.NS.CHAT_STATES});  
499 - Jabber.connection.send(message);  
500 - Jabber.show_message(jid, $own_name, escape_html(body), 'self', Strophe.getNodeFromJid(Jabber.connection.jid));  
501 - move_conversation_to_the_top(jid);  
502 - },  
503 -  
504 - is_a_room: function(jid_id) {  
505 - return Jabber.type_of(jid_id) == 'groupchat';  
506 - },  
507 -  
508 - show_notice: function(jid_id, msg) {  
509 - var tab_id = '#' + Jabber.conversation_prefix + jid_id;  
510 - var notice = $(tab_id).find('.history .notice');  
511 - if (notice.length > 0)  
512 - notice.html(msg)  
513 - else  
514 - $(tab_id).find('.history').append("<span class='notice'>" + msg + "</span>");  
515 - }  
516 - };  
517 -  
518 - $('#chat-connect').live('click', function() {  
519 - Jabber.presence_status = 'chat';  
520 - Jabber.connect();  
521 - });  
522 -  
523 - $('#chat-disconnect').click(function() {  
524 - disconnect();  
525 - });  
526 -  
527 - $('#chat-busy').click(function() {  
528 - Jabber.presence_status = 'dnd';  
529 - Jabber.connect();  
530 - });  
531 -  
532 - $('#chat-retry').live('click', function() {  
533 - Jabber.presence_status = Jabber.presence_status || 'chat';  
534 - Jabber.connect();  
535 - });  
536 -  
537 - $('.conversation textarea').live('keydown', function(e) {  
538 - if (e.keyCode == 13) {  
539 - var jid = $(this).attr('data-to');  
540 - var body = $(this).val();  
541 - body = body.stripScripts();  
542 - save_message(jid, body);  
543 - Jabber.deliver_message(jid, body);  
544 - $(this).val('');  
545 - return false;  
546 - }  
547 - });  
548 -  
549 - function save_message(jid, body) {  
550 - $.post('/chat/save_message', {  
551 - to: getIdentifier(jid),  
552 - body: body  
553 - });  
554 - } 491 + // Handle message errors
  492 + Jabber.connection.addHandler(Jabber.on_message_error, null, "message", "error");
555 493
556 - // open new conversation or change to already opened tab  
557 - $('#buddy-list .buddies li a').live('click', function() {  
558 - var jid_id = $(this).attr('id');  
559 - var name = Jabber.name_of(jid_id);  
560 - var conversation = create_conversation_tab(name, jid_id);  
561 -  
562 - $('.conversation').hide();  
563 - conversation.show();  
564 - count_unread_messages(jid_id, true);  
565 - if(conversation.find('.chat-offset-container-0').length == 0)  
566 - recent_messages(Jabber.jid_of(jid_id));  
567 - conversation.find('.conversation .input-div textarea.input').focus();  
568 - $.post('/chat/tab', {tab_id: jid_id});  
569 - });  
570 -  
571 - // put name into text area when click in one occupant  
572 - $('.occupants .occupant-list li a').live('click', function() {  
573 - var jid_id = $(this).attr('data-id');  
574 - var name = Jabber.name_of(jid_id);  
575 - var val = $('.conversation textarea:visible').val();  
576 - $('.conversation textarea:visible').focus().val(val + name + ', ');  
577 - }); 494 + // discovering MUC support
  495 + Jabber.connection.sendIQ(
  496 + $iq({type: 'get', from: Jabber.connection.jid, to: Jabber.muc_domain})
  497 + .c('query', {xmlns: Strophe.NS.DISCO_INFO}),
  498 + Jabber.on_muc_support
  499 + );
578 500
579 - $('#chat .conversation .history').live('click', function() {  
580 - $('.conversation textarea:visible').focus();  
581 - }); 501 + // uncomment for extra debugging
  502 + //Strophe.log = function (lvl, msg) { log(msg); };
  503 + },
582 504
583 - function toggle_chat_window() {  
584 - if(jQuery('#conversations .conversation').length == 0) jQuery('.buddies a').first().click();  
585 - jQuery('#chat').toggleClass('opened');  
586 - jQuery('#chat-label').toggleClass('opened');  
587 - } 505 + connect: function() {
  506 + if (Notification.permission !== "granted" && Notification.permission !== "denied") {
  507 + Notification.requestPermission(function (permission) {
  508 + if (!('permission' in Notification)) {
  509 + Notification.permission = permission;
  510 + }
  511 + });
  512 + }
588 513
589 - function load_conversation(jid) {  
590 - var jid_id = Jabber.jid_to_id(jid);  
591 - var name = Jabber.name_of(jid_id);  
592 - if (jid) {  
593 - if (Strophe.getDomainFromJid(jid) == Jabber.muc_domain) {  
594 - if (Jabber.muc_supported) {  
595 - log('opening groupchat with ' + jid);  
596 - Jabber.jids[jid_id] = {jid: jid, name: name, type: 'groupchat'};  
597 - var conversation = create_conversation_tab(name, jid_id);  
598 - Jabber.enter_room(jid);  
599 - recent_messages(jid);  
600 - return conversation;  
601 - }  
602 - }  
603 - else {  
604 - log('opening chat with ' + jid);  
605 - Jabber.jids[jid_id] = {jid: jid, name: name, type: 'friendchat'};  
606 - var conversation = create_conversation_tab(name, jid_id);  
607 - recent_messages(jid);  
608 - return conversation;  
609 - } 514 + if (Jabber.connection && Jabber.connection.connected) {
  515 + Jabber.send_availability_status(Jabber.presence_status);
610 } 516 }
611 - }  
612 -  
613 - function open_conversation(jid) {  
614 - var conversation = load_conversation(jid);  
615 - var jid_id = $(this).attr('id');  
616 -  
617 - $('.conversation').hide();  
618 - conversation.show();  
619 - count_unread_messages(jid_id, true);  
620 - if(conversation.find('.chat-offset-container-0').length == 0)  
621 - recent_messages(Jabber.jid_of(jid_id));  
622 - conversation.find('.input').focus();  
623 - $('#chat').addClass('opened');  
624 - $('#chat-label').addClass('opened');  
625 - $.post('/chat/tab', {tab_id: jid_id});  
626 - }  
627 -  
628 - function create_conversation_tab(title, jid_id) {  
629 - var conversation_id = Jabber.conversation_prefix + jid_id;  
630 - var conversation = $('#' + conversation_id);  
631 - if (conversation.length > 0) {  
632 - return conversation; 517 + else {
  518 + log('starting XMPP/BOSH session...');
  519 + $('#buddy-list .toolbar').removeClass('small-loading-dark').addClass('small-loading-dark');
  520 + $('.dialog-error').hide();
  521 + $.ajax({
  522 + url: '/chat/start_session',
  523 + dataType: 'json',
  524 + success: function(data) {
  525 + Jabber.attach_connection(data)
  526 + },
  527 + error: function(error) {
  528 + $('#buddy-list .toolbar').removeClass('small-loading-dark');
  529 + $('#buddy-list .dialog-error')
  530 + .html(error.responseText)
  531 + .show('highlight')
  532 + .unbind('click')
  533 + .click(function() { $(this).hide('highlight'); });
  534 + }
  535 + });
633 } 536 }
  537 + },
634 538
635 - var jid = Jabber.jid_of(jid_id);  
636 - var identifier = getIdentifier(jid); 539 + deliver_message: function(jid, body) {
  540 + var jid_id = Jabber.jid_to_id(jid);
  541 + var type = Jabber.type_of(jid_id);
  542 + var presence = Jabber.presence_of(jid_id);
  543 + var message = $msg({to: jid, from: Jabber.connection.jid, "type": type})
  544 + .c('body').t(body).up()
  545 + .c('active', {xmlns: Strophe.NS.CHAT_STATES});
  546 + Jabber.connection.send(message);
  547 + Jabber.show_message(jid, $own_name, escape_html(body), 'self', Strophe.getNodeFromJid(Jabber.connection.jid));
  548 + save_message(jid, body);
  549 + renew_conversation_order(jid);
  550 + move_conversation_to_the_top(jid);
  551 + if (presence == 'offline')
  552 + Jabber.show_notice(jid_id, $user_unavailable_error);
  553 + },
  554 +
  555 + is_a_room: function(jid_id) {
  556 + return Jabber.type_of(jid_id) == 'groupchat';
  557 + },
  558 +
  559 + show_notice: function(jid_id, msg) {
  560 + var tab_id = '#' + Jabber.conversation_prefix + jid_id;
  561 + var history = $(tab_id).find('.history');
  562 + var notice = $(tab_id).find('.history .notice');
  563 + if (notice.length > 0)
  564 + notice.html(msg)
  565 + else
  566 + $(tab_id).find('.history').append("<span class='notice'>" + msg + "</span>");
  567 + history.scrollTo({top:'100%', left:'0%'});
  568 + },
  569 +
  570 + remove_notice: function(jid_id) {
  571 + var tab_id = '#' + Jabber.conversation_prefix + jid_id;
  572 + var notice = $(tab_id).find('.history .notice').remove();
  573 + },
  574 + };
637 575
638 - var panel = $('#chat-templates .conversation').clone().appendTo($conversations).attr('id', conversation_id);  
639 - panel.find('.chat-target .avatar').replaceWith(getAvatar(identifier));  
640 - panel.find('.chat-target .other-name').html(title);  
641 - $('#chat .history').perfectScrollbar(); 576 + $('#chat-connect').live('click', function() {
  577 + Jabber.presence_status = 'chat';
  578 + Jabber.connect();
  579 + $('#chat .simplemenu-submenu').hide();
  580 + return false;
  581 + });
642 582
643 - panel.find('.history').scroll(function(){  
644 - if($(this).scrollTop() == 0){  
645 - var offset = panel.find('.message p').size();  
646 - recent_messages(jid, offset);  
647 - }  
648 - }); 583 + $('#chat-disconnect').click(function() {
  584 + disconnect();
  585 + $('#chat .simplemenu-submenu').hide();
  586 + return false;
  587 + });
  588 +
  589 + $('#chat-busy').click(function() {
  590 + Jabber.presence_status = 'dnd';
  591 + Jabber.connect();
  592 + $('#chat .simplemenu-submenu').hide();
  593 + return false;
  594 + });
649 595
650 - var textarea = panel.find('textarea');  
651 - textarea.attr('name', panel.id); 596 + $('#chat-retry').live('click', function() {
  597 + Jabber.presence_status = Jabber.presence_status || 'chat';
  598 + Jabber.connect();
  599 + return false;
  600 + });
  601 +
  602 + $('.conversation textarea').live('keydown', function(e) {
  603 + if (e.keyCode == 13) {
  604 + var jid = $(this).attr('data-to');
  605 + var body = $(this).val();
  606 + body = $('<div>'+ body +'</div>').find('script,noscript,style').remove().end().html();
  607 + Jabber.deliver_message(jid, body);
  608 + $(this).val('');
  609 + return false;
  610 + }
  611 + });
652 612
653 - if (Jabber.is_a_room(jid_id)) {  
654 - panel.append(Jabber.template('.occupant-list-template'));  
655 - panel.find('.history').addClass('room');  
656 - var room_actions = $('#chat-templates .room-action').clone();  
657 - room_actions.data('jid', jid);  
658 - panel.find('.history').after(room_actions);  
659 - $('#chat .occupants .occupant-list').perfectScrollbar(); 613 + function save_message(jid, body) {
  614 + $.post('/chat/save_message', {
  615 + to: getIdentifier(jid),
  616 + body: body
  617 + }, function(data){
  618 + if(data.status > 0){
  619 + console.log(data.message);
  620 + if(data.backtrace) console.log(data.backtrace);
660 } 621 }
661 - textarea.attr('data-to', jid);  
662 -  
663 - return panel;  
664 - }  
665 -  
666 - function ensure_scroll(jid, offset) {  
667 - var jid_id = Jabber.jid_to_id(jid);  
668 - var history = jQuery('#conversation-'+jid_id+' .history');  
669 - // Load more messages if was not enough to show the scroll  
670 - if(history.prop('scrollHeight') - history.prop('clientHeight') <= 0){  
671 - var offset = history.find('.message p').size();  
672 - recent_messages(jid, offset);  
673 - }  
674 - }  
675 -  
676 - function recent_messages(jid, offset) {  
677 - if (Jabber.no_more_messages[jid]) return;  
678 -  
679 - if(!offset) offset = 0;  
680 - start_fetching('.history');  
681 - $.getJSON('/chat/recent_messages', {identifier: getIdentifier(jid), offset: offset}, function(data) {  
682 - // Register if no more messages returned and stop trying to load  
683 - // more messages in the future.  
684 - if(data.length == 0)  
685 - Jabber.no_more_messages[jid] = true;  
686 -  
687 - $.each(data, function(i, message) {  
688 - var body = message['body'];  
689 - var from = message['from'];  
690 - var to = message['to'];  
691 - var date = message['created_at'];  
692 - var who = from['id']==getCurrentIdentifier() ? 'self' : from['id']  
693 -  
694 - Jabber.show_message(jid, from['name'], body, who, from['id'], date, offset);  
695 - });  
696 - stop_fetching('.history');  
697 - ensure_scroll(jid, offset);  
698 - });  
699 - }  
700 -  
701 - function move_conversation_to_the_top(jid) {  
702 - id = Jabber.jid_to_id(jid);  
703 - var link = $('#'+id);  
704 - var li = link.closest('li');  
705 - var ul = link.closest('ul');  
706 - ul.prepend(li);  
707 - }  
708 -  
709 - function sort_conversations() {  
710 - $.getJSON('/chat/recent_conversations', {}, function(data) {  
711 - $.each(data['order'], function(i, identifier) {  
712 - move_conversation_to_the_top(identifier+'-'+data['domain']);  
713 - })  
714 - })  
715 - }  
716 -  
717 - function load_defaults() {  
718 - $.getJSON('/chat/my_session', {}, function(data) {  
719 - $.each(data.rooms, function(i, room_jid) {  
720 - $('#chat').trigger('opengroup', room_jid);  
721 - })  
722 -  
723 - $('#'+data.tab_id).click();  
724 -  
725 - if(data.status == 'opened')  
726 - toggle_chat_window();  
727 - })  
728 - }  
729 -  
730 - function count_unread_messages(jid_id, hide) {  
731 - var unread = $('.buddies #'+jid_id+ ' .unread-messages');  
732 - if (hide) {  
733 - unread.hide();  
734 - unread.siblings('img').show();  
735 - Jabber.unread_messages_of(jid_id, 0);  
736 - unread.text(''); 622 + }).fail(function(){
  623 + console.log('500 - Internal server error.')
  624 + });
  625 + }
  626 +
  627 + // open new conversation or change to already opened tab
  628 + $('#buddy-list .buddies li a').live('click', function() {
  629 + var jid = Jabber.jid_of($(this).attr('id'));
  630 + open_conversation(jid);
  631 + return false;
  632 + });
  633 +
  634 + // put name into text area when click in one occupant
  635 + $('.occupants .occupant-list li a').live('click', function() {
  636 + var jid_id = $(this).attr('data-id');
  637 + var name = Jabber.name_of(jid_id);
  638 + var val = $('.conversation textarea:visible').val();
  639 + $('.conversation textarea:visible').focus().val(val + name + ', ');
  640 + return false;
  641 + });
  642 +
  643 + $('#chat .conversation .history').live('click', function() {
  644 + $('.conversation textarea:visible').focus();
  645 + });
  646 +
  647 + function toggle_chat_window() {
  648 + if(jQuery('#conversations .conversation').length == 0) jQuery('.buddies a').first().click();
  649 + jQuery('#chat').toggleClass('opened');
  650 + jQuery('#chat-label').toggleClass('opened');
  651 + }
  652 +
  653 + function load_conversation(jid) {
  654 + var jid_id = Jabber.jid_to_id(jid);
  655 + var name = Jabber.name_of(jid_id);
  656 + if (jid) {
  657 + if (Strophe.getDomainFromJid(jid) == Jabber.muc_domain) {
  658 + if (Jabber.muc_supported) {
  659 + log('opening groupchat with ' + jid);
  660 + Jabber.jids[jid_id] = {jid: jid, name: name, type: 'groupchat'};
  661 + var conversation = create_conversation_tab(name, jid_id);
  662 + Jabber.enter_room(jid);
  663 + recent_messages(jid, 0, true);
  664 + return conversation;
  665 + }
737 } 666 }
738 else { 667 else {
739 - unread.siblings('img').hide();  
740 - unread.css('display', 'inline-block');  
741 - var unread_messages = Jabber.unread_messages_of(jid_id) || 0;  
742 - Jabber.unread_messages_of(jid_id, ++unread_messages);  
743 - unread.text(unread_messages);  
744 - }  
745 - update_total_unread_messages();  
746 - }  
747 -  
748 - function update_total_unread_messages() {  
749 - var total_unread = $('#openchat .unread-messages');  
750 - var sum = 0;  
751 - $('.buddies .unread-messages').each(function() {  
752 - sum += Number($(this).text());  
753 - });  
754 - if(sum>0) {  
755 - total_unread.text(sum);  
756 - } else {  
757 - total_unread.text(''); 668 + log('opening chat with ' + jid);
  669 + Jabber.jids[jid_id] = {jid: jid, name: name, type: 'chat'};
  670 + var conversation = create_conversation_tab(name, jid_id);
  671 + recent_messages(jid, 0, true);
  672 + return conversation;
758 } 673 }
759 - } 674 + }
  675 + }
  676 +
  677 + function open_conversation(jid) {
  678 + var conversation = load_conversation(jid);
  679 + var jid_id = Jabber.jid_to_id(jid);
  680 +
  681 + $('.conversation').hide();
  682 + conversation.show();
  683 + conversation.find('.input').focus();
  684 + $('#chat').addClass('opened');
  685 + $('#chat-label').addClass('opened');
  686 + $.post('/chat/tab', {tab_id: jid_id});
  687 + }
  688 +
  689 + function create_conversation_tab(title, jid_id) {
  690 + var conversation_id = Jabber.conversation_prefix + jid_id;
  691 + var conversation = $('#' + conversation_id);
  692 + if (conversation.length > 0) {
  693 + return conversation;
  694 + }
  695 +
  696 + var jid = Jabber.jid_of(jid_id);
  697 + var identifier = getIdentifier(jid);
760 698
761 - var $conversations = $('#chat-window #conversations'); 699 + var panel = $('#chat-templates .conversation').clone().appendTo($conversations).attr('id', conversation_id);
  700 + panel.find('.chat-target .avatar').replaceWith(getAvatar(identifier));
  701 + panel.find('.chat-target .other-name').html(title);
  702 + $('#chat .history').perfectScrollbar();
762 703
763 - function log(msg) {  
764 - if(Jabber.debug && window.console && window.console.log) {  
765 - var time = new Date();  
766 - window.console.log('['+ time.toTimeString() +'] ' + msg); 704 + panel.find('.history').scroll(function(){
  705 + if($(this).scrollTop() == 0){
  706 + var offset = panel.find('.message p').size();
  707 + recent_messages(jid, offset);
767 } 708 }
768 - } 709 + });
  710 +
  711 + var textarea = panel.find('textarea');
  712 + textarea.attr('name', panel.id);
  713 +
  714 + if (Jabber.is_a_room(jid_id)) {
  715 + panel.append(Jabber.template('.occupant-list-template'));
  716 + panel.find('.history').addClass('room');
  717 + var room_actions = $('#chat-templates .room-action').clone();
  718 + room_actions.data('jid', jid);
  719 + panel.find('.history').after(room_actions);
  720 + $('#chat .occupants .occupant-list').perfectScrollbar();
  721 + }
  722 + textarea.attr('data-to', jid);
  723 +
  724 + return panel;
  725 + }
  726 +
  727 + function ensure_scroll(jid, offset) {
  728 + var jid_id = Jabber.jid_to_id(jid);
  729 + var history = jQuery('#conversation-'+jid_id+' .history');
  730 + // Load more messages if was not enough to show the scroll
  731 + if(history.prop('scrollHeight') - history.prop('clientHeight') <= 0){
  732 + var offset = history.find('.message p').size();
  733 + recent_messages(jid, offset);
  734 + }
  735 + }
  736 +
  737 + function recent_messages(jid, offset, clear_unread) {
  738 + if (Jabber.no_more_messages[jid]) return;
  739 +
  740 + if(!offset) offset = 0;
  741 + start_fetching('.history');
  742 + $.getJSON('/chat/recent_messages', {identifier: getIdentifier(jid), offset: offset}, function(data) {
  743 + // Register if no more messages returned and stop trying to load
  744 + // more messages in the future.
  745 + if(data.length == 0)
  746 + Jabber.no_more_messages[jid] = true;
  747 +
  748 + $.each(data, function(i, message) {
  749 + var body = message['body'];
  750 + var from = message['from'];
  751 + var to = message['to'];
  752 + var date = message['created_at'];
  753 + var who = from['id']==getCurrentIdentifier() ? 'self' : from['id']
  754 +
  755 + Jabber.show_message(jid, from['name'], body, who, from['id'], date, offset);
  756 + });
  757 + stop_fetching('.history');
  758 + ensure_scroll(jid, offset);
769 759
770 - function escape_html(body) {  
771 - return body  
772 - .replace(/&/g, '&amp;')  
773 - .replace(/</g, '&lt;')  
774 - .replace(/>/g, '&gt;');  
775 - } 760 + if(clear_unread){
  761 + var jid_id = Jabber.jid_to_id(jid);
  762 + count_unread_messages(jid_id, true);
  763 + }
  764 + });
  765 + }
  766 +
  767 + function move_conversation_to_the_top(jid) {
  768 + id = Jabber.jid_to_id(jid);
  769 + var link = $('#'+id);
  770 + var li = link.closest('li');
  771 + var ul = link.closest('ul');
  772 + ul.prepend(li);
  773 + }
  774 +
  775 + function renew_conversation_order(jid){
  776 + var i = Jabber.conversations_order.indexOf(jid);
  777 + // Remove element from the list
  778 + if(i >= 0) {
  779 + var elem = Jabber.conversations_order[i];
  780 + var a = Jabber.conversations_order.slice(0,i);
  781 + var b = Jabber.conversations_order.slice(i+1, Jabber.conversations_order.length);
  782 + Jabber.conversations_order = a.concat(b);
  783 + } else
  784 + var elem = jid;
  785 +
  786 + Jabber.conversations_order = Jabber.conversations_order.concat(elem);
  787 + }
  788 +
  789 + function sort_conversations() {
  790 + if(Jabber.conversations_order){
  791 + for (var i = 0; i < Jabber.conversations_order.length; i++)
  792 + move_conversation_to_the_top(Jabber.conversations_order[i]);
  793 + }
  794 + }
  795 +
  796 + function load_defaults() {
  797 + $.getJSON('/chat/my_session', {}, function(data) {
  798 + $.each(data.rooms, function(i, room_jid) {
  799 + load_conversation(room_jid);
  800 + })
  801 +
  802 + $('#'+data.tab_id).click();
  803 +
  804 + if(data.status == 'opened')
  805 + toggle_chat_window();
  806 + })
  807 + }
  808 +
  809 + function count_unread_messages(jid_id, hide) {
  810 + var unread = $('.buddies #'+jid_id+ ' .unread-messages');
  811 + if (hide) {
  812 + unread.hide();
  813 + unread.siblings('img').show();
  814 + Jabber.unread_messages_of(jid_id, 0);
  815 + unread.text('');
  816 + }
  817 + else {
  818 + unread.siblings('img').hide();
  819 + unread.css('display', 'inline-block');
  820 + var unread_messages = Jabber.unread_messages_of(jid_id) || 0;
  821 + Jabber.unread_messages_of(jid_id, ++unread_messages);
  822 + unread.text(unread_messages);
  823 + }
  824 + update_total_unread_messages();
  825 + }
  826 +
  827 + function update_total_unread_messages() {
  828 + var total_unread = $('#unread-messages');
  829 + var sum = 0;
  830 + $('#chat .unread-messages').each(function() {
  831 + sum += Number($(this).text());
  832 + });
  833 + if(sum>0) {
  834 + total_unread.text(sum);
  835 + } else {
  836 + total_unread.text('');
  837 + }
  838 + }
776 839
777 - function getCurrentIdentifier() {  
778 - return getIdentifier(Jabber.connection.jid);  
779 - } 840 + var $conversations = $('#chat-window #conversations');
780 841
781 - function getIdentifier(jid) {  
782 - return Strophe.getNodeFromJid(jid);  
783 - } 842 + function log(msg) {
  843 + if(Jabber.debug && window.console && window.console.log) {
  844 + var time = new Date();
  845 + window.console.log('['+ time.toTimeString() +'] ' + msg);
  846 + }
  847 + }
  848 +
  849 + function escape_html(body) {
  850 + return body
  851 + .replace(/&/g, '&amp;')
  852 + .replace(/</g, '&lt;')
  853 + .replace(/>/g, '&gt;');
  854 + }
  855 +
  856 + function getCurrentIdentifier() {
  857 + return getIdentifier(Jabber.connection.jid);
  858 + }
  859 +
  860 + function getIdentifier(jid) {
  861 + return Strophe.getNodeFromJid(jid);
  862 + }
  863 +
  864 + function getMyAvatar() {
  865 + return getAvatar(getCurrentIdentifier());
  866 + }
  867 +
  868 + function getAvatar(identifier) {
  869 + if(Jabber.avatars[identifier])
  870 + var src = Jabber.avatars[identifier];
  871 + else
  872 + var src = "/chat/avatar/"+identifier;
  873 +
  874 + return '<img class="avatar" src="' + src + '">';
  875 + }
  876 +
  877 + function disconnect() {
  878 + log('disconnect');
  879 + if (Jabber.connection && Jabber.connection.connected) {
  880 + Jabber.connection.disconnect();
  881 + }
  882 + Jabber.presence_status = 'offline';
  883 + Jabber.show_status('offline');
  884 + }
  885 +
  886 + function notifyMessage(message) {
  887 + var jid = Strophe.getBareJidFromJid(message.from);
  888 + var jid_id = Jabber.jid_to_id(jid);
  889 + var name = Jabber.name_of(jid_id);
  890 + var identifier = Strophe.getNodeFromJid(jid);
  891 + var avatar = "/chat/avatar/"+identifier
  892 + if(!$('#chat').hasClass('opened') || window.isHidden() || !Jabber.window_visibility) {
  893 + var options = {body: message.body, icon: avatar, tag: jid_id};
  894 + $(notifyMe(name, options)).on('click', function(){
  895 + open_conversation(jid);
  896 + });
  897 + Jabber.notification_sound.play();
  898 + }
  899 + }
784 900
785 - function getMyAvatar() {  
786 - return getAvatar(getCurrentIdentifier());  
787 - } 901 + $('.title-bar a').click(function() {
  902 + $(this).parents('.status-group').find('.buddies').toggle('fast');
  903 + return false;
  904 + });
  905 + $('#chat').on('click', '.occupants a', function() {
  906 + $(this).siblings('.occupant-list').toggle('fast');
  907 + $(this).toggleClass('up');
  908 + return false;
  909 + });
788 910
789 - function getAvatar(identifier) {  
790 - return '<img class="avatar" src="/chat/avatar/' + identifier + '">';  
791 - } 911 + //restore connection if user was connected
  912 + if($presence=='' || $presence == 'chat') {
  913 + $('#chat-connect').trigger('click');
  914 + } else if($presence == 'dnd') {
  915 + $('#chat-busy').trigger('click');
  916 + }
792 917
793 - function disconnect() {  
794 - log('disconnect');  
795 - if (Jabber.connection && Jabber.connection.connected) {  
796 - Jabber.connection.disconnect();  
797 - }  
798 - Jabber.presence_status = 'offline';  
799 - Jabber.show_status('offline');  
800 - }  
801 -  
802 - function notifyMessage(message) {  
803 - var jid = Strophe.getBareJidFromJid(message.from);  
804 - var jid_id = Jabber.jid_to_id(jid);  
805 - var name = Jabber.name_of(jid_id);  
806 - var identifier = Strophe.getNodeFromJid(jid);  
807 - var avatar = "/chat/avatar/"+identifier  
808 - if(!$('#chat').hasClass('opened') || window.isHidden()) {  
809 - var options = {body: message.body, icon: avatar, tag: jid_id};  
810 - console.log('Notify '+name);  
811 - $(notifyMe(name, options)).on('click', function(){  
812 - open_conversation(jid);  
813 - });  
814 - $.sound.play('/sounds/receive.wav');  
815 - }  
816 - }  
817 -  
818 - $('.title-bar a').click(function() {  
819 - $(this).parents('.status-group').find('.buddies').toggle('fast');  
820 - });  
821 - $('#chat').on('click', '.occupants a', function() {  
822 - $(this).siblings('.occupant-list').toggle('fast');  
823 - $(this).toggleClass('up');  
824 - });  
825 -  
826 - //restore connection if user was connected  
827 - if($presence=='' || $presence == 'chat') {  
828 - $('#chat-connect').trigger('click');  
829 - } else if($presence == 'dnd') {  
830 - $('#chat-busy').trigger('click');  
831 - }  
832 -  
833 - $('#chat #buddy-list .buddies').perfectScrollbar(); 918 + $('#chat #buddy-list .buddies').perfectScrollbar();
834 919
835 // custom css expression for a case-insensitive contains() 920 // custom css expression for a case-insensitive contains()
836 jQuery.expr[':'].Contains = function(a,i,m){ 921 jQuery.expr[':'].Contains = function(a,i,m){
837 - return (a.textContent || a.innerText || "").toUpperCase().indexOf(m[3].toUpperCase())>=0; 922 + return (a.textContent || a.innerText || "").toUpperCase().indexOf(m[3].toUpperCase())>=0;
838 }; 923 };
839 924
840 $('#chat .search').change( function () { 925 $('#chat .search').change( function () {
@@ -856,6 +941,7 @@ jQuery(function($) { @@ -856,6 +941,7 @@ jQuery(function($) {
856 941
857 $('#chat .buddies a').live('click', function(){ 942 $('#chat .buddies a').live('click', function(){
858 $('#chat .search').val('').change(); 943 $('#chat .search').val('').change();
  944 + return false;
859 }); 945 });
860 946
861 $('#chat-label').click(function(){ 947 $('#chat-label').click(function(){
@@ -871,6 +957,14 @@ jQuery(function($) { @@ -871,6 +957,14 @@ jQuery(function($) {
871 $('.room-action.leave').live('click', function(){ 957 $('.room-action.leave').live('click', function(){
872 var jid = $(this).data('jid'); 958 var jid = $(this).data('jid');
873 Jabber.leave_room(jid); 959 Jabber.leave_room(jid);
  960 + return false;
  961 + });
  962 +
  963 + $('.open-conversation').live('click', function(){
  964 + open_conversation($(this).data('jid'));
  965 + return false;
874 }); 966 });
875 967
  968 + window.onfocus = function() {Jabber.window_visibility = true};
  969 + window.onblur = function() {Jabber.window_visibility = false};
876 }); 970 });
public/stylesheets/chat.css
@@ -201,6 +201,21 @@ @@ -201,6 +201,21 @@
201 bottom: 132px; 201 bottom: 132px;
202 } 202 }
203 203
  204 +#chat-label.opened #unread-messages,
  205 +#unread-messages:empty {
  206 + display: none;
  207 +}
  208 +
  209 +#unread-messages {
  210 + padding: 3px 5px;
  211 + background-color: #F57900;
  212 + border-radius: 5px;
  213 + margin-top: -10px;
  214 + margin-left: -30px;
  215 + position: absolute;
  216 + z-index: 1;
  217 +}
  218 +
204 #chat .unread-messages { 219 #chat .unread-messages {
205 height: 32px; 220 height: 32px;
206 line-height: 32px; 221 line-height: 32px;
test/functional/chat_controller_test.rb
@@ -95,6 +95,54 @@ class ChatControllerTest &lt; ActionController::TestCase @@ -95,6 +95,54 @@ class ChatControllerTest &lt; ActionController::TestCase
95 assert_not_equal chat_status_at, @person.user.chat_status_at 95 assert_not_equal chat_status_at, @person.user.chat_status_at
96 end 96 end
97 97
  98 + should 'forbid to register a message without to' do
  99 + @controller.stubs(:current_user).returns(@person.user)
  100 + @request.stubs(:xhr?).returns(true)
  101 +
  102 + post :save_message, {:body =>'Hello!'}
  103 + assert ActiveSupport::JSON.decode(@response.body)['status'] == 1
  104 + end
  105 +
  106 + should 'forbid to register a message without body' do
  107 + @controller.stubs(:current_user).returns(@person.user)
  108 + @request.stubs(:xhr?).returns(true)
  109 +
  110 + post :save_message, {:to =>'mary'}
  111 + assert ActiveSupport::JSON.decode(@response.body)['status'] == 1
  112 + end
  113 +
  114 + should 'forbid user to register a message to a stranger' do
  115 + @controller.stubs(:current_user).returns(@person.user)
  116 + @request.stubs(:xhr?).returns(true)
  117 +
  118 + post :save_message, {:to =>'random', :body => 'Hello, stranger!'}
  119 + assert ActiveSupport::JSON.decode(@response.body)['status'] == 2
  120 + end
  121 +
  122 + should 'register a message to a friend' do
  123 + @controller.stubs(:current_user).returns(@person.user)
  124 + friend = create_user('friend').person
  125 + @person.add_friend friend
  126 + @request.stubs(:xhr?).returns(true)
  127 +
  128 + assert_difference 'ChatMessage.count', 1 do
  129 + post :save_message, {:to => friend.identifier, :body => 'Hey! How is it going?'}
  130 + assert ActiveSupport::JSON.decode(@response.body)['status'] == 0
  131 + end
  132 + end
  133 +
  134 + should 'register a message to a group' do
  135 + @controller.stubs(:current_user).returns(@person.user)
  136 + group = fast_create(Organization)
  137 + group.add_member(@person)
  138 + @request.stubs(:xhr?).returns(true)
  139 +
  140 + assert_difference 'ChatMessage.count', 1 do
  141 + post :save_message, {:to => group.identifier, :body => 'Hey! How is it going?'}
  142 + assert ActiveSupport::JSON.decode(@response.body)['status'] == 0
  143 + end
  144 + end
  145 +
98 should 'toggle chat status' do 146 should 'toggle chat status' do
99 login_as 'testuser' 147 login_as 'testuser'
100 148
test/unit/chat_message_test.rb 0 → 100644
@@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
  1 +require 'test_helper'
  2 +
  3 +class ChatMessageTest < ActiveSupport::TestCase
  4 + should 'create message' do
  5 + assert_difference 'ChatMessage.count', 1 do
  6 + ChatMessage.create!(:from => fast_create(Person), :to => fast_create(Person), :body => 'Hey! How are you?' )
  7 + end
  8 + end
  9 +end