From f438ccfc531572ebc302eed97aaaa4540ff1254a Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Fri, 16 Apr 2010 17:24:34 -0400 Subject: [PATCH] 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 b53318cce..c33eb7285 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 8b1f1c561..7fc48b42d 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 += ' (' + GLib.markup_escape_text(message, -1) + ')'; + + 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'); },