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 2  
3 3 before_filter :login_required
4 4 before_filter :check_environment_feature
  5 + before_filter :can_send_message, :only => :register_message
5 6  
6 7 def start_session
7 8 login = user.jid
... ... @@ -54,6 +55,16 @@ class ChatController &lt; PublicController
54 55 end
55 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 68 def update_presence_status
58 69 if request.xhr?
59 70 current_user.update_attributes({:chat_status_at => DateTime.now}.merge(params[:status] || {}))
... ... @@ -62,11 +73,17 @@ class ChatController &lt; PublicController
62 73 end
63 74  
64 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 87 end
71 88  
72 89 def recent_messages
... ... @@ -90,8 +107,9 @@ class ChatController &lt; PublicController
90 107 end
91 108  
92 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 113 end
96 114  
97 115 #TODO Ideally this is done through roster table on ejabberd.
... ... @@ -108,4 +126,14 @@ class ChatController &lt; PublicController
108 126 end
109 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 139 end
... ...
app/helpers/chat_helper.rb
... ... @@ -9,12 +9,12 @@ module ChatHelper
9 9 avatar = profile_image(user, :portrait, :class => 'avatar')
10 10 content_tag('span',
11 11 link_to(avatar + content_tag('span', user.name) + ui_icon('ui-icon-triangle-1-s'),
12   - '#',
  12 + '',
13 13 :onclick => 'toggleMenu(this); return false',
14 14 :class => icon_class + ' simplemenu-trigger'
15 15 ) +
16 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 18 :style => 'display: none; z-index: 100',
19 19 :class => 'simplemenu-submenu'
20 20 ),
... ...
app/models/chat_message.rb
... ... @@ -3,5 +3,4 @@ class ChatMessage &lt; ActiveRecord::Base
3 3  
4 4 belongs_to :to, :class_name => 'Profile'
5 5 belongs_to :from, :class_name => 'Profile'
6   -
7 6 end
... ...
app/views/blocks/profile_info_actions/_common.html.erb 0 → 100644
... ... @@ -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 13 </li>
14 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 17 <% end %>
20 18 </ul>
... ...
app/views/blocks/profile_info_actions/_enterprise.html.erb
... ... @@ -8,5 +8,5 @@
8 8 <li><%= button(:'menu-mail', _('Send an e-mail'), {:profile => profile.identifier, :controller => 'contact', :action => 'new'}, {:id => 'enterprise-contact-button'} ) %></li>
9 9 <% end %>
10 10  
11   - <li><%= report_abuse(profile, :button) %></li>
  11 + <%= render :partial => 'blocks/profile_info_actions/common' %>
12 12 </ul>
... ...
app/views/blocks/profile_info_actions/_person.html.erb
... ... @@ -11,6 +11,6 @@
11 11 <li><%= button(:back, _('Send an e-mail'), {:profile => profile.identifier, :controller => 'contact', :action => 'new'}) %></li>
12 12 <% end %>
13 13  
14   - <li><%= report_abuse(profile, :button) %></li>
  14 + <%= render :partial => 'blocks/profile_info_actions/common' %>
15 15 <% end %>
16 16 </ul>
... ...
app/views/chat/start_session_error.html.erb
1 1 <p>
2 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 4 </p>
... ...
app/views/shared/logged_in/xmpp_chat.html.erb
... ... @@ -7,13 +7,13 @@
7 7 var $own_name = '<%= user.name %>';
8 8 var $muc_domain = '<%= "conference.#{environment.default_hostname}" %>';
9 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 11 var $update_presence_status_every = <%= User.expires_chat_status_every.minutes %>;
12 12 var $presence = '<%= current_user.last_chat_status %>';
13 13 </script>
14 14  
15   -
16 15 <div id="chat-label">
  16 + <span id="unread-messages"></span>
17 17 <span class="right-arrow">&#9654;</span>
18 18 <span class="title"><%= _('Chat') %></span>
19 19 </div>
... ... @@ -98,10 +98,5 @@
98 98 </div>
99 99 </div>
100 100 </div>
101   -
102   - <div class="error-message">
103   - <span class='error'>%{text}</span>
104   - </div>
105   -
106 101 </div>
107 102 </div>
... ...
app/views/shared/profile_actions/xmpp_chat.html.erb 0 → 100644
... ... @@ -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   -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 @@
  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
... ...
db/schema.rb
... ... @@ -245,13 +245,17 @@ ActiveRecord::Schema.define(:version =&gt; 20150513213939) do
245 245 end
246 246  
247 247 create_table "chat_messages", :force => true do |t|
248   - t.integer "to_id"
249 248 t.integer "from_id"
250   - t.string "body"
  249 + t.integer "to_id"
  250 + t.text "body"
251 251 t.datetime "created_at", :null => false
252 252 t.datetime "updated_at", :null => false
253 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 259 create_table "comments", :force => true do |t|
256 260 t.string "title"
257 261 t.text "body"
... ...
debian/noosfero.install
... ... @@ -34,3 +34,4 @@ public usr/share/noosfero
34 34 script usr/share/noosfero
35 35 util usr/share/noosfero
36 36 vendor usr/share/noosfero
  37 +
... ...
debian/update-noosfero-apache
... ... @@ -18,9 +18,20 @@ if test -x /usr/share/noosfero/script/apacheconf; then
18 18 fi
19 19  
20 20 apache_site='/etc/apache2/sites-available/noosfero'
  21 + apache_site_configs='/etc/noosfero/apache.d'
21 22 if ! test -e "$apache_site"; then
22 23 echo "Generating apache virtual host ..."
23 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 35 fi
25 36  
26 37 echo 'Noosfero Apache configuration updated.'
... ...
etc/noosfero/apache.d/noosfero-chat 0 → 100644
... ... @@ -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 1140  
1141 1141 // If the user is okay, let's create a notification
1142 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 1157 return notification;
1148 1158 // At last, if the user already denied any notification, and you
1149 1159 // want to be respectful there is no need to bother them any more.
... ...
public/javascripts/chat.js
1 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 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 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 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 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 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 920 // custom css expression for a case-insensitive contains()
836 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 925 $('#chat .search').change( function () {
... ... @@ -856,6 +941,7 @@ jQuery(function($) {
856 941  
857 942 $('#chat .buddies a').live('click', function(){
858 943 $('#chat .search').val('').change();
  944 + return false;
859 945 });
860 946  
861 947 $('#chat-label').click(function(){
... ... @@ -871,6 +957,14 @@ jQuery(function($) {
871 957 $('.room-action.leave').live('click', function(){
872 958 var jid = $(this).data('jid');
873 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 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 219 #chat .unread-messages {
205 220 height: 32px;
206 221 line-height: 32px;
... ...
test/functional/chat_controller_test.rb
... ... @@ -95,6 +95,54 @@ class ChatControllerTest &lt; ActionController::TestCase
95 95 assert_not_equal chat_status_at, @person.user.chat_status_at
96 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 146 should 'toggle chat status' do
99 147 login_as 'testuser'
100 148  
... ...
test/unit/chat_message_test.rb 0 → 100644
... ... @@ -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
... ...