Commit f1a6c012dc170adade17b9a89510e95e3a3b9698

Authored by Victor Costa
2 parents 2598c37e 10662c2c

Merge branch 'rails3_chat' into rails3_stable

app/controllers/public/chat_controller.rb
@@ -31,6 +31,34 @@ class ChatController < PublicController @@ -31,6 +31,34 @@ class ChatController < PublicController
31 render :nothing => true 31 render :nothing => true
32 end 32 end
33 33
  34 + def save_message
  35 + to = environment.profiles.find_by_identifier(params[:to])
  36 + body = params[:body]
  37 +
  38 + ChatMessage.create!(:to => to, :from => user, :body => body)
  39 + render :text => 'ok'
  40 + end
  41 +
  42 + def recent_messages
  43 + other = environment.profiles.find_by_identifier(params[:identifier])
  44 + if other.kind_of?(Organization)
  45 + messages = ChatMessage.where('to_id=:other', :other => other.id)
  46 + else
  47 + messages = ChatMessage.where('(to_id=:other and from_id=:me) or (to_id=:me and from_id=:other)', {:me => user.id, :other => other.id})
  48 + end
  49 +
  50 + messages = messages.order('created_at DESC').includes(:to, :from).limit(20)
  51 + messages_json = messages.map do |message|
  52 + {
  53 + :body => message.body,
  54 + :to => {:id => message.to.identifier, :name => message.to.name, :type => message.to.type},
  55 + :from => {:id => message.from.identifier, :name => message.from.name, :type => message.from.type},
  56 + :created_at => message.created_at
  57 + }
  58 + end
  59 + render :json => messages_json.reverse
  60 + end
  61 +
34 protected 62 protected
35 63
36 def check_environment_feature 64 def check_environment_feature
app/models/chat_message.rb 0 → 100644
@@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
  1 +class ChatMessage < ActiveRecord::Base
  2 + attr_accessible :body, :from, :to
  3 +
  4 + belongs_to :to, :class_name => 'Profile'
  5 + belongs_to :from, :class_name => 'Profile'
  6 +
  7 +end
app/views/shared/logged_in/xmpp_chat.html.erb
1 - <%= javascript_include_tag 'strophejs-1.1.3/strophe.min', 'jquery.emoticon', '../designs/icons/pidgin/emoticons.js', 'ba-linkify', 'jquery.ba-hashchange', 'jquery.sound', 'chat', 'perfect-scrollbar.min.js', 'perfect-scrollbar.with-mousewheel.min.js', :cache => 'cache/chat' %> 1 + <%= javascript_include_tag 'strophejs-1.1.3/strophe.min', 'jquery.emoticon', '../designs/icons/pidgin/emoticons.js', 'ba-linkify', 'jquery.ba-hashchange', 'jquery.sound', 'chat', 'perfect-scrollbar.min.js', 'perfect-scrollbar.with-mousewheel.min.js', 'jquery.timeago.js', :cache => 'cache/chat' %>
2 <%= stylesheet_link_tag 'perfect-scrollbar.min.css' %> 2 <%= stylesheet_link_tag 'perfect-scrollbar.min.css' %>
3 3
4 <% extend ChatHelper %> 4 <% extend ChatHelper %>
@@ -19,12 +19,16 @@ @@ -19,12 +19,16 @@
19 <%= profile_image(user, :portrait, :class => 'avatar') %> 19 <%= profile_image(user, :portrait, :class => 'avatar') %>
20 <%= user_status_menu('icon-menu-offline', _('Offline')) %> 20 <%= user_status_menu('icon-menu-offline', _('Offline')) %>
21 </div> 21 </div>
  22 + <a href="#" class="back"><%= _('Close') %></a>
22 <div class='dialog-error' style='display: none'></div> 23 <div class='dialog-error' style='display: none'></div>
23 </div> 24 </div>
24 25
25 <div id="friends"> 26 <div id="friends">
26 <div id='title-bar'><h1 class='title'><%= _("Online") %>&nbsp;(<span id='friends-online'>0</span>)</h1></div> 27 <div id='title-bar'><h1 class='title'><%= _("Online") %>&nbsp;(<span id='friends-online'>0</span>)</h1></div>
27 - <ul class='buddy-list'></ul> 28 + <ul class='buddy-list online'></ul>
  29 +
  30 + <div id='title-bar'><h1 class='title'><%= _("Offline") %></h1></div>
  31 + <ul class='buddy-list offline'></ul>
28 </div> 32 </div>
29 33
30 <div id="rooms"> 34 <div id="rooms">
@@ -70,7 +74,7 @@ @@ -70,7 +74,7 @@
70 <h5 class="%{who}-name">%{name}</h5> 74 <h5 class="%{who}-name">%{name}</h5>
71 </div> 75 </div>
72 <div class="content"> 76 <div class="content">
73 - <span class="time">%{time}</span> 77 + <span class="time" title="%{time}"></span>
74 <p>%{message}</p> 78 <p>%{message}</p>
75 </div> 79 </div>
76 </div> 80 </div>
db/migrate/20140820173129_create_chat_messages.rb 0 → 100644
@@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
  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
public/javascripts/chat.js
@@ -24,7 +24,6 @@ jQuery(function($) { @@ -24,7 +24,6 @@ jQuery(function($) {
24 muc_domain: $muc_domain, 24 muc_domain: $muc_domain,
25 muc_supported: false, 25 muc_supported: false,
26 presence_status: '', 26 presence_status: '',
27 - update_presence_status_every: $update_presence_status_every, // time in seconds of how often update presence status to Noosfero DB  
28 conversation_prefix: 'conversation-', 27 conversation_prefix: 'conversation-',
29 jids: {}, 28 jids: {},
30 rooms: {}, 29 rooms: {},
@@ -62,11 +61,9 @@ jQuery(function($) { @@ -62,11 +61,9 @@ jQuery(function($) {
62 .replace(/%{presence_status}/g, presence) 61 .replace(/%{presence_status}/g, presence)
63 .replace('%{avatar}', getAvatar(identifier)) 62 .replace('%{avatar}', getAvatar(identifier))
64 .replace('%{name}', name); 63 .replace('%{name}', name);
65 - if ($(item).length > 0) {  
66 - $(item).parent('li').replaceWith(html);  
67 - } else {  
68 - $(list).append(html);  
69 - } 64 +
  65 + $(item).parent().remove();
  66 + $(list).append(html);
70 Jabber.jids[jid_id] = {jid: jid, name: name, type: type, presence: presence}; 67 Jabber.jids[jid_id] = {jid: jid, name: name, type: type, presence: presence};
71 }, 68 },
72 insert_or_update_group: function (jid, presence) { 69 insert_or_update_group: function (jid, presence) {
@@ -82,9 +79,10 @@ jQuery(function($) { @@ -82,9 +79,10 @@ jQuery(function($) {
82 }, 79 },
83 insert_or_update_contact: function (jid, name, presence) { 80 insert_or_update_contact: function (jid, name, presence) {
84 var jid_id = Jabber.jid_to_id(jid); 81 var jid_id = Jabber.jid_to_id(jid);
85 - var list = $('#buddy-list #friends .buddy-list');  
86 var item = $('#' + jid_id); 82 var item = $('#' + jid_id);
87 presence = presence || ($(item).length > 0 ? $(item).parent('li').attr('class') : 'offline'); 83 presence = presence || ($(item).length > 0 ? $(item).parent('li').attr('class') : 'offline');
  84 + var list = $('#buddy-list #friends .buddy-list' + (presence=='offline' ? '.offline' : '.online'));
  85 +
88 log('adding or updating contact ' + jid + ' as ' + presence); 86 log('adding or updating contact ' + jid + ' as ' + presence);
89 Jabber.insert_or_update_user(list, item, jid, name, presence, $('#chat #chat-templates .buddy-item').clone().html(), 'chat'); 87 Jabber.insert_or_update_user(list, item, jid, name, presence, $('#chat #chat-templates .buddy-item').clone().html(), 'chat');
90 $("#chat-window .tab a[href='#"+ Jabber.conversation_prefix + jid_id +"']") 88 $("#chat-window .tab a[href='#"+ Jabber.conversation_prefix + jid_id +"']")
@@ -120,7 +118,7 @@ jQuery(function($) { @@ -120,7 +118,7 @@ jQuery(function($) {
120 return body; 118 return body;
121 }, 119 },
122 120
123 - show_message: function (jid, name, body, who, identifier) { 121 + show_message: function (jid, name, body, who, identifier, time) {
124 if (body) { 122 if (body) {
125 body = Jabber.render_body_message(body); 123 body = Jabber.render_body_message(body);
126 var jid_id = Jabber.jid_to_id(jid); 124 var jid_id = Jabber.jid_to_id(jid);
@@ -129,8 +127,9 @@ jQuery(function($) { @@ -129,8 +127,9 @@ jQuery(function($) {
129 $(tab_id).find('.history').find('.message:last .content').append('<p>' + body + '</p>'); 127 $(tab_id).find('.history').find('.message:last .content').append('<p>' + body + '</p>');
130 } 128 }
131 else { 129 else {
132 - var time = new Date();  
133 - time = time.getHours() + ':' + checkTime(time.getMinutes()); 130 + if (time==undefined) {
  131 + time = new Date().toISOString();
  132 + }
134 var message_html = $('#chat #chat-templates .message').clone().html() 133 var message_html = $('#chat #chat-templates .message').clone().html()
135 .replace('%{message}', body) 134 .replace('%{message}', body)
136 .replace(/%{who}/g, who) 135 .replace(/%{who}/g, who)
@@ -138,6 +137,7 @@ jQuery(function($) { @@ -138,6 +137,7 @@ jQuery(function($) {
138 .replace('%{name}', name) 137 .replace('%{name}', name)
139 .replace('%{avatar}', getAvatar(identifier)); 138 .replace('%{avatar}', getAvatar(identifier));
140 $('#' + Jabber.conversation_prefix + jid_id).find('.history').append(message_html); 139 $('#' + Jabber.conversation_prefix + jid_id).find('.history').append(message_html);
  140 + $(".message span.time").timeago();
141 } 141 }
142 $(tab_id).find('.history').scrollTo({top:'100%', left:'0%'}); 142 $(tab_id).find('.history').scrollTo({top:'100%', left:'0%'});
143 if (who != "self") { 143 if (who != "self") {
@@ -180,7 +180,7 @@ jQuery(function($) { @@ -180,7 +180,7 @@ jQuery(function($) {
180 }, 180 },
181 181
182 update_chat_title: function () { 182 update_chat_title: function () {
183 - var friends_online = $('#buddy-list .buddy-list li:not(.offline)').length; 183 + var friends_online = $('#buddy-list .buddy-list.online li').length;
184 $('#friends-online').text(friends_online); 184 $('#friends-online').text(friends_online);
185 }, 185 },
186 186
@@ -198,7 +198,6 @@ jQuery(function($) { @@ -198,7 +198,6 @@ jQuery(function($) {
198 break; 198 break;
199 case Strophe.Status.DISCONNECTED: 199 case Strophe.Status.DISCONNECTED:
200 log('disconnected'); 200 log('disconnected');
201 - //Jabber.show_status('');  
202 $('#buddy-list ul.buddy-list, .occupant-list ul.occupant-list').html(''); 201 $('#buddy-list ul.buddy-list, .occupant-list ul.occupant-list').html('');
203 Jabber.update_chat_title(); 202 Jabber.update_chat_title();
204 $('#buddy-list .toolbar').removeClass('small-loading-dark'); 203 $('#buddy-list .toolbar').removeClass('small-loading-dark');
@@ -400,13 +399,6 @@ jQuery(function($) { @@ -400,13 +399,6 @@ jQuery(function($) {
400 Jabber.on_muc_support 399 Jabber.on_muc_support
401 ); 400 );
402 401
403 - // Timed handle to save presence status to Noosfero DB every (N) seconds  
404 - Jabber.connection.addTimedHandler(Jabber.update_presence_status_every * 1000, function() {  
405 - log('saving presence status to Noosfero DB');  
406 - $.get('/chat/update_presence_status', { status: {chat_status: Jabber.presence_status} });  
407 - return true;  
408 - });  
409 -  
410 // uncomment for extra debugging 402 // uncomment for extra debugging
411 //Strophe.log = function (lvl, msg) { log(msg); }; 403 //Strophe.log = function (lvl, msg) { log(msg); };
412 }, 404 },
@@ -465,11 +457,6 @@ jQuery(function($) { @@ -465,11 +457,6 @@ jQuery(function($) {
465 disconnect(); 457 disconnect();
466 }); 458 });
467 459
468 - // save presence_status as offline in Noosfero database when close or reload chat window  
469 - $(window).unload(function() {  
470 - disconnect();  
471 - });  
472 -  
473 $('#chat-busy').click(function() { 460 $('#chat-busy').click(function() {
474 Jabber.presence_status = 'dnd'; 461 Jabber.presence_status = 'dnd';
475 Jabber.connect(); 462 Jabber.connect();
@@ -494,6 +481,7 @@ jQuery(function($) { @@ -494,6 +481,7 @@ jQuery(function($) {
494 Jabber.enter_room(jid); 481 Jabber.enter_room(jid);
495 var conversation = create_conversation_tab(name, jid_id); 482 var conversation = create_conversation_tab(name, jid_id);
496 conversation.find('.conversation').show(); 483 conversation.find('.conversation').show();
  484 + recent_messages(jid);
497 } 485 }
498 } 486 }
499 else { 487 else {
@@ -508,12 +496,20 @@ jQuery(function($) { @@ -508,12 +496,20 @@ jQuery(function($) {
508 var jid = $(this).attr('data-to'); 496 var jid = $(this).attr('data-to');
509 var body = $(this).val(); 497 var body = $(this).val();
510 body = body.stripScripts(); 498 body = body.stripScripts();
  499 + save_message(jid, body);
511 Jabber.deliver_message(jid, body); 500 Jabber.deliver_message(jid, body);
512 $(this).val(''); 501 $(this).val('');
513 return false; 502 return false;
514 } 503 }
515 }); 504 });
516 505
  506 + function save_message(jid, body) {
  507 + $.post('/chat/save_message', {
  508 + to: getIdentifier(jid),
  509 + body: body
  510 + });
  511 + }
  512 +
517 // open new conversation or change to already opened tab 513 // open new conversation or change to already opened tab
518 $('#buddy-list .buddy-list li a').live('click', function() { 514 $('#buddy-list .buddy-list li a').live('click', function() {
519 var jid_id = $(this).attr('id'); 515 var jid_id = $(this).attr('id');
@@ -522,6 +518,7 @@ jQuery(function($) { @@ -522,6 +518,7 @@ jQuery(function($) {
522 518
523 conversation.find('.conversation').show(); 519 conversation.find('.conversation').show();
524 count_unread_messages(jid_id, true); 520 count_unread_messages(jid_id, true);
  521 + recent_messages(Jabber.jid_of(jid_id));
525 conversation.find('.conversation .input-div textarea.input').focus(); 522 conversation.find('.conversation .input-div textarea.input').focus();
526 }); 523 });
527 524
@@ -534,14 +531,18 @@ jQuery(function($) { @@ -534,14 +531,18 @@ jQuery(function($) {
534 $('.conversation textarea:visible').val(val + name + ', ').focus(); 531 $('.conversation textarea:visible').val(val + name + ', ').focus();
535 }); 532 });
536 533
537 - $('.conversation .history').live('click', function() { 534 + $('#chat .conversation .history').live('click', function() {
538 $('.conversation textarea:visible').focus(); 535 $('.conversation textarea:visible').focus();
539 }); 536 });
540 537
541 - $('.conversation .back').live('click', function() { 538 + $('#chat .conversation .back').live('click', function() {
542 $('#chat #chat-window .conversation').hide(); 539 $('#chat #chat-window .conversation').hide();
543 }); 540 });
544 541
  542 + $('#chat .toolbar .back').live('click', function() {
  543 + $('#chat').hide('fast');
  544 + });
  545 +
545 function create_conversation_tab(title, jid_id) { 546 function create_conversation_tab(title, jid_id) {
546 var conversation_id = Jabber.conversation_prefix + jid_id; 547 var conversation_id = Jabber.conversation_prefix + jid_id;
547 var conversation = $('#' + conversation_id); 548 var conversation = $('#' + conversation_id);
@@ -567,9 +568,32 @@ jQuery(function($) { @@ -567,9 +568,32 @@ jQuery(function($) {
567 panel.find('.history').addClass('room'); 568 panel.find('.history').addClass('room');
568 } 569 }
569 textarea.attr('data-to', jid); 570 textarea.attr('data-to', jid);
  571 +
570 return panel; 572 return panel;
571 } 573 }
572 574
  575 + function recent_messages(jid) {
  576 + $.getJSON('/chat/recent_messages', {
  577 + identifier: getIdentifier(jid)
  578 + }, function(data) {
  579 + $.each(data, function(i, message) {
  580 + var body = message['body'];
  581 + var from = message['from'];
  582 + var to = message['to'];
  583 + var date = message['created_at'];
  584 + var group = to['type']=='Community' ? 'conference.' : '';
  585 + var domain = '127.0.0.1';
  586 +
  587 + if(from['id']==getCurrentIdentifier()) {
  588 + Jabber.show_message(to['id']+'@'+group+domain, $own_name, body, 'self', to['id'], date);
  589 + } else {
  590 + var target = group!='' ? to['id'] : from['id']
  591 + Jabber.show_message(target+'@'+group+domain, from['name'], body, 'other', from['id'], date);
  592 + }
  593 + });
  594 + });
  595 + }
  596 +
573 function count_unread_messages(jid_id, hide) { 597 function count_unread_messages(jid_id, hide) {
574 var unread = $('.buddy-list #'+jid_id+ ' .unread-messages'); 598 var unread = $('.buddy-list #'+jid_id+ ' .unread-messages');
575 if (hide) { 599 if (hide) {
@@ -648,10 +672,3 @@ jQuery(function($) { @@ -648,10 +672,3 @@ jQuery(function($) {
648 } 672 }
649 $('#chat #buddy-list').perfectScrollbar(); 673 $('#chat #buddy-list').perfectScrollbar();
650 }); 674 });
651 -  
652 -function checkTime(i) {  
653 - if (i<10) {  
654 - i="0" + i;  
655 - }  
656 - return i;  
657 -}  
public/javascripts/jquery.timeago.js 0 → 100644
@@ -0,0 +1,221 @@ @@ -0,0 +1,221 @@
  1 +/**
  2 + * Timeago is a jQuery plugin that makes it easy to support automatically
  3 + * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago").
  4 + *
  5 + * @name timeago
  6 + * @version 1.4.1
  7 + * @requires jQuery v1.2.3+
  8 + * @author Ryan McGeary
  9 + * @license MIT License - http://www.opensource.org/licenses/mit-license.php
  10 + *
  11 + * For usage and examples, visit:
  12 + * http://timeago.yarp.com/
  13 + *
  14 + * Copyright (c) 2008-2013, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org)
  15 + */
  16 +
  17 +(function (factory) {
  18 + if (typeof define === 'function' && define.amd) {
  19 + // AMD. Register as an anonymous module.
  20 + define(['jquery'], factory);
  21 + } else {
  22 + // Browser globals
  23 + factory(jQuery);
  24 + }
  25 +}(function ($) {
  26 + $.timeago = function(timestamp) {
  27 + if (timestamp instanceof Date) {
  28 + return inWords(timestamp);
  29 + } else if (typeof timestamp === "string") {
  30 + return inWords($.timeago.parse(timestamp));
  31 + } else if (typeof timestamp === "number") {
  32 + return inWords(new Date(timestamp));
  33 + } else {
  34 + return inWords($.timeago.datetime(timestamp));
  35 + }
  36 + };
  37 + var $t = $.timeago;
  38 +
  39 + $.extend($.timeago, {
  40 + settings: {
  41 + refreshMillis: 60000,
  42 + allowPast: true,
  43 + allowFuture: false,
  44 + localeTitle: false,
  45 + cutoff: 0,
  46 + strings: {
  47 + prefixAgo: null,
  48 + prefixFromNow: null,
  49 + suffixAgo: "ago",
  50 + suffixFromNow: "from now",
  51 + inPast: 'any moment now',
  52 + seconds: "less than a minute",
  53 + minute: "about a minute",
  54 + minutes: "%d minutes",
  55 + hour: "about an hour",
  56 + hours: "about %d hours",
  57 + day: "a day",
  58 + days: "%d days",
  59 + month: "about a month",
  60 + months: "%d months",
  61 + year: "about a year",
  62 + years: "%d years",
  63 + wordSeparator: " ",
  64 + numbers: []
  65 + }
  66 + },
  67 +
  68 + inWords: function(distanceMillis) {
  69 + if(!this.settings.allowPast && ! this.settings.allowFuture) {
  70 + throw 'timeago allowPast and allowFuture settings can not both be set to false.';
  71 + }
  72 +
  73 + var $l = this.settings.strings;
  74 + var prefix = $l.prefixAgo;
  75 + var suffix = $l.suffixAgo;
  76 + if (this.settings.allowFuture) {
  77 + if (distanceMillis < 0) {
  78 + prefix = $l.prefixFromNow;
  79 + suffix = $l.suffixFromNow;
  80 + }
  81 + }
  82 +
  83 + if(!this.settings.allowPast && distanceMillis >= 0) {
  84 + return this.settings.strings.inPast;
  85 + }
  86 +
  87 + var seconds = Math.abs(distanceMillis) / 1000;
  88 + var minutes = seconds / 60;
  89 + var hours = minutes / 60;
  90 + var days = hours / 24;
  91 + var years = days / 365;
  92 +
  93 + function substitute(stringOrFunction, number) {
  94 + var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction;
  95 + var value = ($l.numbers && $l.numbers[number]) || number;
  96 + return string.replace(/%d/i, value);
  97 + }
  98 +
  99 + var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) ||
  100 + seconds < 90 && substitute($l.minute, 1) ||
  101 + minutes < 45 && substitute($l.minutes, Math.round(minutes)) ||
  102 + minutes < 90 && substitute($l.hour, 1) ||
  103 + hours < 24 && substitute($l.hours, Math.round(hours)) ||
  104 + hours < 42 && substitute($l.day, 1) ||
  105 + days < 30 && substitute($l.days, Math.round(days)) ||
  106 + days < 45 && substitute($l.month, 1) ||
  107 + days < 365 && substitute($l.months, Math.round(days / 30)) ||
  108 + years < 1.5 && substitute($l.year, 1) ||
  109 + substitute($l.years, Math.round(years));
  110 +
  111 + var separator = $l.wordSeparator || "";
  112 + if ($l.wordSeparator === undefined) { separator = " "; }
  113 + return $.trim([prefix, words, suffix].join(separator));
  114 + },
  115 +
  116 + parse: function(iso8601) {
  117 + var s = $.trim(iso8601);
  118 + s = s.replace(/\.\d+/,""); // remove milliseconds
  119 + s = s.replace(/-/,"/").replace(/-/,"/");
  120 + s = s.replace(/T/," ").replace(/Z/," UTC");
  121 + s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400
  122 + s = s.replace(/([\+\-]\d\d)$/," $100"); // +09 -> +0900
  123 + return new Date(s);
  124 + },
  125 + datetime: function(elem) {
  126 + var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title");
  127 + return $t.parse(iso8601);
  128 + },
  129 + isTime: function(elem) {
  130 + // jQuery's `is()` doesn't play well with HTML5 in IE
  131 + return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time");
  132 + }
  133 + });
  134 +
  135 + // functions that can be called via $(el).timeago('action')
  136 + // init is default when no action is given
  137 + // functions are called with context of a single element
  138 + var functions = {
  139 + init: function(){
  140 + var refresh_el = $.proxy(refresh, this);
  141 + refresh_el();
  142 + var $s = $t.settings;
  143 + if ($s.refreshMillis > 0) {
  144 + this._timeagoInterval = setInterval(refresh_el, $s.refreshMillis);
  145 + }
  146 + },
  147 + update: function(time){
  148 + var parsedTime = $t.parse(time);
  149 + $(this).data('timeago', { datetime: parsedTime });
  150 + if($t.settings.localeTitle) $(this).attr("title", parsedTime.toLocaleString());
  151 + refresh.apply(this);
  152 + },
  153 + updateFromDOM: function(){
  154 + $(this).data('timeago', { datetime: $t.parse( $t.isTime(this) ? $(this).attr("datetime") : $(this).attr("title") ) });
  155 + refresh.apply(this);
  156 + },
  157 + dispose: function () {
  158 + if (this._timeagoInterval) {
  159 + window.clearInterval(this._timeagoInterval);
  160 + this._timeagoInterval = null;
  161 + }
  162 + }
  163 + };
  164 +
  165 + $.fn.timeago = function(action, options) {
  166 + var fn = action ? functions[action] : functions.init;
  167 + if(!fn){
  168 + throw new Error("Unknown function name '"+ action +"' for timeago");
  169 + }
  170 + // each over objects here and call the requested function
  171 + this.each(function(){
  172 + fn.call(this, options);
  173 + });
  174 + return this;
  175 + };
  176 +
  177 + function refresh() {
  178 + //check if it's still visible
  179 + if(!$.contains(document.documentElement,this)){
  180 + //stop if it has been removed
  181 + $(this).timeago("dispose");
  182 + return this;
  183 + }
  184 +
  185 + var data = prepareData(this);
  186 + var $s = $t.settings;
  187 +
  188 + if (!isNaN(data.datetime)) {
  189 + if ( $s.cutoff == 0 || Math.abs(distance(data.datetime)) < $s.cutoff) {
  190 + $(this).text(inWords(data.datetime));
  191 + }
  192 + }
  193 + return this;
  194 + }
  195 +
  196 + function prepareData(element) {
  197 + element = $(element);
  198 + if (!element.data("timeago")) {
  199 + element.data("timeago", { datetime: $t.datetime(element) });
  200 + var text = $.trim(element.text());
  201 + if ($t.settings.localeTitle) {
  202 + element.attr("title", element.data('timeago').datetime.toLocaleString());
  203 + } else if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) {
  204 + element.attr("title", text);
  205 + }
  206 + }
  207 + return element.data("timeago");
  208 + }
  209 +
  210 + function inWords(date) {
  211 + return $t.inWords(distance(date));
  212 + }
  213 +
  214 + function distance(date) {
  215 + return (new Date().getTime() - date.getTime());
  216 + }
  217 +
  218 + // fix for IE6 suckage
  219 + document.createElement("abbr");
  220 + document.createElement("time");
  221 +}));
public/stylesheets/chat.css
@@ -47,7 +47,7 @@ @@ -47,7 +47,7 @@
47 max-height: 40px; 47 max-height: 40px;
48 } 48 }
49 #buddy-list .buddy-list li.offline, .occupant-list li.offline { 49 #buddy-list .buddy-list li.offline, .occupant-list li.offline {
50 - display: none; 50 +/* display: none;*/
51 } 51 }
52 #chat #buddy-list .toolbar { 52 #chat #buddy-list .toolbar {
53 border: 0; 53 border: 0;
@@ -85,6 +85,7 @@ @@ -85,6 +85,7 @@
85 right: 0; 85 right: 0;
86 bottom: 0; 86 bottom: 0;
87 padding: 0 20px 15px 15px; 87 padding: 0 20px 15px 15px;
  88 + border-top: 1px solid rgb(65, 65, 65);
88 } 89 }
89 .msie7 .conversation .input-div { 90 .msie7 .conversation .input-div {
90 padding-left: 5px; 91 padding-left: 5px;
@@ -124,12 +125,16 @@ @@ -124,12 +125,16 @@
124 } 125 }
125 #chat-window .history .message { 126 #chat-window .history .message {
126 padding: 10px 8px 10px 6px; 127 padding: 10px 8px 10px 6px;
  128 + clear: both;
127 } 129 }
128 #chat-window .history .message .time { 130 #chat-window .history .message .time {
129 float: right; 131 float: right;
130 - color: gray; 132 + color: rgb(184, 184, 184);
131 font-style: italic; 133 font-style: italic;
132 - font-size: 11px; 134 + font-size: 10px;
  135 + border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  136 + width: 100%;
  137 + text-align: right;
133 } 138 }
134 #chat-window .history .message h5, #chat-window .history .message p { 139 #chat-window .history .message h5, #chat-window .history .message p {
135 margin: 0; 140 margin: 0;
@@ -137,6 +142,7 @@ @@ -137,6 +142,7 @@
137 #chat-window .history .message p { 142 #chat-window .history .message p {
138 max-width: 100%; 143 max-width: 100%;
139 overflow: auto; 144 overflow: auto;
  145 + clear: both;
140 } 146 }
141 147
142 #chat-window .history .message .content { 148 #chat-window .history .message .content {
@@ -215,14 +221,14 @@ @@ -215,14 +221,14 @@
215 background-color: rgb(39, 39, 39); 221 background-color: rgb(39, 39, 39);
216 } 222 }
217 .conversation .header .chat-target { 223 .conversation .header .chat-target {
218 - padding: 6px 6px 0 6px; 224 + padding: 0px 6px 0 6px;
219 display: inline-block; 225 display: inline-block;
220 max-width: 70%; 226 max-width: 70%;
221 } 227 }
222 .conversation .header .other-name { 228 .conversation .header .other-name {
223 color: rgb(238, 238, 238); 229 color: rgb(238, 238, 238);
224 } 230 }
225 -.conversation .header .back { 231 +#chat .back {
226 float: right; 232 float: right;
227 margin: 6px; 233 margin: 6px;
228 padding: 7px; 234 padding: 7px;
@@ -232,11 +238,16 @@ @@ -232,11 +238,16 @@
232 font-weight: bold; 238 font-weight: bold;
233 color: white; 239 color: white;
234 } 240 }
235 -#chat #chat-window .other-name, #chat #chat-window .history .self-name, #chat #chat-window .history h5 { 241 +#chat #chat-window .other-name, #chat #chat-window .history .self-name, #chat #chat-window .history h5, #chat .toolbar #user-status span.other-name {
236 color: #257CAD; 242 color: #257CAD;
237 - height: 23px;  
238 overflow: hidden; 243 overflow: hidden;
239 - vertical-align: top; 244 + display: inline-block;
  245 + max-width: 140px;
  246 + width: 100%;
  247 +}
  248 +#chat .toolbar #user-status span, #chat #conversations .header .chat-target span {
  249 + width: auto;
  250 + color: rgb(238, 238, 238);
240 } 251 }
241 #chat #chat-window .history h5 { 252 #chat #chat-window .history h5 {
242 text-align: center; 253 text-align: center;
@@ -251,6 +262,7 @@ @@ -251,6 +262,7 @@
251 } 262 }
252 #chat #user-status { 263 #chat #user-status {
253 padding: 2px 4px; 264 padding: 2px 4px;
  265 + float: left;
254 } 266 }
255 #chat .user-status a { 267 #chat .user-status a {
256 color: rgb(224, 224, 224); 268 color: rgb(224, 224, 224);