Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7cad5a0479 | |||
| 83c53f6a79 | |||
| ae13ed7ded | |||
| b17385120a | |||
| cc0d8da416 | |||
| c796702eba | |||
| 2675442ced | |||
| aa3e6514c6 | |||
| be6d64fbfd | |||
| 4cbab72369 | |||
| 0227b1c68d | |||
| 4c051202af | |||
| 981b9e0595 | |||
| 9e719ba31e | |||
| c65f576f8d | |||
| 2c805bbece | |||
| 02b836698c | |||
| 25112ede58 | |||
| 5888c8a56c | |||
| 1cee7bf397 | |||
| cab7a71a94 | |||
| d7c63e3487 | |||
| bff749fd50 | |||
| 5c286352cb | |||
| 9ec3504c72 | |||
| 26b3e32ca2 | |||
| 4e2c83cc08 | |||
| 17def14eba | |||
| f260de573b | |||
| 4fd45ab278 | |||
| 4a2e9eb927 | |||
| dd8adef9ed | |||
| 9164debf03 | |||
| 534bef8632 | |||
| d8c43d02ba | |||
| ae3738f822 | |||
| be621e1aa7 | |||
| 343d63a28a | |||
| 0a28d6e950 | |||
| b493a62afa | |||
| 37a8c9bd72 | |||
| a9c4345159 | |||
| 5f1153b43f | |||
| 2c213f88d9 | |||
| a236219111 | |||
| 2f9958cca9 | |||
| f26154d0ac | |||
| 5ae87b7c95 | |||
| 219103a4e2 | |||
| 4ec7b9bb3f | |||
| bad8b7fb76 | |||
| a101857cb6 | |||
| a52f92830a | |||
| 40d113a423 | |||
| 7ec8421d19 | |||
| 9048efeb65 | |||
| 43fc200dae | |||
| 6679e93afc |
@@ -5,7 +5,7 @@
|
||||
"nonew": true,
|
||||
"curly": true,
|
||||
"forin": true,
|
||||
"freeze": true,
|
||||
"freeze": false,
|
||||
"undef": true,
|
||||
"unused": "vars"
|
||||
}
|
||||
|
||||
+60
-7
@@ -1,3 +1,56 @@
|
||||
Changes in 0.3.0
|
||||
================
|
||||
|
||||
**BREAKING CHANGES**:
|
||||
* `MatrixClient.getAvatarUrlForMember` has been removed and replaced with
|
||||
`RoomMember.getAvatarUrl`. Arguments remain the same except the homeserver
|
||||
URL must now be supplied from `MatrixClient.getHomeserverUrl()`.
|
||||
|
||||
```javascript
|
||||
// before
|
||||
var url = client.getAvatarUrlForMember(member, width, height, resize, allowDefault)
|
||||
// after
|
||||
var url = member.getAvatarUrl(client.getHomeserverUrl(), width, height, resize, allowDefault)
|
||||
```
|
||||
* `MatrixClient.getAvatarUrlForRoom` has been removed and replaced with
|
||||
`Room.getAvatarUrl`. Arguments remain the same except the homeserver
|
||||
URL must now be supplied from `MatrixClient.getHomeserverUrl()`.
|
||||
|
||||
```javascript
|
||||
// before
|
||||
var url = client.getAvatarUrlForRoom(room, width, height, resize, allowDefault)
|
||||
// after
|
||||
var url = room.getAvatarUrl(client.getHomeserverUrl(), width, height, resize, allowDefault)
|
||||
```
|
||||
|
||||
* `s/Room.getMembersWithMemership/Room.getMembersWithMem`b`ership/g`
|
||||
|
||||
New methods:
|
||||
* Added support for sending receipts via
|
||||
`MatrixClient.sendReceipt(event, receiptType, callback)` and
|
||||
`MatrixClient.sendReadReceipt(event, callback)`.
|
||||
* Added support for receiving receipts via
|
||||
`Room.getReceiptsForEvent(event)` and `Room.getUsersReadUpTo(event)`. Receipts
|
||||
can be directly added to a `Room` using `Room.addReceipt(event)` though the
|
||||
`MatrixClient` does this for you.
|
||||
* Added support for muting local video and audio via the new methods
|
||||
`MatrixCall.setMicrophoneMuted()`, `MatrixCall.isMicrophoneMuted(muted)`,
|
||||
`MatrixCall.isLocalVideoMuted()` and `Matrix.setLocalVideoMuted(muted)`.
|
||||
* Added **experimental** support for screen-sharing in Chrome via
|
||||
`MatrixCall.placeScreenSharingCall(remoteVideoElement, localVideoElement)`.
|
||||
* Added ability to perform server-side searches using
|
||||
`MatrixClient.searchMessageText(opts)` and `MatrixClient.search(opts)`.
|
||||
|
||||
Improvements:
|
||||
* Improve the performance of initial sync processing from `O(n^2)` to `O(n)`.
|
||||
* `Room.name` will now take into account `m.room.canonical_alias` events.
|
||||
* `MatrixClient.startClient` now takes an Object `opts` rather than a Number in
|
||||
a backwards-compatible way. This `opts` allows syncing configuration options
|
||||
to be specified including `includeArchivedRooms` and `resolveInvitesToProfiles`.
|
||||
* `Room` objects which represent room invitations will now have state populated
|
||||
from `invite_room_state` if it is included in the `m.room.member` event.
|
||||
* `Room.getAvatarUrl` will now take into account `m.room.avatar` events.
|
||||
|
||||
Changes in 0.2.2
|
||||
================
|
||||
|
||||
@@ -22,6 +75,10 @@ New methods:
|
||||
Changes in 0.2.1
|
||||
================
|
||||
|
||||
**BREAKING CHANGES**
|
||||
* `MatrixClient.joinRoom` has changed from `(roomIdOrAlias, callback)` to
|
||||
`(roomIdOrAlias, opts, callback)`.
|
||||
|
||||
Bug fixes:
|
||||
* The `Content-Type` of file uploads is now explicitly set, without relying
|
||||
on the browser to do it for us.
|
||||
@@ -33,10 +90,6 @@ Improvements:
|
||||
* There is now a try/catch block around the `request` function which will
|
||||
reject/errback appropriately if an exception is thrown synchronously in it.
|
||||
|
||||
Breaking changes:
|
||||
* `MatrixClient.joinRoom` has changed from `(roomIdOrAlias, callback)` to
|
||||
`(roomIdOrAlias, opts, callback)`.
|
||||
|
||||
New methods:
|
||||
* `MatrixClient.createAlias(alias, roomId)`
|
||||
* `MatrixClient.getRoomIdForAlias(alias)`
|
||||
@@ -57,7 +110,7 @@ Modified methods:
|
||||
Changes in 0.2.0
|
||||
================
|
||||
|
||||
Breaking changes:
|
||||
**BREAKING CHANGES**:
|
||||
* `MatrixClient.setPowerLevel` now expects a `MatrixEvent` and not an `Object`
|
||||
for the `event` parameter.
|
||||
|
||||
@@ -88,7 +141,7 @@ New methods:
|
||||
* `MatrixClient.mxcUrlToHttp(url, w, h, method)`
|
||||
* `MatrixClient.getAvatarUrlForRoom(room, w, h, method)`
|
||||
* `MatrixClient.uploadContent(file, callback)`
|
||||
* `Room.getMembersWithMemership(membership)`
|
||||
* `Room.getMembersWithMembership(membership)`
|
||||
* `MatrixScheduler.getQueueForEvent(event)`
|
||||
* `MatrixScheduler.removeEventFromQueue(event)`
|
||||
* `$DATA_STORE.setSyncToken(token)`
|
||||
@@ -123,7 +176,7 @@ Bug fixes:
|
||||
Changes in 0.1.1
|
||||
================
|
||||
|
||||
Breaking changes:
|
||||
**BREAKING CHANGES**:
|
||||
* `Room.calculateRoomName` is now private. Use `Room.recalculate` instead, and
|
||||
access the calculated name via `Room.name`.
|
||||
* `new MatrixClient(...)` no longer creates a `MatrixInMemoryStore` if
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
var matrixcs = require("./lib/matrix");
|
||||
matrixcs.request(require("request"));
|
||||
module.exports = matrixcs;
|
||||
|
||||
var utils = require("./lib/utils");
|
||||
utils.runPolyfills();
|
||||
|
||||
+190
-79
@@ -17,6 +17,7 @@ var Room = require("./models/room");
|
||||
var User = require("./models/user");
|
||||
var webRtcCall = require("./webrtc/call");
|
||||
var utils = require("./utils");
|
||||
var contentRepo = require("./content-repo");
|
||||
|
||||
var CRYPTO_ENABLED = false;
|
||||
|
||||
@@ -140,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.
|
||||
@@ -1124,6 +1126,37 @@ MatrixClient.prototype.sendHtmlNotice = function(roomId, body, htmlBody, callbac
|
||||
return this.sendMessage(roomId, content, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Send a receipt.
|
||||
* @param {Event} event The event being acknowledged
|
||||
* @param {string} receiptType The kind of receipt e.g. "m.read"
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.sendReceipt = function(event, receiptType, callback) {
|
||||
var path = utils.encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
|
||||
$roomId: event.getRoomId(),
|
||||
$receiptType: receiptType,
|
||||
$eventId: event.getId()
|
||||
});
|
||||
return this._http.authedRequestWithPrefix(
|
||||
callback, "POST", path, undefined, {}, httpApi.PREFIX_V2_ALPHA
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Send a read receipt.
|
||||
* @param {Event} event The event that has been read.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.sendReadReceipt = function(event, callback) {
|
||||
return this.sendReceipt(event, "m.read", callback);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Upload a file to the media repository on the home server.
|
||||
* @param {File} file object
|
||||
@@ -1403,67 +1436,6 @@ MatrixClient.prototype.setAvatarUrl = function(url, callback) {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the avatar URL for a room member. <strong>This method is experimental and
|
||||
* may change.</strong>
|
||||
* @param {module:room-member.RoomMember} member
|
||||
* @param {Number} width The desired width of the thumbnail.
|
||||
* @param {Number} height The desired height of the thumbnail.
|
||||
* @param {string} resizeMethod The thumbnail resize method to use, either
|
||||
* "crop" or "scale".
|
||||
* @param {Boolean} allowDefault (optional) Passing false causes this method to
|
||||
* return null if the user has no avatar image. Otherwise, a default image URL
|
||||
* will be returned.
|
||||
* @return {?string} the avatar URL or null.
|
||||
*/
|
||||
MatrixClient.prototype.getAvatarUrlForMember =
|
||||
function(member, width, height, resizeMethod, allowDefault) {
|
||||
if (!member || !member.events.member) {
|
||||
return null;
|
||||
}
|
||||
if (allowDefault === undefined) { allowDefault = true; }
|
||||
var rawUrl = member.events.member.getContent().avatar_url;
|
||||
if (rawUrl) {
|
||||
return this._http.getHttpUriForMxc(rawUrl, width, height, resizeMethod);
|
||||
} else if (allowDefault) {
|
||||
return this._http.getIdenticonUri(member.userId, width, height);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the avatar URL for a room. <strong>This method is experimental and
|
||||
* may change.</strong>
|
||||
* @param {module:room.Room} room
|
||||
* @param {Number} width The desired width of the thumbnail.
|
||||
* @param {Number} height The desired height of the thumbnail.
|
||||
* @param {string} resizeMethod The thumbnail resize method to use, either
|
||||
* "crop" or "scale".
|
||||
* @param {Boolean} allowDefault (optional) Passing false causes this method to
|
||||
* return null if the user has no avatar image. Otherwise, a default image URL
|
||||
* will be returned.
|
||||
* @return {?string} the avatar URL or null.
|
||||
*/
|
||||
MatrixClient.prototype.getAvatarUrlForRoom =
|
||||
function(room, width, height, resizeMethod, allowDefault) {
|
||||
|
||||
if (!room || !room.currentState || !room.currentState.members) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var userId = this.credentials.userId;
|
||||
var members = utils.filter(room.currentState.getMembers(), function(m) {
|
||||
return (m.membership === "join" && m.userId !== userId);
|
||||
});
|
||||
|
||||
if (members[0]) {
|
||||
return this.getAvatarUrlForMember(
|
||||
members[0], width, height, resizeMethod, allowDefault
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Turn an MXC URL into an HTTP one. <strong>This method is experimental and
|
||||
* may change.</strong>
|
||||
@@ -1476,7 +1448,9 @@ MatrixClient.prototype.getAvatarUrlForRoom =
|
||||
*/
|
||||
MatrixClient.prototype.mxcUrlToHttp =
|
||||
function(mxcUrl, width, height, resizeMethod) {
|
||||
return this._http.getHttpUriForMxc(mxcUrl, width, height, resizeMethod);
|
||||
return contentRepo.getHttpUriForMxc(
|
||||
this.baseUrl, mxcUrl, width, height, resizeMethod
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1810,6 +1784,44 @@ MatrixClient.prototype.deletePushRule = function(scope, kind, ruleId, callback)
|
||||
return this._http.authedRequest(callback, "DELETE", path);
|
||||
};
|
||||
|
||||
/**
|
||||
* Perform a server-side search for messages containing the given text.
|
||||
* @param {Object} opts Options for the search.
|
||||
* @param {string} opts.query The text to query.
|
||||
* @param {string=} opts.keys The keys to search on. Defaults to all keys. One
|
||||
* of "content.body", "content.name", "content.topic".
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.searchMessageText = function(opts, callback) {
|
||||
return this.search({
|
||||
body: {
|
||||
search_categories: {
|
||||
room_events: {
|
||||
keys: opts.keys,
|
||||
search_term: opts.query
|
||||
}
|
||||
}
|
||||
}
|
||||
}, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Perform a server-side search.
|
||||
* @param {Object} opts
|
||||
* @param {Object} opts.body the JSON object to pass to the request body.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.search = function(opts, callback) {
|
||||
return this._http.authedRequest(
|
||||
callback, "POST", "/search", undefined, opts.body
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// VoIP operations
|
||||
// ===============
|
||||
|
||||
@@ -1857,10 +1869,15 @@ MatrixClient.prototype.isLoggedIn = function() {
|
||||
* This is an internal method.
|
||||
* @param {MatrixClient} client
|
||||
* @param {integer} historyLen
|
||||
* @param {integer} includeArchived
|
||||
*/
|
||||
function doInitialSync(client, historyLen) {
|
||||
function doInitialSync(client, historyLen, includeArchived) {
|
||||
var qps = { limit: historyLen };
|
||||
if (includeArchived) {
|
||||
qps.archived = true;
|
||||
}
|
||||
client._http.authedRequest(
|
||||
undefined, "GET", "/initialSync", { limit: (historyLen || 12) }
|
||||
undefined, "GET", "/initialSync", qps
|
||||
).done(function(data) {
|
||||
var i, j;
|
||||
// intercept the results and put them into our store
|
||||
@@ -1872,29 +1889,51 @@ function doInitialSync(client, historyLen) {
|
||||
user.setPresenceEvent(e);
|
||||
client.store.storeUser(user);
|
||||
});
|
||||
|
||||
// group receipts by room ID.
|
||||
var receiptsByRoom = {};
|
||||
data.receipts = data.receipts || [];
|
||||
utils.forEach(data.receipts.map(_PojoToMatrixEventMapper(client)),
|
||||
function(receiptEvent) {
|
||||
if (!receiptsByRoom[receiptEvent.getRoomId()]) {
|
||||
receiptsByRoom[receiptEvent.getRoomId()] = [];
|
||||
}
|
||||
receiptsByRoom[receiptEvent.getRoomId()].push(receiptEvent);
|
||||
}
|
||||
);
|
||||
|
||||
for (i = 0; i < data.rooms.length; i++) {
|
||||
var room = createNewRoom(client, data.rooms[i].room_id);
|
||||
if (!data.rooms[i].state) {
|
||||
data.rooms[i].state = [];
|
||||
}
|
||||
if (data.rooms[i].membership === "invite") {
|
||||
// create fake invite state event (v1 sucks)
|
||||
data.rooms[i].state.push({
|
||||
event_id: "$fake_" + room.roomId,
|
||||
content: {
|
||||
membership: "invite"
|
||||
},
|
||||
state_key: client.credentials.userId,
|
||||
user_id: data.rooms[i].inviter,
|
||||
room_id: room.roomId,
|
||||
type: "m.room.member"
|
||||
});
|
||||
var inviteEvent = data.rooms[i].invite;
|
||||
if (!inviteEvent) {
|
||||
// fallback for servers which don't serve the invite key yet
|
||||
inviteEvent = {
|
||||
event_id: "$fake_" + room.roomId,
|
||||
content: {
|
||||
membership: "invite"
|
||||
},
|
||||
state_key: client.credentials.userId,
|
||||
user_id: data.rooms[i].inviter,
|
||||
room_id: room.roomId,
|
||||
type: "m.room.member"
|
||||
};
|
||||
}
|
||||
data.rooms[i].state.push(inviteEvent);
|
||||
}
|
||||
|
||||
_processRoomEvents(
|
||||
client, room, data.rooms[i].state, data.rooms[i].messages
|
||||
);
|
||||
|
||||
var receipts = receiptsByRoom[room.roomId] || [];
|
||||
for (j = 0; j < receipts.length; j++) {
|
||||
room.addReceipt(receipts[j]);
|
||||
}
|
||||
|
||||
// cache the name/summary/etc prior to storage since we don't
|
||||
// know how the store will serialise the Room.
|
||||
room.recalculate(client.credentials.userId);
|
||||
@@ -1944,14 +1983,32 @@ function doInitialSync(client, historyLen) {
|
||||
* and then start polling the eventStream for new events. To listen for these
|
||||
* events, add a listener for {@link module:client~MatrixClient#event:"event"}
|
||||
* via {@link module:client~MatrixClient#on}.
|
||||
* @param {Number} historyLen amount of historical timeline events to
|
||||
* emit during from the initial sync. Default: 12.
|
||||
* @param {Object} opts Options to apply when syncing.
|
||||
* @param {Number} opts.initialSyncLimit The event <code>limit=</code> to apply
|
||||
* to initial sync. Default: 8.
|
||||
* @param {Boolean} opts.includeArchivedRooms True to put <code>archived=true</code>
|
||||
* on the <code>/initialSync</code> request. Default: false.
|
||||
* @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.
|
||||
*/
|
||||
MatrixClient.prototype.startClient = function(historyLen) {
|
||||
MatrixClient.prototype.startClient = function(opts) {
|
||||
if (this.clientRunning) {
|
||||
// client is already running.
|
||||
return;
|
||||
}
|
||||
// backwards compat for when 'opts' was 'historyLen'.
|
||||
if (typeof opts === "number") {
|
||||
opts = {
|
||||
initialSyncLimit: 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);
|
||||
@@ -1969,7 +2026,7 @@ MatrixClient.prototype.startClient = function(historyLen) {
|
||||
var self = this;
|
||||
this.pushRules().done(function(result) {
|
||||
self.pushRules = result;
|
||||
doInitialSync(self, historyLen);
|
||||
doInitialSync(self, opts.initialSyncLimit, opts.includeArchivedRooms);
|
||||
}, function(err) {
|
||||
self.emit("syncError", err);
|
||||
});
|
||||
@@ -2006,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 = {};
|
||||
@@ -2017,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);
|
||||
@@ -2030,6 +2092,7 @@ function _pollForEvents(client) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add events to room
|
||||
var roomIds = utils.keys(roomIdToEvents);
|
||||
utils.forEach(roomIds, function(roomId) {
|
||||
@@ -2064,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);
|
||||
@@ -2122,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
|
||||
@@ -2166,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]
|
||||
@@ -2177,6 +2287,7 @@ function setupCallEventHandler(client) {
|
||||
var content = event.getContent();
|
||||
var call = content.call_id ? client.callList[content.call_id] : undefined;
|
||||
var i;
|
||||
//console.log("RECV %s content=%s", event.getType(), JSON.stringify(content));
|
||||
|
||||
if (event.getType() === "m.call.invite") {
|
||||
if (event.getSender() === client.credentials.userId) {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* @module content-repo
|
||||
*/
|
||||
var utils = require("./utils");
|
||||
|
||||
/** Content Repo utility functions */
|
||||
module.exports = {
|
||||
/**
|
||||
* Get the HTTP URL for an MXC URI.
|
||||
* @param {string} baseUrl The base homeserver url which has a content repo.
|
||||
* @param {string} mxc The mxc:// URI.
|
||||
* @param {Number} width The desired width of the thumbnail.
|
||||
* @param {Number} height The desired height of the thumbnail.
|
||||
* @param {string} resizeMethod The thumbnail resize method to use, either
|
||||
* "crop" or "scale".
|
||||
* @return {string} The complete URL to the content.
|
||||
*/
|
||||
getHttpUriForMxc: function(baseUrl, mxc, width, height, resizeMethod) {
|
||||
if (typeof mxc !== "string" || !mxc) {
|
||||
return mxc;
|
||||
}
|
||||
if (mxc.indexOf("mxc://") !== 0) {
|
||||
return mxc;
|
||||
}
|
||||
var serverAndMediaId = mxc.slice(6); // strips mxc://
|
||||
var prefix = "/_matrix/media/v1/download/";
|
||||
var params = {};
|
||||
|
||||
if (width) {
|
||||
params.width = width;
|
||||
}
|
||||
if (height) {
|
||||
params.height = height;
|
||||
}
|
||||
if (resizeMethod) {
|
||||
params.method = resizeMethod;
|
||||
}
|
||||
if (utils.keys(params).length > 0) {
|
||||
// these are thumbnailing params so they probably want the
|
||||
// thumbnailing API...
|
||||
prefix = "/_matrix/media/v1/thumbnail/";
|
||||
}
|
||||
|
||||
var fragmentOffset = serverAndMediaId.indexOf("#"),
|
||||
fragment = "";
|
||||
if (fragmentOffset >= 0) {
|
||||
fragment = serverAndMediaId.substr(fragmentOffset);
|
||||
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
|
||||
}
|
||||
return baseUrl + prefix + serverAndMediaId +
|
||||
(utils.keys(params).length === 0 ? "" :
|
||||
("?" + utils.encodeParams(params))) + fragment;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get an identicon URL from an arbitrary string.
|
||||
* @param {string} baseUrl The base homeserver url which has a content repo.
|
||||
* @param {string} identiconString The string to create an identicon for.
|
||||
* @param {Number} width The desired width of the image in pixels. Default: 96.
|
||||
* @param {Number} height The desired height of the image in pixels. Default: 96.
|
||||
* @return {string} The complete URL to the identicon.
|
||||
*/
|
||||
getIdenticonUri: function(baseUrl, identiconString, width, height) {
|
||||
if (!identiconString) {
|
||||
return null;
|
||||
}
|
||||
if (!width) { width = 96; }
|
||||
if (!height) { height = 96; }
|
||||
var params = {
|
||||
width: width,
|
||||
height: height
|
||||
};
|
||||
|
||||
var path = utils.encodeUri("/_matrix/media/v1/identicon/$ident", {
|
||||
$ident: identiconString
|
||||
});
|
||||
return baseUrl + path +
|
||||
(utils.keys(params).length === 0 ? "" :
|
||||
("?" + utils.encodeParams(params)));
|
||||
}
|
||||
};
|
||||
+5
-75
@@ -54,81 +54,6 @@ module.exports.MatrixHttpApi = function MatrixHttpApi(opts) {
|
||||
|
||||
module.exports.MatrixHttpApi.prototype = {
|
||||
|
||||
// URI functions
|
||||
// =============
|
||||
|
||||
/**
|
||||
* Get the HTTP URL for an MXC URI.
|
||||
* @param {string} mxc The mxc:// URI.
|
||||
* @param {Number} width The desired width of the thumbnail.
|
||||
* @param {Number} height The desired height of the thumbnail.
|
||||
* @param {string} resizeMethod The thumbnail resize method to use, either
|
||||
* "crop" or "scale".
|
||||
* @return {string} The complete URL to the content.
|
||||
*/
|
||||
getHttpUriForMxc: function(mxc, width, height, resizeMethod) {
|
||||
if (typeof mxc !== "string" || !mxc) {
|
||||
return mxc;
|
||||
}
|
||||
if (mxc.indexOf("mxc://") !== 0) {
|
||||
return mxc;
|
||||
}
|
||||
var serverAndMediaId = mxc.slice(6); // strips mxc://
|
||||
var prefix = "/_matrix/media/v1/download/";
|
||||
var params = {};
|
||||
|
||||
if (width) {
|
||||
params.width = width;
|
||||
}
|
||||
if (height) {
|
||||
params.height = height;
|
||||
}
|
||||
if (resizeMethod) {
|
||||
params.method = resizeMethod;
|
||||
}
|
||||
if (utils.keys(params).length > 0) {
|
||||
// these are thumbnailing params so they probably want the
|
||||
// thumbnailing API...
|
||||
prefix = "/_matrix/media/v1/thumbnail/";
|
||||
}
|
||||
|
||||
var fragmentOffset = serverAndMediaId.indexOf("#"),
|
||||
fragment = "";
|
||||
if (fragmentOffset >= 0) {
|
||||
fragment = serverAndMediaId.substr(fragmentOffset);
|
||||
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
|
||||
}
|
||||
return this.opts.baseUrl + prefix + serverAndMediaId +
|
||||
(utils.keys(params).length === 0 ? "" :
|
||||
("?" + utils.encodeParams(params))) + fragment;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get an identicon URL from an arbitrary string.
|
||||
* @param {string} identiconString The string to create an identicon for.
|
||||
* @param {Number} width The desired width of the image in pixels.
|
||||
* @param {Number} height The desired height of the image in pixels.
|
||||
* @return {string} The complete URL to the identicon.
|
||||
*/
|
||||
getIdenticonUri: function(identiconString, width, height) {
|
||||
if (!identiconString) {
|
||||
return;
|
||||
}
|
||||
if (!width) { width = 96; }
|
||||
if (!height) { height = 96; }
|
||||
var params = {
|
||||
width: width,
|
||||
height: height
|
||||
};
|
||||
|
||||
var path = utils.encodeUri("/_matrix/media/v1/identicon/$ident", {
|
||||
$ident: identiconString
|
||||
});
|
||||
return this.opts.baseUrl + path +
|
||||
(utils.keys(params).length === 0 ? "" :
|
||||
("?" + utils.encodeParams(params)));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the content repository url with query parameters.
|
||||
* @return {Object} An object with a 'base', 'path' and 'params' for base URL,
|
||||
@@ -186,6 +111,11 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
case global.XMLHttpRequest.DONE:
|
||||
clearTimeout(xhr.timeout_timer);
|
||||
|
||||
if (!xhr.responseText) {
|
||||
cb(new Error('No response body.'));
|
||||
return;
|
||||
}
|
||||
|
||||
var resp = JSON.parse(xhr.responseText);
|
||||
if (resp.content_uri === undefined) {
|
||||
cb(new Error('Bad response'));
|
||||
|
||||
@@ -30,6 +30,8 @@ module.exports.MatrixScheduler = require("./scheduler");
|
||||
module.exports.WebStorageSessionStore = require("./store/session/webstorage");
|
||||
/** True if crypto libraries are being used on this client. */
|
||||
module.exports.CRYPTO_ENABLED = require("./client").CRYPTO_ENABLED;
|
||||
/** {@link module:content-repo|ContentRepo} utility functions. */
|
||||
module.exports.ContentRepo = require("./content-repo");
|
||||
|
||||
/**
|
||||
* Create a new Matrix Call.
|
||||
|
||||
+39
-10
@@ -3,6 +3,7 @@
|
||||
* @module models/room-member
|
||||
*/
|
||||
var EventEmitter = require("events").EventEmitter;
|
||||
var ContentRepo = require("../content-repo");
|
||||
|
||||
var utils = require("../utils");
|
||||
|
||||
@@ -147,6 +148,39 @@ RoomMember.prototype.getLastModifiedTime = function() {
|
||||
return this._modified;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the avatar URL for a room member.
|
||||
* @param {string} baseUrl The base homeserver URL See
|
||||
* {@link module:client~MatrixClient#getHomeserverUrl}.
|
||||
* @param {Number} width The desired width of the thumbnail.
|
||||
* @param {Number} height The desired height of the thumbnail.
|
||||
* @param {string} resizeMethod The thumbnail resize method to use, either
|
||||
* "crop" or "scale".
|
||||
* @param {Boolean} allowDefault (optional) Passing false causes this method to
|
||||
* return null if the user has no avatar image. Otherwise, a default image URL
|
||||
* will be returned. Default: true.
|
||||
* @return {?string} the avatar URL or null.
|
||||
*/
|
||||
RoomMember.prototype.getAvatarUrl =
|
||||
function(baseUrl, width, height, resizeMethod, allowDefault) {
|
||||
if (allowDefault === undefined) { allowDefault = true; }
|
||||
if (!this.events.member && !allowDefault) {
|
||||
return null;
|
||||
}
|
||||
var rawUrl = this.events.member ? this.events.member.getContent().avatar_url : null;
|
||||
if (rawUrl) {
|
||||
return ContentRepo.getHttpUriForMxc(
|
||||
baseUrl, rawUrl, width, height, resizeMethod
|
||||
);
|
||||
}
|
||||
else if (allowDefault) {
|
||||
return ContentRepo.getIdenticonUri(
|
||||
baseUrl, this.userId, width, height
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
function calculateDisplayName(member, event, roomState) {
|
||||
var displayName = event.getDirectionalContent().displayname;
|
||||
var selfUserId = member.userId;
|
||||
@@ -174,18 +208,13 @@ function calculateDisplayName(member, event, 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 > 0) {
|
||||
// need to disambiguate
|
||||
var userIds = roomState.getUserIdsWithDisplayName(displayName);
|
||||
var otherUsers = userIds.filter(function(u) {
|
||||
return u !== selfUserId;
|
||||
});
|
||||
if (otherUsers.length > 0) {
|
||||
return displayName + " (" + selfUserId + ")";
|
||||
}
|
||||
|
||||
return displayName;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ function RoomState(roomId) {
|
||||
// userId: RoomMember
|
||||
};
|
||||
this._updateModifiedTime();
|
||||
this._displayNameToUserIds = {};
|
||||
this._userIdsToDisplayNames = {};
|
||||
}
|
||||
utils.inherits(RoomState, EventEmitter);
|
||||
|
||||
@@ -108,6 +110,11 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
|
||||
self.events[event.getType()] = {};
|
||||
}
|
||||
self.events[event.getType()][event.getStateKey()] = event;
|
||||
if (event.getType() === "m.room.member") {
|
||||
_updateDisplayNameCache(
|
||||
self, event.getStateKey(), event.getContent().displayname
|
||||
);
|
||||
}
|
||||
self.emit("RoomState.events", event, self);
|
||||
});
|
||||
|
||||
@@ -180,11 +187,47 @@ RoomState.prototype.getLastModifiedTime = function() {
|
||||
return this._modified;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get user IDs with the specified display name.
|
||||
* @param {string} displayName The display name to get user IDs from.
|
||||
* @return {string[]} An array of user IDs or an empty array.
|
||||
*/
|
||||
RoomState.prototype.getUserIdsWithDisplayName = function(displayName) {
|
||||
return this._displayNameToUserIds[displayName] || [];
|
||||
};
|
||||
|
||||
/**
|
||||
* The RoomState class.
|
||||
*/
|
||||
module.exports = RoomState;
|
||||
|
||||
|
||||
function _updateDisplayNameCache(roomState, userId, displayName) {
|
||||
var oldName = roomState._userIdsToDisplayNames[userId];
|
||||
delete roomState._userIdsToDisplayNames[userId];
|
||||
if (oldName) {
|
||||
// Remove the old name from the cache.
|
||||
// We clobber the user_id > name lookup but the name -> [user_id] lookup
|
||||
// means we need to remove that user ID from that array rather than nuking
|
||||
// the lot.
|
||||
var existingUserIds = roomState._displayNameToUserIds[oldName] || [];
|
||||
for (var i = 0; i < existingUserIds.length; i++) {
|
||||
if (existingUserIds[i] === userId) {
|
||||
// remove this user ID from this array
|
||||
existingUserIds.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
roomState._displayNameToUserIds[oldName] = existingUserIds;
|
||||
}
|
||||
|
||||
roomState._userIdsToDisplayNames[userId] = displayName;
|
||||
if (!roomState._displayNameToUserIds[displayName]) {
|
||||
roomState._displayNameToUserIds[displayName] = [];
|
||||
}
|
||||
roomState._displayNameToUserIds[displayName].push(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires whenever the event dictionary in room state is updated.
|
||||
* @event module:client~MatrixClient#"RoomState.events"
|
||||
|
||||
+175
-5
@@ -6,7 +6,9 @@ var EventEmitter = require("events").EventEmitter;
|
||||
|
||||
var RoomState = require("./room-state");
|
||||
var RoomSummary = require("./room-summary");
|
||||
var MatrixEvent = require("./event").MatrixEvent;
|
||||
var utils = require("../utils");
|
||||
var ContentRepo = require("../content-repo");
|
||||
|
||||
/**
|
||||
* Construct a new Room.
|
||||
@@ -36,9 +38,63 @@ function Room(roomId, storageToken) {
|
||||
this.summary = null;
|
||||
this.storageToken = storageToken;
|
||||
this._redactions = [];
|
||||
// receipts should clobber based on receipt_type and user_id pairs hence
|
||||
// the form of this structure. This is sub-optimal for the exposed APIs
|
||||
// which pass in an event ID and get back some receipts, so we also store
|
||||
// a pre-cached list for this purpose.
|
||||
this._receipts = {
|
||||
// receipt_type: {
|
||||
// user_id: {
|
||||
// eventId: <event_id>,
|
||||
// data: <receipt_data>
|
||||
// }
|
||||
// }
|
||||
};
|
||||
this._receiptCacheByEventId = {
|
||||
// $event_id: [{
|
||||
// type: $type,
|
||||
// userId: $user_id,
|
||||
// data: <receipt data>
|
||||
// }]
|
||||
};
|
||||
}
|
||||
utils.inherits(Room, EventEmitter);
|
||||
|
||||
/**
|
||||
* Get the avatar URL for a room if one was set.
|
||||
* @param {String} baseUrl The homeserver base URL. See
|
||||
* {@link module:client~MatrixClient#getHomeserverUrl}.
|
||||
* @param {Number} width The desired width of the thumbnail.
|
||||
* @param {Number} height The desired height of the thumbnail.
|
||||
* @param {string} resizeMethod The thumbnail resize method to use, either
|
||||
* "crop" or "scale".
|
||||
* @param {boolean} allowDefault True to allow an identicon for this room if an
|
||||
* avatar URL wasn't explicitly set. Default: true.
|
||||
* @return {?string} the avatar URL or null.
|
||||
*/
|
||||
Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod,
|
||||
allowDefault) {
|
||||
var roomAvatarEvent = this.currentState.getStateEvents("m.room.avatar", "");
|
||||
if (allowDefault === undefined) { allowDefault = true; }
|
||||
if (!roomAvatarEvent && !allowDefault) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var mainUrl = roomAvatarEvent ? roomAvatarEvent.getContent().url : null;
|
||||
if (mainUrl) {
|
||||
return ContentRepo.getHttpUriForMxc(
|
||||
baseUrl, mainUrl, width, height, resizeMethod
|
||||
);
|
||||
}
|
||||
else if (allowDefault) {
|
||||
return ContentRepo.getIdenticonUri(
|
||||
baseUrl, this.roomId, width, height
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a member from the current room state.
|
||||
* @param {string} userId The user ID of the member.
|
||||
@@ -57,7 +113,7 @@ utils.inherits(Room, EventEmitter);
|
||||
* @return {RoomMember[]} A list of currently joined members.
|
||||
*/
|
||||
Room.prototype.getJoinedMembers = function() {
|
||||
return this.getMembersWithMemership("join");
|
||||
return this.getMembersWithMembership("join");
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -65,7 +121,7 @@ utils.inherits(Room, EventEmitter);
|
||||
* @param {string} membership The membership state.
|
||||
* @return {RoomMember[]} A list of members with the given membership state.
|
||||
*/
|
||||
Room.prototype.getMembersWithMemership = function(membership) {
|
||||
Room.prototype.getMembersWithMembership = function(membership) {
|
||||
return utils.filter(this.currentState.getMembers(), function(m) {
|
||||
return m.membership === membership;
|
||||
});
|
||||
@@ -164,6 +220,9 @@ Room.prototype.addEvents = function(events, duplicateStrategy) {
|
||||
if (events[i].getType() === "m.typing") {
|
||||
this.currentState.setTypingEvent(events[i]);
|
||||
}
|
||||
else if (events[i].getType() === "m.receipt") {
|
||||
this.addReceipt(events[i]);
|
||||
}
|
||||
else {
|
||||
if (duplicateStrategy) {
|
||||
// is there a duplicate?
|
||||
@@ -209,6 +268,34 @@ Room.prototype.addEvents = function(events, duplicateStrategy) {
|
||||
* @fires module:client~MatrixClient#event:"Room.name"
|
||||
*/
|
||||
Room.prototype.recalculate = function(userId) {
|
||||
// set fake stripped state events if this is an invite room so logic remains
|
||||
// consistent elsewhere.
|
||||
var self = this;
|
||||
var membershipEvent = this.currentState.getStateEvents(
|
||||
"m.room.member", userId
|
||||
);
|
||||
if (membershipEvent && membershipEvent.getContent().membership === "invite") {
|
||||
var strippedStateEvents = membershipEvent.event.invite_room_state || [];
|
||||
utils.forEach(strippedStateEvents, function(strippedEvent) {
|
||||
var existingEvent = self.currentState.getStateEvents(
|
||||
strippedEvent.type, strippedEvent.state_key
|
||||
);
|
||||
if (!existingEvent) {
|
||||
// set the fake stripped event instead
|
||||
self.currentState.setStateEvents([new MatrixEvent({
|
||||
type: strippedEvent.type,
|
||||
state_key: strippedEvent.state_key,
|
||||
content: strippedEvent.content,
|
||||
event_id: "$fake" + Date.now(),
|
||||
room_id: self.roomId,
|
||||
user_id: userId // technically a lie
|
||||
})]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
var oldName = this.name;
|
||||
this.name = calculateRoomName(this, userId);
|
||||
this.summary = new RoomSummary(this.roomId, {
|
||||
@@ -220,6 +307,82 @@ Room.prototype.recalculate = function(userId) {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get a list of user IDs who have <b>read up to</b> the given event.
|
||||
* @param {MatrixEvent} event the event to get read receipts for.
|
||||
* @return {String[]} A list of user IDs.
|
||||
*/
|
||||
Room.prototype.getUsersReadUpTo = function(event) {
|
||||
return this.getReceiptsForEvent(event).filter(function(receipt) {
|
||||
return receipt.type === "m.read";
|
||||
}).map(function(receipt) {
|
||||
return receipt.userId;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a list of receipts for the given event.
|
||||
* @param {MatrixEvent} event the event to get receipts for
|
||||
* @return {Object[]} A list of receipts with a userId, type and data keys or
|
||||
* an empty list.
|
||||
*/
|
||||
Room.prototype.getReceiptsForEvent = function(event) {
|
||||
return this._receiptCacheByEventId[event.getId()] || [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a receipt event to the room.
|
||||
* @param {MatrixEvent} event The m.receipt event.
|
||||
*/
|
||||
Room.prototype.addReceipt = function(event) {
|
||||
// event content looks like:
|
||||
// content: {
|
||||
// $event_id: {
|
||||
// $receipt_type: {
|
||||
// $user_id: {
|
||||
// ts: $timestamp
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
var self = this;
|
||||
utils.keys(event.getContent()).forEach(function(eventId) {
|
||||
utils.keys(event.getContent()[eventId]).forEach(function(receiptType) {
|
||||
utils.keys(event.getContent()[eventId][receiptType]).forEach(
|
||||
function(userId) {
|
||||
var receipt = event.getContent()[eventId][receiptType][userId];
|
||||
if (!self._receipts[receiptType]) {
|
||||
self._receipts[receiptType] = {};
|
||||
}
|
||||
if (!self._receipts[receiptType][userId]) {
|
||||
self._receipts[receiptType][userId] = {};
|
||||
}
|
||||
self._receipts[receiptType][userId] = {
|
||||
eventId: eventId,
|
||||
data: receipt
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// pre-cache receipts by event
|
||||
self._receiptCacheByEventId = {};
|
||||
utils.keys(self._receipts).forEach(function(receiptType) {
|
||||
utils.keys(self._receipts[receiptType]).forEach(function(userId) {
|
||||
var receipt = self._receipts[receiptType][userId];
|
||||
if (!self._receiptCacheByEventId[receipt.eventId]) {
|
||||
self._receiptCacheByEventId[receipt.eventId] = [];
|
||||
}
|
||||
self._receiptCacheByEventId[receipt.eventId].push({
|
||||
userId: userId,
|
||||
type: receiptType,
|
||||
data: receipt.data
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function setEventMetadata(event, stateContext, toStartOfTimeline) {
|
||||
// set sender and target properties
|
||||
event.sender = stateContext.getSentinelMember(
|
||||
@@ -253,9 +416,16 @@ function calculateRoomName(room, userId) {
|
||||
// check for an alias, if any. for now, assume first alias is the
|
||||
// official one.
|
||||
var alias;
|
||||
var mRoomAliases = room.currentState.getStateEvents("m.room.aliases")[0];
|
||||
if (mRoomAliases && utils.isArray(mRoomAliases.getContent().aliases)) {
|
||||
alias = mRoomAliases.getContent().aliases[0];
|
||||
var canonicalAlias = room.currentState.getStateEvents("m.room.canonical_alias", "");
|
||||
if (canonicalAlias) {
|
||||
alias = canonicalAlias.getContent().alias;
|
||||
}
|
||||
|
||||
if (!alias) {
|
||||
var mRoomAliases = room.currentState.getStateEvents("m.room.aliases")[0];
|
||||
if (mRoomAliases && utils.isArray(mRoomAliases.getContent().aliases)) {
|
||||
alias = mRoomAliases.getContent().aliases[0];
|
||||
}
|
||||
}
|
||||
|
||||
var mRoomName = room.currentState.getStateEvents('m.room.name', '');
|
||||
|
||||
+138
@@ -228,6 +228,144 @@ module.exports.deepCopy = function(obj) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Run polyfills to add Array.map and Array.filter if they are missing.
|
||||
*/
|
||||
module.exports.runPolyfills = function() {
|
||||
// Array.prototype.filter
|
||||
// ========================================================
|
||||
// SOURCE:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
|
||||
if (!Array.prototype.filter) {
|
||||
Array.prototype.filter = function(fun/*, thisArg*/) {
|
||||
|
||||
if (this === void 0 || this === null) {
|
||||
throw new TypeError();
|
||||
}
|
||||
|
||||
var t = Object(this);
|
||||
var len = t.length >>> 0;
|
||||
if (typeof fun !== 'function') {
|
||||
throw new TypeError();
|
||||
}
|
||||
|
||||
var res = [];
|
||||
var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
|
||||
for (var i = 0; i < len; i++) {
|
||||
if (i in t) {
|
||||
var val = t[i];
|
||||
|
||||
// NOTE: Technically this should Object.defineProperty at
|
||||
// the next index, as push can be affected by
|
||||
// properties on Object.prototype and Array.prototype.
|
||||
// But that method's new, and collisions should be
|
||||
// rare, so use the more-compatible alternative.
|
||||
if (fun.call(thisArg, val, i, t)) {
|
||||
res.push(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
}
|
||||
|
||||
// Array.prototype.map
|
||||
// ========================================================
|
||||
// SOURCE:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
|
||||
// Production steps of ECMA-262, Edition 5, 15.4.4.19
|
||||
// Reference: http://es5.github.io/#x15.4.4.19
|
||||
if (!Array.prototype.map) {
|
||||
|
||||
Array.prototype.map = function(callback, thisArg) {
|
||||
|
||||
var T, A, k;
|
||||
|
||||
if (this === null || this === undefined) {
|
||||
throw new TypeError(' this is null or not defined');
|
||||
}
|
||||
|
||||
// 1. Let O be the result of calling ToObject passing the |this|
|
||||
// value as the argument.
|
||||
var O = Object(this);
|
||||
|
||||
// 2. Let lenValue be the result of calling the Get internal
|
||||
// method of O with the argument "length".
|
||||
// 3. Let len be ToUint32(lenValue).
|
||||
var len = O.length >>> 0;
|
||||
|
||||
// 4. If IsCallable(callback) is false, throw a TypeError exception.
|
||||
// See: http://es5.github.com/#x9.11
|
||||
if (typeof callback !== 'function') {
|
||||
throw new TypeError(callback + ' is not a function');
|
||||
}
|
||||
|
||||
// 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
|
||||
if (arguments.length > 1) {
|
||||
T = thisArg;
|
||||
}
|
||||
|
||||
// 6. Let A be a new array created as if by the expression new Array(len)
|
||||
// where Array is the standard built-in constructor with that name and
|
||||
// len is the value of len.
|
||||
A = new Array(len);
|
||||
|
||||
// 7. Let k be 0
|
||||
k = 0;
|
||||
|
||||
// 8. Repeat, while k < len
|
||||
while (k < len) {
|
||||
|
||||
var kValue, mappedValue;
|
||||
|
||||
// a. Let Pk be ToString(k).
|
||||
// This is implicit for LHS operands of the in operator
|
||||
// b. Let kPresent be the result of calling the HasProperty internal
|
||||
// method of O with argument Pk.
|
||||
// This step can be combined with c
|
||||
// c. If kPresent is true, then
|
||||
if (k in O) {
|
||||
|
||||
// i. Let kValue be the result of calling the Get internal
|
||||
// method of O with argument Pk.
|
||||
kValue = O[k];
|
||||
|
||||
// ii. Let mappedValue be the result of calling the Call internal
|
||||
// method of callback with T as the this value and argument
|
||||
// list containing kValue, k, and O.
|
||||
mappedValue = callback.call(T, kValue, k, O);
|
||||
|
||||
// iii. Call the DefineOwnProperty internal method of A with arguments
|
||||
// Pk, Property Descriptor
|
||||
// { Value: mappedValue,
|
||||
// Writable: true,
|
||||
// Enumerable: true,
|
||||
// Configurable: true },
|
||||
// and false.
|
||||
|
||||
// In browsers that support Object.defineProperty, use the following:
|
||||
// Object.defineProperty(A, k, {
|
||||
// value: mappedValue,
|
||||
// writable: true,
|
||||
// enumerable: true,
|
||||
// configurable: true
|
||||
// });
|
||||
|
||||
// For best browser support, use the following:
|
||||
A[k] = mappedValue;
|
||||
}
|
||||
// d. Increase k by 1.
|
||||
k++;
|
||||
}
|
||||
|
||||
// 9. return A
|
||||
return A;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Inherit the prototype methods from one constructor into another. This is a
|
||||
* port of the Node.js implementation with an Object.create polyfill.
|
||||
|
||||
+243
-22
@@ -44,6 +44,8 @@ function MatrixCall(opts) {
|
||||
// possible
|
||||
this.candidateSendQueue = [];
|
||||
this.candidateSendTries = 0;
|
||||
|
||||
this.screenSharingStream = null;
|
||||
}
|
||||
/** The length of time a call can be ringing for. */
|
||||
MatrixCall.CALL_TIMEOUT_MS = 60000;
|
||||
@@ -64,6 +66,7 @@ utils.inherits(MatrixCall, EventEmitter);
|
||||
* @throws If you have not specified a listener for 'error' events.
|
||||
*/
|
||||
MatrixCall.prototype.placeVoiceCall = function() {
|
||||
debuglog("placeVoiceCall");
|
||||
checkForErrorListener(this);
|
||||
_placeCallWithConstraints(this, _getUserMediaVideoContraints('voice'));
|
||||
this.type = 'voice';
|
||||
@@ -78,6 +81,7 @@ MatrixCall.prototype.placeVoiceCall = function() {
|
||||
* @throws If you have not specified a listener for 'error' events.
|
||||
*/
|
||||
MatrixCall.prototype.placeVideoCall = function(remoteVideoElement, localVideoElement) {
|
||||
debuglog("placeVideoCall");
|
||||
checkForErrorListener(this);
|
||||
this.localVideoElement = localVideoElement;
|
||||
this.remoteVideoElement = remoteVideoElement;
|
||||
@@ -86,6 +90,45 @@ MatrixCall.prototype.placeVideoCall = function(remoteVideoElement, localVideoEle
|
||||
_tryPlayRemoteStream(this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Place a screen-sharing call to this room. This includes audio.
|
||||
* <b>This method is EXPERIMENTAL and subject to change without warning. It
|
||||
* only works in Google Chrome.</b>
|
||||
* @param {Element} remoteVideoElement a <code><video></code> DOM element
|
||||
* to render video to.
|
||||
* @param {Element} localVideoElement a <code><video></code> DOM element
|
||||
* to render the local camera preview.
|
||||
* @throws If you have not specified a listener for 'error' events.
|
||||
*/
|
||||
MatrixCall.prototype.placeScreenSharingCall =
|
||||
function(remoteVideoElement, localVideoElement)
|
||||
{
|
||||
debuglog("placeScreenSharingCall");
|
||||
checkForErrorListener(this);
|
||||
var screenConstraints = _getChromeScreenSharingConstraints(this);
|
||||
if (!screenConstraints) {
|
||||
return;
|
||||
}
|
||||
this.localVideoElement = localVideoElement;
|
||||
this.remoteVideoElement = remoteVideoElement;
|
||||
var self = this;
|
||||
this.webRtc.getUserMedia(screenConstraints, function(stream) {
|
||||
self.screenSharingStream = stream;
|
||||
debuglog("Got screen stream, requesting audio stream...");
|
||||
var audioConstraints = _getUserMediaVideoContraints('voice');
|
||||
_placeCallWithConstraints(self, audioConstraints);
|
||||
}, function(err) {
|
||||
self.emit("error",
|
||||
callError(
|
||||
MatrixCall.ERR_NO_USER_MEDIA,
|
||||
"Failed to get screen-sharing stream: " + err
|
||||
)
|
||||
);
|
||||
});
|
||||
this.type = 'video';
|
||||
_tryPlayRemoteStream(this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the local <code><video></code> DOM element.
|
||||
* @return {Element} The dom element
|
||||
@@ -95,13 +138,23 @@ MatrixCall.prototype.getLocalVideoElement = function() {
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the remote <code><video></code> DOM element.
|
||||
* Retrieve the remote <code><video></code> DOM element
|
||||
* used for playing back video capable streams.
|
||||
* @return {Element} The dom element
|
||||
*/
|
||||
MatrixCall.prototype.getRemoteVideoElement = function() {
|
||||
return this.remoteVideoElement;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the remote <code><audio></code> DOM element
|
||||
* used for playing back audio only streams.
|
||||
* @return {Element} The dom element
|
||||
*/
|
||||
MatrixCall.prototype.getRemoteAudioElement = function() {
|
||||
return this.remoteAudioElement;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the local <code><video></code> DOM element. If this call is active,
|
||||
* video will be rendered to it immediately.
|
||||
@@ -126,7 +179,7 @@ MatrixCall.prototype.setLocalVideoElement = function(element) {
|
||||
|
||||
/**
|
||||
* Set the remote <code><video></code> DOM element. If this call is active,
|
||||
* video will be rendered to it immediately.
|
||||
* the first received video-capable stream will be rendered to it immediately.
|
||||
* @param {Element} element The <code><video></code> DOM element.
|
||||
*/
|
||||
MatrixCall.prototype.setRemoteVideoElement = function(element) {
|
||||
@@ -134,6 +187,16 @@ MatrixCall.prototype.setRemoteVideoElement = function(element) {
|
||||
_tryPlayRemoteStream(this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the remote <code><audio></code> DOM element. If this call is active,
|
||||
* the first received audio-only stream will be rendered to it immediately.
|
||||
* @param {Element} element The <code><video></code> DOM element.
|
||||
*/
|
||||
MatrixCall.prototype.setRemoteAudioElement = function(element) {
|
||||
this.remoteAudioElement = element;
|
||||
_tryPlayRemoteAudioStream(this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Configure this call from an invite event. Used by MatrixClient.
|
||||
* @protected
|
||||
@@ -170,6 +233,7 @@ MatrixCall.prototype._initWithInvite = function(event) {
|
||||
if (event.getAge()) {
|
||||
setTimeout(function() {
|
||||
if (self.state == 'ringing') {
|
||||
debuglog("Call invite has expired. Hanging up.");
|
||||
self.hangupParty = 'remote'; // effectively
|
||||
setState(self, 'ended');
|
||||
stopAllMedia(self);
|
||||
@@ -238,6 +302,7 @@ MatrixCall.prototype._replacedBy = function(newCall) {
|
||||
}
|
||||
newCall.localVideoElement = this.localVideoElement;
|
||||
newCall.remoteVideoElement = this.remoteVideoElement;
|
||||
newCall.remoteAudioElement = this.remoteAudioElement;
|
||||
this.successor = newCall;
|
||||
this.emit("replaced", newCall);
|
||||
this.hangup(true);
|
||||
@@ -259,6 +324,60 @@ MatrixCall.prototype.hangup = function(reason, suppressEvent) {
|
||||
sendEvent(this, 'm.call.hangup', content);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set whether the local video preview should be muted or not.
|
||||
* @param {boolean} muted True to mute the local video.
|
||||
*/
|
||||
MatrixCall.prototype.setLocalVideoMuted = function(muted) {
|
||||
if (!this.localAVStream) {
|
||||
return;
|
||||
}
|
||||
setTracksEnabled(this.localAVStream.getVideoTracks(), !muted);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if local video is muted.
|
||||
*
|
||||
* If there are multiple video tracks, <i>all</i> of the tracks need to be muted
|
||||
* for this to return true. This means if there are no video tracks, this will
|
||||
* return true.
|
||||
* @return {Boolean} True if the local preview video is muted, else false
|
||||
* (including if the call is not set up yet).
|
||||
*/
|
||||
MatrixCall.prototype.isLocalVideoMuted = function() {
|
||||
if (!this.localAVStream) {
|
||||
return false;
|
||||
}
|
||||
return !isTracksEnabled(this.localAVStream.getVideoTracks());
|
||||
};
|
||||
|
||||
/**
|
||||
* Set whether the microphone should be muted or not.
|
||||
* @param {boolean} muted True to mute the mic.
|
||||
*/
|
||||
MatrixCall.prototype.setMicrophoneMuted = function(muted) {
|
||||
if (!this.localAVStream) {
|
||||
return;
|
||||
}
|
||||
setTracksEnabled(this.localAVStream.getAudioTracks(), !muted);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the microphone is muted.
|
||||
*
|
||||
* If there are multiple audio tracks, <i>all</i> of the tracks need to be muted
|
||||
* for this to return true. This means if there are no audio tracks, this will
|
||||
* return true.
|
||||
* @return {Boolean} True if the mic is muted, else false (including if the call
|
||||
* is not set up yet).
|
||||
*/
|
||||
MatrixCall.prototype.isMicrophoneMuted = function() {
|
||||
if (!this.localAVStream) {
|
||||
return false;
|
||||
}
|
||||
return !isTracksEnabled(this.localAVStream.getAudioTracks());
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal
|
||||
* @private
|
||||
@@ -272,12 +391,19 @@ MatrixCall.prototype._gotUserMediaForInvite = function(stream) {
|
||||
if (this.state == 'ended') {
|
||||
return;
|
||||
}
|
||||
debuglog("_gotUserMediaForInvite -> " + this.type);
|
||||
var self = this;
|
||||
var videoEl = this.getLocalVideoElement();
|
||||
|
||||
if (videoEl && this.type == 'video') {
|
||||
videoEl.autoplay = true;
|
||||
videoEl.src = this.URL.createObjectURL(stream);
|
||||
if (this.screenSharingStream) {
|
||||
debuglog("Setting screen sharing stream to the local video element");
|
||||
videoEl.src = this.URL.createObjectURL(this.screenSharingStream);
|
||||
}
|
||||
else {
|
||||
videoEl.src = this.URL.createObjectURL(stream);
|
||||
}
|
||||
videoEl.muted = true;
|
||||
setTimeout(function() {
|
||||
var vel = self.getLocalVideoElement();
|
||||
@@ -288,12 +414,16 @@ MatrixCall.prototype._gotUserMediaForInvite = function(stream) {
|
||||
}
|
||||
|
||||
this.localAVStream = stream;
|
||||
var audioTracks = stream.getAudioTracks();
|
||||
for (var i = 0; i < audioTracks.length; i++) {
|
||||
audioTracks[i].enabled = true;
|
||||
}
|
||||
// why do we enable audio (and only audio) tracks here? -- matthew
|
||||
setTracksEnabled(stream.getAudioTracks(), true);
|
||||
this.peerConn = _createPeerConnection(this);
|
||||
this.peerConn.addStream(stream);
|
||||
if (this.screenSharingStream) {
|
||||
console.log("Adding screen-sharing stream to peer connection");
|
||||
this.peerConn.addStream(this.screenSharingStream);
|
||||
// let's use this for the local preview...
|
||||
this.localAVStream = this.screenSharingStream;
|
||||
}
|
||||
this.peerConn.createOffer(
|
||||
hookCallback(self, self._gotLocalOffer),
|
||||
hookCallback(self, self._getLocalOfferFailed)
|
||||
@@ -326,10 +456,7 @@ MatrixCall.prototype._gotUserMediaForAnswer = function(stream) {
|
||||
}
|
||||
|
||||
self.localAVStream = stream;
|
||||
var audioTracks = stream.getAudioTracks();
|
||||
for (var i = 0; i < audioTracks.length; i++) {
|
||||
audioTracks[i].enabled = true;
|
||||
}
|
||||
setTracksEnabled(stream.getAudioTracks(), true);
|
||||
self.peerConn.addStream(stream);
|
||||
|
||||
var constraints = {
|
||||
@@ -481,8 +608,9 @@ MatrixCall.prototype._getLocalOfferFailed = function(error) {
|
||||
/**
|
||||
* Internal
|
||||
* @private
|
||||
* @param {Object} error
|
||||
*/
|
||||
MatrixCall.prototype._getUserMediaFailed = function() {
|
||||
MatrixCall.prototype._getUserMediaFailed = function(error) {
|
||||
this.emit(
|
||||
"error",
|
||||
callError(
|
||||
@@ -550,22 +678,21 @@ MatrixCall.prototype._onSetRemoteDescriptionError = function(e) {
|
||||
* @param {Object} event
|
||||
*/
|
||||
MatrixCall.prototype._onAddStream = function(event) {
|
||||
debuglog("Stream added" + event);
|
||||
debuglog("Stream id " + event.stream.id + " added");
|
||||
|
||||
var s = event.stream;
|
||||
|
||||
this.remoteAVStream = s;
|
||||
|
||||
if (this.direction == 'inbound') {
|
||||
if (s.getVideoTracks().length > 0) {
|
||||
this.type = 'video';
|
||||
} else {
|
||||
this.type = 'voice';
|
||||
}
|
||||
if (s.getVideoTracks().length > 0) {
|
||||
this.type = 'video';
|
||||
this.remoteAVStream = s;
|
||||
} else {
|
||||
this.type = 'voice';
|
||||
this.remoteAStream = s;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
forAllTracksOnStream(s, function(t) {
|
||||
debuglog("Track id " + t.id + " added");
|
||||
// not currently implemented in chrome
|
||||
t.onstarted = hookCallback(self, self._onRemoteStreamTrackStarted);
|
||||
});
|
||||
@@ -574,7 +701,12 @@ MatrixCall.prototype._onAddStream = function(event) {
|
||||
// not currently implemented in chrome
|
||||
event.stream.onstarted = hookCallback(self, self._onRemoteStreamStarted);
|
||||
|
||||
_tryPlayRemoteStream(this);
|
||||
if (this.type === 'video') {
|
||||
_tryPlayRemoteStream(this);
|
||||
}
|
||||
else {
|
||||
_tryPlayRemoteAudioStream(this);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -631,6 +763,21 @@ MatrixCall.prototype._onAnsweredElsewhere = function(msg) {
|
||||
terminate(this, "remote", "answered_elsewhere", true);
|
||||
};
|
||||
|
||||
var setTracksEnabled = function(tracks, enabled) {
|
||||
for (var i = 0; i < tracks.length; i++) {
|
||||
tracks[i].enabled = enabled;
|
||||
}
|
||||
};
|
||||
|
||||
var isTracksEnabled = function(tracks) {
|
||||
for (var i = 0; i < tracks.length; i++) {
|
||||
if (tracks[i].enabled) {
|
||||
return true; // at least one track is enabled
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
var setState = function(self, state) {
|
||||
var oldState = self.state;
|
||||
self.state = state;
|
||||
@@ -666,6 +813,12 @@ var terminate = function(self, hangupParty, hangupReason, shouldEmit) {
|
||||
}
|
||||
self.getRemoteVideoElement().src = "";
|
||||
}
|
||||
if (self.getRemoteAudioElement()) {
|
||||
if (self.getRemoteAudioElement().pause) {
|
||||
self.getRemoteAudioElement().pause();
|
||||
}
|
||||
self.getRemoteAudioElement().src = "";
|
||||
}
|
||||
if (self.getLocalVideoElement()) {
|
||||
if (self.getLocalVideoElement().pause) {
|
||||
self.getLocalVideoElement().pause();
|
||||
@@ -685,6 +838,7 @@ var terminate = function(self, hangupParty, hangupReason, shouldEmit) {
|
||||
};
|
||||
|
||||
var stopAllMedia = function(self) {
|
||||
debuglog("stopAllMedia (stream=%s)", self.localAVStream);
|
||||
if (self.localAVStream) {
|
||||
forAllTracksOnStream(self.localAVStream, function(t) {
|
||||
if (t.stop) {
|
||||
@@ -697,6 +851,16 @@ var stopAllMedia = function(self) {
|
||||
self.localAVStream.stop();
|
||||
}
|
||||
}
|
||||
if (self.screenSharingStream) {
|
||||
forAllTracksOnStream(self.screenSharingStream, function(t) {
|
||||
if (t.stop) {
|
||||
t.stop();
|
||||
}
|
||||
});
|
||||
if (self.screenSharingStream.stop) {
|
||||
self.screenSharingStream.stop();
|
||||
}
|
||||
}
|
||||
if (self.remoteAVStream) {
|
||||
forAllTracksOnStream(self.remoteAVStream, function(t) {
|
||||
if (t.stop) {
|
||||
@@ -704,6 +868,13 @@ var stopAllMedia = function(self) {
|
||||
}
|
||||
});
|
||||
}
|
||||
if (self.remoteAStream) {
|
||||
forAllTracksOnStream(self.remoteAStream, function(t) {
|
||||
if (t.stop) {
|
||||
t.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var _tryPlayRemoteStream = function(self) {
|
||||
@@ -724,6 +895,24 @@ var _tryPlayRemoteStream = function(self) {
|
||||
}
|
||||
};
|
||||
|
||||
var _tryPlayRemoteAudioStream = function(self) {
|
||||
if (self.getRemoteAudioElement() && self.remoteAStream) {
|
||||
var player = self.getRemoteAudioElement();
|
||||
player.autoplay = true;
|
||||
player.src = self.URL.createObjectURL(self.remoteAStream);
|
||||
setTimeout(function() {
|
||||
var ael = self.getRemoteAudioElement();
|
||||
if (ael.play) {
|
||||
ael.play();
|
||||
}
|
||||
// OpenWebRTC does not support oniceconnectionstatechange yet
|
||||
if (self.webRtc.isOpenWebRTC()) {
|
||||
setState(self, 'connected');
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
var checkForErrorListener = function(self) {
|
||||
if (self.listeners("error").length === 0) {
|
||||
throw new Error(
|
||||
@@ -822,6 +1011,38 @@ var _createPeerConnection = function(self) {
|
||||
return pc;
|
||||
};
|
||||
|
||||
var _getChromeScreenSharingConstraints = function(call) {
|
||||
var screen = global.screen;
|
||||
if (!screen) {
|
||||
call.emit("error", callError(
|
||||
MatrixCall.ERR_NO_USER_MEDIA,
|
||||
"Couldn't determine screen sharing constaints."
|
||||
));
|
||||
return;
|
||||
}
|
||||
// it won't work at all if you're not on HTTPS so whine whine whine
|
||||
if (!global.window || global.window.location.protocol !== "https:") {
|
||||
call.emit("error", callError(
|
||||
MatrixCall.ERR_NO_USER_MEDIA,
|
||||
"You need to be using HTTPS to place a screen-sharing call."
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
video: {
|
||||
mandatory: {
|
||||
chromeMediaSource: "screen",
|
||||
chromeMediaSourceId: "" + Date.now(),
|
||||
maxWidth: screen.width,
|
||||
maxHeight: screen.height,
|
||||
minFrameRate: 1,
|
||||
maxFrameRate: 10
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
var _getUserMediaVideoContraints = function(callType) {
|
||||
switch (callType) {
|
||||
case 'voice':
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "0.2.2",
|
||||
"version": "0.3.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -43,4 +43,48 @@ describe("MatrixClient", function() {
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
});
|
||||
});
|
||||
|
||||
describe("searching", function() {
|
||||
|
||||
var response = {
|
||||
search_categories: {
|
||||
room_events: {
|
||||
count: 24,
|
||||
results: {
|
||||
"$flibble:localhost": {
|
||||
rank: 0.1,
|
||||
result: {
|
||||
type: "m.room.message",
|
||||
user_id: "@alice:localhost",
|
||||
room_id: "!feuiwhf:localhost",
|
||||
content: {
|
||||
body: "a result",
|
||||
msgtype: "m.text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
it("searchMessageText should perform a /search for room_events", function(done) {
|
||||
client.searchMessageText({
|
||||
query: "monkeys"
|
||||
});
|
||||
httpBackend.when("POST", "/search").check(function(req) {
|
||||
expect(req.data).toEqual({
|
||||
search_categories: {
|
||||
room_events: {
|
||||
search_term: "monkeys"
|
||||
}
|
||||
}
|
||||
});
|
||||
}).respond(200, response);
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
var sdk = require("../..");
|
||||
var HttpBackend = require("../mock-request");
|
||||
var utils = require("../test-utils");
|
||||
var MatrixEvent = sdk.MatrixEvent;
|
||||
|
||||
describe("MatrixClient syncing", function() {
|
||||
var baseUrl = "http://localhost.or.something";
|
||||
@@ -9,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);
|
||||
@@ -64,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: [
|
||||
@@ -112,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 = {
|
||||
@@ -270,6 +420,121 @@ describe("MatrixClient syncing", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("receipts", function() {
|
||||
var initialSync = {
|
||||
end: "s_5_3",
|
||||
presence: [],
|
||||
receipts: [],
|
||||
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.mkEvent({
|
||||
type: "m.room.name", room: roomOne, user: otherUserId,
|
||||
content: {
|
||||
name: "Old room name"
|
||||
}
|
||||
}),
|
||||
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 = [];
|
||||
initialSync.receipts = [];
|
||||
});
|
||||
|
||||
it("should sync receipts from /initialSync.", function(done) {
|
||||
var ackEvent = initialSync.rooms[0].messages.chunk[0];
|
||||
var receipt = {};
|
||||
receipt[ackEvent.event_id] = {
|
||||
"m.read": {}
|
||||
};
|
||||
receipt[ackEvent.event_id]["m.read"][otherUserId] = {
|
||||
ts: 176592842636
|
||||
};
|
||||
initialSync.receipts = [{
|
||||
content: receipt,
|
||||
room_id: roomOne,
|
||||
type: "m.receipt"
|
||||
}];
|
||||
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
|
||||
httpBackend.when("GET", "/events").respond(200, eventData);
|
||||
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
var room = client.getRoom(roomOne);
|
||||
expect(room.getReceiptsForEvent(new MatrixEvent(ackEvent))).toEqual([{
|
||||
type: "m.read",
|
||||
userId: otherUserId,
|
||||
data: {
|
||||
ts: 176592842636
|
||||
}
|
||||
}]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should sync receipts from /events.", function(done) {
|
||||
var ackEvent = initialSync.rooms[0].messages.chunk[0];
|
||||
var receipt = {};
|
||||
receipt[ackEvent.event_id] = {
|
||||
"m.read": {}
|
||||
};
|
||||
receipt[ackEvent.event_id]["m.read"][otherUserId] = {
|
||||
ts: 176592842636
|
||||
};
|
||||
eventData.chunk = [{
|
||||
content: receipt,
|
||||
room_id: roomOne,
|
||||
type: "m.receipt"
|
||||
}];
|
||||
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
|
||||
httpBackend.when("GET", "/events").respond(200, eventData);
|
||||
|
||||
client.startClient();
|
||||
|
||||
httpBackend.flush().done(function() {
|
||||
var room = client.getRoom(roomOne);
|
||||
expect(room.getReceiptsForEvent(new MatrixEvent(ackEvent))).toEqual([{
|
||||
type: "m.read",
|
||||
userId: otherUserId,
|
||||
data: {
|
||||
ts: 176592842636
|
||||
}
|
||||
}]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("of a room", function() {
|
||||
xit("should sync when a join event (which changes state) for the user" +
|
||||
" arrives down the event stream (e.g. join from another device)", function() {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
"use strict";
|
||||
var ContentRepo = require("../../lib/content-repo");
|
||||
var testUtils = require("../test-utils");
|
||||
|
||||
describe("ContentRepo", function() {
|
||||
var baseUrl = "https://my.home.server";
|
||||
|
||||
beforeEach(function() {
|
||||
testUtils.beforeEach(this);
|
||||
});
|
||||
|
||||
describe("getHttpUriForMxc", function() {
|
||||
it("should do nothing to HTTP URLs", function() {
|
||||
var httpUrl = "http://example.com/image.jpeg";
|
||||
expect(ContentRepo.getHttpUriForMxc(baseUrl, httpUrl)).toEqual(httpUrl);
|
||||
});
|
||||
|
||||
it("should return a download URL if no width/height/resize are specified",
|
||||
function() {
|
||||
var mxcUri = "mxc://server.name/resourceid";
|
||||
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
|
||||
baseUrl + "/_matrix/media/v1/download/server.name/resourceid"
|
||||
);
|
||||
});
|
||||
|
||||
it("should do nothing for null input", function() {
|
||||
expect(ContentRepo.getHttpUriForMxc(null)).toEqual(null);
|
||||
});
|
||||
|
||||
it("should return a thumbnail URL if a width/height/resize is specified",
|
||||
function() {
|
||||
var mxcUri = "mxc://server.name/resourceid";
|
||||
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
|
||||
baseUrl + "/_matrix/media/v1/thumbnail/server.name/resourceid" +
|
||||
"?width=32&height=64&method=crop"
|
||||
);
|
||||
});
|
||||
|
||||
it("should put fragments from mxc:// URIs after any query parameters",
|
||||
function() {
|
||||
var mxcUri = "mxc://server.name/resourceid#automade";
|
||||
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
|
||||
baseUrl + "/_matrix/media/v1/thumbnail/server.name/resourceid" +
|
||||
"?width=32#automade"
|
||||
);
|
||||
});
|
||||
|
||||
it("should put fragments from mxc:// URIs at the end of the HTTP URI",
|
||||
function() {
|
||||
var mxcUri = "mxc://server.name/resourceid#automade";
|
||||
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
|
||||
baseUrl + "/_matrix/media/v1/download/server.name/resourceid#automade"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getIdenticonUri", function() {
|
||||
it("should do nothing for null input", function() {
|
||||
expect(ContentRepo.getIdenticonUri(null)).toEqual(null);
|
||||
});
|
||||
|
||||
it("should set w/h by default to 96", function() {
|
||||
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar")).toEqual(
|
||||
baseUrl + "/_matrix/media/v1/identicon/foobar" +
|
||||
"?width=96&height=96"
|
||||
);
|
||||
});
|
||||
|
||||
it("should be able to set custom w/h", function() {
|
||||
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar", 32, 64)).toEqual(
|
||||
baseUrl + "/_matrix/media/v1/identicon/foobar" +
|
||||
"?width=32&height=64"
|
||||
);
|
||||
});
|
||||
|
||||
it("should URL encode the identicon string", function() {
|
||||
expect(ContentRepo.getIdenticonUri(baseUrl, "foo#bar", 32, 64)).toEqual(
|
||||
baseUrl + "/_matrix/media/v1/identicon/foo%23bar" +
|
||||
"?width=32&height=64"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,40 @@ describe("RoomMember", function() {
|
||||
member = new RoomMember(roomId, userA);
|
||||
});
|
||||
|
||||
describe("getAvatarUrl", function() {
|
||||
var hsUrl = "https://my.home.server";
|
||||
|
||||
it("should return the URL from m.room.member preferentially", function() {
|
||||
member.events.member = utils.mkEvent({
|
||||
event: true,
|
||||
type: "m.room.member",
|
||||
skey: userA,
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
membership: "join",
|
||||
avatar_url: "mxc://flibble/wibble"
|
||||
}
|
||||
});
|
||||
var url = member.getAvatarUrl(hsUrl);
|
||||
// we don't care about how the mxc->http conversion is done, other
|
||||
// than it contains the mxc body.
|
||||
expect(url.indexOf("flibble/wibble")).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("should return an identicon HTTP URL if allowDefault was set and there " +
|
||||
"was no m.room.member event", function() {
|
||||
var url = member.getAvatarUrl(hsUrl, 64, 64, "crop", true);
|
||||
expect(url.indexOf("http")).toEqual(0); // don't care about form
|
||||
});
|
||||
|
||||
it("should return nothing if there is no m.room.member and allowDefault=false",
|
||||
function() {
|
||||
var url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false);
|
||||
expect(url).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setPowerLevelEvent", function() {
|
||||
it("should set 'powerLevel' and 'powerLevelNorm'.", function() {
|
||||
var event = utils.mkEvent({
|
||||
@@ -167,6 +201,9 @@ describe("RoomMember", function() {
|
||||
}),
|
||||
joinEvent
|
||||
];
|
||||
},
|
||||
getUserIdsWithDisplayName: function(displayName) {
|
||||
return [userA, userC];
|
||||
}
|
||||
};
|
||||
expect(member.name).toEqual(userA); // default = user_id
|
||||
|
||||
+368
-131
@@ -2,6 +2,7 @@
|
||||
var sdk = require("../..");
|
||||
var Room = sdk.Room;
|
||||
var RoomState = sdk.RoomState;
|
||||
var MatrixEvent = sdk.MatrixEvent;
|
||||
var utils = require("../test-utils");
|
||||
|
||||
describe("Room", function() {
|
||||
@@ -20,6 +21,43 @@ describe("Room", function() {
|
||||
room.currentState = utils.mock(sdk.RoomState, "currentState");
|
||||
});
|
||||
|
||||
describe("getAvatarUrl", function() {
|
||||
var hsUrl = "https://my.home.server";
|
||||
|
||||
it("should return the URL from m.room.avatar preferentially", function() {
|
||||
room.currentState.getStateEvents.andCallFake(function(type, key) {
|
||||
if (type === "m.room.avatar" && key === "") {
|
||||
return utils.mkEvent({
|
||||
event: true,
|
||||
type: "m.room.avatar",
|
||||
skey: "",
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
url: "mxc://flibble/wibble"
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
var url = room.getAvatarUrl(hsUrl);
|
||||
// we don't care about how the mxc->http conversion is done, other
|
||||
// than it contains the mxc body.
|
||||
expect(url.indexOf("flibble/wibble")).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("should return an identicon HTTP URL if allowDefault was set and there " +
|
||||
"was no m.room.avatar event", function() {
|
||||
var url = room.getAvatarUrl(hsUrl, 64, 64, "crop", true);
|
||||
expect(url.indexOf("http")).toEqual(0); // don't care about form
|
||||
});
|
||||
|
||||
it("should return nothing if there is no m.room.avatar and allowDefault=false",
|
||||
function() {
|
||||
var url = room.getAvatarUrl(hsUrl, 64, 64, "crop", false);
|
||||
expect(url).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getMember", function() {
|
||||
beforeEach(function() {
|
||||
// clobber members property with test data
|
||||
@@ -337,7 +375,7 @@ describe("Room", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("recalculate (Room Name)", function() {
|
||||
describe("recalculate", function() {
|
||||
var stateLookup = {
|
||||
// event.type + "$" event.state_key : MatrixEvent
|
||||
};
|
||||
@@ -404,149 +442,348 @@ describe("Room", function() {
|
||||
});
|
||||
});
|
||||
|
||||
it("should return the names of members in a private (invite join_rules)" +
|
||||
" room if a room name and alias don't exist and there are >3 members.",
|
||||
function() {
|
||||
setJoinRule("invite");
|
||||
addMember(userA);
|
||||
addMember(userB);
|
||||
addMember(userC);
|
||||
addMember(userD);
|
||||
room.recalculate(userA);
|
||||
var name = room.name;
|
||||
// we expect at least 1 member to be mentioned
|
||||
var others = [userB, userC, userD];
|
||||
var found = false;
|
||||
for (var i = 0; i < others.length; i++) {
|
||||
if (name.indexOf(others[i]) !== -1) {
|
||||
found = true;
|
||||
break;
|
||||
describe("Room.recalculate => Stripped State Events", function() {
|
||||
it("should set stripped state events as actual state events if the " +
|
||||
"room is an invite room", function() {
|
||||
var roomName = "flibble";
|
||||
|
||||
addMember(userA, "invite");
|
||||
stateLookup["m.room.member$" + userA].event.invite_room_state = [
|
||||
{
|
||||
type: "m.room.name",
|
||||
state_key: "",
|
||||
content: {
|
||||
name: roomName
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
room.recalculate(userA);
|
||||
expect(room.currentState.setStateEvents).toHaveBeenCalled();
|
||||
// first call, first arg (which is an array), first element in array
|
||||
var fakeEvent = room.currentState.setStateEvents.calls[0].args[0][0];
|
||||
expect(fakeEvent.getContent()).toEqual({
|
||||
name: roomName
|
||||
});
|
||||
});
|
||||
|
||||
it("should not clobber state events if it isn't an invite room", function() {
|
||||
addMember(userA, "join");
|
||||
stateLookup["m.room.member$" + userA].event.invite_room_state = [
|
||||
{
|
||||
type: "m.room.name",
|
||||
state_key: "",
|
||||
content: {
|
||||
name: "flibble"
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
room.recalculate(userA);
|
||||
expect(room.currentState.setStateEvents).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Room.recalculate => Room Name", function() {
|
||||
|
||||
it("should return the names of members in a private (invite join_rules)" +
|
||||
" room if a room name and alias don't exist and there are >3 members.",
|
||||
function() {
|
||||
setJoinRule("invite");
|
||||
addMember(userA);
|
||||
addMember(userB);
|
||||
addMember(userC);
|
||||
addMember(userD);
|
||||
room.recalculate(userA);
|
||||
var name = room.name;
|
||||
// we expect at least 1 member to be mentioned
|
||||
var others = [userB, userC, userD];
|
||||
var found = false;
|
||||
for (var i = 0; i < others.length; i++) {
|
||||
if (name.indexOf(others[i]) !== -1) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(found).toEqual(true, name);
|
||||
});
|
||||
expect(found).toEqual(true, name);
|
||||
});
|
||||
|
||||
it("should return the names of members in a private (invite join_rules)" +
|
||||
" room if a room name and alias don't exist and there are >2 members.",
|
||||
function() {
|
||||
setJoinRule("invite");
|
||||
addMember(userA);
|
||||
addMember(userB);
|
||||
addMember(userC);
|
||||
room.recalculate(userA);
|
||||
var name = room.name;
|
||||
expect(name.indexOf(userB)).not.toEqual(-1, name);
|
||||
expect(name.indexOf(userC)).not.toEqual(-1, name);
|
||||
});
|
||||
it("should return the names of members in a private (invite join_rules)" +
|
||||
" room if a room name and alias don't exist and there are >2 members.",
|
||||
function() {
|
||||
setJoinRule("invite");
|
||||
addMember(userA);
|
||||
addMember(userB);
|
||||
addMember(userC);
|
||||
room.recalculate(userA);
|
||||
var name = room.name;
|
||||
expect(name.indexOf(userB)).not.toEqual(-1, name);
|
||||
expect(name.indexOf(userC)).not.toEqual(-1, name);
|
||||
});
|
||||
|
||||
it("should return the names of members in a public (public join_rules)" +
|
||||
" room if a room name and alias don't exist and there are >2 members.",
|
||||
function() {
|
||||
setJoinRule("public");
|
||||
addMember(userA);
|
||||
addMember(userB);
|
||||
addMember(userC);
|
||||
room.recalculate(userA);
|
||||
var name = room.name;
|
||||
expect(name.indexOf(userB)).not.toEqual(-1, name);
|
||||
expect(name.indexOf(userC)).not.toEqual(-1, name);
|
||||
});
|
||||
it("should return the names of members in a public (public join_rules)" +
|
||||
" room if a room name and alias don't exist and there are >2 members.",
|
||||
function() {
|
||||
setJoinRule("public");
|
||||
addMember(userA);
|
||||
addMember(userB);
|
||||
addMember(userC);
|
||||
room.recalculate(userA);
|
||||
var name = room.name;
|
||||
expect(name.indexOf(userB)).not.toEqual(-1, name);
|
||||
expect(name.indexOf(userC)).not.toEqual(-1, name);
|
||||
});
|
||||
|
||||
it("should show the other user's name for public (public join_rules)" +
|
||||
" rooms if a room name and alias don't exist and it is a 1:1-chat.",
|
||||
function() {
|
||||
setJoinRule("public");
|
||||
addMember(userA);
|
||||
addMember(userB);
|
||||
room.recalculate(userA);
|
||||
var name = room.name;
|
||||
expect(name.indexOf(userB)).not.toEqual(-1, name);
|
||||
});
|
||||
it("should show the other user's name for public (public join_rules)" +
|
||||
" rooms if a room name and alias don't exist and it is a 1:1-chat.",
|
||||
function() {
|
||||
setJoinRule("public");
|
||||
addMember(userA);
|
||||
addMember(userB);
|
||||
room.recalculate(userA);
|
||||
var name = room.name;
|
||||
expect(name.indexOf(userB)).not.toEqual(-1, name);
|
||||
});
|
||||
|
||||
it("should show the other user's name for private " +
|
||||
"(invite join_rules) rooms if a room name and alias don't exist and it" +
|
||||
" is a 1:1-chat.", function() {
|
||||
setJoinRule("invite");
|
||||
addMember(userA);
|
||||
addMember(userB);
|
||||
room.recalculate(userA);
|
||||
var name = room.name;
|
||||
expect(name.indexOf(userB)).not.toEqual(-1, name);
|
||||
});
|
||||
it("should show the other user's name for private " +
|
||||
"(invite join_rules) rooms if a room name and alias don't exist and it" +
|
||||
" is a 1:1-chat.", function() {
|
||||
setJoinRule("invite");
|
||||
addMember(userA);
|
||||
addMember(userB);
|
||||
room.recalculate(userA);
|
||||
var name = room.name;
|
||||
expect(name.indexOf(userB)).not.toEqual(-1, name);
|
||||
});
|
||||
|
||||
it("should show the other user's name for private" +
|
||||
" (invite join_rules) rooms if you are invited to it.", function() {
|
||||
setJoinRule("invite");
|
||||
addMember(userA, "invite");
|
||||
addMember(userB);
|
||||
room.recalculate(userA);
|
||||
var name = room.name;
|
||||
expect(name.indexOf(userB)).not.toEqual(-1, name);
|
||||
});
|
||||
it("should show the other user's name for private" +
|
||||
" (invite join_rules) rooms if you are invited to it.", function() {
|
||||
setJoinRule("invite");
|
||||
addMember(userA, "invite");
|
||||
addMember(userB);
|
||||
room.recalculate(userA);
|
||||
var name = room.name;
|
||||
expect(name.indexOf(userB)).not.toEqual(-1, name);
|
||||
});
|
||||
|
||||
it("should show the room alias if one exists for private " +
|
||||
"(invite join_rules) rooms if a room name doesn't exist.", function() {
|
||||
var alias = "#room_alias:here";
|
||||
setJoinRule("invite");
|
||||
setAliases([alias, "#another:one"]);
|
||||
room.recalculate(userA);
|
||||
var name = room.name;
|
||||
expect(name).toEqual(alias);
|
||||
});
|
||||
it("should show the room alias if one exists for private " +
|
||||
"(invite join_rules) rooms if a room name doesn't exist.", function() {
|
||||
var alias = "#room_alias:here";
|
||||
setJoinRule("invite");
|
||||
setAliases([alias, "#another:one"]);
|
||||
room.recalculate(userA);
|
||||
var name = room.name;
|
||||
expect(name).toEqual(alias);
|
||||
});
|
||||
|
||||
it("should show the room alias if one exists for public " +
|
||||
"(public join_rules) rooms if a room name doesn't exist.", function() {
|
||||
var alias = "#room_alias:here";
|
||||
setJoinRule("public");
|
||||
setAliases([alias, "#another:one"]);
|
||||
room.recalculate(userA);
|
||||
var name = room.name;
|
||||
expect(name).toEqual(alias);
|
||||
});
|
||||
it("should show the room alias if one exists for public " +
|
||||
"(public join_rules) rooms if a room name doesn't exist.", function() {
|
||||
var alias = "#room_alias:here";
|
||||
setJoinRule("public");
|
||||
setAliases([alias, "#another:one"]);
|
||||
room.recalculate(userA);
|
||||
var name = room.name;
|
||||
expect(name).toEqual(alias);
|
||||
});
|
||||
|
||||
it("should show the room name if one exists for private " +
|
||||
"(invite join_rules) rooms.", function() {
|
||||
var roomName = "A mighty name indeed";
|
||||
setJoinRule("invite");
|
||||
setRoomName(roomName);
|
||||
room.recalculate(userA);
|
||||
var name = room.name;
|
||||
expect(name).toEqual(roomName);
|
||||
});
|
||||
it("should show the room name if one exists for private " +
|
||||
"(invite join_rules) rooms.", function() {
|
||||
var roomName = "A mighty name indeed";
|
||||
setJoinRule("invite");
|
||||
setRoomName(roomName);
|
||||
room.recalculate(userA);
|
||||
var name = room.name;
|
||||
expect(name).toEqual(roomName);
|
||||
});
|
||||
|
||||
it("should show the room name if one exists for public " +
|
||||
"(public join_rules) rooms.", function() {
|
||||
var roomName = "A mighty name indeed";
|
||||
setJoinRule("public");
|
||||
setRoomName(roomName);
|
||||
room.recalculate(userA);
|
||||
var name = room.name;
|
||||
expect(name).toEqual(roomName);
|
||||
});
|
||||
it("should show the room name if one exists for public " +
|
||||
"(public join_rules) rooms.", function() {
|
||||
var roomName = "A mighty name indeed";
|
||||
setJoinRule("public");
|
||||
setRoomName(roomName);
|
||||
room.recalculate(userA);
|
||||
var name = room.name;
|
||||
expect(name).toEqual(roomName);
|
||||
});
|
||||
|
||||
it("should show your name for private (invite join_rules) rooms if" +
|
||||
" a room name and alias don't exist and it is a self-chat.", function() {
|
||||
setJoinRule("invite");
|
||||
addMember(userA);
|
||||
room.recalculate(userA);
|
||||
var name = room.name;
|
||||
expect(name).toEqual(userA);
|
||||
});
|
||||
it("should show your name for private (invite join_rules) rooms if" +
|
||||
" a room name and alias don't exist and it is a self-chat.", function() {
|
||||
setJoinRule("invite");
|
||||
addMember(userA);
|
||||
room.recalculate(userA);
|
||||
var name = room.name;
|
||||
expect(name).toEqual(userA);
|
||||
});
|
||||
|
||||
it("should show your name for public (public join_rules) rooms if a" +
|
||||
" room name and alias don't exist and it is a self-chat.", function() {
|
||||
setJoinRule("public");
|
||||
addMember(userA);
|
||||
room.recalculate(userA);
|
||||
var name = room.name;
|
||||
expect(name).toEqual(userA);
|
||||
});
|
||||
it("should show your name for public (public join_rules) rooms if a" +
|
||||
" room name and alias don't exist and it is a self-chat.", function() {
|
||||
setJoinRule("public");
|
||||
addMember(userA);
|
||||
room.recalculate(userA);
|
||||
var name = room.name;
|
||||
expect(name).toEqual(userA);
|
||||
});
|
||||
|
||||
it("should return '?' if there is no name, alias or members in the room.",
|
||||
function() {
|
||||
room.recalculate(userA);
|
||||
var name = room.name;
|
||||
expect(name).toEqual("?");
|
||||
});
|
||||
|
||||
it("should return '?' if there is no name, alias or members in the room.",
|
||||
function() {
|
||||
room.recalculate(userA);
|
||||
var name = room.name;
|
||||
expect(name).toEqual("?");
|
||||
});
|
||||
});
|
||||
|
||||
describe("receipts", function() {
|
||||
|
||||
var eventToAck = utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "PLEASE ACKNOWLEDGE MY EXISTENCE",
|
||||
event: true
|
||||
});
|
||||
|
||||
function mkReceipt(roomId, records) {
|
||||
var content = {};
|
||||
records.forEach(function(r) {
|
||||
if (!content[r.eventId]) { content[r.eventId] = {}; }
|
||||
if (!content[r.eventId][r.type]) { content[r.eventId][r.type] = {}; }
|
||||
content[r.eventId][r.type][r.userId] = {
|
||||
ts: r.ts
|
||||
};
|
||||
});
|
||||
return new MatrixEvent({
|
||||
content: content,
|
||||
room_id: roomId,
|
||||
type: "m.receipt"
|
||||
});
|
||||
}
|
||||
|
||||
function mkRecord(eventId, type, userId, ts) {
|
||||
ts = ts || Date.now();
|
||||
return {
|
||||
eventId: eventId,
|
||||
type: type,
|
||||
userId: userId,
|
||||
ts: ts
|
||||
};
|
||||
}
|
||||
|
||||
describe("addReceipt", function() {
|
||||
|
||||
it("should store the receipt so it can be obtained via getReceiptsForEvent",
|
||||
function() {
|
||||
var ts = 13787898424;
|
||||
room.addReceipt(mkReceipt(roomId, [
|
||||
mkRecord(eventToAck.getId(), "m.read", userB, ts)
|
||||
]));
|
||||
expect(room.getReceiptsForEvent(eventToAck)).toEqual([{
|
||||
type: "m.read",
|
||||
userId: userB,
|
||||
data: {
|
||||
ts: ts
|
||||
}
|
||||
}]);
|
||||
});
|
||||
|
||||
it("should clobber receipts based on type and user ID", function() {
|
||||
var nextEventToAck = utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "I AM HERE YOU KNOW",
|
||||
event: true
|
||||
});
|
||||
var ts = 13787898424;
|
||||
room.addReceipt(mkReceipt(roomId, [
|
||||
mkRecord(eventToAck.getId(), "m.read", userB, ts)
|
||||
]));
|
||||
var ts2 = 13787899999;
|
||||
room.addReceipt(mkReceipt(roomId, [
|
||||
mkRecord(nextEventToAck.getId(), "m.read", userB, ts2)
|
||||
]));
|
||||
expect(room.getReceiptsForEvent(eventToAck)).toEqual([]);
|
||||
expect(room.getReceiptsForEvent(nextEventToAck)).toEqual([{
|
||||
type: "m.read",
|
||||
userId: userB,
|
||||
data: {
|
||||
ts: ts2
|
||||
}
|
||||
}]);
|
||||
});
|
||||
|
||||
it("should persist multiple receipts for a single event ID", function() {
|
||||
var ts = 13787898424;
|
||||
room.addReceipt(mkReceipt(roomId, [
|
||||
mkRecord(eventToAck.getId(), "m.read", userB, ts),
|
||||
mkRecord(eventToAck.getId(), "m.read", userC, ts),
|
||||
mkRecord(eventToAck.getId(), "m.read", userD, ts)
|
||||
]));
|
||||
expect(room.getUsersReadUpTo(eventToAck)).toEqual(
|
||||
[userB, userC, userD]
|
||||
);
|
||||
});
|
||||
|
||||
it("should persist multiple receipts for a single receipt type", function() {
|
||||
var eventTwo = utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "2222",
|
||||
event: true
|
||||
});
|
||||
var eventThree = utils.mkMessage({
|
||||
room: roomId, user: userA, msg: "3333",
|
||||
event: true
|
||||
});
|
||||
var ts = 13787898424;
|
||||
room.addReceipt(mkReceipt(roomId, [
|
||||
mkRecord(eventToAck.getId(), "m.read", userB, ts),
|
||||
mkRecord(eventTwo.getId(), "m.read", userC, ts),
|
||||
mkRecord(eventThree.getId(), "m.read", userD, ts)
|
||||
]));
|
||||
expect(room.getUsersReadUpTo(eventToAck)).toEqual([userB]);
|
||||
expect(room.getUsersReadUpTo(eventTwo)).toEqual([userC]);
|
||||
expect(room.getUsersReadUpTo(eventThree)).toEqual([userD]);
|
||||
});
|
||||
|
||||
it("should persist multiple receipts for a single user ID", function() {
|
||||
room.addReceipt(mkReceipt(roomId, [
|
||||
mkRecord(eventToAck.getId(), "m.delivered", userB, 13787898424),
|
||||
mkRecord(eventToAck.getId(), "m.read", userB, 22222222),
|
||||
mkRecord(eventToAck.getId(), "m.seen", userB, 33333333),
|
||||
]));
|
||||
expect(room.getReceiptsForEvent(eventToAck)).toEqual([
|
||||
{
|
||||
type: "m.delivered",
|
||||
userId: userB,
|
||||
data: {
|
||||
ts: 13787898424
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "m.read",
|
||||
userId: userB,
|
||||
data: {
|
||||
ts: 22222222
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "m.seen",
|
||||
userId: userB,
|
||||
data: {
|
||||
ts: 33333333
|
||||
}
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("getUsersReadUpTo", function() {
|
||||
|
||||
it("should return user IDs read up to the given event", function() {
|
||||
var ts = 13787898424;
|
||||
room.addReceipt(mkReceipt(roomId, [
|
||||
mkRecord(eventToAck.getId(), "m.read", userB, ts)
|
||||
]));
|
||||
expect(room.getUsersReadUpTo(eventToAck)).toEqual([userB]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user