diff --git a/lib/client.js b/lib/client.js index 699d0bcda..61b4c0121 100644 --- a/lib/client.js +++ b/lib/client.js @@ -594,24 +594,32 @@ module.exports.MatrixClient.prototype = { }); } if (self.store) { - for (var i = 0; i < events.length; i++) { + // bucket events based on room. + var i = 0; + var roomIdToEvents = {}; + for (i = 0; i < events.length; i++) { var roomId = events[i].getRoomId(); // possible to have no room ID e.g. for presence events. if (roomId) { - var room = self.store.getRoom(roomId); - if (!room) { - // TODO: whine about this. We got an event for a room - // we don't know about (we should really be doing a - // roomInitialSync at this point to pull in state). - room = new Room(roomId); - } - room.addEventsToTimeline([events[i]]); - if (events[i].isState()) { - room.currentState.setStateEvents([events[i]]); - room.recalculate(self.credentials.userId); + if (!roomIdToEvents[roomId]) { + roomIdToEvents[roomId] = []; } + roomIdToEvents[roomId].push(events[i]); } } + // add events to room + var roomIds = utils.keys(roomIdToEvents); + for (i = 0; i < roomIds.length; i++) { + var room = self.store.getRoom(roomIds[i]); + if (!room) { + // TODO: whine about this. We got an event for a room + // we don't know about (we should really be doing a + // roomInitialSync at this point to pull in state). + room = new Room(roomIds[i]); + } + room.addEvents(roomIdToEvents[roomIds[i]]); + room.recalculate(self.credentials.userId); + } } if (data) { self.fromToken = data.end; diff --git a/lib/models/room-state.js b/lib/models/room-state.js index c7e1e2e63..a8c112031 100644 --- a/lib/models/room-state.js +++ b/lib/models/room-state.js @@ -77,6 +77,37 @@ RoomState.prototype = { this.members[event.getStateKey()] = member; } } + }, + + /** + * Set the current typing event for this room. + * @param {MatrixEvent} event The typing event + * @throws If the provided event type isn't 'm.typing'. + */ + setTypingEvent: function(event) { + if (event.getType() !== "m.typing") { + throw new Error("Not a typing event -> " + event.getType()); + } + // typing events clobber and specify only those who are typing, so + // reset all users to say they are not typing then selectively set + // the specified users to be typing. + var self = this; + var members = utils.values(this.members); + utils.forEach(members, function(member) { + member.typing = false; + }); + var typingList = event.getContent().user_ids; + if (!utils.isArray(typingList)) { + // malformed event :/ bail early. TODO: whine? + return; + } + utils.forEach(typingList, function(userId) { + if (!self.members[userId]) { + // user_id in typing list but not member list, TODO: whine? + return; + } + self.members[userId].typing = true; + }); } }; diff --git a/lib/models/room.js b/lib/models/room.js index e89260e70..72ef8e840 100644 --- a/lib/models/room.js +++ b/lib/models/room.js @@ -29,6 +29,19 @@ function Room(roomId) { this.summary = null; } Room.prototype = { + /** + * Get a member from the current room state. + * @param {string} userId The user ID of the member. + * @return {RoomMember} The member or null. + */ + getMember: function(userId) { + var member = this.currentState.members[userId]; + if (!member) { + return null; + } + return member; + }, + /** * Add some events to this room's timeline. * @param {MatrixEvent[]} events A list of events to add. @@ -47,6 +60,28 @@ Room.prototype = { } }, + /** + * Add some events to this room. This can include state events, message + * events and typing notifications. These events are treated as "live" so + * they will go to the end of the timeline. + * @param {MatrixEvent[]} events A list of events to add. + */ + addEvents: function(events) { + for (var i = 0; i < events.length; i++) { + if (events[i].getType() === "m.typing") { + this.currentState.setTypingEvent(events[i]); + } + else { + // TODO: We should have a filter to say "only add state event + // types X Y Z to the timeline". + this.addEventsToTimeline([events[i]]); + if (events[i].isState()) { + this.currentState.setStateEvents([events[i]]); + } + } + } + }, + /** * Recalculate various aspects of the room, including the room name and * room summary. Call this any time the room's current state is modified. diff --git a/lib/utils.js b/lib/utils.js index b048f6581..48db9ed8b 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -99,6 +99,18 @@ module.exports.values = function(obj) { return values; }; +/** + * Invoke a function for each item in the array. + * @param {Array} array The array. + * @param {Function} fn The function to invoke for each element. Has the + * function signature fn(element, index). + */ +module.exports.forEach = function(array, fn) { + for (var i = 0; i < array.length; i++) { + fn(array[i], i); + } +}; + /** * Checks if the given thing is a function. * @param {*} value The thing to check. diff --git a/spec/integ/matrix-client.spec.js b/spec/integ/matrix-client.spec.js index 3d3d54903..bdee5eb83 100644 --- a/spec/integ/matrix-client.spec.js +++ b/spec/integ/matrix-client.spec.js @@ -131,7 +131,10 @@ describe("MatrixClient", function() { utils.mkEvent("m.room.name", roomOne, selfUserId, { name: "A new room name" }), - utils.mkMessage(roomTwo, otherUserId, msgText) + utils.mkMessage(roomTwo, otherUserId, msgText), + utils.mkEvent("m.typing", roomTwo, undefined, { + user_ids: [otherUserId] + }) ] }; @@ -176,6 +179,24 @@ describe("MatrixClient", function() { done(); }); }); + + it("should set the right user's typing flag.", function(done) { + httpBackend.when("GET", "/initialSync").respond(200, initialSync); + httpBackend.when("GET", "/events").respond(200, eventData); + + client.startClient(function(err, data, isLive) {}); + + httpBackend.flush().done(function() { + var room = client.getStore().getRoom(roomTwo); + var member = room.getMember(otherUserId); + expect(member).toBeDefined(); + expect(member.typing).toEqual(true); + member = room.getMember(selfUserId); + expect(member).toBeDefined(); + expect(member.typing).toEqual(false); + done(); + }); + }); }); });