[gnome-shell] TelepathyClient: show notifications for presence changes



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]