[polari/wip/raresv/nick-popover: 6/15] userTracker: Allow tracking users across rooms



commit a493887eca36c0f6a07a89b0a5abe300ab09fc9e
Author: raresv <rares visalom gmail com>
Date:   Tue Aug 2 00:46:26 2016 +0300

    userTracker: Allow tracking users across rooms
    
    The UserTracker can be used to track the status of users
    in a particular room, however sometimes it is more
    interesting to know whether a user is online at all (for
    example to determine if it is possible to start a private
    conversation with them). While we don't have a way to get
    that information, we can approximate it by tracking users
    across all rooms we have joined.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=760853

 src/chatView.js    |   20 ++++-
 src/userTracker.js |  217 +++++++++++++++++++++++++++++++++++++++++++++-------
 2 files changed, 203 insertions(+), 34 deletions(-)
---
diff --git a/src/chatView.js b/src/chatView.js
index 16b8ee9..4abd6b6 100644
--- a/src/chatView.js
+++ b/src/chatView.js
@@ -294,8 +294,8 @@ const ChatView = new Lang.Class({
         this._initialPending = [];
         this._statusCount = { left: 0, joined: 0, total: 0 };
 
-        this._userTracker = new UserTracker.UserTracker(this._room);
-        this._userTracker.connect('status-changed', Lang.bind(this, this._onNickStatusChanged));
+        let statusMonitor = UserTracker.getUserStatusMonitor();
+        this._userTracker = statusMonitor.getUserTrackerForAccount(room.account);
 
         this._room.account.connect('notify::nickname', Lang.bind(this,
             function() {
@@ -350,6 +350,15 @@ const ChatView = new Lang.Class({
             this._roomSignals.push(room.connect(signal.name, signal.handler));
         }));
         this._onChannelChanged();
+
+        this._nickStatusChangedId = this._userTracker.watchRoomStatus(this._room,
+                                    null,
+                                    Lang.bind(this, this._onNickStatusChanged));
+
+        this.connect('destroy', () => {
+            this._userTracker.unwatchRoomStatus(this._room, this._nickStatusChangedId);
+            this._userTracker = null;
+        });
     },
 
     _createTags: function() {
@@ -449,7 +458,7 @@ const ChatView = new Lang.Class({
             if (!nickname)
                 return;
 
-            this._updateNickTag(tag, this._userTracker.getNickStatus(nickname));
+            this._updateNickTag(tag, this._userTracker.getNickRoomStatus(nickname));
         });
     },
 
@@ -1224,8 +1233,9 @@ const ChatView = new Lang.Class({
                               this._view.buffer.create_mark(null, iter, true));
     },
 
-    _onNickStatusChanged: function(tracker, nickName, status) {
-        let nickTag = this._lookupTag(this._getNickTagName(nickName));
+    _onNickStatusChanged: function(baseNick, status) {
+        let nickTagName = this._getNickTagName(baseNick);
+        let nickTag = this._lookupTag(nickTagName);
 
         if (!nickTag)
             return;
diff --git a/src/userTracker.js b/src/userTracker.js
index 57e2937..5d8faef 100644
--- a/src/userTracker.js
+++ b/src/userTracker.js
@@ -7,6 +7,50 @@ const Utils = imports.utils;
 const Gio = imports.gi.Gio;
 const GLib = imports.gi.GLib;
 
+const AccountsMonitor = imports.accountsMonitor;
+const RoomManager = imports.roomManager;
+
+let _singleton = null;
+
+function getUserStatusMonitor() {
+    if (_singleton == null)
+        _singleton = new UserStatusMonitor();
+    return _singleton;
+}
+
+const UserStatusMonitor = new Lang.Class({
+    Name: 'UserStatusMonitor',
+
+    _init: function() {
+        this._userTrackers = new Map();
+        this._accountsMonitor = AccountsMonitor.getDefault();
+
+        this._accountsMonitor.connect('account-added',
+                                      Lang.bind(this, this._onAccountAdded));
+        this._accountsMonitor.connect('account-removed',
+                                      Lang.bind(this, this._onAccountRemoved));
+
+        this._accountsMonitor.accounts.forEach(
+                    a => { this._onAccountAdded(this._accountsMonitor, a); });
+    },
+
+    _onAccountAdded: function(accountsMonitor, account) {
+        if (this._userTrackers.has(account))
+            return;
+
+        this._userTrackers.set(account, new UserTracker(account));
+    },
+
+    _onAccountRemoved: function(accountsMonitor, account) {
+        this._userTrackers.delete(account);
+    },
+
+    getUserTrackerForAccount: function(account) {
+            return this._userTrackers.get(account);
+    }
+});
+
+
 const UserTracker = new Lang.Class({
     Name: 'UserTracker',
     Extends: GObject.Object,
@@ -15,21 +59,53 @@ const UserTracker = new Lang.Class({
         'status-changed': {
             flags: GObject.SignalFlags.DETAILED,
             param_types: [GObject.TYPE_STRING, GObject.TYPE_INT]
+        },
+        'contacts-changed': {
+            flags: GObject.SignalFlags.DETAILED,
+            param_types: [GObject.TYPE_STRING]
         }
     },
 
-    _init: function(room) {
+    _init: function(account) {
         this.parent();
 
+        this._account = account;
+
         this._baseNickContacts = new Map();
+        this._roomData = new Map();
+        this._handlerCounter = 0;
+        this._app = Gio.Application.get_default();
 
-        this._room = room;
+        this._app.connect('prepare-shutdown', Lang.bind(this, this._onShutdown));
 
-        this._onRoomAdded(this._room);
-        this._onChannelChanged(this._room);
+        this._roomManager = RoomManager.getDefault();
+        this._roomManager.connect('room-added', Lang.bind(this, this._onRoomAdded));
+        this._roomManager.connect('room-removed', Lang.bind(this, this._onRoomRemoved));
     },
 
-    _onRoomAdded: function(room) {
+    _onShutdown: function() {
+        for (let room of this._roomData.keys())
+            this._onRoomRemoved(this._roomManager, room);
+    },
+
+    _getRoomContacts: function(room) {
+        return this._roomData.get(room).contactMapping;
+    },
+
+    _getRoomHandlers: function(room) {
+        return this._roomData.get(room).handlerMapping;
+    },
+
+    _getRoomSignals: function(room) {
+        return this._roomData.get(room).roomSignals;
+    },
+
+    _onRoomAdded: function(roomManager, room) {
+        if (room.account != this._account)
+            return;
+
+        this._ensureRoomMappingForRoom(room);
+
         let roomSignals = [
             { name: 'notify::channel',
               handler: Lang.bind(this, this._onChannelChanged) },
@@ -47,14 +123,24 @@ const UserTracker = new Lang.Class({
               handler: Lang.bind(this, this._onMemberLeft) }
         ];
 
-        roomSignals.forEach(Lang.bind(this, function(signal) {
-            room.connect(signal.name, signal.handler);
-        }));
-    }
+        let signalIds = this._getRoomSignals(room);
+        roomSignals.forEach(signal => {
+            signalIds.push(room.connect(signal.name, signal.handler));
+        });
+    },
+
+    _onRoomRemoved: function(roomManager, room) {
+        if (!this._roomData.has(room))
+            return;
+
+        this._getRoomSignals(room).forEach(id => { room.disconnect(id); });
+        this._clearUsersFromRoom(room);
+        this._roomData.delete(room);
+    },
 
     _onChannelChanged: function(room) {
         if (!room.channel) {
-            this._clearUsers();
+            this._clearUsersFromRoom(room);
             return;
         }
 
@@ -64,44 +150,70 @@ const UserTracker = new Lang.Class({
         else
             members = [room.channel.connection.self_contact, room.channel.target_contact];
 
-        members.forEach(m => { this._trackMember(m); });
+        /*keep track of initial members in the room, both locally and
+        globally*/
+        members.forEach(m => { this._trackMember(m, room); });
     },
 
-    _clearUsers: function() {
-        for (let [baseNick, contacts] of this._baseNickContacts)
-            contacts.slice().forEach((m) => { this._untrackMember(m); });
+    _clearUsersFromRoom: function(room) {
+        let map = this._getRoomContacts(room);
+        for (let [baseNick, contacts] of map)
+            contacts.slice().forEach((m) => { this._untrackMember(m, room); });
+    },
+
+    _ensureRoomMappingForRoom: function(room) {
+        if (this._roomData.has(room))
+            return;
+        this._roomData.set(room, { contactMapping: new Map(),
+                                   handlerMapping: new Map(),
+                                   roomSignals: [] });
     },
 
     _onMemberRenamed: function(room, oldMember, newMember) {
-        this._untrackMember(oldMember);
-        this._trackMember(newMember);
+        this._untrackMember(oldMember, room);
+        this._trackMember(newMember, room);
     },
 
     _onMemberJoined: function(room, member) {
-        this._trackMember(member);
+        this._trackMember(member, room);
     },
 
     _onMemberLeft: function(room, member) {
-        this._untrackMember(member);
+        this._untrackMember(member, room);
     },
 
-    _pushMember: function(baseNick, member) {
-        if (!this._baseNickContacts.has(baseNick))
-            this._baseNickContacts.set(baseNick, []);
-        let contacts = this._baseNickContacts.get(baseNick);
+    _runHandlers: function(room, member, status) {
+        let baseNick = Polari.util_get_basenick(member.alias);
+        let roomHandlers = this._getRoomHandlers(room);
+        for (let [id, info] of roomHandlers)
+            if (!info.nickName || info.nickName == baseNick)
+                info.handler(baseNick, status);
+    },
+
+    _pushMember: function(map, baseNick, member) {
+        if (!map.has(baseNick))
+            map.set(baseNick, []);
+        let contacts = map.get(baseNick);
         return contacts.push(member);
     },
 
-    _trackMember: function(member) {
+    _trackMember: function(member, room) {
         let baseNick = Polari.util_get_basenick(member.alias);
         let status = Tp.ConnectionPresenceType.AVAILABLE;
 
-        if (this._pushMember(baseNick, member) == 1)
+        let roomMap = this._getRoomContacts(room);
+        if (this._pushMember(roomMap, baseNick, member) == 1)
+            this._runHandlers(room, member, status);
+
+        let map = this._baseNickContacts;
+        if (this._pushMember(map, baseNick, member) == 1)
             this.emit("status-changed::" + baseNick, baseNick, status);
+
+        this.emit("contacts-changed::" + baseNick, member.alias);
     },
 
-    _popMember: function(baseNick, member) {
-        let contacts = this._baseNickContacts.get(baseNick) || [];
+    _popMember: function(map, baseNick, member) {
+        let contacts = map.get(baseNick) || [];
         let index = contacts.map(c => c.alias).indexOf(member.alias);
         if (index < 0)
             return [false, contacts.length];
@@ -109,14 +221,22 @@ const UserTracker = new Lang.Class({
         return [true, contacts.length];
     },
 
-    _untrackMember: function(member) {
+    _untrackMember: function(member, room) {
         let baseNick = Polari.util_get_basenick(member.alias);
         let status = Tp.ConnectionPresenceType.OFFLINE;
 
-        let [found, nContacts] = this._popMember(baseNick, member);
-        if (found)
+        let roomMap = this._getRoomContacts(room);
+        [found, nContacts] = this._popMember(roomMap, baseNick, member);
+        if (found && nContacts == 0)
+            this._runHandlers(room, member, status);
+
+        let map = this._baseNickContacts;
+        let [found, nContacts] = this._popMember(map, baseNick, member);
+        if (found) {
             if (nContacts == 0)
                 this.emit("status-changed::" + baseNick, member.alias, status);
+            this.emit("contacts-changed::" + baseNick, member.alias);
+        }
     },
 
     getNickStatus: function(nickName) {
@@ -125,5 +245,44 @@ const UserTracker = new Lang.Class({
         let contacts = this._baseNickContacts.get(baseNick) || [];
         return contacts.length == 0 ? Tp.ConnectionPresenceType.OFFLINE
                                     : Tp.ConnectionPresenceType.AVAILABLE;
+    },
+
+    getNickRoomStatus: function(nickName, room) {
+        let baseNick = Polari.util_get_basenick(nickName);
+
+        this._ensureRoomMappingForRoom(room);
+
+        let contacts = this._getRoomContacts(room).get(baseNick) || [];
+        return contacts.length == 0 ? Tp.ConnectionPresenceType.OFFLINE
+                                    : Tp.ConnectionPresenceType.AVAILABLE;
+    },
+
+    lookupContact: function(nickName) {
+        let baseNick = Polari.util_get_basenick(nickName);
+
+        let contacts = this._baseNickContacts.get(baseNick) || [];
+
+        for (let i = 0; i < contacts.length; i++)
+            if (contacts[i].alias == nickName)
+                return contacts[i];
+
+        return contacts[0];
+    },
+
+    watchRoomStatus: function(room, baseNick, callback) {
+        this._ensureRoomMappingForRoom(room);
+
+        this._getRoomHandlers(room).set(++this._handlerCounter, {
+            nickName: baseNick,
+            handler: callback
+        });
+
+        return this._handlerCounter;
+    },
+
+    unwatchRoomStatus: function(room, handlerID) {
+        if (!this._roomData.has(room))
+            return;
+        this._getRoomHandlers(room).delete(handlerID);
     }
 });


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]