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);
};
}