diff --git a/lib/client.js b/lib/client.js
index 76c6c6938..0d786e1c5 100644
--- a/lib/client.js
+++ b/lib/client.js
@@ -993,7 +993,23 @@ function createNewUser(client, userId) {
function createNewRoom(client, roomId) {
var room = new Room(roomId);
reEmit(client, room, ["Room.name", "Room.timeline"]);
- client.emit("Room", room);
+ client.emit("Room", room); // emit created room event
+
+ // we need to also re-emit room state and room member events, so hook it up
+ // to the client now. We need to add a listener for RoomState.members in
+ // order to hook them correctly. (TODO: find a better way?)
+ reEmit(client, room.currentState, [
+ "RoomState.events", "RoomState.members", "RoomState.newMember"
+ ]);
+ room.currentState.on("RoomState.newMember", function(event, state, member) {
+ reEmit(
+ client, member,
+ [
+ "RoomMember.name", "RoomMember.typing", "RoomMember.powerLevel",
+ "RoomMember.membership"
+ ]
+ );
+ });
return room;
}
diff --git a/lib/models/room-member.js b/lib/models/room-member.js
index b8d461bc5..795bed5b5 100644
--- a/lib/models/room-member.js
+++ b/lib/models/room-member.js
@@ -2,6 +2,8 @@
/**
* @module models/room-member
*/
+var EventEmitter = require("events").EventEmitter;
+
var utils = require("../utils");
/**
@@ -17,6 +19,7 @@ var utils = require("../utils");
* @prop {Number} powerLevelNorm The normalised power level (0-100) for this
* room member.
* @prop {User} user The User object for this room member, if one exists.
+ * @prop {string} membership The membership state for this room member e.g. 'join'.
* @throws If the event provided is not m.room.member
*/
function RoomMember(roomId, userId) {
@@ -27,103 +30,130 @@ function RoomMember(roomId, userId) {
this.powerLevel = 0;
this.powerLevelNorm = 0;
this.user = null;
+ this.membership = null;
}
-RoomMember.prototype = {
+utils.inherits(RoomMember, EventEmitter);
- /**
- * Update this room member's membership event. May fire "RoomMember.name" if
- * this event updates this member's name.
- * @param {MatrixEvent} event The m.room.member
event
- * @param {RoomState} roomState Optional. The room state to take into account
- * when calculating (e.g. for disambiguating users with the same name).
- * @fires module:client~MatrixClient#event:"RoomMember.name"
- */
- setMembershipEvent: function(event, roomState) {
- if (event.getType() !== "m.room.member") {
- return;
- }
- var displayName = event.getContent().displayname;
- var selfUserId = this.userId;
- if (!displayName) {
- this.name = selfUserId;
- return;
- }
- if (!roomState) {
- this.name = displayName;
- return;
- }
+/**
+ * Update this room member's membership event. May fire "RoomMember.name" if
+ * this event updates this member's name.
+ * @param {MatrixEvent} event The m.room.member
event
+ * @param {RoomState} roomState Optional. The room state to take into account
+ * when calculating (e.g. for disambiguating users with the same name).
+ * @fires module:client~MatrixClient#event:"RoomMember.name"
+ * @fires module:client~MatrixClient#event:"RoomMember.membership"
+ */
+RoomMember.prototype.setMembershipEvent = function(event, roomState) {
+ if (event.getType() !== "m.room.member") {
+ return;
+ }
+ var oldMembership = this.membership;
+ this.membership = event.getContent().membership;
- var stateEvents = utils.filter(
- roomState.getStateEvents("m.room.member"),
- function(e) {
- return e.getContent().displayname === displayName &&
- e.getSender() !== selfUserId;
- }
- );
- if (stateEvents.length > 1) {
- this.name = displayName + " (" + selfUserId + ")";
- return;
- }
+ var oldName = this.name;
+ this.name = calculateDisplayName(this, event, roomState);
- this.name = displayName;
- },
-
- /**
- * Update this room member's power level event. May fire
- * "RoomMember.powerLevel" if this event updates this member's power levels.
- * @param {MatrixEvent} powerLevelEvent The m.room.power_levels
- * event
- * @fires module:client~MatrixClient#event:"RoomMember.powerLevel"
- */
- setPowerLevelEvent: function(powerLevelEvent) {
- if (powerLevelEvent.getType() !== "m.room.power_levels") {
- return;
- }
- var maxLevel = powerLevelEvent.getContent().users_default || 0;
- utils.forEach(utils.values(powerLevelEvent.getContent().users), function(lvl) {
- maxLevel = Math.max(maxLevel, lvl);
- });
- this.powerLevel = (
- powerLevelEvent.getContent().users[this.userId] ||
- powerLevelEvent.getContent().users_default ||
- 0
- );
- this.powerLevelNorm = 0;
- if (maxLevel > 0) {
- this.powerLevelNorm = (this.powerLevel * 100) / maxLevel;
- }
- },
-
- /**
- * Update this room member's typing event. May fire "RoomMember.typing" if
- * this event changes this member's typing state.
- * @param {MatrixEvent} event The typing event
- * @fires module:client~MatrixClient#event:"RoomMember.typing"
- */
- setTypingEvent: function(event) {
- if (event.getType() !== "m.typing") {
- return;
- }
- this.typing = false;
- var typingList = event.getContent().user_ids;
- if (!utils.isArray(typingList)) {
- // malformed event :/ bail early. TODO: whine?
- return;
- }
- if (typingList.indexOf(this.userId) !== -1) {
- this.typing = true;
- }
- },
-
- /**
- * Get the membership state of this room member.
- * @return {string} The membership state e.g. 'join'.
- */
- getMembershipState: function() {
- return this.event.getContent().membership;
+ if (oldMembership !== this.membership) {
+ this.emit("RoomMember.membership", event, this);
+ }
+ if (oldName !== this.name) {
+ this.emit("RoomMember.name", event, this);
}
};
+/**
+ * Update this room member's power level event. May fire
+ * "RoomMember.powerLevel" if this event updates this member's power levels.
+ * @param {MatrixEvent} powerLevelEvent The m.room.power_levels
+ * event
+ * @fires module:client~MatrixClient#event:"RoomMember.powerLevel"
+ */
+RoomMember.prototype.setPowerLevelEvent = function(powerLevelEvent) {
+ if (powerLevelEvent.getType() !== "m.room.power_levels") {
+ return;
+ }
+ var maxLevel = powerLevelEvent.getContent().users_default || 0;
+ utils.forEach(utils.values(powerLevelEvent.getContent().users), function(lvl) {
+ maxLevel = Math.max(maxLevel, lvl);
+ });
+ var oldPowerLevel = this.powerLevel;
+ var oldPowerLevelNorm = this.powerLevelNorm;
+ this.powerLevel = (
+ powerLevelEvent.getContent().users[this.userId] ||
+ powerLevelEvent.getContent().users_default ||
+ 0
+ );
+ this.powerLevelNorm = 0;
+ if (maxLevel > 0) {
+ this.powerLevelNorm = (this.powerLevel * 100) / maxLevel;
+ }
+
+ // emit for changes in powerLevelNorm as well (since the app will need to
+ // redraw everyone's level if the max has changed)
+ if (oldPowerLevel !== this.powerLevel || oldPowerLevelNorm !== this.powerLevelNorm) {
+ this.emit("RoomMember.powerLevel", powerLevelEvent, this);
+ }
+};
+
+/**
+ * Update this room member's typing event. May fire "RoomMember.typing" if
+ * this event changes this member's typing state.
+ * @param {MatrixEvent} event The typing event
+ * @fires module:client~MatrixClient#event:"RoomMember.typing"
+ */
+RoomMember.prototype.setTypingEvent = function(event) {
+ if (event.getType() !== "m.typing") {
+ return;
+ }
+ var oldTyping = this.typing;
+ this.typing = false;
+ var typingList = event.getContent().user_ids;
+ if (!utils.isArray(typingList)) {
+ // malformed event :/ bail early. TODO: whine?
+ return;
+ }
+ if (typingList.indexOf(this.userId) !== -1) {
+ this.typing = true;
+ }
+ if (oldTyping !== this.typing) {
+ this.emit("RoomMember.typing", event, this);
+ }
+};
+
+/**
+ * Get the membership state of this room member.
+ * @return {string} The membership state e.g. 'join'.
+ */
+RoomMember.prototype.getMembershipState = function() {
+ return this.event.getContent().membership;
+};
+
+
+function calculateDisplayName(member, event, roomState) {
+ var displayName = event.getContent().displayname;
+ var selfUserId = member.userId;
+ if (!displayName) {
+ return selfUserId;
+ }
+ if (!roomState) {
+ return displayName;
+ }
+
+ var stateEvents = utils.filter(
+ roomState.getStateEvents("m.room.member"),
+ function(e) {
+ return e.getContent().displayname === displayName &&
+ e.getSender() !== selfUserId;
+ }
+ );
+ if (stateEvents.length > 1) {
+ // need to disambiguate
+ return displayName + " (" + selfUserId + ")";
+ }
+
+ return displayName;
+}
+
/**
* The RoomMember class.
*/
@@ -140,6 +170,17 @@ module.exports = RoomMember;
* });
*/
+/**
+ * Fires whenever any room member's membership state changes.
+ * @event module:client~MatrixClient#"RoomMember.membership"
+ * @param {MatrixEvent} event The matrix event which caused this event to fire.
+ * @param {RoomMember} member The member whose RoomMember.membership changed.
+ * @example
+ * matrixClient.on("RoomMember.membership", function(event, member){
+ * var newState = member.membership;
+ * });
+ */
+
/**
* Fires whenever any room member's typing state changes.
* @event module:client~MatrixClient#"RoomMember.typing"
diff --git a/lib/models/room-state.js b/lib/models/room-state.js
index 49309c883..d44a592e8 100644
--- a/lib/models/room-state.js
+++ b/lib/models/room-state.js
@@ -2,6 +2,8 @@
/**
* @module models/room-state
*/
+var EventEmitter = require("events").EventEmitter;
+
var utils = require("../utils");
var RoomMember = require("./room-member");
@@ -25,84 +27,90 @@ function RoomState(roomId) {
};
this.paginationToken = null;
}
-RoomState.prototype = {
- /**
- * Get all RoomMembers in this room.
- * @return {Array} A list of RoomMembers.
- */
- getMembers: function() {
- return utils.values(this.members);
- },
+utils.inherits(RoomState, EventEmitter);
- /**
- * Get state events from the state of the room.
- * @param {string} eventType The event type of the state event.
- * @param {string} stateKey Optional. The state_key of the state event. If
- * this is undefined
then all matching state events will be
- * returned.
- * @return {MatrixEvent[]|MatrixEvent} A list of events if state_key was
- * undefined
, else a single event (or null if no match found).
- */
- getStateEvents: function(eventType, stateKey) {
- if (!this.events[eventType]) {
- // no match
- return stateKey === undefined ? [] : null;
- }
- if (stateKey === undefined) { // return all values
- return utils.values(this.events[eventType]);
- }
- var event = this.events[eventType][stateKey];
- return event ? event : null;
- },
+/**
+ * Get all RoomMembers in this room.
+ * @return {Array} A list of RoomMembers.
+ */
+RoomState.prototype.getMembers = function() {
+ return utils.values(this.members);
+};
- /**
- * Add an array of one or more state MatrixEvents, overwriting
- * any existing state with the same {type, stateKey} tuple. Will fire
- * "RoomState.events" for every event added. May fire "RoomState.members"
- * if there are m.room.member
events.
- * @param {MatrixEvent[]} stateEvents a list of state events for this room.
- * @fires module:client~MatrixClient#event:"RoomState.members"
- * @fires module:client~MatrixClient#event:"RoomState.events"
- */
- setStateEvents: function(stateEvents) {
- var self = this;
- utils.forEach(stateEvents, function(event) {
- if (event.getRoomId() !== self.roomId) { return; }
- if (!event.isState()) { return; }
-
- if (self.events[event.getType()] === undefined) {
- self.events[event.getType()] = {};
- }
- self.events[event.getType()][event.getStateKey()] = event;
-
- if (event.getType() === "m.room.member") {
- var member = new RoomMember(event.getRoomId(), event.getSender());
- member.setMembershipEvent(event, self);
- // this member may have a power level already, so set it.
- var pwrLvlEvent = self.getStateEvents("m.room.power_levels", "");
- if (pwrLvlEvent) {
- member.setPowerLevelEvent(pwrLvlEvent);
- }
- self.members[event.getStateKey()] = member;
- }
- else if (event.getType() === "m.room.power_levels") {
- var members = utils.values(self.members);
- utils.forEach(members, function(member) {
- member.setPowerLevelEvent(event);
- });
- }
- });
- },
-
- /**
- * Set the current typing event for this room.
- * @param {MatrixEvent} event The typing event
- */
- setTypingEvent: function(event) {
- utils.forEach(utils.values(this.members), function(member) {
- member.setTypingEvent(event);
- });
+/**
+ * Get state events from the state of the room.
+ * @param {string} eventType The event type of the state event.
+ * @param {string} stateKey Optional. The state_key of the state event. If
+ * this is undefined
then all matching state events will be
+ * returned.
+ * @return {MatrixEvent[]|MatrixEvent} A list of events if state_key was
+ * undefined
, else a single event (or null if no match found).
+ */
+RoomState.prototype.getStateEvents = function(eventType, stateKey) {
+ if (!this.events[eventType]) {
+ // no match
+ return stateKey === undefined ? [] : null;
}
+ if (stateKey === undefined) { // return all values
+ return utils.values(this.events[eventType]);
+ }
+ var event = this.events[eventType][stateKey];
+ return event ? event : null;
+};
+
+/**
+ * Add an array of one or more state MatrixEvents, overwriting
+ * any existing state with the same {type, stateKey} tuple. Will fire
+ * "RoomState.events" for every event added. May fire "RoomState.members"
+ * if there are m.room.member
events.
+ * @param {MatrixEvent[]} stateEvents a list of state events for this room.
+ * @fires module:client~MatrixClient#event:"RoomState.members"
+ * @fires module:client~MatrixClient#event:"RoomState.events"
+ */
+RoomState.prototype.setStateEvents = function(stateEvents) {
+ var self = this;
+ utils.forEach(stateEvents, function(event) {
+ if (event.getRoomId() !== self.roomId) { return; }
+ if (!event.isState()) { return; }
+
+ if (self.events[event.getType()] === undefined) {
+ self.events[event.getType()] = {};
+ }
+ self.events[event.getType()][event.getStateKey()] = event;
+ self.emit("RoomState.events", event, self);
+
+ if (event.getType() === "m.room.member") {
+ var member = self.members[event.getStateKey()];
+ if (!member) {
+ member = new RoomMember(event.getRoomId(), event.getSender());
+ self.emit("RoomState.newMember", event, self, member);
+ }
+ member.setMembershipEvent(event, self);
+ // this member may have a power level already, so set it.
+ var pwrLvlEvent = self.getStateEvents("m.room.power_levels", "");
+ if (pwrLvlEvent) {
+ member.setPowerLevelEvent(pwrLvlEvent);
+ }
+ self.members[event.getStateKey()] = member;
+ self.emit("RoomState.members", event, self, member);
+ }
+ else if (event.getType() === "m.room.power_levels") {
+ var members = utils.values(self.members);
+ utils.forEach(members, function(member) {
+ member.setPowerLevelEvent(event);
+ });
+ }
+ });
+};
+
+/**
+ * Set the current typing event for this room.
+ * @param {MatrixEvent} event The typing event
+ */
+RoomState.prototype.setTypingEvent = function(event) {
+ utils.forEach(utils.values(this.members), function(member) {
+ member.setTypingEvent(event);
+ });
};
/**
@@ -123,7 +131,7 @@ module.exports = RoomState;
*/
/**
- * Fires whenever the member dictionary in room state is updated.
+ * Fires whenever a member in the members dictionary is updated in any way.
* @event module:client~MatrixClient#"RoomState.members"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {RoomState} state The room state whose RoomState.members dictionary
@@ -134,3 +142,17 @@ module.exports = RoomState;
* var newMembershipState = member.getMembershipState();
* });
*/
+
+ /**
+ * Fires whenever a member is added to the members dictionary. The RoomMember
+ * will not be fully populated yet (e.g. no membership state).
+ * @event module:client~MatrixClient#"RoomState.newMember"
+ * @param {MatrixEvent} event The matrix event which caused this event to fire.
+ * @param {RoomState} state The room state whose RoomState.members dictionary
+ * was updated with a new entry.
+ * @param {RoomMember} member The room member that was added.
+ * @example
+ * matrixClient.on("RoomState.newMember", function(event, state, member){
+ * // add event listeners on 'member'
+ * });
+ */
diff --git a/spec/integ/matrix-client.spec.js b/spec/integ/matrix-client.spec.js
index 5f1245f66..62e3df71b 100644
--- a/spec/integ/matrix-client.spec.js
+++ b/spec/integ/matrix-client.spec.js
@@ -86,7 +86,10 @@ describe("MatrixClient", function() {
]
},
state: [
- utils.mkMembership("!erufh:bar", "join", "@foo:bar")
+ utils.mkMembership("!erufh:bar", "join", "@foo:bar"),
+ utils.mkEvent("m.room.create", "!erufh:bar", "@foo:bar", {
+ creator: "@foo:bar"
+ })
]
}]
};
@@ -95,7 +98,10 @@ describe("MatrixClient", function() {
end: "e_6_7",
chunk: [
utils.mkMessage("!erufh:bar", "@foo:bar", "ello ello"),
- utils.mkMessage("!erufh:bar", "@foo:bar", ":D")
+ utils.mkMessage("!erufh:bar", "@foo:bar", ":D"),
+ utils.mkEvent("m.typing", "!erufh:bar", "bar", {
+ user_ids: ["@foo:bar"]
+ })
]
};
@@ -107,7 +113,7 @@ describe("MatrixClient", function() {
// that should be emitted and we'll just pick them off one by one,
// so long as this is emptied we're good.
var initialSyncEventTypes = [
- "m.presence", "m.room.member", "m.room.message"
+ "m.presence", "m.room.member", "m.room.message", "m.room.create"
];
var chunkIndex = 0;
client.on("event", function(event) {
@@ -171,11 +177,11 @@ describe("MatrixClient", function() {
it("should emit Room events", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
- var firedRoom = false;
- var firedName = false;
+ var roomInvokeCount = 0;
+ var roomNameInvokeCount = 0;
var timelineFireCount = 0;
client.on("Room", function(room) {
- firedRoom = true;
+ roomInvokeCount++;
expect(room.roomId).toEqual("!erufh:bar");
});
client.on("Room.timeline", function(event, room) {
@@ -183,20 +189,115 @@ describe("MatrixClient", function() {
expect(room.roomId).toEqual("!erufh:bar");
});
client.on("Room.name", function(room) {
- firedName = true;
+ roomNameInvokeCount++;
});
client.startClient();
httpBackend.flush().done(function() {
- expect(firedRoom).toBe(true, "Room didn't fire.");
- expect(firedName).toBe(true, "Room.name didn't fire.");
+ expect(roomInvokeCount).toEqual(
+ 1, "Room fired wrong number of times."
+ );
+ expect(roomNameInvokeCount).toEqual(
+ 1, "Room.name fired wrong number of times."
+ );
expect(timelineFireCount).toEqual(
3, "Room.timeline fired the wrong number of times"
);
done();
});
});
+
+ it("should emit RoomState events", function(done) {
+ httpBackend.when("GET", "/initialSync").respond(200, initialSync);
+ httpBackend.when("GET", "/events").respond(200, eventData);
+
+ var roomStateEventTypes = [
+ "m.room.member", "m.room.create"
+ ];
+ var eventsInvokeCount = 0;
+ var membersInvokeCount = 0;
+ var newMemberInvokeCount = 0;
+ client.on("RoomState.events", function(event, state) {
+ eventsInvokeCount++;
+ var index = roomStateEventTypes.indexOf(event.getType());
+ expect(index).not.toEqual(
+ -1, "Unexpected room state event type: " + event.getType()
+ );
+ if (index >= 0) {
+ roomStateEventTypes.splice(index, 1);
+ }
+ });
+ client.on("RoomState.members", function(event, state, member) {
+ membersInvokeCount++;
+ expect(member.roomId).toEqual("!erufh:bar");
+ expect(member.userId).toEqual("@foo:bar");
+ expect(member.membership).toEqual("join");
+ });
+ client.on("RoomState.newMember", function(event, state, member) {
+ newMemberInvokeCount++;
+ expect(member.roomId).toEqual("!erufh:bar");
+ expect(member.userId).toEqual("@foo:bar");
+ expect(member.membership).toBeFalsy();
+ });
+
+ client.startClient();
+
+ httpBackend.flush().done(function() {
+ expect(membersInvokeCount).toEqual(
+ 1, "RoomState.members fired wrong number of times"
+ );
+ expect(newMemberInvokeCount).toEqual(
+ 1, "RoomState.newMember fired wrong number of times"
+ );
+ expect(eventsInvokeCount).toEqual(
+ 2, "RoomState.events fired wrong number of times"
+ );
+ done();
+ });
+ });
+
+ it("should emit RoomMember events", function(done) {
+ httpBackend.when("GET", "/initialSync").respond(200, initialSync);
+ httpBackend.when("GET", "/events").respond(200, eventData);
+
+ var typingInvokeCount = 0;
+ var powerLevelInvokeCount = 0;
+ var nameInvokeCount = 0;
+ var membershipInvokeCount = 0;
+ client.on("RoomMember.name", function(event, member) {
+ nameInvokeCount++;
+ });
+ client.on("RoomMember.typing", function(event, member) {
+ typingInvokeCount++;
+ expect(member.typing).toBe(true);
+ });
+ client.on("RoomMember.powerLevel", function(event, member) {
+ powerLevelInvokeCount++;
+ });
+ client.on("RoomMember.membership", function(event, member) {
+ membershipInvokeCount++;
+ expect(member.membership).toEqual("join");
+ });
+
+ client.startClient();
+
+ httpBackend.flush().done(function() {
+ expect(typingInvokeCount).toEqual(
+ 1, "RoomMember.typing fired wrong number of times"
+ );
+ expect(powerLevelInvokeCount).toEqual(
+ 0, "RoomMember.powerLevel fired wrong number of times"
+ );
+ expect(nameInvokeCount).toEqual(
+ 0, "RoomMember.name fired wrong number of times"
+ );
+ expect(membershipInvokeCount).toEqual(
+ 1, "RoomMember.membership fired wrong number of times"
+ );
+ done();
+ });
+ });
});
describe("room state", function() {