[gnome-shell] TelepathyClient: show notifications for presence changes
- From: Dan Winship <danw src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-shell] TelepathyClient: show notifications for presence changes
- Date: Mon, 17 May 2010 14:00:02 +0000 (UTC)
commit f438ccfc531572ebc302eed97aaaa4540ff1254a
Author: Dan Winship <danw gnome org>
Date: Fri Apr 16 17:24:34 2010 -0400
TelepathyClient: show notifications for presence changes
Fetch the names of the user's "subscribed" contacts, and use the
SimplePresence interface to watch for available/away/busy/etc messages
and create notifications for them.
Currently we display notifications when switching between "available"
and "offline"/"extended away", but when switching between "available"
and "away"/"busy" we just add the information to the chat window
without popping up a notification, to avoid spamming the user with
"Bob's screensaver activated" messages.
https://bugzilla.gnome.org/show_bug.cgi?id=611613
js/misc/telepathy.js | 62 ++++++++++++++++
js/ui/telepathyClient.js | 183 +++++++++++++++++++++++++++++++++++++++++-----
2 files changed, 227 insertions(+), 18 deletions(-)
---
diff --git a/js/misc/telepathy.js b/js/misc/telepathy.js
index b53318c..c33eb72 100644
--- a/js/misc/telepathy.js
+++ b/js/misc/telepathy.js
@@ -176,6 +176,18 @@ const ConnectionAvatarsIface = {
};
let ConnectionAvatars = makeProxyClass(ConnectionAvatarsIface);
+const CONNECTION_CONTACTS_NAME = CONNECTION_NAME + '.Interface.Contacts';
+const ConnectionContactsIface = {
+ name: CONNECTION_CONTACTS_NAME,
+ methods: [
+ { name: 'GetContactAttributes',
+ inSignature: 'auasb',
+ outSignature: 'a{ua{sv}}'
+ }
+ ]
+};
+let ConnectionContacts = makeProxyClass(ConnectionContactsIface);
+
const CONNECTION_REQUESTS_NAME = CONNECTION_NAME + '.Interface.Requests';
const ConnectionRequestsIface = {
name: CONNECTION_REQUESTS_NAME,
@@ -205,6 +217,37 @@ const ConnectionRequestsIface = {
};
let ConnectionRequests = makeProxyClass(ConnectionRequestsIface);
+const CONNECTION_SIMPLE_PRESENCE_NAME = CONNECTION_NAME + '.Interface.SimplePresence';
+const ConnectionSimplePresenceIface = {
+ name: CONNECTION_SIMPLE_PRESENCE_NAME,
+ methods: [
+ { name: 'SetPresence',
+ inSignature: 'ss'
+ },
+ { name: 'GetPresences',
+ inSignature: 'au',
+ outSignature: 'a{u(uss)}'
+ }
+ ],
+ signals: [
+ { name: 'PresencesChanged',
+ inSignature: 'a{u(uss)}' }
+ ]
+};
+let ConnectionSimplePresence = makeProxyClass(ConnectionSimplePresenceIface);
+
+const ConnectionPresenceType = {
+ UNSET: 0,
+ OFFLINE: 1,
+ AVAILABLE: 2,
+ AWAY: 3,
+ EXTENDED_AWAY: 4,
+ HIDDEN: 5,
+ BUSY: 6,
+ UNKNOWN: 7,
+ ERROR: 8
+};
+
const HandleType = {
NONE: 0,
CONTACT: 1,
@@ -255,6 +298,25 @@ const ChannelTextMessageType = {
DELIVERY_REPORT: 4
};
+const CHANNEL_CONTACT_LIST_NAME = CHANNEL_NAME + '.Type.ContactList';
+// There is no interface associated with ContactList; it's just a
+// special kind of Channel.Interface.Group
+
+const CHANNEL_GROUP_NAME = CHANNEL_NAME + '.Interface.Group';
+const ChannelGroupIface = {
+ name: CHANNEL_GROUP_NAME,
+ properties: [
+ { name: 'Members',
+ signature: 'au',
+ access: 'read' }
+ ],
+ signals: [
+ { name: 'MembersChanged',
+ inSignature: 'sauauauauuu' }
+ ]
+};
+let ChannelGroup = makeProxyClass(ChannelGroupIface);
+
const ACCOUNT_MANAGER_NAME = TELEPATHY + '.AccountManager';
const AccountManagerIface = {
name: ACCOUNT_MANAGER_NAME,
diff --git a/js/ui/telepathyClient.js b/js/ui/telepathyClient.js
index 8b1f1c5..7fc48b4 100644
--- a/js/ui/telepathyClient.js
+++ b/js/ui/telepathyClient.js
@@ -5,7 +5,10 @@ const DBus = imports.dbus;
const GLib = imports.gi.GLib;
const Lang = imports.lang;
const Shell = imports.gi.Shell;
+const Signals = imports.signals;
const St = imports.gi.St;
+const Gettext = imports.gettext.domain('gnome-shell');
+const _ = Gettext.gettext;
const Main = imports.ui.main;
const MessageTray = imports.ui.messageTray;
@@ -34,6 +37,13 @@ let oneOrMoreUserTextChannel = {};
oneOrMoreUserTextChannel[Telepathy.CHANNEL_NAME + '.ChannelType'] = Telepathy.CHANNEL_TEXT_NAME;
oneOrMoreUserTextChannel[Telepathy.CHANNEL_NAME + '.TargetHandleType'] = Telepathy.HandleType.NONE;
+// The (non-chat) channel indicating the users whose presence
+// information we subscribe to
+let subscribedContactsChannel = {};
+subscribedContactsChannel[Telepathy.CHANNEL_NAME + '.ChannelType'] = Telepathy.CHANNEL_CONTACT_LIST_NAME;
+subscribedContactsChannel[Telepathy.CHANNEL_NAME + '.TargetHandleType'] = Telepathy.HandleType.LIST;
+subscribedContactsChannel[Telepathy.CHANNEL_NAME + '.TargetID'] = 'subscribe';
+
// This is GNOME Shell's implementation of the Telepathy 'Client'
// interface. Specifically, the shell is a Telepathy 'Observer', which
@@ -53,9 +63,10 @@ Client.prototype = {
function (name) { /* FIXME: lost */ });
this._accounts = {};
- this._channels = {};
+ this._sources = {};
contactManager = new ContactManager();
+ contactManager.connect('presence-changed', Lang.bind(this, this._presenceChanged));
channelDispatcher = new Telepathy.ChannelDispatcher(DBus.session,
Telepathy.CHANNEL_DISPATCHER_NAME,
@@ -106,6 +117,8 @@ Client.prototype = {
this._addChannels(accountPath, connPath, channels);
}));
+
+ contactManager.addConnection(connPath);
}));
},
@@ -126,8 +139,6 @@ Client.prototype = {
_addChannels: function(accountPath, connPath, channelDetailsList) {
for (let i = 0; i < channelDetailsList.length; i++) {
let [channelPath, props] = channelDetailsList[i];
- if (this._channels[channelPath])
- continue;
// If this is being called from the startup code then it
// won't have passed through our filters, so we need to
@@ -145,14 +156,26 @@ Client.prototype = {
let targetHandle = props[Telepathy.CHANNEL_NAME + '.TargetHandle'];
let targetId = props[Telepathy.CHANNEL_NAME + '.TargetID'];
+ if (this._sources[connPath + ':' + targetHandle])
+ continue;
+
let source = new Source(accountPath, connPath, channelPath,
targetHandle, targetHandleType, targetId);
- this._channels[channelPath] = source;
+ this._sources[connPath + ':' + targetHandle] = source;
source.connect('destroy', Lang.bind(this,
function() {
- delete this._channels[channelPath];
+ delete this._sources[connPath + ':' + targetHandle];
}));
}
+ },
+
+ _presenceChanged: function(contactManager, connPath, handle,
+ type, message) {
+ let source = this._sources[connPath + ':' + handle];
+ if (!source)
+ return;
+
+ source.setPresence(type, message);
}
};
DBus.conformExport(Client.prototype, Telepathy.ClientIface);
@@ -172,8 +195,12 @@ ContactManager.prototype = {
this._cacheDir = GLib.get_user_cache_dir() + '/gnome-shell/avatars';
},
- _addConnection: function(conn) {
- let info = {};
+ addConnection: function(connPath) {
+ let info = this._connections[connPath];
+ if (info)
+ return info;
+
+ info = {};
// Figure out the cache subdirectory for this connection by
// parsing the connection manager name (eg, 'gabble') and
@@ -181,14 +208,16 @@ ContactManager.prototype = {
// Telepathy requires the D-Bus path for a connection to have
// a specific form, and explicitly says that clients are
// allowed to parse it.
- let match = conn.getPath().match(/\/org\/freedesktop\/Telepathy\/Connection\/([^\/]*\/[^\/]*)\/.*/);
+ let match = connPath.match(/\/org\/freedesktop\/Telepathy\/Connection\/([^\/]*\/[^\/]*)\/.*/);
if (!match)
- throw new Error('Could not parse connection path ' + conn.getPath());
+ throw new Error('Could not parse connection path ' + connPath);
info.cacheDir = this._cacheDir + '/' + match[1];
GLib.mkdir_with_parents(info.cacheDir, 0700);
+ // info.names[handle] is @handle's real name
// info.tokens[handle] is the token for @handle's avatar
+ info.names = {};
info.tokens = {};
// info.icons[handle] is an array of the icon actors currently
@@ -196,24 +225,97 @@ ContactManager.prototype = {
// automatically if @handle's avatar changes.
info.icons = {};
- info.connectionAvatars = new Telepathy.ConnectionAvatars(DBus.session,
- conn.getBusName(),
- conn.getPath());
+ let connName = Telepathy.pathToName(connPath);
+
+ info.connectionAvatars = new Telepathy.ConnectionAvatars(DBus.session, connName, connPath);
info.updatedId = info.connectionAvatars.connect(
'AvatarUpdated', Lang.bind(this, this._avatarUpdated));
info.retrievedId = info.connectionAvatars.connect(
'AvatarRetrieved', Lang.bind(this, this._avatarRetrieved));
+ 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);
info.statusChangedId = conn.connect('StatusChanged', Lang.bind(this,
function (status, reason) {
if (status == Telepathy.ConnectionStatus.DISCONNECTED)
this._removeConnection(conn);
}));
- this._connections[conn.getPath()] = info;
+ 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;
return info;
},
+ _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(
+ contacts, [Telepathy.CONNECTION_ALIASING_NAME], false,
+ 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(
+ added, [Telepathy.CONNECTION_ALIASING_NAME], false,
+ Lang.bind(this, this._gotContactAttributes, info));
+ },
+
+ _gotContactAttributes: function(attrs, err, info) {
+ if (!attrs)
+ return;
+
+ for (let handle in attrs)
+ info.names[handle] = attrs[handle][Telepathy.CONNECTION_ALIASING_NAME + '/alias'];
+ },
+
+ _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);
+ }
+ },
+
_removeConnection: function(conn) {
let info = this._connections[conn.getPath()];
if (!info)
@@ -222,6 +324,8 @@ ContactManager.prototype = {
conn.disconnect(info.statusChangedId);
info.connectionAvatars.disconnect(info.updatedId);
info.connectionAvatars.disconnect(info.retrievedId);
+ info.connectionPresence.disconnect(info.presenceChangedId);
+ info.contactsGroup.disconnect(info.contactsListChangedId);
delete this._connections[conn.getPath()];
},
@@ -302,7 +406,7 @@ ContactManager.prototype = {
let info = this._connections[conn.getPath()];
if (!info)
- info = this._addConnection(conn);
+ info = this.addConnection(conn);
if (!info.icons[handle])
info.icons[handle] = [];
@@ -332,6 +436,7 @@ ContactManager.prototype = {
return iconBox;
}
};
+Signals.addSignalMethods(ContactManager.prototype);
function Source(accountPath, connPath, channelPath, targetHandle, targetHandleType, targetId) {
@@ -365,6 +470,10 @@ Source.prototype = {
}));
}
+ // Since we only create sources when receiving a message, this
+ // is a plausible default
+ this._presence = Telepathy.ConnectionPresenceType.AVAILABLE;
+
this._channelText = new Telepathy.ChannelText(DBus.session, connName, channelPath);
this._receivedId = this._channelText.connect('Received', Lang.bind(this, this._messageReceived));
@@ -411,19 +520,54 @@ Source.prototype = {
this.destroy();
},
- _messageReceived: function(channel, id, timestamp, sender,
- type, flags, text) {
+ _ensureNotification: function() {
if (!Main.messageTray.contains(this))
Main.messageTray.add(this);
if (!this._notification)
this._notification = new Notification(this._targetId, this);
+ },
+
+ _messageReceived: function(channel, id, timestamp, sender,
+ type, flags, text) {
+ this._ensureNotification();
this._notification.appendMessage(text);
this.notify(this._notification);
},
respond: function(text) {
this._channelText.SendRemote(Telepathy.ChannelTextMessageType.NORMAL, text);
+ },
+
+ setPresence: function(presence, message) {
+ let msg, notify;
+
+ if (presence == Telepathy.ConnectionPresenceType.AVAILABLE) {
+ msg = _("%s is online.").format(this.name);
+ notify = (this._presence == Telepathy.ConnectionPresenceType.OFFLINE);
+ } else if (presence == Telepathy.ConnectionPresenceType.OFFLINE ||
+ presence == Telepathy.ConnectionPresenceType.EXTENDED_AWAY) {
+ presence = Telepathy.ConnectionPresenceType.OFFLINE;
+ msg = _("%s is offline.").format(this.name);
+ notify = (this._presence != Telepathy.ConnectionPresenceType.OFFLINE);
+ } else if (presence == Telepathy.ConnectionPresenceType.AWAY) {
+ msg = _("%s is away.").format(this.name);
+ notify = false;
+ } else if (presence == Telepathy.ConnectionPresenceType.BUSY) {
+ msg = _("%s is busy.").format(this.name);
+ notify = false;
+ } else
+ return;
+
+ this._presence = presence;
+
+ if (message)
+ msg += ' <i>(' + GLib.markup_escape_text(message, -1) + ')</i>';
+
+ this._ensureNotification();
+ this._notification.appendMessage(msg, true);
+ if (notify)
+ this.notify(this._notification);
}
};
@@ -446,8 +590,11 @@ Notification.prototype = {
this._history = [];
},
- appendMessage: function(text) {
- this.update(this.source.name, text);
+ appendMessage: function(text, asTitle) {
+ if (asTitle)
+ this.update(text);
+ else
+ this.update(this.source.name, text);
this._append(text, 'chat-received');
},
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]