diff --git a/app/controllers/public/chat_controller.rb b/app/controllers/public/chat_controller.rb index 2d835bc..a88051b 100644 --- a/app/controllers/public/chat_controller.rb +++ b/app/controllers/public/chat_controller.rb @@ -2,6 +2,7 @@ class ChatController < PublicController before_filter :login_required before_filter :check_environment_feature + before_filter :can_send_message, :only => :register_message def start_session login = user.jid @@ -54,6 +55,16 @@ class ChatController < PublicController end end + def avatars + profiles = environment.profiles.where(:identifier => params[:profiles]) + avatar_map = profiles.inject({}) do |result, profile| + result[profile.identifier] = profile_icon(profile, :minor) + result + end + + render_json avatar_map + end + def update_presence_status if request.xhr? current_user.update_attributes({:chat_status_at => DateTime.now}.merge(params[:status] || {})) @@ -62,11 +73,17 @@ class ChatController < PublicController end def save_message - to = environment.profiles.find_by_identifier(params[:to]) - body = params[:body] - - ChatMessage.create!(:to => to, :from => user, :body => body) - render :text => 'ok' + if request.post? + to = environment.profiles.where(:identifier => params[:to]).first + body = params[:body] + + begin + ChatMessage.create!(:to => to, :from => user, :body => body) + return render_json({:status => 0}) + rescue Exception => exception + return render_json({:status => 3, :message => exception.to_s, :backtrace => exception.backtrace}) + end + end end def recent_messages @@ -90,8 +107,9 @@ class ChatController < PublicController end def recent_conversations - 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']} - render :json => {:order => conversations_order.reverse, :domain => environment.default_hostname.gsub('.','-')}.to_json + 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") + jids = profiles.map(&:jid).reverse + render :json => jids.to_json end #TODO Ideally this is done through roster table on ejabberd. @@ -108,4 +126,14 @@ class ChatController < PublicController end end + def can_send_message + return render_json({:status => 1, :message => 'Missing parameters!'}) if params[:from].nil? || params[:to].nil? || params[:message].nil? + return render_json({:status => 2, :message => 'You can not send message as another user!'}) if params[:from] != user.jid + # TODO Maybe register the jid in a table someday to avoid this below + return render_json({:status => 3, :messsage => 'You can not send messages to strangers!'}) if user.friends.where(:identifier => params[:to].split('@').first).blank? + end + + def render_json(result) + render :text => result.to_json + end end diff --git a/app/helpers/chat_helper.rb b/app/helpers/chat_helper.rb index a799852..584d3af 100644 --- a/app/helpers/chat_helper.rb +++ b/app/helpers/chat_helper.rb @@ -9,12 +9,12 @@ module ChatHelper avatar = profile_image(user, :portrait, :class => 'avatar') content_tag('span', link_to(avatar + content_tag('span', user.name) + ui_icon('ui-icon-triangle-1-s'), - '#', + '', :onclick => 'toggleMenu(this); return false', :class => icon_class + ' simplemenu-trigger' ) + content_tag('ul', - 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"), + 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"), :style => 'display: none; z-index: 100', :class => 'simplemenu-submenu' ), diff --git a/app/models/chat_message.rb b/app/models/chat_message.rb index 6fecffb..af8caf8 100644 --- a/app/models/chat_message.rb +++ b/app/models/chat_message.rb @@ -3,5 +3,4 @@ class ChatMessage < ActiveRecord::Base belongs_to :to, :class_name => 'Profile' belongs_to :from, :class_name => 'Profile' - end diff --git a/app/views/blocks/profile_info_actions/_common.html.erb b/app/views/blocks/profile_info_actions/_common.html.erb new file mode 100644 index 0000000..ee8d800 --- /dev/null +++ b/app/views/blocks/profile_info_actions/_common.html.erb @@ -0,0 +1,2 @@ +
  • <%= report_abuse(profile, :button) %>
  • +<%= render_environment_features(:profile_actions) %> diff --git a/app/views/blocks/profile_info_actions/_community.html.erb b/app/views/blocks/profile_info_actions/_community.html.erb index a7fb8e9..3cdcf14 100644 --- a/app/views/blocks/profile_info_actions/_community.html.erb +++ b/app/views/blocks/profile_info_actions/_community.html.erb @@ -13,8 +13,6 @@ <% end %> -
  • <%= report_abuse(profile, :button) %>
  • - - <%= render_environment_features(:profile_actions) %> + <%= render :partial => 'blocks/profile_info_actions/common' %> <% end %> diff --git a/app/views/blocks/profile_info_actions/_enterprise.html.erb b/app/views/blocks/profile_info_actions/_enterprise.html.erb index 0662687..4a5ad83 100644 --- a/app/views/blocks/profile_info_actions/_enterprise.html.erb +++ b/app/views/blocks/profile_info_actions/_enterprise.html.erb @@ -8,5 +8,5 @@
  • <%= button(:'menu-mail', _('Send an e-mail'), {:profile => profile.identifier, :controller => 'contact', :action => 'new'}, {:id => 'enterprise-contact-button'} ) %>
  • <% end %> -
  • <%= report_abuse(profile, :button) %>
  • + <%= render :partial => 'blocks/profile_info_actions/common' %> diff --git a/app/views/blocks/profile_info_actions/_person.html.erb b/app/views/blocks/profile_info_actions/_person.html.erb index 42f7d75..c655b4e 100644 --- a/app/views/blocks/profile_info_actions/_person.html.erb +++ b/app/views/blocks/profile_info_actions/_person.html.erb @@ -11,6 +11,6 @@
  • <%= button(:back, _('Send an e-mail'), {:profile => profile.identifier, :controller => 'contact', :action => 'new'}) %>
  • <% end %> -
  • <%= report_abuse(profile, :button) %>
  • + <%= render :partial => 'blocks/profile_info_actions/common' %> <% end %> diff --git a/app/views/chat/start_session_error.html.erb b/app/views/chat/start_session_error.html.erb index 405245e..cfd17dc 100644 --- a/app/views/chat/start_session_error.html.erb +++ b/app/views/chat/start_session_error.html.erb @@ -1,4 +1,4 @@

    <%= ui_icon('ui-icon-alert') %> -<%= _('Could not connect to chat') %>, <%= _('try again') %>. +<%= _('Could not connect to chat') %>, <%= _('try again') %>.

    diff --git a/app/views/shared/logged_in/xmpp_chat.html.erb b/app/views/shared/logged_in/xmpp_chat.html.erb index b0b0122..85a1c0a 100644 --- a/app/views/shared/logged_in/xmpp_chat.html.erb +++ b/app/views/shared/logged_in/xmpp_chat.html.erb @@ -7,13 +7,13 @@ var $own_name = '<%= user.name %>'; var $muc_domain = '<%= "conference.#{environment.default_hostname}" %>'; var $bosh_service = '//<%= environment.default_hostname %>/http-bind'; - var $user_unavailable_error = '<%= _("ooops! The message could not be sent because the user is not online") %>'; + var $user_unavailable_error = '<%= _("The user is not online now. He/She will receive these messages as soon as he/she gets online.") %>'; var $update_presence_status_every = <%= User.expires_chat_status_every.minutes %>; var $presence = '<%= current_user.last_chat_status %>'; -
    + <%= _('Chat') %>
    @@ -98,10 +98,5 @@ - -
    - %{text} -
    - diff --git a/app/views/shared/profile_actions/xmpp_chat.html.erb b/app/views/shared/profile_actions/xmpp_chat.html.erb new file mode 100644 index 0000000..1528112 --- /dev/null +++ b/app/views/shared/profile_actions/xmpp_chat.html.erb @@ -0,0 +1,8 @@ +<% label_name = profile.person? ? _('Open chat') : _('Join chat room') %> +<% display = profile.person? ? profile.friends.include?(user) : profile.members.include?(user) %> + +<% if display %> +
  • + <%= button(:chat, label_name , {}, :class => 'open-conversation', 'data-jid' => profile.jid) %> +
  • +<% end %> diff --git a/db/migrate/20140820173129_create_chat_messages.rb b/db/migrate/20140820173129_create_chat_messages.rb deleted file mode 100644 index 83ee2ea..0000000 --- a/db/migrate/20140820173129_create_chat_messages.rb +++ /dev/null @@ -1,11 +0,0 @@ -class CreateChatMessages < ActiveRecord::Migration - def change - create_table :chat_messages do |t| - t.integer :to_id - t.integer :from_id - t.string :body - - t.timestamps - end - end -end diff --git a/db/migrate/20141014205254_create_chat_messages.rb b/db/migrate/20141014205254_create_chat_messages.rb new file mode 100644 index 0000000..ff65b56 --- /dev/null +++ b/db/migrate/20141014205254_create_chat_messages.rb @@ -0,0 +1,20 @@ +class CreateChatMessages < ActiveRecord::Migration + def up + create_table :chat_messages do |t| + t.references :from + t.references :to + t.text :body + t.timestamps + end + add_index :chat_messages, :from_id + add_index :chat_messages, :to_id + add_index :chat_messages, :created_at + end + + def down + drop_table :chat_messages + remove_index :chat_messages, :from + remove_index :chat_messages, :to + remove_index :chat_messages, :created_at + end +end diff --git a/db/schema.rb b/db/schema.rb index 31a8586..6430958 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -245,13 +245,17 @@ ActiveRecord::Schema.define(:version => 20150513213939) do end create_table "chat_messages", :force => true do |t| - t.integer "to_id" t.integer "from_id" - t.string "body" + t.integer "to_id" + t.text "body" t.datetime "created_at", :null => false t.datetime "updated_at", :null => false end + add_index "chat_messages", ["created_at"], :name => "index_chat_messages_on_created_at" + add_index "chat_messages", ["from_id"], :name => "index_chat_messages_on_from_id" + add_index "chat_messages", ["to_id"], :name => "index_chat_messages_on_to_id" + create_table "comments", :force => true do |t| t.string "title" t.text "body" diff --git a/debian/noosfero.install b/debian/noosfero.install index 0eef3ee..3f9419a 100644 --- a/debian/noosfero.install +++ b/debian/noosfero.install @@ -34,3 +34,4 @@ public usr/share/noosfero script usr/share/noosfero util usr/share/noosfero vendor usr/share/noosfero + diff --git a/debian/update-noosfero-apache b/debian/update-noosfero-apache index 2414458..06fcfa9 100755 --- a/debian/update-noosfero-apache +++ b/debian/update-noosfero-apache @@ -18,9 +18,20 @@ if test -x /usr/share/noosfero/script/apacheconf; then fi apache_site='/etc/apache2/sites-available/noosfero' + apache_site_configs='/etc/noosfero/apache.d' if ! test -e "$apache_site"; then echo "Generating apache virtual host ..." cd /usr/share/noosfero && su noosfero -c "RAILS_ENV=production ./script/apacheconf virtualhosts" > "$apache_site" + if ! test -d "$apache_site_configs"; then + echo "Creating noosfero site config folder ..." + mkdir $apache_site_configs + fi + else + pattern="Include \/etc\/noosfero\/apache\/virtualhost.conf" + include="Include \/etc\/noosfero\/apache.d\/*" + if ! cat $apache_site | grep "^ *$include" > /dev/null ; then + sed -i "s/.*$pattern.*/ $include\n&/" $apache_site + fi fi echo 'Noosfero Apache configuration updated.' diff --git a/etc/noosfero/apache.d/noosfero-chat b/etc/noosfero/apache.d/noosfero-chat new file mode 100644 index 0000000..7b4875a --- /dev/null +++ b/etc/noosfero/apache.d/noosfero-chat @@ -0,0 +1,2 @@ +RewriteEngine On +Include /usr/share/noosfero/util/chat/apache/xmpp.conf diff --git a/public/javascripts/application.js b/public/javascripts/application.js index df39ade..3206d8d 100644 --- a/public/javascripts/application.js +++ b/public/javascripts/application.js @@ -1140,10 +1140,20 @@ function notifyMe(title, options) { // If the user is okay, let's create a notification if (permission === "granted") { - notification = new Notification(title, options); + notification = new Notification(title, options); } }); } + + setTimeout(function() {notification.close()}, 5000); + notification.onclick = function(){ + notification.close(); + // Chromium tweak + window.open().close(); + window.focus(); + this.cancel(); + }; + return notification; // At last, if the user already denied any notification, and you // want to be respectful there is no need to bother them any more. diff --git a/public/javascripts/chat.js b/public/javascripts/chat.js index 6eee22c..fc787df 100644 --- a/public/javascripts/chat.js +++ b/public/javascripts/chat.js @@ -1,840 +1,925 @@ /* XMPP/Jabber Noosfero's client - XMPP Core: - http://xmpp.org/rfcs/rfc3920.html +XMPP Core: +http://xmpp.org/rfcs/rfc3920.html - MUC support: - http://xmpp.org/extensions/xep-0045.html +MUC support: +http://xmpp.org/extensions/xep-0045.html - Messages and presence: - http://xmpp.org/rfcs/rfc3921.html +Messages and presence: +http://xmpp.org/rfcs/rfc3921.html */ jQuery(function($) { - // extending the current namespaces in Strophe.NS - Strophe.addNamespace('MUC_USER', 'http://jabber.org/protocol/muc#user'); - Strophe.addNamespace('MUC_OWNER', 'http://jabber.org/protocol/muc#owner'); - Strophe.addNamespace('CHAT_STATES', 'http://jabber.org/protocol/chatstates'); - Strophe.addNamespace('DATA_FORMS', 'jabber:x:data'); - - var Jabber = { - debug: true, - connection: null, - bosh_service: $bosh_service, - muc_domain: $muc_domain, - muc_supported: false, - presence_status: '', - conversation_prefix: 'conversation-', - jids: {}, - rooms: {}, - no_more_messages: {}, - - template: function(selector) { - return $('#chat #chat-templates '+selector).clone().html(); - }, - - jid_to_id: function (jid) { - return Strophe.getBareJidFromJid(jid).replace(/@/g, "-").replace(/\./g, "-"); - }, - - jid_of: function(jid_id) { - return Jabber.jids[jid_id].jid; - }, - name_of: function(jid_id) { - return Jabber.jids[jid_id].name; - }, - type_of: function(jid_id) { - return Jabber.jids[jid_id].type; - }, - unread_messages_of: function(jid_id, value) { - Jabber.jids[jid_id].unread_messages = (value == undefined ? Jabber.jids[jid_id].unread_messages : value); - return Jabber.jids[jid_id].unread_messages; - }, - - insert_or_update_user: function (list, item, jid, name, presence, template, type, remove_on_offline) { - var jid_id = Jabber.jid_to_id(jid); - var identifier = Strophe.getNodeFromJid(jid); - var html = template - .replace('%{jid_id}', jid_id) - .replace(/%{presence_status}/g, presence) - .replace('%{avatar}', getAvatar(identifier)) - .replace('%{name}', name); - - $(item).parent().remove(); - if(presence != 'offline' || !remove_on_offline) - $(list).append(html); - Jabber.jids[jid_id] = {jid: jid, name: name, type: type, presence: presence}; - }, - insert_or_update_group: function (jid, presence) { - var jid_id = Jabber.jid_to_id(jid); - var list = $('#buddy-list .buddies ul.'+presence); - var item = $('#' + jid_id); - presence = presence || ($(item).length > 0 ? $(item).parent('li').attr('class') : 'offline'); - log('adding or updating contact ' + jid + ' as ' + presence); - Jabber.insert_or_update_user(list, item, jid, Jabber.name_of(jid_id), presence, Jabber.template('.buddy-item'), 'groupchat'); - $("#chat-window .tab a[href='#"+ Jabber.conversation_prefix + jid_id +"']") - .removeClass() - .addClass('icon-menu-' + presence + '-11'); - }, - insert_or_update_contact: function (jid, name, presence) { - var jid_id = Jabber.jid_to_id(jid); - var item = $('#' + jid_id); - presence = presence || ($(item).length > 0 ? $(item).parent('li').attr('class') : 'offline'); - var list = $('#buddy-list .buddies ul' + (presence=='offline' ? '.offline' : '.online')); - - log('adding or updating contact ' + jid + ' as ' + presence); - Jabber.insert_or_update_user(list, item, jid, name, presence, Jabber.template('.buddy-item'), 'chat'); - $("#chat-window .tab a[href='#"+ Jabber.conversation_prefix + jid_id +"']") - .removeClass() - .addClass('icon-menu-' + presence + '-11'); - }, - insert_or_update_occupant: function (jid, name, presence, room_jid) { - log('adding or updating occupant ' + jid + ' as ' + presence); - var jid_id = Jabber.jid_to_id(jid); - var room_jid_id = Jabber.jid_to_id(room_jid); - var list = $('#' + Jabber.conversation_prefix + room_jid_id + ' .occupants ul'); - var item = $(list).find('a[data-id='+ jid_id +']'); - Jabber.insert_or_update_user(list, item, jid, name, presence, Jabber.template('.occupant-item'), 'chat', true); - if (Jabber.rooms[room_jid_id] === undefined) - Jabber.rooms[room_jid_id] = {}; - - var room = Jabber.rooms[room_jid_id]; - if(presence == 'offline') { - delete Jabber.rooms[room_jid_id][name]; - } - else { - Jabber.rooms[room_jid_id][name] = jid; - } + // extending the current namespaces in Strophe.NS + Strophe.addNamespace('MUC_USER', 'http://jabber.org/protocol/muc#user'); + Strophe.addNamespace('MUC_OWNER', 'http://jabber.org/protocol/muc#owner'); + Strophe.addNamespace('CHAT_STATES', 'http://jabber.org/protocol/chatstates'); + Strophe.addNamespace('DATA_FORMS', 'jabber:x:data'); + + var Jabber = { + debug: true, + connection: null, + bosh_service: $bosh_service, + muc_domain: $muc_domain, + muc_supported: false, + presence_status: '', + conversation_prefix: 'conversation-', + conversations_order: null, + notification_sound: new Audio('/sounds/receive.wav'), + window_visibility: null, + jids: {}, + rooms: {}, + no_more_messages: {}, + avatars: {}, + + template: function(selector) { + return $('#chat #chat-templates '+selector).clone().html(); + }, + + jid_to_id: function (jid) { + return Strophe.getBareJidFromJid(jid).replace(/@/g, "-").replace(/\./g, "-"); + }, + + jid_of: function(jid_id) { + return Jabber.jids[jid_id].jid; + }, + name_of: function(jid_id) { + return Jabber.jids[jid_id].name; + }, + type_of: function(jid_id) { + return Jabber.jids[jid_id].type; + }, + presence_of: function(jid_id) { + return Jabber.jids[jid_id].presence; + }, + unread_messages_of: function(jid_id, value) { + Jabber.jids[jid_id].unread_messages = (value == undefined ? Jabber.jids[jid_id].unread_messages : value); + return Jabber.jids[jid_id].unread_messages; + }, + + insert_or_update_user: function (list, item, jid, name, presence, template, type, remove_on_offline) { + var jid_id = Jabber.jid_to_id(jid); + var identifier = Strophe.getNodeFromJid(jid); + var html = template + .replace('%{jid_id}', jid_id) + .replace(/%{presence_status}/g, presence) + .replace('%{avatar}', getAvatar(identifier)) + .replace('%{name}', name); + + $(item).parent().remove(); + if(presence != 'offline' || !remove_on_offline){ + $(list).append(html); + sort_conversations(); + } + Jabber.jids[jid_id] = {jid: jid, name: name, type: type, presence: presence}; + }, + insert_or_update_group: function (jid, presence) { + var jid_id = Jabber.jid_to_id(jid); + var list = $('#buddy-list .buddies ul.'+presence); + var item = $('#' + jid_id); + presence = presence || ($(item).length > 0 ? $(item).parent('li').attr('class') : 'offline'); + log('adding or updating contact ' + jid + ' as ' + presence); + Jabber.insert_or_update_user(list, item, jid, Jabber.name_of(jid_id), presence, Jabber.template('.buddy-item'), 'groupchat'); + $("#chat-window .tab a[href='#"+ Jabber.conversation_prefix + jid_id +"']") + .removeClass() + .addClass('icon-menu-' + presence + '-11'); + }, + insert_or_update_contact: function (jid, name, presence) { + var jid_id = Jabber.jid_to_id(jid); + var item = $('#' + jid_id); + presence = presence || ($(item).length > 0 ? $(item).parent('li').attr('class') : 'offline'); + var list = $('#buddy-list .buddies ul' + (presence=='offline' ? '.offline' : '.online')); + + log('adding or updating contact ' + jid + ' as ' + presence); + Jabber.insert_or_update_user(list, item, jid, name, presence, Jabber.template('.buddy-item'), 'chat'); + $("#chat-window .tab a[href='#"+ Jabber.conversation_prefix + jid_id +"']") + .removeClass() + .addClass('icon-menu-' + presence + '-11'); + }, + insert_or_update_occupant: function (jid, name, presence, room_jid) { + log('adding or updating occupant ' + jid + ' as ' + presence); + var jid_id = Jabber.jid_to_id(jid); + var room_jid_id = Jabber.jid_to_id(room_jid); + var list = $('#' + Jabber.conversation_prefix + room_jid_id + ' .occupants ul'); + var item = $(list).find('a[data-id='+ jid_id +']'); + Jabber.insert_or_update_user(list, item, jid, name, presence, Jabber.template('.occupant-item'), 'chat', true); + if (Jabber.rooms[room_jid_id] === undefined) + Jabber.rooms[room_jid_id] = {}; + + var room = Jabber.rooms[room_jid_id]; + if(presence == 'offline') { + delete Jabber.rooms[room_jid_id][name]; + } + else { + Jabber.rooms[room_jid_id][name] = jid; + } - list.parents('.occupants').find('.occupants-online').text(Object.keys(Jabber.rooms[room_jid_id]).length); - }, - - remove_contact: function(jid) { - var jid_id = Jabber.jid_to_id(jid) - log('Removing contact ' + jid); - $('#' + jid_id).parent('li').remove(); - }, - - render_body_message: function(body) { - body = body.replace(/\r?\n/g, '
    '); - body = $().emoticon(body); - body = linkify(body, { - callback: function(text, href) { - return href ? '' + text + '' : text; - } - }); - return body; - }, + list.parents('.occupants').find('.occupants-online').text(Object.keys(Jabber.rooms[room_jid_id]).length); + }, + + remove_contact: function(jid) { + var jid_id = Jabber.jid_to_id(jid) + log('Removing contact ' + jid); + $('#' + jid_id).parent('li').remove(); + }, + + render_body_message: function(body) { + body = body.replace(/\r?\n/g, '
    '); + body = $().emoticon(body); + body = linkify(body, { + callback: function(text, href) { + return href ? '' + text + '' : text; + } + }); + return body; + }, - show_message: function (jid, name, body, who, identifier, time, offset) { - if(!offset) offset = 0; - if (body) { - body = Jabber.render_body_message(body); - var jid_id = Jabber.jid_to_id(jid); - var tab_id = '#' + Jabber.conversation_prefix + jid_id; - var history = $(tab_id).find('.history'); + show_message: function (jid, name, body, who, identifier, time, offset) { + if(!offset) offset = 0; + if (body) { + body = Jabber.render_body_message(body); + var jid_id = Jabber.jid_to_id(jid); + var tab_id = '#' + Jabber.conversation_prefix + jid_id; + var history = $(tab_id).find('.history'); - var offset_container = history.find('.chat-offset-container-'+offset); - if(offset_container.length == 0) - offset_container = $('
    ').prependTo(history); + var offset_container = history.find('.chat-offset-container-'+offset); + if(offset_container.length == 0) + offset_container = $('
    ').prependTo(history); - if (offset_container.find('.message:last').attr('data-who') == who) { - offset_container.find('.message:last .content').append('

    ' + body + '

    '); - } - else { - if (time==undefined) { - time = new Date().toISOString(); - } - var message_html = Jabber.template('.message') - .replace('%{message}', body) - .replace(/%{who}/g, who) - .replace('%{time}', time) - .replace('%{name}', name) - .replace('%{avatar}', getAvatar(identifier)); - offset_container.append(message_html); - $(".message span.time").timeago(); - } - if(offset == 0) history.scrollTo({top:'100%', left:'0%'}); - else history.scrollTo(offset_container.height()); - if (who != "self") { - if ($(tab_id).find('.history:visible').length == 0) { - count_unread_messages(jid_id); - } - document.alert_title = name; - } - } - }, - - show_status: function(presence) { - log('changing my status to ' + presence); - $('#buddy-list .user-status .simplemenu-trigger') - .removeClass('icon-menu-chat') - .removeClass('icon-menu-offline') - .removeClass('icon-menu-dnd') - .addClass('icon-menu-' + (presence || 'offline')); - $('#buddy-list #user-status img.avatar').replaceWith(getMyAvatar()); - $.get('/chat/update_presence_status', { status: {chat_status: presence, last_chat_status: presence} }); - }, - - send_availability_status: function(presence) { - log('send availability status ' + presence); - Jabber.connection.send($pres().c('show').t(presence).up()); - Jabber.show_status(presence); - }, - - enter_room: function(jid, push) { - if(push == undefined) - push = true - var jid_id = Jabber.jid_to_id(jid); - var conversation_id = Jabber.conversation_prefix + jid_id; - var button = $('#' + conversation_id + ' .join'); - button.hide(); - button.siblings('.leave').show(); - Jabber.connection.send( - $pres({to: jid + '/' + $own_name}).c('x', {xmlns: Strophe.NS.MUC}).c('history', {maxchars: 0}) - ); - Jabber.insert_or_update_group(jid, 'online'); + if (offset_container.find('.message:last').attr('data-who') == who) { + offset_container.find('.message:last .content').append('

    ' + body + '

    '); + } + else { + if (time==undefined) { + time = new Date().toISOString(); + } + var message_html = Jabber.template('.message') + .replace('%{message}', body) + .replace(/%{who}/g, who) + .replace('%{time}', time) + .replace('%{name}', name) + .replace('%{avatar}', getAvatar(identifier)); + offset_container.append(message_html); + $(".message span.time").timeago(); + } + if(offset == 0) history.scrollTo({top:'100%', left:'0%'}); + else history.scrollTo(offset_container.height()); + if (who != "self") { + if ($(tab_id).find('.history:visible').length == 0) { + count_unread_messages(jid_id); + } + document.alert_title = name; + } + } + }, + + show_status: function(presence) { + log('changing my status to ' + presence); + $('#buddy-list .user-status .simplemenu-trigger') + .removeClass('icon-menu-chat') + .removeClass('icon-menu-offline') + .removeClass('icon-menu-dnd') + .addClass('icon-menu-' + (presence || 'offline')); + $('#buddy-list #user-status img.avatar').replaceWith(getMyAvatar()); + $.get('/chat/update_presence_status', { status: {chat_status: presence, last_chat_status: presence} }); + }, + + send_availability_status: function(presence) { + log('send availability status ' + presence); + Jabber.connection.send($pres().c('show').t(presence).up()); + Jabber.show_status(presence); + }, + + enter_room: function(jid, push) { + if(push == undefined) + push = true + var jid_id = Jabber.jid_to_id(jid); + var conversation_id = Jabber.conversation_prefix + jid_id; + var button = $('#' + conversation_id + ' .join'); + button.hide(); + button.siblings('.leave').show(); + Jabber.connection.send( + $pres({to: jid + '/' + $own_name}).c('x', {xmlns: Strophe.NS.MUC}).c('history', {maxchars: 0}) + ); + Jabber.insert_or_update_group(jid, 'online'); + Jabber.update_chat_title(); + sort_conversations(); + if(push) + $.post('/chat/join', {room_id: jid}); + }, + + leave_room: function(jid, push) { + if(push == undefined) + push = true + var jid_id = Jabber.jid_to_id(jid); + var conversation_id = Jabber.conversation_prefix + jid_id; + var button = $('#' + conversation_id + ' .leave'); + button.hide(); + button.siblings('.join').show(); + Jabber.connection.send($pres({from: Jabber.connection.jid, to: jid + '/' + $own_name, type: 'unavailable'})) + Jabber.insert_or_update_group(jid, 'offline'); + sort_conversations(); + if(push) + $.post('/chat/leave', {room_id: jid}); + }, + + update_chat_title: function () { + var friends_online = $('#buddy-list #friends .buddy-list.online li').length; + $('#friends-online').text(friends_online); + var friends_offline = $('#buddy-list #friends .buddy-list.offline li').length; + $('#friends-offline').text(friends_offline); + var groups_online = $('#buddy-list #rooms .buddy-list li').length; + $('#groups-online').text(groups_online); + }, + + on_connect: function (status) { + switch (status) { + case Strophe.Status.CONNECTING: + log('connecting...'); + break; + case Strophe.Status.CONNFAIL: + log('failed to connect'); + setTimeout(function(){Jabber.connect()}, 10000); + break; + case Strophe.Status.DISCONNECTING: + log('disconnecting...'); + $('#buddy-list .toolbar').addClass('small-loading-dark'); + break; + case Strophe.Status.DISCONNECTED: + log('disconnected'); + $('#buddy-list ul.buddy-list, .occupants ul.occupant-list').html(''); Jabber.update_chat_title(); - sort_conversations(); - if(push) - $.post('/chat/join', {room_id: jid}); - }, - - leave_room: function(jid, push) { - if(push == undefined) - push = true + $('#buddy-list .toolbar').removeClass('small-loading-dark'); + $('textarea').prop('disabled', 'disabled'); + if(Jabber.presence_status != 'offline') + Jabber.connect(); + break; + case Strophe.Status.CONNECTED: + log('connected'); + case Strophe.Status.ATTACHED: + log('XMPP/BOSH session attached'); + $('#buddy-list .toolbar').removeClass('small-loading-dark'); + $('textarea').prop('disabled', ''); + break; + } + }, + + on_roster: function (iq) { + log('receiving roster'); + var profiles = []; + var contacts_to_insert = {}; + var groups_to_insert = []; + + $(iq).find('item').each(function () { + var jid = $(this).attr('jid'); + profiles.push(getIdentifier(jid)); + var name = $(this).attr('name') || jid; var jid_id = Jabber.jid_to_id(jid); - var conversation_id = Jabber.conversation_prefix + jid_id; - var button = $('#' + conversation_id + ' .leave'); - button.hide(); - button.siblings('.join').show(); - Jabber.connection.send($pres({from: Jabber.connection.jid, to: jid + '/' + $own_name, type: 'unavailable'})) - Jabber.insert_or_update_group(jid, 'offline'); - sort_conversations(); - if(push) - $.post('/chat/leave', {room_id: jid}); - }, - - update_chat_title: function () { - var friends_online = $('#buddy-list #friends .buddy-list.online li').length; - $('#friends-online').text(friends_online); - var friends_offline = $('#buddy-list #friends .buddy-list.offline li').length; - $('#friends-offline').text(friends_offline); - var groups_online = $('#buddy-list #rooms .buddy-list li').length; - $('#groups-online').text(groups_online); - }, - - on_connect: function (status) { - switch (status) { - case Strophe.Status.CONNECTING: - log('connecting...'); - break; - case Strophe.Status.CONNFAIL: - log('failed to connect'); - break; - case Strophe.Status.DISCONNECTING: - log('disconnecting...'); - $('#buddy-list .toolbar').addClass('small-loading-dark'); - break; - case Strophe.Status.DISCONNECTED: - log('disconnected'); - $('#buddy-list ul.buddy-list, .occupants ul.occupant-list').html(''); - Jabber.update_chat_title(); - $('#buddy-list .toolbar').removeClass('small-loading-dark'); - $('textarea').prop('disabled', 'disabled'); - break; - case Strophe.Status.CONNECTED: - log('connected'); - case Strophe.Status.ATTACHED: - log('XMPP/BOSH session attached'); - $('#buddy-list .toolbar').removeClass('small-loading-dark'); - $('textarea').prop('disabled', ''); - break; - } - }, - - on_roster: function (iq) { - log('receiving roster'); - $(iq).find('item').each(function () { - var jid = $(this).attr('jid'); - var name = $(this).attr('name') || jid; - var jid_id = Jabber.jid_to_id(jid); - Jabber.insert_or_update_contact(jid, name); - }); - //TODO Add groups through roster too... - $.ajax({ - url: '/chat/roster_groups', - dataType: 'json', - success: function(data){ - data.each(function(room){ - var jid_id = Jabber.jid_to_id(room.jid); - Jabber.jids[jid_id] = {jid: room.jid, name: room.name, type: 'groupchat'}; - //FIXME This must check on session if the user is inside the room... - Jabber.insert_or_update_group(room.jid, 'offline'); + contacts_to_insert[jid] = name; + }); + + //TODO Add groups through roster too... + $.ajax({ + url: '/chat/roster_groups', + dataType: 'json', + success: function(data){ + $(data).each(function(index, room){ + profiles.push(getIdentifier(room.jid)); + var jid_id = Jabber.jid_to_id(room.jid); + Jabber.jids[jid_id] = {jid: room.jid, name: room.name, type: 'groupchat'}; + //FIXME This must check on session if the user is inside the room... + groups_to_insert.push(room.jid); + + }); + $.getJSON('/chat/avatars', {profiles: profiles}, function(data) { + for(identifier in data) + Jabber.avatars[identifier] = data[identifier]; + + // Insert contacts + for(contact_jid in contacts_to_insert) + Jabber.insert_or_update_contact(contact_jid, contacts_to_insert[contact_jid]); + + // Insert groups + for (var i = 0; i < groups_to_insert.length; i++) + Jabber.insert_or_update_group(groups_to_insert[i], 'offline'); + + $.getJSON('/chat/recent_conversations', {}, function(data) { + Jabber.conversations_order = data; + sort_conversations(); }); - }, - error: function(data, textStatus, jqXHR){ - console.log(data); - }, - }); - sort_conversations(); - // set up presence handler and send initial presence - Jabber.connection.addHandler(Jabber.on_presence, null, "presence"); - Jabber.send_availability_status(Jabber.presence_status); - load_defaults(); - }, - - // NOTE: cause Noosfero store's rosters in database based on friendship relation between people - // these event never occurs cause jabber service (ejabberd) didn't know when a roster was changed - on_roster_changed: function (iq) { - log('roster changed'); - $(iq).find('item').each(function () { - var sub = $(this).attr('subscription'); - var jid = $(this).attr('jid'); - var name = $(this).attr('name') || jid; - if (sub == 'remove') { - // contact is being removed - Jabber.remove_contact(jid); - } else { - // contact is being added or modified - Jabber.insert_or_update_contact(jid, name); - } - }); - return true; - }, - - parse: function (stanza) { - var result = {}; - if (Strophe.isTagEqual(stanza, 'presence')) { - result.from = $(stanza).attr('from'); - result.type = $(stanza).attr('type'); - if (result.type == 'unavailable') { - result.show = 'offline'; - } else { - var show = $(stanza).find("show").text(); - if (show === "" || show == "chat") { - result.show = 'chat'; - } - else if (show == "dnd" || show == "xa") { - result.show = 'dnd'; - } - else { - result.show = 'away'; - } - } - if ($(stanza).find('x[xmlns="'+ Strophe.NS.MUC_USER +'"]').length > 0) { - result.is_from_room = true; - result.from_user = $(stanza).find('x item').attr('jid'); - if ($(stanza).find('x item').attr('affiliation') == 'owner') { - result.awaiting_configuration = ($(stanza).find('x status').attr('code') == '201'); - } - } + + // set up presence handler and send initial presence + Jabber.connection.addHandler(Jabber.on_presence, null, "presence"); + Jabber.send_availability_status(Jabber.presence_status); + load_defaults(); + }); + }, + error: function(data, textStatus, jqXHR){ + console.log(data); + }, + }); + + }, + + // NOTE: cause Noosfero store's rosters in database based on friendship relation between people + // these event never occurs cause jabber service (ejabberd) didn't know when a roster was changed + on_roster_changed: function (iq) { + log('roster changed'); + $(iq).find('item').each(function () { + var sub = $(this).attr('subscription'); + var jid = $(this).attr('jid'); + var name = $(this).attr('name') || jid; + if (sub == 'remove') { + // contact is being removed + Jabber.remove_contact(jid); + } else { + // contact is being added or modified + Jabber.insert_or_update_contact(jid, name); } - else if (Strophe.isTagEqual(stanza, 'message')) { - result.from = $(stanza).attr('from'); - result.body = $(stanza).find('body').text(); - if ($(stanza).find('error').length > 0) { - result.error = $(stanza).find('error text').text(); - if (!result.error && $(stanza).find('error').find('service-unavailable').length > 0) { - result.error = $user_unavailable_error; - } - } + }); + return true; + }, + + parse: function (stanza) { + var result = {}; + if (Strophe.isTagEqual(stanza, 'presence')) { + result.from = $(stanza).attr('from'); + result.type = $(stanza).attr('type'); + if (result.type == 'unavailable') { + result.show = 'offline'; + } else { + var show = $(stanza).find("show").text(); + if (show === "" || show == "chat") { + result.show = 'chat'; + } + else if (show == "dnd" || show == "xa") { + result.show = 'dnd'; + } + else { + result.show = 'away'; + } } - return result; - }, - - on_presence: function (presence) { - presence = Jabber.parse(presence); - if (presence.type != 'error') { - if (presence.is_from_room) { - log('receiving room presence from ' + presence.from + ' as ' + presence.show); - var name = Strophe.getResourceFromJid(presence.from); - if (presence.from_user) { - Jabber.insert_or_update_occupant(presence.from_user, name, presence.show, presence.from); - } - else { - log('ooops! user jid not found in presence stanza'); - } - if (presence.awaiting_configuration) { - log('sending instant room configuration to ' + Strophe.getBareJidFromJid(presence.from)); - Jabber.connection.sendIQ( - $iq({type: 'set', to: Strophe.getBareJidFromJid(presence.from)}) - .c('query', {xmlns: Strophe.NS.MUC_OWNER}) - .c('x', {xmlns: Strophe.NS.DATA_FORMS, type: 'submit'}) - ); - } - } - else { - log('receiving contact presence from ' + presence.from + ' as ' + presence.show); - var jid = Strophe.getBareJidFromJid(presence.from); - if (jid != Jabber.connection.jid) { - var name = Jabber.name_of(Jabber.jid_to_id(jid)); - Jabber.insert_or_update_contact(jid, name, presence.show); - Jabber.update_chat_title(); - } - else { - // why server sends presence from myself to me? - log('ignoring presence from myself'); - if(presence.show=='offline') { - console.log(Jabber.presence_status); - Jabber.send_availability_status(Jabber.presence_status); - } - } - } + if ($(stanza).find('x[xmlns="'+ Strophe.NS.MUC_USER +'"]').length > 0) { + result.is_from_room = true; + result.from_user = $(stanza).find('x item').attr('jid'); + if ($(stanza).find('x item').attr('affiliation') == 'owner') { + result.awaiting_configuration = ($(stanza).find('x status').attr('code') == '201'); + } } - return true; - }, - - on_private_message: function (message) { - message = Jabber.parse(message); - log('receiving message from ' + message.from); - var jid = Strophe.getBareJidFromJid(message.from); - var jid_id = Jabber.jid_to_id(jid); - var name = Jabber.name_of(jid_id); - create_conversation_tab(name, jid_id); - Jabber.show_message(jid, name, escape_html(message.body), 'other', Strophe.getNodeFromJid(jid)); - notifyMessage(message); - return true; - }, - - on_public_message: function (message) { - message = Jabber.parse(message); - log('receiving message from ' + message.from); - var name = Strophe.getResourceFromJid(message.from); - // is a message from the room itself - if (! name) { - Jabber.show_notice(Jabber.jid_to_id(message.from), message.body); - } - // is a message from another user, not mine - else if ($own_name != name) { - var jid = Jabber.rooms[Jabber.jid_to_id(message.from)][name]; - Jabber.show_message(message.from, name, escape_html(message.body), name, Strophe.getNodeFromJid(jid)); + } + else if (Strophe.isTagEqual(stanza, 'message')) { + result.from = $(stanza).attr('from'); + result.body = $(stanza).find('body').text(); + if ($(stanza).find('error').length > 0) { + result.error = $(stanza).find('error text').text(); + if (!result.error && $(stanza).find('error').find('service-unavailable').length > 0) { + result.error = $user_unavailable_error; + } } - notifyMessage(message); - return true; - }, - - on_message_error: function (message) { - message = Jabber.parse(message) - var jid = Strophe.getBareJidFromJid(message.from); - log('Receiving error message from ' + jid); - var body = Jabber.template('.error-message').replace('%{text}', message.error); - Jabber.show_message(jid, Jabber.name_of(Jabber.jid_to_id(jid)), body, 'other', Strophe.getNodeFromJid(jid)); - return true; - }, - - on_muc_support: function(iq) { - if ($(iq).find('identity[category=conference]').length > 0 && $(iq).find('feature[var="'+ Strophe.NS.MUC +'"]').length > 0) { - var name = $(iq).find('identity[category=conference]').attr('name'); - log('muc support found with identity '+ name); - Jabber.muc_supported = true; + } + return result; + }, + + on_presence: function (presence) { + presence = Jabber.parse(presence); + if (presence.type != 'error') { + if (presence.is_from_room) { + log('receiving room presence from ' + presence.from + ' as ' + presence.show); + var name = Strophe.getResourceFromJid(presence.from); + if (presence.from_user) { + Jabber.insert_or_update_occupant(presence.from_user, name, presence.show, presence.from); + } + else { + log('ooops! user jid not found in presence stanza'); + } + if (presence.awaiting_configuration) { + log('sending instant room configuration to ' + Strophe.getBareJidFromJid(presence.from)); + Jabber.connection.sendIQ( + $iq({type: 'set', to: Strophe.getBareJidFromJid(presence.from)}) + .c('query', {xmlns: Strophe.NS.MUC_OWNER}) + .c('x', {xmlns: Strophe.NS.DATA_FORMS, type: 'submit'}) + ); + } } else { - log('muc support not found'); + log('receiving contact presence from ' + presence.from + ' as ' + presence.show); + var jid = Strophe.getBareJidFromJid(presence.from); + if (jid != Jabber.connection.jid) { + var jid_id = Jabber.jid_to_id(jid); + var name = Jabber.name_of(jid_id); + if(presence.show == 'chat') + Jabber.remove_notice(jid_id); + Jabber.insert_or_update_contact(jid, name, presence.show); + Jabber.update_chat_title(); + } + else { + // why server sends presence from myself to me? + log('ignoring presence from myself'); + if(presence.show=='offline') { + Jabber.send_availability_status(Jabber.presence_status); + } + } } - }, + } + return true; + }, - attach_connection: function(data) { - // create the connection and attach it - Jabber.connection = new Strophe.Connection(Jabber.bosh_service); - Jabber.connection.attach(data.jid, data.sid, data.rid, Jabber.on_connect); + on_private_message: function (message) { + message = Jabber.parse(message); + log('receiving message from ' + message.from); + var jid = Strophe.getBareJidFromJid(message.from); + var jid_id = Jabber.jid_to_id(jid); + var name = Jabber.name_of(jid_id); + create_conversation_tab(name, jid_id); + Jabber.show_message(jid, name, escape_html(message.body), 'other', Strophe.getNodeFromJid(jid)); + renew_conversation_order(jid); + notifyMessage(message); + return true; + }, + + on_public_message: function (message) { + message = Jabber.parse(message); + log('receiving message from ' + message.from); + var name = Strophe.getResourceFromJid(message.from); + // is a message from the room itself + if (! name) { + // FIXME Ignoring message from room for now. + // Jabber.show_notice(Jabber.jid_to_id(message.from), message.body); + } + // is a message from another user, not mine + else if ($own_name != name) { + var jid = Jabber.rooms[Jabber.jid_to_id(message.from)][name]; + Jabber.show_message(message.from, name, escape_html(message.body), name, Strophe.getNodeFromJid(jid)); + renew_conversation_order(jid); + notifyMessage(message); + } + return true; + }, - // handle get roster list (buddy list) - Jabber.connection.sendIQ($iq({type: 'get'}).c('query', {xmlns: Strophe.NS.ROSTER}), Jabber.on_roster); + on_message_error: function (message) { + }, - // handle presence updates in roster list - Jabber.connection.addHandler(Jabber.on_roster_changed, 'jabber:iq:roster', 'iq', 'set'); + on_muc_support: function(iq) { + if ($(iq).find('identity[category=conference]').length > 0 && $(iq).find('feature[var="'+ Strophe.NS.MUC +'"]').length > 0) { + var name = $(iq).find('identity[category=conference]').attr('name'); + log('muc support found with identity '+ name); + Jabber.muc_supported = true; + } + else { + log('muc support not found'); + } + }, - // Handle messages - Jabber.connection.addHandler(Jabber.on_private_message, null, "message", "chat"); + attach_connection: function(data) { + // create the connection and attach it + Jabber.connection = new Strophe.Connection(Jabber.bosh_service); + Jabber.connection.attach(data.jid, data.sid, data.rid, Jabber.on_connect); - // Handle conference messages - Jabber.connection.addHandler(Jabber.on_public_message, null, "message", "groupchat"); + // handle get roster list (buddy list) + Jabber.connection.sendIQ($iq({type: 'get'}).c('query', {xmlns: Strophe.NS.ROSTER}), Jabber.on_roster); - // Handle message errors - Jabber.connection.addHandler(Jabber.on_message_error, null, "message", "error"); + // handle presence updates in roster list + Jabber.connection.addHandler(Jabber.on_roster_changed, 'jabber:iq:roster', 'iq', 'set'); - // discovering MUC support - Jabber.connection.sendIQ( - $iq({type: 'get', from: Jabber.connection.jid, to: Jabber.muc_domain}) - .c('query', {xmlns: Strophe.NS.DISCO_INFO}), - Jabber.on_muc_support - ); + // Handle messages + Jabber.connection.addHandler(Jabber.on_private_message, null, "message", "chat"); - // uncomment for extra debugging - //Strophe.log = function (lvl, msg) { log(msg); }; - }, + // Handle conference messages + Jabber.connection.addHandler(Jabber.on_public_message, null, "message", "groupchat"); - connect: function() { - if (Jabber.connection && Jabber.connection.connected) { - Jabber.send_availability_status(Jabber.presence_status); - } - else { - log('starting XMPP/BOSH session...'); - $('#buddy-list .toolbar').removeClass('small-loading-dark').addClass('small-loading-dark'); - $('.dialog-error').hide(); - $.ajax({ - url: '/chat/start_session', - dataType: 'json', - success: function(data) { - Jabber.attach_connection(data) - }, - error: function(error) { - $('#buddy-list .toolbar').removeClass('small-loading-dark'); - $('#buddy-list .dialog-error') - .html(error.responseText) - .show('highlight') - .unbind('click') - .click(function() { $(this).hide('highlight'); }); - } - }); - } - }, - - deliver_message: function(jid, body) { - var type = Jabber.type_of(Jabber.jid_to_id(jid)); - var message = $msg({to: jid, from: Jabber.connection.jid, "type": type}) - .c('body').t(body).up() - .c('active', {xmlns: Strophe.NS.CHAT_STATES}); - Jabber.connection.send(message); - Jabber.show_message(jid, $own_name, escape_html(body), 'self', Strophe.getNodeFromJid(Jabber.connection.jid)); - move_conversation_to_the_top(jid); - }, - - is_a_room: function(jid_id) { - return Jabber.type_of(jid_id) == 'groupchat'; - }, - - show_notice: function(jid_id, msg) { - var tab_id = '#' + Jabber.conversation_prefix + jid_id; - var notice = $(tab_id).find('.history .notice'); - if (notice.length > 0) - notice.html(msg) - else - $(tab_id).find('.history').append("" + msg + ""); - } - }; - - $('#chat-connect').live('click', function() { - Jabber.presence_status = 'chat'; - Jabber.connect(); - }); - - $('#chat-disconnect').click(function() { - disconnect(); - }); - - $('#chat-busy').click(function() { - Jabber.presence_status = 'dnd'; - Jabber.connect(); - }); - - $('#chat-retry').live('click', function() { - Jabber.presence_status = Jabber.presence_status || 'chat'; - Jabber.connect(); - }); - - $('.conversation textarea').live('keydown', function(e) { - if (e.keyCode == 13) { - var jid = $(this).attr('data-to'); - var body = $(this).val(); - body = body.stripScripts(); - save_message(jid, body); - Jabber.deliver_message(jid, body); - $(this).val(''); - return false; - } - }); - - function save_message(jid, body) { - $.post('/chat/save_message', { - to: getIdentifier(jid), - body: body - }); - } + // Handle message errors + Jabber.connection.addHandler(Jabber.on_message_error, null, "message", "error"); - // open new conversation or change to already opened tab - $('#buddy-list .buddies li a').live('click', function() { - var jid_id = $(this).attr('id'); - var name = Jabber.name_of(jid_id); - var conversation = create_conversation_tab(name, jid_id); - - $('.conversation').hide(); - conversation.show(); - count_unread_messages(jid_id, true); - if(conversation.find('.chat-offset-container-0').length == 0) - recent_messages(Jabber.jid_of(jid_id)); - conversation.find('.conversation .input-div textarea.input').focus(); - $.post('/chat/tab', {tab_id: jid_id}); - }); - - // put name into text area when click in one occupant - $('.occupants .occupant-list li a').live('click', function() { - var jid_id = $(this).attr('data-id'); - var name = Jabber.name_of(jid_id); - var val = $('.conversation textarea:visible').val(); - $('.conversation textarea:visible').focus().val(val + name + ', '); - }); + // discovering MUC support + Jabber.connection.sendIQ( + $iq({type: 'get', from: Jabber.connection.jid, to: Jabber.muc_domain}) + .c('query', {xmlns: Strophe.NS.DISCO_INFO}), + Jabber.on_muc_support + ); - $('#chat .conversation .history').live('click', function() { - $('.conversation textarea:visible').focus(); - }); + // uncomment for extra debugging + //Strophe.log = function (lvl, msg) { log(msg); }; + }, - function toggle_chat_window() { - if(jQuery('#conversations .conversation').length == 0) jQuery('.buddies a').first().click(); - jQuery('#chat').toggleClass('opened'); - jQuery('#chat-label').toggleClass('opened'); - } + connect: function() { + if (Notification.permission !== "granted" && Notification.permission !== "denied") { + Notification.requestPermission(function (permission) { + if (!('permission' in Notification)) { + Notification.permission = permission; + } + }); + } - function load_conversation(jid) { - var jid_id = Jabber.jid_to_id(jid); - var name = Jabber.name_of(jid_id); - if (jid) { - if (Strophe.getDomainFromJid(jid) == Jabber.muc_domain) { - if (Jabber.muc_supported) { - log('opening groupchat with ' + jid); - Jabber.jids[jid_id] = {jid: jid, name: name, type: 'groupchat'}; - var conversation = create_conversation_tab(name, jid_id); - Jabber.enter_room(jid); - recent_messages(jid); - return conversation; - } - } - else { - log('opening chat with ' + jid); - Jabber.jids[jid_id] = {jid: jid, name: name, type: 'friendchat'}; - var conversation = create_conversation_tab(name, jid_id); - recent_messages(jid); - return conversation; - } + if (Jabber.connection && Jabber.connection.connected) { + Jabber.send_availability_status(Jabber.presence_status); } - } - - function open_conversation(jid) { - var conversation = load_conversation(jid); - var jid_id = $(this).attr('id'); - - $('.conversation').hide(); - conversation.show(); - count_unread_messages(jid_id, true); - if(conversation.find('.chat-offset-container-0').length == 0) - recent_messages(Jabber.jid_of(jid_id)); - conversation.find('.input').focus(); - $('#chat').addClass('opened'); - $('#chat-label').addClass('opened'); - $.post('/chat/tab', {tab_id: jid_id}); - } - - function create_conversation_tab(title, jid_id) { - var conversation_id = Jabber.conversation_prefix + jid_id; - var conversation = $('#' + conversation_id); - if (conversation.length > 0) { - return conversation; + else { + log('starting XMPP/BOSH session...'); + $('#buddy-list .toolbar').removeClass('small-loading-dark').addClass('small-loading-dark'); + $('.dialog-error').hide(); + $.ajax({ + url: '/chat/start_session', + dataType: 'json', + success: function(data) { + Jabber.attach_connection(data) + }, + error: function(error) { + $('#buddy-list .toolbar').removeClass('small-loading-dark'); + $('#buddy-list .dialog-error') + .html(error.responseText) + .show('highlight') + .unbind('click') + .click(function() { $(this).hide('highlight'); }); + } + }); } + }, - var jid = Jabber.jid_of(jid_id); - var identifier = getIdentifier(jid); + deliver_message: function(jid, body) { + var jid_id = Jabber.jid_to_id(jid); + var type = Jabber.type_of(jid_id); + var presence = Jabber.presence_of(jid_id); + var message = $msg({to: jid, from: Jabber.connection.jid, "type": type}) + .c('body').t(body).up() + .c('active', {xmlns: Strophe.NS.CHAT_STATES}); + Jabber.connection.send(message); + Jabber.show_message(jid, $own_name, escape_html(body), 'self', Strophe.getNodeFromJid(Jabber.connection.jid)); + save_message(jid, body); + renew_conversation_order(jid); + move_conversation_to_the_top(jid); + if (presence == 'offline') + Jabber.show_notice(jid_id, $user_unavailable_error); + }, + + is_a_room: function(jid_id) { + return Jabber.type_of(jid_id) == 'groupchat'; + }, + + show_notice: function(jid_id, msg) { + var tab_id = '#' + Jabber.conversation_prefix + jid_id; + var history = $(tab_id).find('.history'); + var notice = $(tab_id).find('.history .notice'); + if (notice.length > 0) + notice.html(msg) + else + $(tab_id).find('.history').append("" + msg + ""); + history.scrollTo({top:'100%', left:'0%'}); + }, + + remove_notice: function(jid_id) { + var tab_id = '#' + Jabber.conversation_prefix + jid_id; + var notice = $(tab_id).find('.history .notice').remove(); + }, + }; - var panel = $('#chat-templates .conversation').clone().appendTo($conversations).attr('id', conversation_id); - panel.find('.chat-target .avatar').replaceWith(getAvatar(identifier)); - panel.find('.chat-target .other-name').html(title); - $('#chat .history').perfectScrollbar(); + $('#chat-connect').live('click', function() { + Jabber.presence_status = 'chat'; + Jabber.connect(); + $('#chat .simplemenu-submenu').hide(); + return false; + }); - panel.find('.history').scroll(function(){ - if($(this).scrollTop() == 0){ - var offset = panel.find('.message p').size(); - recent_messages(jid, offset); - } - }); + $('#chat-disconnect').click(function() { + disconnect(); + $('#chat .simplemenu-submenu').hide(); + return false; + }); + + $('#chat-busy').click(function() { + Jabber.presence_status = 'dnd'; + Jabber.connect(); + $('#chat .simplemenu-submenu').hide(); + return false; + }); - var textarea = panel.find('textarea'); - textarea.attr('name', panel.id); + $('#chat-retry').live('click', function() { + Jabber.presence_status = Jabber.presence_status || 'chat'; + Jabber.connect(); + return false; + }); + + $('.conversation textarea').live('keydown', function(e) { + if (e.keyCode == 13) { + var jid = $(this).attr('data-to'); + var body = $(this).val(); + body = $('
    '+ body +'
    ').find('script,noscript,style').remove().end().html(); + Jabber.deliver_message(jid, body); + $(this).val(''); + return false; + } + }); - if (Jabber.is_a_room(jid_id)) { - panel.append(Jabber.template('.occupant-list-template')); - panel.find('.history').addClass('room'); - var room_actions = $('#chat-templates .room-action').clone(); - room_actions.data('jid', jid); - panel.find('.history').after(room_actions); - $('#chat .occupants .occupant-list').perfectScrollbar(); + function save_message(jid, body) { + $.post('/chat/save_message', { + to: getIdentifier(jid), + body: body + }, function(data){ + if(data.status > 0){ + console.log(data.message); + if(data.backtrace) console.log(data.backtrace); } - textarea.attr('data-to', jid); - - return panel; - } - - function ensure_scroll(jid, offset) { - var jid_id = Jabber.jid_to_id(jid); - var history = jQuery('#conversation-'+jid_id+' .history'); - // Load more messages if was not enough to show the scroll - if(history.prop('scrollHeight') - history.prop('clientHeight') <= 0){ - var offset = history.find('.message p').size(); - recent_messages(jid, offset); - } - } - - function recent_messages(jid, offset) { - if (Jabber.no_more_messages[jid]) return; - - if(!offset) offset = 0; - start_fetching('.history'); - $.getJSON('/chat/recent_messages', {identifier: getIdentifier(jid), offset: offset}, function(data) { - // Register if no more messages returned and stop trying to load - // more messages in the future. - if(data.length == 0) - Jabber.no_more_messages[jid] = true; - - $.each(data, function(i, message) { - var body = message['body']; - var from = message['from']; - var to = message['to']; - var date = message['created_at']; - var who = from['id']==getCurrentIdentifier() ? 'self' : from['id'] - - Jabber.show_message(jid, from['name'], body, who, from['id'], date, offset); - }); - stop_fetching('.history'); - ensure_scroll(jid, offset); - }); - } - - function move_conversation_to_the_top(jid) { - id = Jabber.jid_to_id(jid); - var link = $('#'+id); - var li = link.closest('li'); - var ul = link.closest('ul'); - ul.prepend(li); - } - - function sort_conversations() { - $.getJSON('/chat/recent_conversations', {}, function(data) { - $.each(data['order'], function(i, identifier) { - move_conversation_to_the_top(identifier+'-'+data['domain']); - }) - }) - } - - function load_defaults() { - $.getJSON('/chat/my_session', {}, function(data) { - $.each(data.rooms, function(i, room_jid) { - $('#chat').trigger('opengroup', room_jid); - }) - - $('#'+data.tab_id).click(); - - if(data.status == 'opened') - toggle_chat_window(); - }) - } - - function count_unread_messages(jid_id, hide) { - var unread = $('.buddies #'+jid_id+ ' .unread-messages'); - if (hide) { - unread.hide(); - unread.siblings('img').show(); - Jabber.unread_messages_of(jid_id, 0); - unread.text(''); + }).fail(function(){ + console.log('500 - Internal server error.') + }); + } + + // open new conversation or change to already opened tab + $('#buddy-list .buddies li a').live('click', function() { + var jid = Jabber.jid_of($(this).attr('id')); + open_conversation(jid); + return false; + }); + + // put name into text area when click in one occupant + $('.occupants .occupant-list li a').live('click', function() { + var jid_id = $(this).attr('data-id'); + var name = Jabber.name_of(jid_id); + var val = $('.conversation textarea:visible').val(); + $('.conversation textarea:visible').focus().val(val + name + ', '); + return false; + }); + + $('#chat .conversation .history').live('click', function() { + $('.conversation textarea:visible').focus(); + }); + + function toggle_chat_window() { + if(jQuery('#conversations .conversation').length == 0) jQuery('.buddies a').first().click(); + jQuery('#chat').toggleClass('opened'); + jQuery('#chat-label').toggleClass('opened'); + } + + function load_conversation(jid) { + var jid_id = Jabber.jid_to_id(jid); + var name = Jabber.name_of(jid_id); + if (jid) { + if (Strophe.getDomainFromJid(jid) == Jabber.muc_domain) { + if (Jabber.muc_supported) { + log('opening groupchat with ' + jid); + Jabber.jids[jid_id] = {jid: jid, name: name, type: 'groupchat'}; + var conversation = create_conversation_tab(name, jid_id); + Jabber.enter_room(jid); + recent_messages(jid, 0, true); + return conversation; + } } else { - unread.siblings('img').hide(); - unread.css('display', 'inline-block'); - var unread_messages = Jabber.unread_messages_of(jid_id) || 0; - Jabber.unread_messages_of(jid_id, ++unread_messages); - unread.text(unread_messages); - } - update_total_unread_messages(); - } - - function update_total_unread_messages() { - var total_unread = $('#openchat .unread-messages'); - var sum = 0; - $('.buddies .unread-messages').each(function() { - sum += Number($(this).text()); - }); - if(sum>0) { - total_unread.text(sum); - } else { - total_unread.text(''); + log('opening chat with ' + jid); + Jabber.jids[jid_id] = {jid: jid, name: name, type: 'chat'}; + var conversation = create_conversation_tab(name, jid_id); + recent_messages(jid, 0, true); + return conversation; } - } + } + } + + function open_conversation(jid) { + var conversation = load_conversation(jid); + var jid_id = Jabber.jid_to_id(jid); + + $('.conversation').hide(); + conversation.show(); + conversation.find('.input').focus(); + $('#chat').addClass('opened'); + $('#chat-label').addClass('opened'); + $.post('/chat/tab', {tab_id: jid_id}); + } + + function create_conversation_tab(title, jid_id) { + var conversation_id = Jabber.conversation_prefix + jid_id; + var conversation = $('#' + conversation_id); + if (conversation.length > 0) { + return conversation; + } + + var jid = Jabber.jid_of(jid_id); + var identifier = getIdentifier(jid); - var $conversations = $('#chat-window #conversations'); + var panel = $('#chat-templates .conversation').clone().appendTo($conversations).attr('id', conversation_id); + panel.find('.chat-target .avatar').replaceWith(getAvatar(identifier)); + panel.find('.chat-target .other-name').html(title); + $('#chat .history').perfectScrollbar(); - function log(msg) { - if(Jabber.debug && window.console && window.console.log) { - var time = new Date(); - window.console.log('['+ time.toTimeString() +'] ' + msg); + panel.find('.history').scroll(function(){ + if($(this).scrollTop() == 0){ + var offset = panel.find('.message p').size(); + recent_messages(jid, offset); } - } + }); + + var textarea = panel.find('textarea'); + textarea.attr('name', panel.id); + + if (Jabber.is_a_room(jid_id)) { + panel.append(Jabber.template('.occupant-list-template')); + panel.find('.history').addClass('room'); + var room_actions = $('#chat-templates .room-action').clone(); + room_actions.data('jid', jid); + panel.find('.history').after(room_actions); + $('#chat .occupants .occupant-list').perfectScrollbar(); + } + textarea.attr('data-to', jid); + + return panel; + } + + function ensure_scroll(jid, offset) { + var jid_id = Jabber.jid_to_id(jid); + var history = jQuery('#conversation-'+jid_id+' .history'); + // Load more messages if was not enough to show the scroll + if(history.prop('scrollHeight') - history.prop('clientHeight') <= 0){ + var offset = history.find('.message p').size(); + recent_messages(jid, offset); + } + } + + function recent_messages(jid, offset, clear_unread) { + if (Jabber.no_more_messages[jid]) return; + + if(!offset) offset = 0; + start_fetching('.history'); + $.getJSON('/chat/recent_messages', {identifier: getIdentifier(jid), offset: offset}, function(data) { + // Register if no more messages returned and stop trying to load + // more messages in the future. + if(data.length == 0) + Jabber.no_more_messages[jid] = true; + + $.each(data, function(i, message) { + var body = message['body']; + var from = message['from']; + var to = message['to']; + var date = message['created_at']; + var who = from['id']==getCurrentIdentifier() ? 'self' : from['id'] + + Jabber.show_message(jid, from['name'], body, who, from['id'], date, offset); + }); + stop_fetching('.history'); + ensure_scroll(jid, offset); - function escape_html(body) { - return body - .replace(/&/g, '&') - .replace(//g, '>'); - } + if(clear_unread){ + var jid_id = Jabber.jid_to_id(jid); + count_unread_messages(jid_id, true); + } + }); + } + + function move_conversation_to_the_top(jid) { + id = Jabber.jid_to_id(jid); + var link = $('#'+id); + var li = link.closest('li'); + var ul = link.closest('ul'); + ul.prepend(li); + } + + function renew_conversation_order(jid){ + var i = Jabber.conversations_order.indexOf(jid); + // Remove element from the list + if(i >= 0) { + var elem = Jabber.conversations_order[i]; + var a = Jabber.conversations_order.slice(0,i); + var b = Jabber.conversations_order.slice(i+1, Jabber.conversations_order.length); + Jabber.conversations_order = a.concat(b); + } else + var elem = jid; + + Jabber.conversations_order = Jabber.conversations_order.concat(elem); + } + + function sort_conversations() { + if(Jabber.conversations_order){ + for (var i = 0; i < Jabber.conversations_order.length; i++) + move_conversation_to_the_top(Jabber.conversations_order[i]); + } + } + + function load_defaults() { + $.getJSON('/chat/my_session', {}, function(data) { + $.each(data.rooms, function(i, room_jid) { + load_conversation(room_jid); + }) + + $('#'+data.tab_id).click(); + + if(data.status == 'opened') + toggle_chat_window(); + }) + } + + function count_unread_messages(jid_id, hide) { + var unread = $('.buddies #'+jid_id+ ' .unread-messages'); + if (hide) { + unread.hide(); + unread.siblings('img').show(); + Jabber.unread_messages_of(jid_id, 0); + unread.text(''); + } + else { + unread.siblings('img').hide(); + unread.css('display', 'inline-block'); + var unread_messages = Jabber.unread_messages_of(jid_id) || 0; + Jabber.unread_messages_of(jid_id, ++unread_messages); + unread.text(unread_messages); + } + update_total_unread_messages(); + } + + function update_total_unread_messages() { + var total_unread = $('#unread-messages'); + var sum = 0; + $('#chat .unread-messages').each(function() { + sum += Number($(this).text()); + }); + if(sum>0) { + total_unread.text(sum); + } else { + total_unread.text(''); + } + } - function getCurrentIdentifier() { - return getIdentifier(Jabber.connection.jid); - } + var $conversations = $('#chat-window #conversations'); - function getIdentifier(jid) { - return Strophe.getNodeFromJid(jid); - } + function log(msg) { + if(Jabber.debug && window.console && window.console.log) { + var time = new Date(); + window.console.log('['+ time.toTimeString() +'] ' + msg); + } + } + + function escape_html(body) { + return body + .replace(/&/g, '&') + .replace(//g, '>'); + } + + function getCurrentIdentifier() { + return getIdentifier(Jabber.connection.jid); + } + + function getIdentifier(jid) { + return Strophe.getNodeFromJid(jid); + } + + function getMyAvatar() { + return getAvatar(getCurrentIdentifier()); + } + + function getAvatar(identifier) { + if(Jabber.avatars[identifier]) + var src = Jabber.avatars[identifier]; + else + var src = "/chat/avatar/"+identifier; + + return ''; + } + + function disconnect() { + log('disconnect'); + if (Jabber.connection && Jabber.connection.connected) { + Jabber.connection.disconnect(); + } + Jabber.presence_status = 'offline'; + Jabber.show_status('offline'); + } + + function notifyMessage(message) { + var jid = Strophe.getBareJidFromJid(message.from); + var jid_id = Jabber.jid_to_id(jid); + var name = Jabber.name_of(jid_id); + var identifier = Strophe.getNodeFromJid(jid); + var avatar = "/chat/avatar/"+identifier + if(!$('#chat').hasClass('opened') || window.isHidden() || !Jabber.window_visibility) { + var options = {body: message.body, icon: avatar, tag: jid_id}; + $(notifyMe(name, options)).on('click', function(){ + open_conversation(jid); + }); + Jabber.notification_sound.play(); + } + } - function getMyAvatar() { - return getAvatar(getCurrentIdentifier()); - } + $('.title-bar a').click(function() { + $(this).parents('.status-group').find('.buddies').toggle('fast'); + return false; + }); + $('#chat').on('click', '.occupants a', function() { + $(this).siblings('.occupant-list').toggle('fast'); + $(this).toggleClass('up'); + return false; + }); - function getAvatar(identifier) { - return ''; - } + //restore connection if user was connected + if($presence=='' || $presence == 'chat') { + $('#chat-connect').trigger('click'); + } else if($presence == 'dnd') { + $('#chat-busy').trigger('click'); + } - function disconnect() { - log('disconnect'); - if (Jabber.connection && Jabber.connection.connected) { - Jabber.connection.disconnect(); - } - Jabber.presence_status = 'offline'; - Jabber.show_status('offline'); - } - - function notifyMessage(message) { - var jid = Strophe.getBareJidFromJid(message.from); - var jid_id = Jabber.jid_to_id(jid); - var name = Jabber.name_of(jid_id); - var identifier = Strophe.getNodeFromJid(jid); - var avatar = "/chat/avatar/"+identifier - if(!$('#chat').hasClass('opened') || window.isHidden()) { - var options = {body: message.body, icon: avatar, tag: jid_id}; - console.log('Notify '+name); - $(notifyMe(name, options)).on('click', function(){ - open_conversation(jid); - }); - $.sound.play('/sounds/receive.wav'); - } - } - - $('.title-bar a').click(function() { - $(this).parents('.status-group').find('.buddies').toggle('fast'); - }); - $('#chat').on('click', '.occupants a', function() { - $(this).siblings('.occupant-list').toggle('fast'); - $(this).toggleClass('up'); - }); - - //restore connection if user was connected - if($presence=='' || $presence == 'chat') { - $('#chat-connect').trigger('click'); - } else if($presence == 'dnd') { - $('#chat-busy').trigger('click'); - } - - $('#chat #buddy-list .buddies').perfectScrollbar(); + $('#chat #buddy-list .buddies').perfectScrollbar(); // custom css expression for a case-insensitive contains() jQuery.expr[':'].Contains = function(a,i,m){ - return (a.textContent || a.innerText || "").toUpperCase().indexOf(m[3].toUpperCase())>=0; + return (a.textContent || a.innerText || "").toUpperCase().indexOf(m[3].toUpperCase())>=0; }; $('#chat .search').change( function () { @@ -856,6 +941,7 @@ jQuery(function($) { $('#chat .buddies a').live('click', function(){ $('#chat .search').val('').change(); + return false; }); $('#chat-label').click(function(){ @@ -871,6 +957,14 @@ jQuery(function($) { $('.room-action.leave').live('click', function(){ var jid = $(this).data('jid'); Jabber.leave_room(jid); + return false; + }); + + $('.open-conversation').live('click', function(){ + open_conversation($(this).data('jid')); + return false; }); + window.onfocus = function() {Jabber.window_visibility = true}; + window.onblur = function() {Jabber.window_visibility = false}; }); diff --git a/public/stylesheets/chat.css b/public/stylesheets/chat.css index ab9c3bb..c9f1407 100644 --- a/public/stylesheets/chat.css +++ b/public/stylesheets/chat.css @@ -201,6 +201,21 @@ bottom: 132px; } +#chat-label.opened #unread-messages, +#unread-messages:empty { + display: none; +} + +#unread-messages { + padding: 3px 5px; + background-color: #F57900; + border-radius: 5px; + margin-top: -10px; + margin-left: -30px; + position: absolute; + z-index: 1; +} + #chat .unread-messages { height: 32px; line-height: 32px; diff --git a/test/functional/chat_controller_test.rb b/test/functional/chat_controller_test.rb index 3a5e8e7..7d85d89 100644 --- a/test/functional/chat_controller_test.rb +++ b/test/functional/chat_controller_test.rb @@ -95,6 +95,54 @@ class ChatControllerTest < ActionController::TestCase assert_not_equal chat_status_at, @person.user.chat_status_at end + should 'forbid to register a message without to' do + @controller.stubs(:current_user).returns(@person.user) + @request.stubs(:xhr?).returns(true) + + post :save_message, {:body =>'Hello!'} + assert ActiveSupport::JSON.decode(@response.body)['status'] == 1 + end + + should 'forbid to register a message without body' do + @controller.stubs(:current_user).returns(@person.user) + @request.stubs(:xhr?).returns(true) + + post :save_message, {:to =>'mary'} + assert ActiveSupport::JSON.decode(@response.body)['status'] == 1 + end + + should 'forbid user to register a message to a stranger' do + @controller.stubs(:current_user).returns(@person.user) + @request.stubs(:xhr?).returns(true) + + post :save_message, {:to =>'random', :body => 'Hello, stranger!'} + assert ActiveSupport::JSON.decode(@response.body)['status'] == 2 + end + + should 'register a message to a friend' do + @controller.stubs(:current_user).returns(@person.user) + friend = create_user('friend').person + @person.add_friend friend + @request.stubs(:xhr?).returns(true) + + assert_difference 'ChatMessage.count', 1 do + post :save_message, {:to => friend.identifier, :body => 'Hey! How is it going?'} + assert ActiveSupport::JSON.decode(@response.body)['status'] == 0 + end + end + + should 'register a message to a group' do + @controller.stubs(:current_user).returns(@person.user) + group = fast_create(Organization) + group.add_member(@person) + @request.stubs(:xhr?).returns(true) + + assert_difference 'ChatMessage.count', 1 do + post :save_message, {:to => group.identifier, :body => 'Hey! How is it going?'} + assert ActiveSupport::JSON.decode(@response.body)['status'] == 0 + end + end + should 'toggle chat status' do login_as 'testuser' diff --git a/test/unit/chat_message_test.rb b/test/unit/chat_message_test.rb new file mode 100644 index 0000000..9320eff --- /dev/null +++ b/test/unit/chat_message_test.rb @@ -0,0 +1,9 @@ +require 'test_helper' + +class ChatMessageTest < ActiveSupport::TestCase + should 'create message' do + assert_difference 'ChatMessage.count', 1 do + ChatMessage.create!(:from => fast_create(Person), :to => fast_create(Person), :body => 'Hey! How are you?' ) + end + end +end -- libgit2 0.21.2