2010-02-02 10:21:47 -05:00
|
|
|
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
|
|
|
|
|
|
|
|
const DBus = imports.dbus;
|
2010-04-14 10:24:14 -04:00
|
|
|
const GLib = imports.gi.GLib;
|
2010-02-02 10:21:47 -05:00
|
|
|
const Lang = imports.lang;
|
2010-11-25 09:29:41 -05:00
|
|
|
const Mainloop = imports.mainloop;
|
2011-02-08 07:17:29 -05:00
|
|
|
const Shell = imports.gi.Shell;
|
2010-04-16 17:24:34 -04:00
|
|
|
const Signals = imports.signals;
|
2010-02-02 10:21:47 -05:00
|
|
|
const St = imports.gi.St;
|
2011-02-07 11:34:08 -05:00
|
|
|
const Tp = imports.gi.TelepathyGLib;
|
2010-04-16 17:24:34 -04:00
|
|
|
const Gettext = imports.gettext.domain('gnome-shell');
|
|
|
|
const _ = Gettext.gettext;
|
2010-02-02 10:21:47 -05:00
|
|
|
|
|
|
|
const Main = imports.ui.main;
|
|
|
|
const MessageTray = imports.ui.messageTray;
|
|
|
|
const Telepathy = imports.misc.telepathy;
|
|
|
|
|
2010-05-11 12:26:00 -04:00
|
|
|
let contactManager;
|
2010-02-02 10:21:47 -05:00
|
|
|
|
2010-02-22 14:23:36 -05:00
|
|
|
// See Notification.appendMessage
|
2010-11-25 09:29:41 -05:00
|
|
|
const SCROLLBACK_IMMEDIATE_TIME = 60; // 1 minute
|
2010-02-22 14:23:36 -05:00
|
|
|
const SCROLLBACK_RECENT_TIME = 15 * 60; // 15 minutes
|
|
|
|
const SCROLLBACK_RECENT_LENGTH = 20;
|
|
|
|
const SCROLLBACK_IDLE_LENGTH = 5;
|
|
|
|
|
2010-04-16 17:24:34 -04:00
|
|
|
// The (non-chat) channel indicating the users whose presence
|
|
|
|
// information we subscribe to
|
|
|
|
let subscribedContactsChannel = {};
|
2011-02-07 12:02:04 -05:00
|
|
|
subscribedContactsChannel[Tp.PROP_CHANNEL_CHANNEL_TYPE] = Tp.IFACE_CHANNEL_TYPE_CONTACT_LIST
|
|
|
|
subscribedContactsChannel[Tp.PROP_CHANNEL_TARGET_HANDLE_TYPE] = Tp.HandleType.LIST;
|
|
|
|
subscribedContactsChannel[Tp.PROP_CHANNEL_TARGET_ID] = 'subscribe';
|
2010-04-16 17:24:34 -04:00
|
|
|
|
2010-11-28 10:14:34 -05:00
|
|
|
const NotificationDirection = {
|
|
|
|
SENT: 'chat-sent',
|
|
|
|
RECEIVED: 'chat-received'
|
|
|
|
};
|
2010-04-16 15:18:32 -04:00
|
|
|
|
2011-02-08 07:17:29 -05:00
|
|
|
let contactFeatures = [Tp.ContactFeature.ALIAS,
|
|
|
|
Tp.ContactFeature.AVATAR_DATA,
|
|
|
|
Tp.ContactFeature.PRESENCE];
|
|
|
|
|
2010-05-13 15:46:04 -04:00
|
|
|
// This is GNOME Shell's implementation of the Telepathy 'Client'
|
|
|
|
// interface. Specifically, the shell is a Telepathy 'Observer', which
|
2010-04-08 10:55:22 -04:00
|
|
|
// lets us see messages even if they belong to another app (eg,
|
|
|
|
// Empathy).
|
2010-02-02 10:21:47 -05:00
|
|
|
|
|
|
|
function Client() {
|
|
|
|
this._init();
|
|
|
|
};
|
|
|
|
|
|
|
|
Client.prototype = {
|
|
|
|
_init : function() {
|
2010-05-11 13:00:55 -04:00
|
|
|
this._accounts = {};
|
2010-04-16 17:24:34 -04:00
|
|
|
this._sources = {};
|
2010-02-02 10:21:47 -05:00
|
|
|
|
2010-05-11 12:26:00 -04:00
|
|
|
contactManager = new ContactManager();
|
2010-04-16 17:24:34 -04:00
|
|
|
contactManager.connect('presence-changed', Lang.bind(this, this._presenceChanged));
|
2010-02-02 10:21:47 -05:00
|
|
|
|
2011-02-07 11:34:08 -05: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-02-08 05:04:22 -05:00
|
|
|
this._observer = Tp.SimpleObserver.new(dbus, true, 'GnomeShell', true,
|
2011-02-07 11:34:08 -05:00
|
|
|
Lang.bind(this, this._observeChannels));
|
|
|
|
|
|
|
|
// We only care about single-user text-based chats
|
2011-02-07 12:02:04 -05:00
|
|
|
let props = {};
|
|
|
|
props[Tp.PROP_CHANNEL_TARGET_HANDLE_TYPE] = Tp.IFACE_CHANNEL_TYPE_TEXT;
|
|
|
|
props[Tp.PROP_CHANNEL_TARGET_HANDLE_TYPE] = Tp.HandleType.CONTACT;
|
|
|
|
this._observer.add_observer_filter(props);
|
2011-02-07 11:34:08 -05:00
|
|
|
|
|
|
|
try {
|
|
|
|
this._observer.register();
|
|
|
|
} catch (e) {
|
|
|
|
throw new Error('Couldn\'t register SimpleObserver. Error: \n' + e);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_observeChannels: function(observer, account, conn, channels,
|
|
|
|
dispatchOp, requests, context) {
|
|
|
|
let connPath = conn.get_object_path();
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
if (this._sources[connPath + ':' + targetHandle])
|
|
|
|
continue;
|
|
|
|
|
2011-02-08 05:22:46 -05:00
|
|
|
let source = new Source(account, conn, chan);
|
|
|
|
|
2011-02-07 11:34:08 -05:00
|
|
|
this._sources[connPath + ':' + targetHandle] = source;
|
|
|
|
source.connect('destroy', Lang.bind(this,
|
|
|
|
function() {
|
|
|
|
delete this._sources[connPath + ':' + targetHandle];
|
|
|
|
}));
|
|
|
|
|
2011-02-08 07:17:29 -05:00
|
|
|
/* Request a TpContact */
|
|
|
|
Shell.get_tp_contacts(conn, 1, [targetHandle],
|
|
|
|
contactFeatures.length, contactFeatures,
|
|
|
|
Lang.bind(this, function (connection, contacts, failed) {
|
|
|
|
if (contacts.length < 1)
|
|
|
|
return;
|
|
|
|
|
|
|
|
/* We got the TpContact */
|
|
|
|
if (this._sources[connPath + ':' + targetHandle])
|
|
|
|
return;
|
|
|
|
|
|
|
|
let source = new Source(account, conn, chan, contacts[0]);
|
|
|
|
|
|
|
|
this._sources[connPath + ':' + targetHandle] = source;
|
|
|
|
source.connect('destroy', Lang.bind(this,
|
|
|
|
function() {
|
|
|
|
delete this._sources[connPath + ':' + targetHandle];
|
|
|
|
}));
|
|
|
|
}));
|
2011-02-07 11:34:08 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// Allow dbus method to return
|
|
|
|
context.accept();
|
2010-02-02 10:21:47 -05:00
|
|
|
},
|
|
|
|
|
2010-04-16 17:24:34 -04:00
|
|
|
_presenceChanged: function(contactManager, connPath, handle,
|
|
|
|
type, message) {
|
|
|
|
let source = this._sources[connPath + ':' + handle];
|
|
|
|
if (!source)
|
|
|
|
return;
|
|
|
|
|
|
|
|
source.setPresence(type, message);
|
2010-02-02 10:21:47 -05:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2010-05-11 12:26:00 -04:00
|
|
|
function ContactManager() {
|
2010-02-02 10:21:47 -05:00
|
|
|
this._init();
|
|
|
|
};
|
|
|
|
|
2010-05-11 12:26:00 -04:00
|
|
|
ContactManager.prototype = {
|
2010-02-02 10:21:47 -05:00
|
|
|
_init: function() {
|
|
|
|
this._connections = {};
|
2010-04-14 10:24:14 -04:00
|
|
|
// Note that if we changed this to '/telepathy/avatars' then
|
|
|
|
// we would share cache files with empathy. But since this is
|
|
|
|
// not documented/guaranteed, it seems a little sketchy
|
|
|
|
this._cacheDir = GLib.get_user_cache_dir() + '/gnome-shell/avatars';
|
2010-02-02 10:21:47 -05:00
|
|
|
},
|
|
|
|
|
2010-04-16 17:24:34 -04:00
|
|
|
addConnection: function(connPath) {
|
|
|
|
let info = this._connections[connPath];
|
|
|
|
if (info)
|
|
|
|
return info;
|
|
|
|
|
|
|
|
info = {};
|
2010-02-02 10:21:47 -05:00
|
|
|
|
2010-04-14 10:24:14 -04:00
|
|
|
// Figure out the cache subdirectory for this connection by
|
|
|
|
// parsing the connection manager name (eg, 'gabble') and
|
|
|
|
// protocol name (eg, 'jabber') from the Connection's path.
|
|
|
|
// Telepathy requires the D-Bus path for a connection to have
|
|
|
|
// a specific form, and explicitly says that clients are
|
|
|
|
// allowed to parse it.
|
2010-04-16 17:24:34 -04:00
|
|
|
let match = connPath.match(/\/org\/freedesktop\/Telepathy\/Connection\/([^\/]*\/[^\/]*)\/.*/);
|
2010-04-14 10:24:14 -04:00
|
|
|
if (!match)
|
2010-04-16 17:24:34 -04:00
|
|
|
throw new Error('Could not parse connection path ' + connPath);
|
2010-04-14 10:24:14 -04:00
|
|
|
|
|
|
|
info.cacheDir = this._cacheDir + '/' + match[1];
|
2010-09-30 12:41:20 -04:00
|
|
|
GLib.mkdir_with_parents(info.cacheDir, 0x1c0); // 0x1c0 = octal 0700
|
2010-04-14 10:24:14 -04:00
|
|
|
|
2010-04-16 17:24:34 -04:00
|
|
|
// info.names[handle] is @handle's real name
|
2010-05-11 12:26:00 -04:00
|
|
|
// info.tokens[handle] is the token for @handle's avatar
|
2010-04-16 17:24:34 -04:00
|
|
|
info.names = {};
|
2010-05-11 12:26:00 -04:00
|
|
|
info.tokens = {};
|
2010-02-02 10:21:47 -05:00
|
|
|
|
2010-04-14 10:24:14 -04:00
|
|
|
// info.icons[handle] is an array of the icon actors currently
|
2010-02-02 10:21:47 -05:00
|
|
|
// being displayed for @handle. These will be updated
|
|
|
|
// automatically if @handle's avatar changes.
|
|
|
|
info.icons = {};
|
|
|
|
|
2010-04-16 17:24:34 -04:00
|
|
|
let connName = Telepathy.pathToName(connPath);
|
|
|
|
|
|
|
|
info.connectionAvatars = new Telepathy.ConnectionAvatars(DBus.session, connName, connPath);
|
2010-02-02 10:21:47 -05:00
|
|
|
info.updatedId = info.connectionAvatars.connect(
|
|
|
|
'AvatarUpdated', Lang.bind(this, this._avatarUpdated));
|
|
|
|
info.retrievedId = info.connectionAvatars.connect(
|
|
|
|
'AvatarRetrieved', Lang.bind(this, this._avatarRetrieved));
|
|
|
|
|
2010-04-16 17:24:34 -04:00
|
|
|
info.connectionContacts = new Telepathy.ConnectionContacts(DBus.session, connName, connPath);
|
|
|
|
|
|
|
|
info.connectionPresence = new Telepathy.ConnectionSimplePresence(DBus.session, connName, connPath);
|
|
|
|
info.presenceChangedId = info.connectionPresence.connect(
|
|
|
|
'PresencesChanged', Lang.bind(this, this._presencesChanged));
|
|
|
|
|
|
|
|
let conn = new Telepathy.Connection(DBus.session, connName, connPath);
|
2010-02-02 10:21:47 -05:00
|
|
|
info.statusChangedId = conn.connect('StatusChanged', Lang.bind(this,
|
|
|
|
function (status, reason) {
|
2011-02-07 12:02:04 -05:00
|
|
|
if (status == Tp.ConnectionStatus.DISCONNECTED)
|
2010-02-02 10:21:47 -05:00
|
|
|
this._removeConnection(conn);
|
|
|
|
}));
|
|
|
|
|
2010-04-16 17:24:34 -04:00
|
|
|
let connReq = new Telepathy.ConnectionRequests(DBus.session,
|
|
|
|
connName, connPath);
|
|
|
|
connReq.EnsureChannelRemote(subscribedContactsChannel, Lang.bind(this,
|
|
|
|
function (result, err) {
|
|
|
|
if (!result)
|
|
|
|
return;
|
|
|
|
|
|
|
|
let [mine, channelPath, props] = result;
|
|
|
|
this._gotContactsChannel(connPath, channelPath, props);
|
|
|
|
}));
|
|
|
|
|
|
|
|
this._connections[connPath] = info;
|
2010-02-02 10:21:47 -05:00
|
|
|
return info;
|
|
|
|
},
|
|
|
|
|
2010-04-16 17:24:34 -04:00
|
|
|
_gotContactsChannel: function(connPath, channelPath, props) {
|
|
|
|
let info = this._connections[connPath];
|
|
|
|
if (!info)
|
|
|
|
return;
|
|
|
|
|
|
|
|
info.contactsGroup = new Telepathy.ChannelGroup(DBus.session,
|
|
|
|
Telepathy.pathToName(connPath),
|
|
|
|
channelPath);
|
|
|
|
info.contactsListChangedId =
|
|
|
|
info.contactsGroup.connect('MembersChanged', Lang.bind(this, this._contactsListChanged, info));
|
|
|
|
|
|
|
|
info.contactsGroup.GetRemote('Members', Lang.bind(this,
|
|
|
|
function(contacts, err) {
|
|
|
|
if (!contacts)
|
|
|
|
return;
|
|
|
|
|
|
|
|
info.connectionContacts.GetContactAttributesRemote(
|
2011-02-07 12:02:04 -05:00
|
|
|
contacts, [Tp.IFACE_CONNECTION_INTERFACE_ALIASING], false,
|
2010-04-16 17:24:34 -04:00
|
|
|
Lang.bind(this, this._gotContactAttributes, info));
|
|
|
|
}));
|
|
|
|
},
|
|
|
|
|
|
|
|
_contactsListChanged: function(group, message, added, removed,
|
|
|
|
local_pending, remote_pending,
|
|
|
|
actor, reason, info) {
|
|
|
|
for (let i = 0; i < removed.length; i++)
|
|
|
|
delete info.names[removed[i]];
|
|
|
|
|
|
|
|
info.connectionContacts.GetContactAttributesRemote(
|
2011-02-07 12:02:04 -05:00
|
|
|
added, [Tp.IFACE_CONNECTION_INTERFACE_ALIASING], false,
|
2010-04-16 17:24:34 -04:00
|
|
|
Lang.bind(this, this._gotContactAttributes, info));
|
|
|
|
},
|
|
|
|
|
|
|
|
_gotContactAttributes: function(attrs, err, info) {
|
|
|
|
if (!attrs)
|
|
|
|
return;
|
|
|
|
|
|
|
|
for (let handle in attrs)
|
2011-02-07 12:02:04 -05:00
|
|
|
info.names[handle] = attrs[handle][Tp.TOKEN_CONNECTION_INTERFACE_ALIASING_ALIAS];
|
2010-04-16 17:24:34 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
_presencesChanged: function(conn, presences, err) {
|
|
|
|
if (!presences)
|
|
|
|
return;
|
|
|
|
|
|
|
|
let info = this._connections[conn.getPath()];
|
|
|
|
if (!info)
|
|
|
|
return;
|
|
|
|
|
|
|
|
for (let handle in presences) {
|
|
|
|
let [type, status, message] = presences[handle];
|
|
|
|
this.emit('presence-changed', conn.getPath(), handle, type, message);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2010-02-02 10:21:47 -05:00
|
|
|
_removeConnection: function(conn) {
|
|
|
|
let info = this._connections[conn.getPath()];
|
|
|
|
if (!info)
|
|
|
|
return;
|
|
|
|
|
|
|
|
conn.disconnect(info.statusChangedId);
|
|
|
|
info.connectionAvatars.disconnect(info.updatedId);
|
|
|
|
info.connectionAvatars.disconnect(info.retrievedId);
|
2010-04-16 17:24:34 -04:00
|
|
|
info.connectionPresence.disconnect(info.presenceChangedId);
|
|
|
|
info.contactsGroup.disconnect(info.contactsListChangedId);
|
2010-02-02 10:21:47 -05:00
|
|
|
|
|
|
|
delete this._connections[conn.getPath()];
|
|
|
|
},
|
|
|
|
|
2010-04-14 10:24:14 -04:00
|
|
|
_getFileForToken: function(info, token) {
|
|
|
|
return info.cacheDir + '/' + Telepathy.escapeAsIdentifier(token);
|
|
|
|
},
|
|
|
|
|
|
|
|
_setIcon: function(iconBox, info, handle) {
|
|
|
|
let textureCache = St.TextureCache.get_default();
|
2010-05-11 12:26:00 -04:00
|
|
|
let token = info.tokens[handle];
|
2010-04-14 10:24:14 -04:00
|
|
|
let file;
|
|
|
|
|
|
|
|
if (token) {
|
|
|
|
file = this._getFileForToken(info, token);
|
|
|
|
if (!GLib.file_test(file, GLib.FileTest.EXISTS))
|
|
|
|
file = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (file) {
|
|
|
|
let uri = GLib.filename_to_uri(file, null);
|
|
|
|
iconBox.child = textureCache.load_uri_async(uri, iconBox._size, iconBox._size);
|
|
|
|
} else {
|
2010-11-16 14:08:52 -05:00
|
|
|
iconBox.child = new St.Icon({ icon_name: 'stock_person',
|
|
|
|
icon_type: St.IconType.FULLCOLOR,
|
|
|
|
icon_size: iconBox._size });
|
2010-04-14 10:24:14 -04:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_updateIcons: function(info, handle) {
|
|
|
|
if (!info.icons[handle])
|
|
|
|
return;
|
|
|
|
|
|
|
|
for (let i = 0; i < info.icons[handle].length; i++) {
|
|
|
|
let iconBox = info.icons[handle][i];
|
|
|
|
this._setIcon(iconBox, info, handle);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2010-02-02 10:21:47 -05:00
|
|
|
_avatarUpdated: function(conn, handle, token) {
|
|
|
|
let info = this._connections[conn.getPath()];
|
|
|
|
if (!info)
|
|
|
|
return;
|
|
|
|
|
2010-05-11 12:26:00 -04:00
|
|
|
if (info.tokens[handle] == token)
|
2010-02-02 10:21:47 -05:00
|
|
|
return;
|
|
|
|
|
2010-05-11 12:26:00 -04:00
|
|
|
info.tokens[handle] = token;
|
2010-04-14 10:24:14 -04:00
|
|
|
if (token != '') {
|
|
|
|
let file = this._getFileForToken(info, token);
|
|
|
|
if (!GLib.file_test(file, GLib.FileTest.EXISTS)) {
|
|
|
|
info.connectionAvatars.RequestAvatarsRemote([handle]);
|
|
|
|
return;
|
|
|
|
}
|
2010-02-02 10:21:47 -05:00
|
|
|
}
|
|
|
|
|
2010-04-14 10:24:14 -04:00
|
|
|
this._updateIcons(info, handle);
|
2010-02-02 10:21:47 -05:00
|
|
|
},
|
|
|
|
|
|
|
|
_avatarRetrieved: function(conn, handle, token, avatarData, mimeType) {
|
|
|
|
let info = this._connections[conn.getPath()];
|
|
|
|
if (!info)
|
|
|
|
return;
|
|
|
|
|
2010-04-14 10:24:14 -04:00
|
|
|
let file = this._getFileForToken(info, token);
|
|
|
|
let success = false;
|
|
|
|
try {
|
|
|
|
success = GLib.file_set_contents(file, avatarData, avatarData.length);
|
|
|
|
} catch (e) {
|
|
|
|
logError(e, 'Error caching avatar data');
|
2010-02-02 10:21:47 -05:00
|
|
|
}
|
2010-04-14 10:24:14 -04:00
|
|
|
|
|
|
|
if (success)
|
|
|
|
this._updateIcons(info, handle);
|
2010-02-02 10:21:47 -05:00
|
|
|
},
|
|
|
|
|
|
|
|
createAvatar: function(conn, handle, size) {
|
|
|
|
let iconBox = new St.Bin({ style_class: 'avatar-box' });
|
2010-04-14 10:24:14 -04:00
|
|
|
iconBox._size = size;
|
2010-02-02 10:21:47 -05:00
|
|
|
|
2011-02-08 05:22:46 -05:00
|
|
|
let info = this._connections[conn.get_object_path()];
|
2010-02-02 10:21:47 -05:00
|
|
|
if (!info)
|
2011-02-08 05:22:46 -05:00
|
|
|
info = this.addConnection(conn.get_object_path());
|
2010-02-02 10:21:47 -05:00
|
|
|
|
|
|
|
if (!info.icons[handle])
|
|
|
|
info.icons[handle] = [];
|
|
|
|
info.icons[handle].push(iconBox);
|
|
|
|
|
|
|
|
iconBox.connect('destroy', Lang.bind(this,
|
|
|
|
function() {
|
|
|
|
let i = info.icons[handle].indexOf(iconBox);
|
|
|
|
if (i != -1)
|
|
|
|
info.icons[handle].splice(i, 1);
|
|
|
|
}));
|
|
|
|
|
2010-04-14 10:24:14 -04:00
|
|
|
// If we already have the icon cached and know its token, this
|
|
|
|
// will fill it in. Otherwise it will fill in the default
|
|
|
|
// icon.
|
|
|
|
this._setIcon(iconBox, info, handle);
|
|
|
|
|
|
|
|
// Asynchronously load the real avatar if we don't have it yet.
|
2010-05-11 12:26:00 -04:00
|
|
|
if (info.tokens[handle] == null) {
|
2010-04-14 10:24:14 -04:00
|
|
|
info.connectionAvatars.GetKnownAvatarTokensRemote([handle], Lang.bind(this,
|
|
|
|
function (tokens, err) {
|
|
|
|
let token = tokens && tokens[handle] ? tokens[handle] : '';
|
2011-02-08 05:22:46 -05:00
|
|
|
this._avatarUpdated(info.connectionAvatars, handle, token);
|
2010-04-14 10:24:14 -04:00
|
|
|
}));
|
2010-02-02 10:21:47 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
return iconBox;
|
|
|
|
}
|
|
|
|
};
|
2010-04-16 17:24:34 -04:00
|
|
|
Signals.addSignalMethods(ContactManager.prototype);
|
2010-02-02 10:21:47 -05:00
|
|
|
|
|
|
|
|
2011-02-08 07:17:29 -05:00
|
|
|
function Source(account, conn, channel, contact) {
|
|
|
|
this._init(account, conn, channel, contact);
|
2010-02-02 10:21:47 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
Source.prototype = {
|
|
|
|
__proto__: MessageTray.Source.prototype,
|
|
|
|
|
2011-02-08 07:17:29 -05:00
|
|
|
_init: function(account, conn, channel, contact) {
|
|
|
|
MessageTray.Source.prototype._init.call(this, channel.get_identifier());
|
2010-12-30 16:09:56 -05:00
|
|
|
|
2011-02-08 05:22:46 -05:00
|
|
|
this.isChat = true;
|
2010-04-08 10:55:22 -04:00
|
|
|
|
2011-02-08 05:22:46 -05:00
|
|
|
this._account = account;
|
2011-02-08 07:17:29 -05:00
|
|
|
this._contact = contact;
|
2010-02-02 10:21:47 -05:00
|
|
|
|
2011-02-08 05:22:46 -05:00
|
|
|
this._conn = conn;
|
|
|
|
this._channel = channel;
|
|
|
|
this._closedId = this._channel.connect('invalidated', Lang.bind(this, this._channelClosed));
|
2010-02-02 10:21:47 -05:00
|
|
|
|
2011-02-08 07:17:29 -05:00
|
|
|
this._updateAlias();
|
2010-02-02 10:21:47 -05:00
|
|
|
|
2010-11-28 10:14:34 -05:00
|
|
|
this._notification = new Notification(this);
|
2011-01-04 04:34:57 -05:00
|
|
|
this._notification.setUrgency(MessageTray.Urgency.HIGH);
|
2010-11-28 10:14:34 -05:00
|
|
|
|
2010-04-16 17:24:34 -04:00
|
|
|
// Since we only create sources when receiving a message, this
|
|
|
|
// is a plausible default
|
2011-02-07 12:02:04 -05:00
|
|
|
this._presence = Tp.ConnectionPresenceType.AVAILABLE;
|
2010-04-16 17:24:34 -04:00
|
|
|
|
2011-02-08 05:22:46 -05:00
|
|
|
this._channelText = new Telepathy.ChannelText(DBus.session, conn.get_bus_name(), channel.get_object_path());
|
2010-11-28 10:14:34 -05:00
|
|
|
this._sentId = this._channelText.connect('Sent', Lang.bind(this, this._messageSent));
|
2010-02-22 14:23:36 -05:00
|
|
|
this._receivedId = this._channelText.connect('Received', Lang.bind(this, this._messageReceived));
|
2010-02-02 10:21:47 -05:00
|
|
|
|
|
|
|
this._channelText.ListPendingMessagesRemote(false, Lang.bind(this, this._gotPendingMessages));
|
2010-08-05 13:09:27 -04:00
|
|
|
|
|
|
|
this._setSummaryIcon(this.createNotificationIcon());
|
2011-02-08 07:17:29 -05:00
|
|
|
|
|
|
|
this._notifyAliasId = this._contact.connect('notify::alias', Lang.bind(this, this._updateAlias));
|
|
|
|
},
|
|
|
|
|
|
|
|
_updateAlias: function() {
|
|
|
|
this.title = this._contact.get_alias();
|
2010-02-02 10:21:47 -05:00
|
|
|
},
|
|
|
|
|
2010-08-05 13:09:27 -04:00
|
|
|
createNotificationIcon: function() {
|
|
|
|
return contactManager.createAvatar(this._conn, this._targetHandle,
|
|
|
|
this.ICON_SIZE);
|
2010-02-02 10:21:47 -05:00
|
|
|
},
|
|
|
|
|
MessageTray: untangle click notifications
Previously, when you clicked on a notification, it would call
this.source.clicked(), which would emit a 'clicked' signal on the
source, and then various other stuff would happen from there. This
used to make a little bit of sense, when clicking on a notification
was supposed to do the same thing as clicking on its source, but makes
less sense now, when clicking on the source itself *doesn't* call
source.clicked()...
Change it so that when you click on a notification, the notification
emits 'clicked' itself, and the source notices that and calls its
notificationClicked() method, and the various source subclasses do
what they need to do with that, and Source no longer has a clicked
method/signal.
https://bugzilla.gnome.org/show_bug.cgi?id=631042
2010-09-30 16:41:38 -04:00
|
|
|
_notificationClicked: function(notification) {
|
2011-02-07 12:02:04 -05:00
|
|
|
let props = {};
|
|
|
|
props[Tp.PROP_CHANNEL_CHANNEL_TYPE] = Tp.IFACE_CHANNEL_TYPE_TEXT;
|
2011-02-08 05:53:40 -05:00
|
|
|
[props[Tp.PROP_CHANNEL_TARGET_HANDLE], props[Tp.PROP_CHANNEL_TARGET_HANDLE_TYPE]] = this._channel.get_handle();
|
2010-04-08 10:55:22 -04:00
|
|
|
|
2011-02-08 05:53:40 -05:00
|
|
|
let req = Tp.AccountChannelRequest.new(this._account, props, global.get_current_time());
|
2010-04-08 10:55:22 -04:00
|
|
|
|
2011-02-08 05:53:40 -05:00
|
|
|
req.ensure_channel_async('', null, null);
|
2010-04-08 10:55:22 -04:00
|
|
|
},
|
|
|
|
|
2010-02-02 10:21:47 -05:00
|
|
|
_gotPendingMessages: function(msgs, err) {
|
|
|
|
if (!msgs)
|
|
|
|
return;
|
|
|
|
|
|
|
|
for (let i = 0; i < msgs.length; i++)
|
2010-02-22 14:23:36 -05:00
|
|
|
this._messageReceived.apply(this, [this._channel].concat(msgs[i]));
|
2010-02-02 10:21:47 -05:00
|
|
|
},
|
|
|
|
|
|
|
|
_channelClosed: function() {
|
|
|
|
this._channel.disconnect(this._closedId);
|
|
|
|
this._channelText.disconnect(this._receivedId);
|
2010-11-28 10:14:34 -05:00
|
|
|
this._channelText.disconnect(this._sentId);
|
2011-02-08 07:17:29 -05:00
|
|
|
|
|
|
|
this._contact.disconnect(this._notifyAliasId);
|
2010-02-02 10:21:47 -05:00
|
|
|
this.destroy();
|
|
|
|
},
|
|
|
|
|
2010-11-28 10:14:34 -05:00
|
|
|
_messageReceived: function(channel, id, timestamp, sender,
|
|
|
|
type, flags, text) {
|
|
|
|
this._notification.appendMessage(text, timestamp, NotificationDirection.RECEIVED);
|
|
|
|
this.notify();
|
|
|
|
},
|
2010-02-02 10:21:47 -05:00
|
|
|
|
2010-11-28 10:14:34 -05:00
|
|
|
// This is called for both messages we send from
|
|
|
|
// our client and other clients as well.
|
|
|
|
_messageSent: function(channel, timestamp, type, text) {
|
|
|
|
this._notification.appendMessage(text, timestamp, NotificationDirection.SENT);
|
2010-04-16 17:24:34 -04:00
|
|
|
},
|
|
|
|
|
2010-11-28 10:14:34 -05:00
|
|
|
notify: function() {
|
|
|
|
if (!Main.messageTray.contains(this))
|
|
|
|
Main.messageTray.add(this);
|
|
|
|
|
|
|
|
MessageTray.Source.prototype.notify.call(this, this._notification);
|
2010-02-22 14:23:36 -05:00
|
|
|
},
|
|
|
|
|
|
|
|
respond: function(text) {
|
2011-02-07 12:02:04 -05:00
|
|
|
this._channelText.SendRemote(Tp.ChannelTextMessageType.NORMAL, text);
|
2010-04-16 17:24:34 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
setPresence: function(presence, message) {
|
2011-02-13 23:13:07 -05:00
|
|
|
let msg, shouldNotify, title;
|
|
|
|
|
|
|
|
title = GLib.markup_escape_text(this.title, -1);
|
2010-04-16 17:24:34 -04:00
|
|
|
|
2011-02-07 12:02:04 -05:00
|
|
|
if (presence == Tp.ConnectionPresenceType.AVAILABLE) {
|
2011-02-13 23:13:07 -05:00
|
|
|
msg = _("%s is online.").format(title);
|
2011-02-07 12:02:04 -05:00
|
|
|
shouldNotify = (this._presence == Tp.ConnectionPresenceType.OFFLINE);
|
|
|
|
} else if (presence == Tp.ConnectionPresenceType.OFFLINE ||
|
|
|
|
presence == Tp.ConnectionPresenceType.EXTENDED_AWAY) {
|
|
|
|
presence = Tp.ConnectionPresenceType.OFFLINE;
|
2011-02-13 23:13:07 -05:00
|
|
|
msg = _("%s is offline.").format(title);
|
2011-02-07 12:02:04 -05:00
|
|
|
shouldNotify = (this._presence != Tp.ConnectionPresenceType.OFFLINE);
|
|
|
|
} else if (presence == Tp.ConnectionPresenceType.AWAY) {
|
2011-02-13 23:13:07 -05:00
|
|
|
msg = _("%s is away.").format(title);
|
2010-11-28 10:14:34 -05:00
|
|
|
shouldNotify = false;
|
2011-02-07 12:02:04 -05:00
|
|
|
} else if (presence == Tp.ConnectionPresenceType.BUSY) {
|
2011-02-13 23:13:07 -05:00
|
|
|
msg = _("%s is busy.").format(title);
|
2010-11-28 10:14:34 -05:00
|
|
|
shouldNotify = false;
|
2010-04-16 17:24:34 -04:00
|
|
|
} else
|
|
|
|
return;
|
|
|
|
|
|
|
|
this._presence = presence;
|
|
|
|
|
|
|
|
if (message)
|
|
|
|
msg += ' <i>(' + GLib.markup_escape_text(message, -1) + ')</i>';
|
|
|
|
|
2010-11-28 10:14:34 -05:00
|
|
|
this._notification.appendPresence(msg, shouldNotify);
|
|
|
|
if (shouldNotify)
|
|
|
|
this.notify();
|
2010-02-02 10:21:47 -05:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2010-08-11 09:43:40 -04:00
|
|
|
function Notification(source) {
|
|
|
|
this._init(source);
|
2010-02-02 10:21:47 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
Notification.prototype = {
|
|
|
|
__proto__: MessageTray.Notification.prototype,
|
|
|
|
|
2010-08-11 09:43:40 -04:00
|
|
|
_init: function(source) {
|
2010-08-30 16:03:08 -04:00
|
|
|
MessageTray.Notification.prototype._init.call(this, source, source.title, null, { customContent: true });
|
2010-12-22 02:41:11 -05:00
|
|
|
this.setResident(true);
|
2010-02-22 14:23:36 -05:00
|
|
|
|
2011-02-09 22:45:50 -05:00
|
|
|
this._responseEntry = new St.Entry({ style_class: 'chat-response',
|
|
|
|
can_focus: true });
|
2010-02-22 14:23:36 -05:00
|
|
|
this._responseEntry.clutter_text.connect('activate', Lang.bind(this, this._onEntryActivated));
|
|
|
|
this.setActionArea(this._responseEntry);
|
|
|
|
|
|
|
|
this._history = [];
|
2010-11-25 09:29:41 -05:00
|
|
|
this._timestampTimeoutId = 0;
|
2010-02-22 14:23:36 -05:00
|
|
|
},
|
|
|
|
|
2010-11-28 10:14:34 -05:00
|
|
|
appendMessage: function(text, timestamp, direction) {
|
2010-11-25 09:29:41 -05:00
|
|
|
this.update(this.source.title, text, { customContent: true });
|
2010-11-28 10:14:34 -05:00
|
|
|
this._append(text, direction, timestamp);
|
2010-02-22 14:23:36 -05:00
|
|
|
},
|
|
|
|
|
2010-11-25 09:29:41 -05:00
|
|
|
_append: function(text, style, timestamp) {
|
|
|
|
let currentTime = (Date.now() / 1000);
|
|
|
|
if (!timestamp)
|
|
|
|
timestamp = currentTime;
|
|
|
|
let lastMessageTime = -1;
|
|
|
|
if (this._history.length > 0)
|
|
|
|
lastMessageTime = this._history[0].time;
|
|
|
|
|
|
|
|
// Reset the old message timeout
|
|
|
|
if (this._timestampTimeoutId)
|
|
|
|
Mainloop.source_remove(this._timestampTimeoutId);
|
|
|
|
|
2010-02-22 14:23:36 -05:00
|
|
|
let body = this.addBody(text);
|
|
|
|
body.add_style_class_name(style);
|
|
|
|
this.scrollTo(St.Side.BOTTOM);
|
|
|
|
|
2010-11-25 09:29:41 -05:00
|
|
|
this._history.unshift({ actor: body, time: timestamp, realMessage: true });
|
|
|
|
|
|
|
|
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 14:23:36 -05:00
|
|
|
|
|
|
|
if (this._history.length > 1) {
|
|
|
|
// 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.
|
|
|
|
|
2010-11-25 09:29:41 -05:00
|
|
|
let maxLength = (lastMessageTime < currentTime - SCROLLBACK_RECENT_TIME) ?
|
2010-02-22 14:23:36 -05:00
|
|
|
SCROLLBACK_IDLE_LENGTH : SCROLLBACK_RECENT_LENGTH;
|
2010-11-25 09:29:41 -05:00
|
|
|
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));
|
2010-02-22 14:23:36 -05:00
|
|
|
for (let i = 0; i < expired.length; i++)
|
|
|
|
expired[i].actor.destroy();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2010-11-25 09:29:41 -05:00
|
|
|
_appendTimestamp: function() {
|
|
|
|
let lastMessageTime = this._history[0].time;
|
|
|
|
let lastMessageDate = new Date(lastMessageTime * 1000);
|
|
|
|
|
|
|
|
/* 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. */
|
2010-12-13 17:07:37 -05:00
|
|
|
// xgettext:no-c-format
|
2010-11-25 09:29:41 -05:00
|
|
|
let timeLabel = this.addBody(lastMessageDate.toLocaleFormat(_("Sent at %X on %A")), false, { expand: true, x_fill: false, x_align: St.Align.END });
|
|
|
|
timeLabel.add_style_class_name('chat-meta-message');
|
|
|
|
this._history.unshift({ actor: timeLabel, time: lastMessageTime, realMessage: false });
|
|
|
|
|
|
|
|
this._timestampTimeoutId = 0;
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
|
|
|
|
appendPresence: function(text, asTitle) {
|
|
|
|
if (asTitle)
|
2011-02-13 23:13:07 -05:00
|
|
|
this.update(text, null, { customContent: true, titleMarkup: true });
|
2010-11-25 09:29:41 -05:00
|
|
|
else
|
|
|
|
this.update(this.source.title, null, { customContent: true });
|
2011-02-12 22:18:41 -05:00
|
|
|
let label = this.addBody(text, true);
|
2010-11-25 09:29:41 -05:00
|
|
|
label.add_style_class_name('chat-meta-message');
|
|
|
|
this._history.unshift({ actor: label, time: (Date.now() / 1000), realMessage: false});
|
|
|
|
},
|
|
|
|
|
2010-02-22 14:23:36 -05:00
|
|
|
_onEntryActivated: function() {
|
|
|
|
let text = this._responseEntry.get_text();
|
2010-07-21 00:42:37 -04:00
|
|
|
if (text == '')
|
2010-02-22 14:23:36 -05:00
|
|
|
return;
|
|
|
|
|
2010-11-28 10:14:34 -05:00
|
|
|
// Telepathy sends out the Sent signal for us.
|
|
|
|
// see Source._messageSent
|
2010-02-22 14:23:36 -05:00
|
|
|
this._responseEntry.set_text('');
|
|
|
|
this.source.respond(text);
|
2010-02-02 10:21:47 -05:00
|
|
|
}
|
|
|
|
};
|