diff --git a/lib/client.js b/lib/client.js index a703280a9..d33107204 100644 --- a/lib/client.js +++ b/lib/client.js @@ -141,6 +141,7 @@ function MatrixClient(opts) { this.callList = { // callId: MatrixCall }; + this._config = {}; // see startClient() // try constructing a MatrixCall to see if we are running in an environment // which has WebRTC. If we are, listen for and handle m.call.* events. @@ -1986,6 +1987,9 @@ function doInitialSync(client, historyLen, includeArchived) { * @param {Number} opts.initialSyncLimit The event limit= to apply * to initial sync. Default: 8. * @param {Boolean} opts.includeArchivedRooms True to put archived=true + * @param {Boolean} opts.resolveInvitesToProfiles True to do /profile requests + * on every invite event if the displayname/avatar_url is not known for this user ID. + * Default: false. * on the /initialSync request. Default: false. */ MatrixClient.prototype.startClient = function(opts) { @@ -2003,6 +2007,8 @@ MatrixClient.prototype.startClient = function(opts) { opts = opts || {}; opts.initialSyncLimit = opts.initialSyncLimit || 8; opts.includeArchivedRooms = opts.includeArchivedRooms || false; + opts.resolveInvitesToProfiles = opts.resolveInvitesToProfiles || false; + this._config = opts; if (CRYPTO_ENABLED && this.sessionStore !== null) { this.uploadKeys(5); @@ -2057,6 +2063,7 @@ function _pollForEvents(client) { events = utils.map(data.chunk, _PojoToMatrixEventMapper(self)); } if (!(self.store instanceof StubStore)) { + var roomIdsWithNewInvites = {}; // bucket events based on room. var i = 0; var roomIdToEvents = {}; @@ -2068,6 +2075,10 @@ function _pollForEvents(client) { roomIdToEvents[roomId] = []; } roomIdToEvents[roomId].push(events[i]); + if (events[i].getType() === "m.room.member" && + events[i].getContent().membership === "invite") { + roomIdsWithNewInvites[roomId] = true; + } } else if (events[i].getType() === "m.presence") { var usr = self.store.getUser(events[i].getContent().user_id); @@ -2081,6 +2092,7 @@ function _pollForEvents(client) { } } } + // add events to room var roomIds = utils.keys(roomIdToEvents); utils.forEach(roomIds, function(roomId) { @@ -2115,6 +2127,10 @@ function _pollForEvents(client) { _syncRoom(self, room); } }); + + Object.keys(roomIdsWithNewInvites).forEach(function(inviteRoomId) { + _resolveInvites(self, self.store.getRoom(inviteRoomId)); + }); } if (data) { self.store.setSyncToken(data.end); @@ -2173,6 +2189,8 @@ function _processRoomEvents(client, room, stateEventList, messageChunk) { room.oldState.setStateEvents(oldStateEvents); room.currentState.setStateEvents(stateEvents); + _resolveInvites(client, room); + // add events to the timeline *after* setting the state // events so messages use the right display names. Initial sync // returns messages in chronological order, so we need to reverse @@ -2217,6 +2235,47 @@ function reEmit(reEmitEntity, emittableEntity, eventNames) { }); } +function _resolveInvites(client, room) { + if (!room || !client._config.resolveInvitesToProfiles) { + return; + } + // For each invited room member we want to give them a displayname/avatar url + // if they have one (the m.room.member invites don't contain this). + room.getMembersWithMembership("invite").forEach(function(member) { + if (member._requestedProfileInfo) { + return; + } + member._requestedProfileInfo = true; + // try to get a cached copy first. + var user = client.getUser(member.userId); + var promise; + if (user) { + promise = q({ + avatar_url: user.avatarUrl, + displayname: user.displayName + }); + } + else { + promise = client.getProfileInfo(member.userId); + } + promise.done(function(info) { + // slightly naughty by doctoring the invite event but this means all + // the code paths remain the same between invite/join display name stuff + // which is a worthy trade-off for some minor pollution. + var inviteEvent = member.events.member; + if (inviteEvent.getContent().membership !== "invite") { + // between resolving and now they have since joined, so don't clobber + return; + } + inviteEvent.getContent().avatar_url = info.avatar_url; + inviteEvent.getContent().displayname = info.displayname; + member.setMembershipEvent(inviteEvent, room.currentState); // fire listeners + }, function(err) { + // OH WELL. + }); + }); +} + function setupCallEventHandler(client) { var candidatesByCall = { // callId: [Candidate] diff --git a/spec/integ/matrix-client-syncing.spec.js b/spec/integ/matrix-client-syncing.spec.js index 6e85f0c36..7238f6429 100644 --- a/spec/integ/matrix-client-syncing.spec.js +++ b/spec/integ/matrix-client-syncing.spec.js @@ -10,6 +10,11 @@ describe("MatrixClient syncing", function() { var selfUserId = "@alice:localhost"; var selfAccessToken = "aseukfgwef"; var otherUserId = "@bob:localhost"; + var userA = "@alice:bar"; + var userB = "@bob:bar"; + var userC = "@claire:bar"; + var roomOne = "!foo:localhost"; + var roomTwo = "!bar:localhost"; beforeEach(function() { utils.beforeEach(this); @@ -65,10 +70,156 @@ describe("MatrixClient syncing", function() { }); }); + describe("resolving invites to profile info", function() { + var initialSync = { + end: "s_5_3", + presence: [], + rooms: [{ + membership: "join", + room_id: roomOne, + messages: { + start: "f_1_1", + end: "f_2_2", + chunk: [ + utils.mkMessage({ + room: roomOne, user: otherUserId, msg: "hello" + }) + ] + }, + state: [ + utils.mkMembership({ + room: roomOne, mship: "join", user: otherUserId + }), + utils.mkMembership({ + room: roomOne, mship: "join", user: selfUserId + }), + utils.mkEvent({ + type: "m.room.create", room: roomOne, user: selfUserId, + content: { + creator: selfUserId + } + }) + ] + }] + }; + var eventData = { + start: "s_5_3", + end: "e_6_7", + chunk: [] + }; + + beforeEach(function() { + eventData.chunk = []; + }); + + it("should resolve incoming invites from /events", function(done) { + eventData.chunk = [ + utils.mkMembership({ + room: roomOne, mship: "invite", user: userC + }) + ]; + + httpBackend.when("GET", "/initialSync").respond(200, initialSync); + httpBackend.when("GET", "/events").respond(200, eventData); + httpBackend.when("GET", "/profile/" + encodeURIComponent(userC)).respond( + 200, { + avatar_url: "mxc://flibble/wibble", + displayname: "The Boss" + } + ); + + client.startClient({ + resolveInvitesToProfiles: true + }); + + httpBackend.flush().done(function() { + var member = client.getRoom(roomOne).getMember(userC); + expect(member.name).toEqual("The Boss"); + expect( + member.getAvatarUrl("home.server.url", null, null, null, false) + ).toBeDefined(); + done(); + }); + }); + + it("should use cached values from m.presence wherever possible", function(done) { + eventData.chunk = [ + utils.mkPresence({ + user: userC, presence: "online", name: "The Ghost" + }), + utils.mkMembership({ + room: roomOne, mship: "invite", user: userC + }) + ]; + + httpBackend.when("GET", "/initialSync").respond(200, initialSync); + httpBackend.when("GET", "/events").respond(200, eventData); + + client.startClient({ + resolveInvitesToProfiles: true + }); + + httpBackend.flush().done(function() { + var member = client.getRoom(roomOne).getMember(userC); + expect(member.name).toEqual("The Ghost"); + done(); + }); + }); + + it("should result in events on the room member firing", function(done) { + eventData.chunk = [ + utils.mkPresence({ + user: userC, presence: "online", name: "The Ghost" + }), + utils.mkMembership({ + room: roomOne, mship: "invite", user: userC + }) + ]; + + httpBackend.when("GET", "/initialSync").respond(200, initialSync); + httpBackend.when("GET", "/events").respond(200, eventData); + + var latestFiredName = null; + client.on("RoomMember.name", function(event, m) { + if (m.userId === userC && m.roomId === roomOne) { + latestFiredName = m.name; + } + }); + + client.startClient({ + resolveInvitesToProfiles: true + }); + + httpBackend.flush().done(function() { + expect(latestFiredName).toEqual("The Ghost"); + done(); + }); + }); + + it("should no-op if resolveInvitesToProfiles is not set", function(done) { + eventData.chunk = [ + utils.mkMembership({ + room: roomOne, mship: "invite", user: userC + }) + ]; + + httpBackend.when("GET", "/initialSync").respond(200, initialSync); + httpBackend.when("GET", "/events").respond(200, eventData); + + client.startClient(); + + httpBackend.flush().done(function() { + var member = client.getRoom(roomOne).getMember(userC); + expect(member.name).toEqual(userC); + expect( + member.getAvatarUrl("home.server.url", null, null, null, false) + ).toBeNull(); + done(); + }); + }); + }); + describe("users", function() { - var userA = "@alice:bar"; - var userB = "@bob:bar"; - var userC = "@claire:bar"; var initialSync = { end: "s_5_3", presence: [ @@ -113,8 +264,6 @@ describe("MatrixClient syncing", function() { }); describe("room state", function() { - var roomOne = "!foo:localhost"; - var roomTwo = "!bar:localhost"; var msgText = "some text here"; var otherDisplayName = "Bob Smith"; var initialSync = { @@ -272,7 +421,6 @@ describe("MatrixClient syncing", function() { }); describe("receipts", function() { - var roomOne = "!foo:localhost"; var initialSync = { end: "s_5_3", presence: [], diff --git a/spec/mock-request.js b/spec/mock-request.js index 4e10f6275..9a541b757 100644 --- a/spec/mock-request.js +++ b/spec/mock-request.js @@ -13,6 +13,7 @@ function HttpBackend() { this.requestFn = function(opts, callback) { var realReq = new Request(opts.method, opts.uri, opts.body, opts.qs); realReq.callback = callback; + console.log("HTTP backend received request: %s %s", opts.method, opts.uri); self.requests.push(realReq); }; }