2010-02-02 15:21:47 +00:00
|
|
|
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
|
|
|
|
|
|
|
|
const DBus = imports.dbus;
|
2010-04-14 14:24:14 +00:00
|
|
|
const GLib = imports.gi.GLib;
|
2010-02-02 15:21:47 +00:00
|
|
|
const Lang = imports.lang;
|
2010-11-25 14:29:41 +00:00
|
|
|
const Mainloop = imports.mainloop;
|
2011-02-08 12:17:29 +00:00
|
|
|
const Shell = imports.gi.Shell;
|
2010-04-16 21:24:34 +00:00
|
|
|
const Signals = imports.signals;
|
2010-02-02 15:21:47 +00:00
|
|
|
const St = imports.gi.St;
|
2011-03-07 22:10:53 +00:00
|
|
|
const Tpl = imports.gi.TelepathyLogger;
|
2011-02-07 16:34:08 +00:00
|
|
|
const Tp = imports.gi.TelepathyGLib;
|
2010-02-02 15:21:47 +00:00
|
|
|
|
2011-02-20 08:41:51 +00:00
|
|
|
const History = imports.misc.history;
|
2010-02-02 15:21:47 +00:00
|
|
|
const Main = imports.ui.main;
|
|
|
|
const MessageTray = imports.ui.messageTray;
|
|
|
|
|
|
|
|
|
2010-02-22 19:23:36 +00:00
|
|
|
// See Notification.appendMessage
|
2010-11-25 14:29:41 +00:00
|
|
|
const SCROLLBACK_IMMEDIATE_TIME = 60; // 1 minute
|
2010-02-22 19:23:36 +00:00
|
|
|
const SCROLLBACK_RECENT_TIME = 15 * 60; // 15 minutes
|
|
|
|
const SCROLLBACK_RECENT_LENGTH = 20;
|
|
|
|
const SCROLLBACK_IDLE_LENGTH = 5;
|
|
|
|
|
2011-03-07 22:10:53 +00:00
|
|
|
// See Source._displayPendingMessages
|
|
|
|
const SCROLLBACK_HISTORY_LINES = 10;
|
|
|
|
|
2010-11-28 15:14:34 +00:00
|
|
|
const NotificationDirection = {
|
|
|
|
SENT: 'chat-sent',
|
|
|
|
RECEIVED: 'chat-received'
|
|
|
|
};
|
2010-04-16 19:18:32 +00:00
|
|
|
|
2011-02-08 12:17:29 +00:00
|
|
|
let contactFeatures = [Tp.ContactFeature.ALIAS,
|
|
|
|
Tp.ContactFeature.AVATAR_DATA,
|
|
|
|
Tp.ContactFeature.PRESENCE];
|
|
|
|
|
2010-05-13 19:46:04 +00:00
|
|
|
// This is GNOME Shell's implementation of the Telepathy 'Client'
|
|
|
|
// interface. Specifically, the shell is a Telepathy 'Observer', which
|
2010-04-08 14:55:22 +00:00
|
|
|
// lets us see messages even if they belong to another app (eg,
|
|
|
|
// Empathy).
|
2010-02-02 15:21:47 +00:00
|
|
|
|
2011-03-07 22:10:53 +00:00
|
|
|
function makeMessageFromTpMessage(tpMessage, direction) {
|
|
|
|
let [text, flags] = tpMessage.to_text();
|
|
|
|
return {
|
|
|
|
messageType: tpMessage.get_message_type(),
|
|
|
|
text: text,
|
|
|
|
sender: tpMessage.sender.alias,
|
|
|
|
timestamp: tpMessage.get_received_timestamp(),
|
|
|
|
direction: direction
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function makeMessageFromTplEvent(event) {
|
|
|
|
let sent = event.get_sender().get_entity_type() == Tpl.EntityType.SELF;
|
|
|
|
let direction = sent ? NotificationDirection.SENT : NotificationDirection.RECEIVED;
|
|
|
|
|
|
|
|
return {
|
|
|
|
messageType: event.get_message_type(),
|
|
|
|
text: event.get_message(),
|
|
|
|
sender: event.get_sender().get_alias(),
|
|
|
|
timestamp: event.get_timestamp(),
|
|
|
|
direction: direction
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2010-02-02 15:21:47 +00:00
|
|
|
function Client() {
|
|
|
|
this._init();
|
|
|
|
};
|
|
|
|
|
|
|
|
Client.prototype = {
|
|
|
|
_init : function() {
|
2011-06-30 09:53:44 +00:00
|
|
|
// channel path -> ChatSource
|
|
|
|
this._chatSources = {};
|
2010-02-02 15:21:47 +00:00
|
|
|
|
2011-02-07 16:34:08 +00:00
|
|
|
// Set up a SimpleObserver, which will call _observeChannels whenever a
|
|
|
|
// channel matching its filters is detected.
|
|
|
|
// The second argument, recover, means _observeChannels will be run
|
|
|
|
// for any existing channel as well.
|
|
|
|
let dbus = Tp.DBusDaemon.dup();
|
2011-06-23 21:57:48 +00:00
|
|
|
this._tpClient = new Shell.TpClient({ 'dbus_daemon': dbus,
|
|
|
|
'name': 'GnomeShell',
|
|
|
|
'uniquify-name': true })
|
|
|
|
this._tpClient.set_observe_channels_func(
|
2011-03-23 10:22:42 +00:00
|
|
|
Lang.bind(this, this._observeChannels));
|
2011-06-23 21:57:48 +00:00
|
|
|
this._tpClient.set_approve_channels_func(
|
2011-04-05 11:28:11 +00:00
|
|
|
Lang.bind(this, this._approveChannels));
|
2011-06-23 21:57:48 +00:00
|
|
|
this._tpClient.set_handle_channels_func(
|
2011-04-05 11:28:11 +00:00
|
|
|
Lang.bind(this, this._handleChannels));
|
2011-02-07 16:34:08 +00:00
|
|
|
|
|
|
|
try {
|
2011-06-23 21:57:48 +00:00
|
|
|
this._tpClient.register();
|
2011-02-07 16:34:08 +00:00
|
|
|
} catch (e) {
|
2011-04-05 11:28:11 +00:00
|
|
|
throw new Error('Couldn\'t register Telepathy client. Error: \n' + e);
|
2011-02-07 16:34:08 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_observeChannels: function(observer, account, conn, channels,
|
|
|
|
dispatchOp, requests, context) {
|
2011-02-26 14:25:11 +00:00
|
|
|
// If the self_contact doesn't have the ALIAS, make sure
|
|
|
|
// to fetch it before trying to grab the channels.
|
|
|
|
let self_contact = conn.get_self_contact();
|
|
|
|
if (self_contact.has_feature(Tp.ContactFeature.ALIAS)) {
|
|
|
|
this._finishObserveChannels(account, conn, channels, context);
|
|
|
|
} else {
|
|
|
|
Shell.get_self_contact_features(conn,
|
2011-06-23 22:20:43 +00:00
|
|
|
contactFeatures,
|
2011-02-26 14:25:11 +00:00
|
|
|
Lang.bind(this, function() {
|
|
|
|
this._finishObserveChannels(account, conn, channels, context);
|
|
|
|
}));
|
|
|
|
context.delay();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_finishObserveChannels: function(account, conn, channels, context) {
|
2011-02-07 16:34:08 +00:00
|
|
|
let len = channels.length;
|
|
|
|
for (let i = 0; i < len; i++) {
|
|
|
|
let channel = channels[i];
|
|
|
|
let [targetHandle, targetHandleType] = channel.get_handle();
|
|
|
|
|
|
|
|
/* Only observe contact text channels */
|
|
|
|
if ((!(channel instanceof Tp.TextChannel)) ||
|
|
|
|
targetHandleType != Tp.HandleType.CONTACT)
|
|
|
|
continue;
|
|
|
|
|
2011-02-08 12:17:29 +00:00
|
|
|
/* Request a TpContact */
|
2011-06-23 22:20:43 +00:00
|
|
|
Shell.get_tp_contacts(conn, [targetHandle],
|
|
|
|
contactFeatures,
|
2011-02-08 12:17:29 +00:00
|
|
|
Lang.bind(this, function (connection, contacts, failed) {
|
|
|
|
if (contacts.length < 1)
|
|
|
|
return;
|
|
|
|
|
|
|
|
/* We got the TpContact */
|
2011-02-09 10:35:50 +00:00
|
|
|
this._createSource(account, conn, channel, contacts[0]);
|
|
|
|
}), null);
|
2011-02-07 16:34:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
context.accept();
|
2011-02-09 10:35:50 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
_createSource: function(account, conn, channel, contact) {
|
2011-06-30 09:53:44 +00:00
|
|
|
if (this._chatSources[channel.get_object_path()])
|
2011-02-09 10:35:50 +00:00
|
|
|
return;
|
|
|
|
|
2011-06-30 09:53:44 +00:00
|
|
|
let source = new ChatSource(account, conn, channel, contact, this._tpClient);
|
2011-02-09 10:35:50 +00:00
|
|
|
|
2011-06-30 09:53:44 +00:00
|
|
|
this._chatSources[channel.get_object_path()] = source;
|
2011-02-09 10:35:50 +00:00
|
|
|
source.connect('destroy', Lang.bind(this,
|
|
|
|
function() {
|
2011-06-23 21:57:48 +00:00
|
|
|
if (this._tpClient.is_handling_channel(channel)) {
|
2011-04-05 11:28:11 +00:00
|
|
|
// The chat box has been destroyed so it can't
|
|
|
|
// handle the channel any more.
|
2011-06-28 16:06:54 +00:00
|
|
|
channel.close_async(function(src, result) {
|
|
|
|
channel.close_finish(result);
|
|
|
|
});
|
2011-04-05 11:28:11 +00:00
|
|
|
}
|
|
|
|
|
2011-06-30 09:53:44 +00:00
|
|
|
delete this._chatSources[channel.get_object_path()];
|
2011-02-09 10:35:50 +00:00
|
|
|
}));
|
2011-04-05 11:28:11 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
_handlingChannels: function(account, conn, channels) {
|
|
|
|
let len = channels.length;
|
|
|
|
for (let i = 0; i < len; i++) {
|
|
|
|
let channel = channels[i];
|
|
|
|
|
|
|
|
// We can only handle text channel, so close any other channel
|
|
|
|
if (!(channel instanceof Tp.TextChannel)) {
|
|
|
|
channel.close_async(null);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2011-06-23 21:57:48 +00:00
|
|
|
if (this._tpClient.is_handling_channel(channel)) {
|
2011-04-05 11:28:11 +00:00
|
|
|
// We are already handling the channel, display the source
|
2011-06-30 09:53:44 +00:00
|
|
|
let source = this._chatSources[channel.get_object_path()];
|
2011-04-05 11:28:11 +00:00
|
|
|
if (source)
|
|
|
|
source.notify();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_approveChannels: function(approver, account, conn, channels,
|
|
|
|
dispatchOp, context) {
|
|
|
|
// Approve the channels right away as we are going to handle it
|
2011-06-23 21:57:48 +00:00
|
|
|
dispatchOp.claim_with_async(this._tpClient,
|
2011-04-05 11:28:11 +00:00
|
|
|
Lang.bind (this, function(dispatchOp, result) {
|
|
|
|
try {
|
|
|
|
dispatchOp.claim_with_finish(result);
|
|
|
|
this._handlingChannels(account, conn, channels);
|
|
|
|
} catch (err) {
|
|
|
|
global.logError('Failed to Claim channel: ' + err);
|
|
|
|
}}));
|
|
|
|
|
|
|
|
context.accept();
|
|
|
|
},
|
|
|
|
|
|
|
|
_handleChannels: function(handler, account, conn, channels,
|
|
|
|
requests, user_action_time, context) {
|
|
|
|
this._handlingChannels(account, conn, channels);
|
|
|
|
context.accept();
|
2010-02-02 15:21:47 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2011-06-30 09:53:44 +00:00
|
|
|
function ChatSource(account, conn, channel, contact, client) {
|
2011-04-05 11:28:11 +00:00
|
|
|
this._init(account, conn, channel, contact, client);
|
2010-02-02 15:21:47 +00:00
|
|
|
}
|
|
|
|
|
2011-06-30 09:53:44 +00:00
|
|
|
ChatSource.prototype = {
|
2010-02-02 15:21:47 +00:00
|
|
|
__proto__: MessageTray.Source.prototype,
|
|
|
|
|
2011-04-05 11:28:11 +00:00
|
|
|
_init: function(account, conn, channel, contact, client) {
|
2011-02-28 18:15:02 +00:00
|
|
|
MessageTray.Source.prototype._init.call(this, contact.get_alias());
|
2010-12-30 21:09:56 +00:00
|
|
|
|
2011-02-08 10:22:46 +00:00
|
|
|
this.isChat = true;
|
2010-04-08 14:55:22 +00:00
|
|
|
|
2011-02-08 10:22:46 +00:00
|
|
|
this._account = account;
|
2011-02-08 12:17:29 +00:00
|
|
|
this._contact = contact;
|
2011-04-05 11:28:11 +00:00
|
|
|
this._client = client;
|
2010-02-02 15:21:47 +00:00
|
|
|
|
2011-05-27 09:39:50 +00:00
|
|
|
this._pendingMessages = [];
|
|
|
|
|
2011-02-08 10:22:46 +00:00
|
|
|
this._conn = conn;
|
|
|
|
this._channel = channel;
|
|
|
|
this._closedId = this._channel.connect('invalidated', Lang.bind(this, this._channelClosed));
|
2010-02-02 15:21:47 +00:00
|
|
|
|
2011-06-30 09:53:44 +00:00
|
|
|
this._notification = new ChatNotification(this);
|
2011-01-04 09:34:57 +00:00
|
|
|
this._notification.setUrgency(MessageTray.Urgency.HIGH);
|
2010-11-28 15:14:34 +00:00
|
|
|
|
2011-05-27 09:39:50 +00:00
|
|
|
// We ack messages when the message box is collapsed if user has
|
|
|
|
// interacted with it before and so read the messages:
|
|
|
|
// - user clicked on it the tray
|
|
|
|
// - user expanded the notification by hovering over the toaster notification
|
|
|
|
this._shouldAck = false;
|
|
|
|
|
|
|
|
this.connect('summary-item-clicked', Lang.bind(this, this._summaryItemClicked));
|
|
|
|
this._notification.connect('expanded', Lang.bind(this, this._notificationExpanded));
|
|
|
|
this._notification.connect('collapsed', Lang.bind(this, this._notificationCollapsed));
|
|
|
|
|
2011-02-08 13:50:06 +00:00
|
|
|
this._presence = contact.get_presence_type();
|
2010-04-16 21:24:34 +00:00
|
|
|
|
2011-02-08 15:19:01 +00:00
|
|
|
this._sentId = this._channel.connect('message-sent', Lang.bind(this, this._messageSent));
|
|
|
|
this._receivedId = this._channel.connect('message-received', Lang.bind(this, this._messageReceived));
|
2011-05-27 09:39:50 +00:00
|
|
|
this._pendingId = this._channel.connect('pending-message-removed', Lang.bind(this, this._pendingRemoved));
|
2010-08-05 17:09:27 +00:00
|
|
|
|
|
|
|
this._setSummaryIcon(this.createNotificationIcon());
|
2011-02-08 12:17:29 +00:00
|
|
|
|
|
|
|
this._notifyAliasId = this._contact.connect('notify::alias', Lang.bind(this, this._updateAlias));
|
2011-02-08 13:05:20 +00:00
|
|
|
this._notifyAvatarId = this._contact.connect('notify::avatar-file', Lang.bind(this, this._updateAvatarIcon));
|
2011-02-08 13:50:06 +00:00
|
|
|
this._presenceChangedId = this._contact.connect('presence-changed', Lang.bind(this, this._presenceChanged));
|
2011-02-08 15:19:01 +00:00
|
|
|
|
2011-02-28 18:14:07 +00:00
|
|
|
// Add ourselves as a source.
|
|
|
|
Main.messageTray.add(this);
|
|
|
|
this.pushNotification(this._notification);
|
|
|
|
|
2011-03-07 22:10:53 +00:00
|
|
|
this._getLogMessages();
|
2011-02-08 12:17:29 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
_updateAlias: function() {
|
2011-02-28 18:15:02 +00:00
|
|
|
let oldAlias = this.title;
|
2011-06-08 06:27:57 +00:00
|
|
|
this.setTitle(this._contact.get_alias());
|
2011-02-28 18:15:02 +00:00
|
|
|
this._notification.appendAliasChange(oldAlias, this.title);
|
|
|
|
this.pushNotification(this._notification);
|
2010-02-02 15:21:47 +00:00
|
|
|
},
|
|
|
|
|
2010-08-05 17:09:27 +00:00
|
|
|
createNotificationIcon: function() {
|
2011-02-08 13:05:20 +00:00
|
|
|
this._iconBox = new St.Bin({ style_class: 'avatar-box' });
|
|
|
|
this._iconBox._size = this.ICON_SIZE;
|
|
|
|
|
|
|
|
this._updateAvatarIcon();
|
|
|
|
|
|
|
|
return this._iconBox;
|
|
|
|
},
|
|
|
|
|
|
|
|
_updateAvatarIcon: function() {
|
|
|
|
let textureCache = St.TextureCache.get_default();
|
|
|
|
let file = this._contact.get_avatar_file();
|
|
|
|
|
|
|
|
if (file) {
|
|
|
|
let uri = file.get_uri();
|
|
|
|
this._iconBox.child = textureCache.load_uri_async(uri, this._iconBox._size, this._iconBox._size);
|
|
|
|
} else {
|
2011-02-17 00:51:49 +00:00
|
|
|
this._iconBox.child = new St.Icon({ icon_name: 'avatar-default',
|
2011-02-08 13:05:20 +00:00
|
|
|
icon_type: St.IconType.FULLCOLOR,
|
|
|
|
icon_size: this._iconBox._size });
|
|
|
|
}
|
2010-02-02 15:21:47 +00:00
|
|
|
},
|
|
|
|
|
2011-02-11 19:43:01 +00:00
|
|
|
open: function(notification) {
|
2011-04-05 11:28:11 +00:00
|
|
|
if (this._client.is_handling_channel(this._channel)) {
|
|
|
|
// We are handling the channel, try to pass it to Empathy
|
|
|
|
this._client.delegate_channels_async([this._channel], global.get_current_time(), "", null);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
// We are not the handler, just ask to present the channel
|
|
|
|
let dbus = Tp.DBusDaemon.dup();
|
|
|
|
let cd = Tp.ChannelDispatcher.new(dbus);
|
|
|
|
|
|
|
|
cd.present_channel_async(this._channel, global.get_current_time(), null);
|
|
|
|
}
|
2010-04-08 14:55:22 +00:00
|
|
|
},
|
|
|
|
|
2011-03-07 22:10:53 +00:00
|
|
|
_getLogMessages: function() {
|
|
|
|
let logManager = Tpl.LogManager.dup_singleton();
|
|
|
|
let entity = Tpl.Entity.new_from_tp_contact(this._contact, Tpl.EntityType.CONTACT);
|
|
|
|
Shell.get_contact_events(logManager,
|
|
|
|
this._account, entity,
|
|
|
|
SCROLLBACK_HISTORY_LINES,
|
|
|
|
Lang.bind(this, this._displayPendingMessages));
|
|
|
|
},
|
|
|
|
|
|
|
|
_displayPendingMessages: function(logManager, result) {
|
|
|
|
let [success, events] = logManager.get_filtered_events_finish(result);
|
|
|
|
|
|
|
|
let logMessages = events.map(makeMessageFromTplEvent);
|
|
|
|
|
2011-03-23 19:25:42 +00:00
|
|
|
let pendingTpMessages = this._channel.get_pending_messages();
|
2011-05-27 08:52:08 +00:00
|
|
|
let pendingMessages = [];
|
|
|
|
|
|
|
|
for (let i = 0; i < pendingTpMessages.length; i++) {
|
|
|
|
let message = pendingTpMessages[i];
|
|
|
|
|
|
|
|
if (message.get_message_type() == Tp.ChannelTextMessageType.DELIVERY_REPORT)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
pendingMessages.push(makeMessageFromTpMessage(message, NotificationDirection.RECEIVED));
|
2011-05-27 09:39:50 +00:00
|
|
|
|
|
|
|
this._pendingMessages.push(message);
|
2011-05-27 08:52:08 +00:00
|
|
|
}
|
2011-03-23 19:25:42 +00:00
|
|
|
|
2011-03-24 11:09:04 +00:00
|
|
|
let showTimestamp = false;
|
|
|
|
|
2011-03-23 19:25:42 +00:00
|
|
|
for (let i = 0; i < logMessages.length; i++) {
|
|
|
|
let logMessage = logMessages[i];
|
|
|
|
let isPending = false;
|
|
|
|
|
|
|
|
// Skip any log messages that are also in pendingMessages
|
|
|
|
for (let j = 0; j < pendingMessages.length; j++) {
|
|
|
|
let pending = pendingMessages[j];
|
|
|
|
if (logMessage.timestamp == pending.timestamp && logMessage.text == pending.text) {
|
|
|
|
isPending = true;
|
|
|
|
break;
|
2011-03-07 22:10:53 +00:00
|
|
|
}
|
|
|
|
}
|
2010-02-02 15:21:47 +00:00
|
|
|
|
2011-03-24 11:09:04 +00:00
|
|
|
if (!isPending) {
|
|
|
|
showTimestamp = true;
|
2011-03-24 11:11:17 +00:00
|
|
|
this._notification.appendMessage(logMessage, true, ['chat-log-message']);
|
2011-03-24 11:09:04 +00:00
|
|
|
}
|
2011-02-08 15:19:01 +00:00
|
|
|
}
|
2011-03-07 22:10:53 +00:00
|
|
|
|
2011-03-24 11:09:04 +00:00
|
|
|
if (showTimestamp)
|
|
|
|
this._notification.appendTimestamp();
|
|
|
|
|
2011-03-23 19:25:42 +00:00
|
|
|
for (let i = 0; i < pendingMessages.length; i++)
|
|
|
|
this._notification.appendMessage(pendingMessages[i], true);
|
|
|
|
|
|
|
|
if (pendingMessages.length > 0)
|
2011-03-07 22:10:53 +00:00
|
|
|
this.notify();
|
2010-02-02 15:21:47 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
_channelClosed: function() {
|
|
|
|
this._channel.disconnect(this._closedId);
|
2011-02-08 15:19:01 +00:00
|
|
|
this._channel.disconnect(this._receivedId);
|
2011-05-27 09:39:50 +00:00
|
|
|
this._channel.disconnect(this._pendingId);
|
2011-02-08 15:19:01 +00:00
|
|
|
this._channel.disconnect(this._sentId);
|
2011-02-08 12:17:29 +00:00
|
|
|
|
|
|
|
this._contact.disconnect(this._notifyAliasId);
|
2011-02-08 13:05:20 +00:00
|
|
|
this._contact.disconnect(this._notifyAvatarId);
|
2011-02-08 13:50:06 +00:00
|
|
|
this._contact.disconnect(this._presenceChangedId);
|
2011-02-08 15:19:01 +00:00
|
|
|
|
2010-02-02 15:21:47 +00:00
|
|
|
this.destroy();
|
|
|
|
},
|
|
|
|
|
2011-02-08 15:19:01 +00:00
|
|
|
_messageReceived: function(channel, message) {
|
2011-05-05 12:19:59 +00:00
|
|
|
if (message.get_message_type() == Tp.ChannelTextMessageType.DELIVERY_REPORT)
|
|
|
|
return;
|
|
|
|
|
2011-05-27 09:39:50 +00:00
|
|
|
this._pendingMessages.push(message);
|
|
|
|
|
2011-03-07 22:10:53 +00:00
|
|
|
message = makeMessageFromTpMessage(message, NotificationDirection.RECEIVED);
|
|
|
|
this._notification.appendMessage(message);
|
2010-11-28 15:14:34 +00:00
|
|
|
this.notify();
|
|
|
|
},
|
2010-02-02 15:21:47 +00:00
|
|
|
|
2010-11-28 15:14:34 +00:00
|
|
|
// This is called for both messages we send from
|
|
|
|
// our client and other clients as well.
|
2011-02-08 15:19:01 +00:00
|
|
|
_messageSent: function(channel, message, flags, token) {
|
2011-03-07 22:10:53 +00:00
|
|
|
message = makeMessageFromTpMessage(message, NotificationDirection.SENT);
|
|
|
|
this._notification.appendMessage(message);
|
2010-04-16 21:24:34 +00:00
|
|
|
},
|
|
|
|
|
2010-11-28 15:14:34 +00:00
|
|
|
notify: function() {
|
|
|
|
MessageTray.Source.prototype.notify.call(this, this._notification);
|
2010-02-22 19:23:36 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
respond: function(text) {
|
2011-02-26 14:38:25 +00:00
|
|
|
let type;
|
|
|
|
if (text.slice(0, 4) == '/me ') {
|
|
|
|
type = Tp.ChannelTextMessageType.ACTION;
|
|
|
|
text = text.slice(4);
|
|
|
|
} else {
|
|
|
|
type = Tp.ChannelTextMessageType.NORMAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
let msg = Tp.ClientMessage.new_text(type, text);
|
2011-06-28 15:42:41 +00:00
|
|
|
this._channel.send_message_async(msg, 0, Lang.bind(this, function (src, result) {
|
|
|
|
this._channel.send_message_finish(result);
|
|
|
|
}));
|
2010-04-16 21:24:34 +00:00
|
|
|
},
|
|
|
|
|
2011-06-03 20:07:29 +00:00
|
|
|
_presenceChanged: function (contact, presence, status, message) {
|
2011-02-14 04:13:07 +00:00
|
|
|
let msg, shouldNotify, title;
|
|
|
|
|
2011-02-08 13:50:06 +00:00
|
|
|
if (this._presence == presence)
|
|
|
|
return;
|
|
|
|
|
2011-02-14 04:13:07 +00:00
|
|
|
title = GLib.markup_escape_text(this.title, -1);
|
2010-04-16 21:24:34 +00:00
|
|
|
|
2011-02-07 17:02:04 +00:00
|
|
|
if (presence == Tp.ConnectionPresenceType.AVAILABLE) {
|
2011-02-14 04:13:07 +00:00
|
|
|
msg = _("%s is online.").format(title);
|
2011-02-07 17:02:04 +00:00
|
|
|
shouldNotify = (this._presence == Tp.ConnectionPresenceType.OFFLINE);
|
|
|
|
} else if (presence == Tp.ConnectionPresenceType.OFFLINE ||
|
|
|
|
presence == Tp.ConnectionPresenceType.EXTENDED_AWAY) {
|
|
|
|
presence = Tp.ConnectionPresenceType.OFFLINE;
|
2011-02-14 04:13:07 +00:00
|
|
|
msg = _("%s is offline.").format(title);
|
2011-02-07 17:02:04 +00:00
|
|
|
shouldNotify = (this._presence != Tp.ConnectionPresenceType.OFFLINE);
|
|
|
|
} else if (presence == Tp.ConnectionPresenceType.AWAY) {
|
2011-02-14 04:13:07 +00:00
|
|
|
msg = _("%s is away.").format(title);
|
2010-11-28 15:14:34 +00:00
|
|
|
shouldNotify = false;
|
2011-02-07 17:02:04 +00:00
|
|
|
} else if (presence == Tp.ConnectionPresenceType.BUSY) {
|
2011-02-14 04:13:07 +00:00
|
|
|
msg = _("%s is busy.").format(title);
|
2010-11-28 15:14:34 +00:00
|
|
|
shouldNotify = false;
|
2010-04-16 21:24:34 +00:00
|
|
|
} else
|
|
|
|
return;
|
|
|
|
|
|
|
|
this._presence = presence;
|
|
|
|
|
|
|
|
if (message)
|
|
|
|
msg += ' <i>(' + GLib.markup_escape_text(message, -1) + ')</i>';
|
|
|
|
|
2010-11-28 15:14:34 +00:00
|
|
|
this._notification.appendPresence(msg, shouldNotify);
|
|
|
|
if (shouldNotify)
|
|
|
|
this.notify();
|
2011-05-27 09:39:50 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
_pendingRemoved: function(channel, message) {
|
|
|
|
let idx = this._pendingMessages.indexOf(message);
|
|
|
|
|
|
|
|
if (idx >= 0)
|
|
|
|
this._pendingMessages.splice(idx, 1);
|
|
|
|
else
|
|
|
|
throw new Error('Message not in our pending list: ' + message);
|
|
|
|
},
|
|
|
|
|
|
|
|
_ackMessages: function() {
|
|
|
|
if (this._pendingMessages.length == 0)
|
|
|
|
return;
|
|
|
|
|
|
|
|
// Don't clear our messages here, tp-glib will send a
|
|
|
|
// 'pending-message-removed' for each one.
|
|
|
|
this._channel.ack_messages_async(this._pendingMessages, Lang.bind(this, function(src, result) {
|
|
|
|
this._channel.ack_messages_finish(result);}));
|
|
|
|
},
|
|
|
|
|
|
|
|
_summaryItemClicked: function(source, button) {
|
|
|
|
if (button != 1)
|
|
|
|
return;
|
|
|
|
|
|
|
|
this._shouldAck = true;
|
|
|
|
},
|
|
|
|
|
|
|
|
_notificationExpanded: function() {
|
|
|
|
this._shouldAck = true;
|
|
|
|
},
|
|
|
|
|
|
|
|
_notificationCollapsed: function() {
|
|
|
|
if (this._shouldAck)
|
|
|
|
this._ackMessages();
|
|
|
|
|
|
|
|
this._shouldAck = false;
|
2010-02-02 15:21:47 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2011-06-30 09:53:44 +00:00
|
|
|
function ChatNotification(source) {
|
2010-08-11 13:43:40 +00:00
|
|
|
this._init(source);
|
2010-02-02 15:21:47 +00:00
|
|
|
}
|
|
|
|
|
2011-06-30 09:53:44 +00:00
|
|
|
ChatNotification.prototype = {
|
2010-02-02 15:21:47 +00:00
|
|
|
__proto__: MessageTray.Notification.prototype,
|
|
|
|
|
2010-08-11 13:43:40 +00:00
|
|
|
_init: function(source) {
|
2010-08-30 20:03:08 +00:00
|
|
|
MessageTray.Notification.prototype._init.call(this, source, source.title, null, { customContent: true });
|
2010-12-22 07:41:11 +00:00
|
|
|
this.setResident(true);
|
2010-02-22 19:23:36 +00:00
|
|
|
|
2011-02-10 03:45:50 +00:00
|
|
|
this._responseEntry = new St.Entry({ style_class: 'chat-response',
|
|
|
|
can_focus: true });
|
2010-02-22 19:23:36 +00:00
|
|
|
this._responseEntry.clutter_text.connect('activate', Lang.bind(this, this._onEntryActivated));
|
|
|
|
this.setActionArea(this._responseEntry);
|
|
|
|
|
2011-03-09 12:56:35 +00:00
|
|
|
this._oldMaxScrollAdjustment = 0;
|
|
|
|
this._createScrollArea();
|
|
|
|
|
|
|
|
this._scrollArea.vscroll.adjustment.connect('changed', Lang.bind(this, function(adjustment) {
|
|
|
|
let currentValue = adjustment.value + adjustment.page_size;
|
|
|
|
if (currentValue == this._oldMaxScrollAdjustment)
|
|
|
|
this.scrollTo(St.Side.BOTTOM);
|
|
|
|
this._oldMaxScrollAdjustment = adjustment.upper;
|
|
|
|
}));
|
|
|
|
|
2011-02-20 08:41:51 +00:00
|
|
|
this._inputHistory = new History.HistoryManager({ entry: this._responseEntry.clutter_text });
|
|
|
|
|
2010-02-22 19:23:36 +00:00
|
|
|
this._history = [];
|
2010-11-25 14:29:41 +00:00
|
|
|
this._timestampTimeoutId = 0;
|
2010-02-22 19:23:36 +00:00
|
|
|
},
|
|
|
|
|
2011-03-07 22:10:53 +00:00
|
|
|
/**
|
|
|
|
* appendMessage:
|
|
|
|
* @message: An object with the properties:
|
|
|
|
* text: the body of the message,
|
|
|
|
* messageType: a #Tp.ChannelTextMessageType,
|
|
|
|
* sender: the name of the sender,
|
|
|
|
* timestamp: the time the message was sent
|
|
|
|
* direction: a #NotificationDirection
|
|
|
|
*
|
|
|
|
* @noTimestamp: Whether to add a timestamp. If %true, no timestamp
|
|
|
|
* will be added, regardless of the difference since the
|
|
|
|
* last timestamp
|
2011-03-24 11:11:17 +00:00
|
|
|
* @styles: A list of CSS class names.
|
2011-03-07 22:10:53 +00:00
|
|
|
*/
|
2011-03-24 11:11:17 +00:00
|
|
|
appendMessage: function(message, noTimestamp, styles) {
|
2011-03-07 22:10:53 +00:00
|
|
|
let messageBody = GLib.markup_escape_text(message.text, -1);
|
2011-03-24 11:11:17 +00:00
|
|
|
styles = styles || [];
|
|
|
|
styles.push(message.direction);
|
2011-03-07 22:10:53 +00:00
|
|
|
|
|
|
|
if (message.messageType == Tp.ChannelTextMessageType.ACTION) {
|
|
|
|
let senderAlias = GLib.markup_escape_text(message.sender, -1);
|
2011-02-26 14:38:25 +00:00
|
|
|
messageBody = '<i>%s</i> %s'.format(senderAlias, messageBody);
|
|
|
|
styles.push('chat-action');
|
|
|
|
}
|
|
|
|
|
2011-05-15 09:07:23 +00:00
|
|
|
if (message.direction == NotificationDirection.RECEIVED) {
|
|
|
|
this.update(this.source.title, messageBody, { customContent: true,
|
|
|
|
bannerMarkup: true });
|
|
|
|
}
|
|
|
|
|
2011-03-07 22:10:53 +00:00
|
|
|
this._append(messageBody, styles, message.timestamp, noTimestamp);
|
2010-02-22 19:23:36 +00:00
|
|
|
},
|
|
|
|
|
2011-05-25 17:18:56 +00:00
|
|
|
_filterMessages: function() {
|
|
|
|
if (this._history.length < 1)
|
|
|
|
return;
|
|
|
|
|
|
|
|
let lastMessageTime = this._history[0].time;
|
|
|
|
let currentTime = (Date.now() / 1000);
|
|
|
|
|
|
|
|
// Keep the scrollback from growing too long. If the most
|
|
|
|
// recent message (before the one we just added) is within
|
|
|
|
// SCROLLBACK_RECENT_TIME, we will keep
|
|
|
|
// SCROLLBACK_RECENT_LENGTH previous messages. Otherwise
|
|
|
|
// we'll keep SCROLLBACK_IDLE_LENGTH messages.
|
|
|
|
|
|
|
|
let maxLength = (lastMessageTime < currentTime - SCROLLBACK_RECENT_TIME) ?
|
|
|
|
SCROLLBACK_IDLE_LENGTH : SCROLLBACK_RECENT_LENGTH;
|
|
|
|
|
|
|
|
let filteredHistory = this._history.filter(function(item) { return item.realMessage });
|
|
|
|
if (filteredHistory.length > maxLength) {
|
|
|
|
let lastMessageToKeep = filteredHistory[maxLength];
|
|
|
|
let expired = this._history.splice(this._history.indexOf(lastMessageToKeep));
|
|
|
|
for (let i = 0; i < expired.length; i++)
|
|
|
|
expired[i].actor.destroy();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2011-03-07 22:10:53 +00:00
|
|
|
_append: function(text, styles, timestamp, noTimestamp) {
|
2010-11-25 14:29:41 +00:00
|
|
|
let currentTime = (Date.now() / 1000);
|
|
|
|
if (!timestamp)
|
|
|
|
timestamp = currentTime;
|
|
|
|
|
|
|
|
// Reset the old message timeout
|
|
|
|
if (this._timestampTimeoutId)
|
|
|
|
Mainloop.source_remove(this._timestampTimeoutId);
|
|
|
|
|
2011-02-26 14:38:25 +00:00
|
|
|
let body = this.addBody(text, true);
|
|
|
|
for (let i = 0; i < styles.length; i ++)
|
|
|
|
body.add_style_class_name(styles[i]);
|
2010-02-22 19:23:36 +00:00
|
|
|
|
2010-11-25 14:29:41 +00:00
|
|
|
this._history.unshift({ actor: body, time: timestamp, realMessage: true });
|
|
|
|
|
2011-03-07 22:10:53 +00:00
|
|
|
if (!noTimestamp) {
|
|
|
|
if (timestamp < currentTime - SCROLLBACK_IMMEDIATE_TIME)
|
|
|
|
this.appendTimestamp();
|
|
|
|
else
|
|
|
|
// Schedule a new timestamp in SCROLLBACK_IMMEDIATE_TIME
|
|
|
|
// from the timestamp of the message.
|
|
|
|
this._timestampTimeoutId = Mainloop.timeout_add_seconds(
|
|
|
|
SCROLLBACK_IMMEDIATE_TIME - (currentTime - timestamp),
|
|
|
|
Lang.bind(this, this.appendTimestamp));
|
|
|
|
}
|
2010-02-22 19:23:36 +00:00
|
|
|
|
2011-05-25 17:18:56 +00:00
|
|
|
this._filterMessages();
|
2010-02-22 19:23:36 +00:00
|
|
|
},
|
|
|
|
|
2011-03-28 20:37:53 +00:00
|
|
|
_formatTimestamp: function(date) {
|
|
|
|
let now = new Date();
|
|
|
|
|
|
|
|
var daysAgo = (now.getTime() - date.getTime()) / (24 * 60 * 60 * 1000);
|
|
|
|
|
2011-03-30 09:44:53 +00:00
|
|
|
let format;
|
|
|
|
|
2011-03-28 20:37:53 +00:00
|
|
|
// Show a week day and time if date is in the last week
|
|
|
|
if (daysAgo < 1 || (daysAgo < 7 && now.getDay() != date.getDay())) {
|
|
|
|
/* Translators: this is a time format string followed by a date.
|
|
|
|
If applicable, replace %X with a strftime format valid for your
|
|
|
|
locale, without seconds. */
|
|
|
|
// xgettext:no-c-format
|
|
|
|
format = _("Sent at %X on %A");
|
|
|
|
|
|
|
|
// FIXME: The next two are stolen from calendar.js with the comment to avoid
|
|
|
|
// a string-freeze break. They should be replaced with better strings
|
|
|
|
// with 'Sent at', appropriate context and appropriate translator comment.
|
|
|
|
|
|
|
|
} else if (date.getYear() == now.getYear()) {
|
|
|
|
/* Translators: Shown on calendar heading when selected day occurs on current year */
|
|
|
|
format = C_("calendar heading", "%A, %B %d");
|
|
|
|
} else {
|
|
|
|
/* Translators: Shown on calendar heading when selected day occurs on different year */
|
|
|
|
format = C_("calendar heading", "%A, %B %d, %Y");
|
|
|
|
}
|
|
|
|
|
|
|
|
return date.toLocaleFormat(format);
|
|
|
|
},
|
|
|
|
|
2011-03-07 22:10:53 +00:00
|
|
|
appendTimestamp: function() {
|
2010-11-25 14:29:41 +00:00
|
|
|
let lastMessageTime = this._history[0].time;
|
|
|
|
let lastMessageDate = new Date(lastMessageTime * 1000);
|
|
|
|
|
2011-03-28 20:37:53 +00:00
|
|
|
let timeLabel = this.addBody(this._formatTimestamp(lastMessageDate), false, { expand: true, x_fill: false, x_align: St.Align.END });
|
2010-11-25 14:29:41 +00:00
|
|
|
timeLabel.add_style_class_name('chat-meta-message');
|
|
|
|
this._history.unshift({ actor: timeLabel, time: lastMessageTime, realMessage: false });
|
|
|
|
|
|
|
|
this._timestampTimeoutId = 0;
|
2011-05-25 17:18:56 +00:00
|
|
|
|
|
|
|
this._filterMessages();
|
|
|
|
|
2010-11-25 14:29:41 +00:00
|
|
|
return false;
|
|
|
|
},
|
|
|
|
|
|
|
|
appendPresence: function(text, asTitle) {
|
|
|
|
if (asTitle)
|
2011-02-14 04:13:07 +00:00
|
|
|
this.update(text, null, { customContent: true, titleMarkup: true });
|
2010-11-25 14:29:41 +00:00
|
|
|
else
|
|
|
|
this.update(this.source.title, null, { customContent: true });
|
2011-02-13 03:18:41 +00:00
|
|
|
let label = this.addBody(text, true);
|
2010-11-25 14:29:41 +00:00
|
|
|
label.add_style_class_name('chat-meta-message');
|
|
|
|
this._history.unshift({ actor: label, time: (Date.now() / 1000), realMessage: false});
|
2011-05-25 17:18:56 +00:00
|
|
|
|
|
|
|
this._filterMessages();
|
2010-11-25 14:29:41 +00:00
|
|
|
},
|
|
|
|
|
2011-02-28 18:15:02 +00:00
|
|
|
appendAliasChange: function(oldAlias, newAlias) {
|
2011-03-15 13:45:53 +00:00
|
|
|
oldAlias = GLib.markup_escape_text(oldAlias, -1);
|
|
|
|
newAlias = GLib.markup_escape_text(newAlias, -1);
|
|
|
|
|
|
|
|
/* Translators: this is the other person changing their old IM name to their new
|
|
|
|
IM name. */
|
|
|
|
let message = '<i>' + _("%s is now known as %s").format(oldAlias, newAlias) + '</i>';
|
|
|
|
let label = this.addBody(message, true);
|
|
|
|
label.add_style_class_name('chat-meta-message');
|
|
|
|
this._history.unshift({ actor: label, time: (Date.now() / 1000), realMessage: false });
|
|
|
|
this.update(newAlias, null, { customContent: true });
|
2011-05-25 17:18:56 +00:00
|
|
|
|
|
|
|
this._filterMessages();
|
2011-02-28 18:15:02 +00:00
|
|
|
},
|
|
|
|
|
2010-02-22 19:23:36 +00:00
|
|
|
_onEntryActivated: function() {
|
|
|
|
let text = this._responseEntry.get_text();
|
2010-07-21 04:42:37 +00:00
|
|
|
if (text == '')
|
2010-02-22 19:23:36 +00:00
|
|
|
return;
|
|
|
|
|
2011-02-20 08:41:51 +00:00
|
|
|
this._inputHistory.addItem(text);
|
|
|
|
|
2010-11-28 15:14:34 +00:00
|
|
|
// Telepathy sends out the Sent signal for us.
|
|
|
|
// see Source._messageSent
|
2010-02-22 19:23:36 +00:00
|
|
|
this._responseEntry.set_text('');
|
|
|
|
this.source.respond(text);
|
2010-02-02 15:21:47 +00:00
|
|
|
}
|
|
|
|
};
|