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