Compare commits

...

58 Commits

Author SHA1 Message Date
Kegan Dougal 7cad5a0479 Merge branch 'develop' 2015-10-28 16:49:11 +00:00
Kegan Dougal 83c53f6a79 Fix doc typo 2015-10-28 16:48:08 +00:00
Kegan Dougal ae13ed7ded Add disclaimer to screensharing 2015-10-28 16:45:07 +00:00
Kegan Dougal b17385120a Bump to 0.3.0 and add CHANGELOG 2015-10-28 16:42:44 +00:00
Kegsay cc0d8da416 Merge pull request #32 from matrix-org/member-info-for-invites
Retrieving profile info for invites
2015-10-26 16:42:21 +00:00
Kegsay c796702eba Merge pull request #31 from matrix-org/search-api
Add search functions and tests
2015-10-26 16:36:15 +00:00
Kegan Dougal 2675442ced Line lengths 2015-10-26 16:31:10 +00:00
Kegan Dougal aa3e6514c6 Add test for firing (pew pew) of events 2015-10-26 16:30:15 +00:00
Kegan Dougal be6d64fbfd Add integration tests; fix bugs. 2015-10-26 16:12:06 +00:00
Kegan Dougal 4cbab72369 Resolve invites to profile info
This is so inviters/invitees have a display name and avatar_url if they have
set one. This info isn't contained in the m.room.member event so we get it
direct from /profile.

This is gated behind `resolveInvitesToProfiles` on `startClient(opts)`.
2015-10-26 15:27:44 +00:00
Kegan Dougal 0227b1c68d Add search functions and tests 2015-10-26 13:27:45 +00:00
Matthew Hodgson 4c051202af s/getMembersWithMemership/getMembersWithMembership/g 2015-10-24 01:45:02 +01:00
Matthew Hodgson 981b9e0595 Merge branch 'screen-sharing' into develop 2015-10-23 12:58:55 +01:00
Matthew Hodgson 9e719ba31e drop res back to 640x360 as 1024x576 gave us the wrong aspect ratio 2015-10-23 12:57:46 +01:00
Kegan Dougal c65f576f8d More logging 2015-10-21 17:15:26 +01:00
Kegan Dougal 2c805bbece More paranoia when handling responses 2015-10-21 16:00:31 +01:00
Kegan Dougal 02b836698c More clarity on cache updating 2015-10-21 14:07:14 +01:00
Kegsay 25112ede58 Merge pull request #30 from matrix-org/set-state-events-perf
Change calculating display names from O(n^2) to O(n)
2015-10-21 13:47:54 +01:00
Kegan Dougal 5888c8a56c Commenting on splice 2015-10-21 13:47:23 +01:00
Kegan Dougal 1cee7bf397 JSDoc 2015-10-21 13:30:32 +01:00
Kegan Dougal cab7a71a94 Change calculating display names from O(n^2) to O(n)
Reduces initial sync times from ~30s to ~1s on accounts with heavily
populated rooms.

The problem was that f.e. RoomMember it would try to calculate the
display name, which involved looping each RoomMember to get their
display name to check for disambiguation. We now cache display names
to user IDs so we don't need to loop every member when disambiguating.
2015-10-21 13:25:23 +01:00
Matthew Hodgson d7c63e3487 Merge pull request #29 from matrix-org/screen-sharing
Basic screen-sharing support; adds a screensharing stream into the call and adds support for playback of both an audio-only stream (i.e. the voice-over) alongside an AV stream like the screenshare.
2015-10-21 01:45:38 +01:00
Matthew Hodgson bff749fd50 fix linter 2015-10-21 01:44:54 +01:00
Matthew Hodgson 5c286352cb improve constraints a bit; fix comments; try to stop sharing more aggressively 2015-10-21 01:40:20 +01:00
Matthew Hodgson 9ec3504c72 dial down logging 2015-10-21 01:40:01 +01:00
Matthew Hodgson 26b3e32ca2 add the concept of a dedicated remote audio element used for playing back audio-only streams (i.e. voice calls, and the voice stream that accompanies a screenshare). Correctly tidy up screen capture calls. 2015-10-21 01:18:55 +01:00
Kegan Dougal 4e2c83cc08 Debug logging 2015-10-20 17:21:25 +01:00
Kegan Dougal 17def14eba Get screen-sharing with audio working 2015-10-20 16:43:51 +01:00
Kegan Dougal f260de573b Add right constraints to get screen-sharing working
Requires --enable-usermedia-screen-capturing flag on chrome enabled.
2015-10-20 15:11:17 +01:00
David Baker 4fd45ab278 Merge pull request #28 from matrix-org/voip-mute
VoIP local muting
2015-10-20 10:33:24 +01:00
Kegsay 4a2e9eb927 Merge pull request #27 from matrix-org/room-avatars
Room avatars
2015-10-19 16:51:30 +01:00
Kegan Dougal dd8adef9ed Remove unused args 2015-10-19 16:50:16 +01:00
Kegan Dougal 9164debf03 Add the same for video 2015-10-19 16:48:47 +01:00
Kegan Dougal 534bef8632 Add MatrixCall.isMicrophoneMuted() 2015-10-19 16:28:01 +01:00
Kegan Dougal d8c43d02ba Add MatrixCall.setMicrophoneMuted 2015-10-19 16:21:13 +01:00
Kegsay ae3738f822 Formatting 2015-10-19 15:39:23 +01:00
Kegan Dougal be621e1aa7 Add breaking changes to CHANGELOG 2015-10-19 15:38:39 +01:00
Kegan Dougal 343d63a28a Merge branch 'develop' into room-avatars 2015-10-19 15:33:42 +01:00
Kegsay 0a28d6e950 Merge pull request #26 from matrix-org/invite-room-state
Invite room state
2015-10-19 15:31:45 +01:00
Kegsay b493a62afa Merge pull request #25 from matrix-org/initial-sync-improvements
Add support for archived=true in initial sync
2015-10-19 15:31:32 +01:00
Kegsay 37a8c9bd72 Merge pull request #23 from matrix-org/read_receipts
Receipts
2015-10-19 15:30:23 +01:00
Kegan Dougal a9c4345159 Clarify the link is the source of the code 2015-10-19 15:29:57 +01:00
Kegsay 5f1153b43f Merge pull request #24 from matrix-org/canonical-alias
Look for a canonical alias when determining the room name
2015-10-19 15:28:33 +01:00
Kegan Dougal 2c213f88d9 Units! Tests! Linting! 2015-10-19 15:24:24 +01:00
Kegan Dougal a236219111 ContentRepo unit tests 2015-10-19 15:00:06 +01:00
Kegan Dougal 2f9958cca9 JSDoc linkify 2015-10-19 14:37:17 +01:00
Kegan Dougal f26154d0ac Add support for m.room.avatar: refactor avatar URLs
BREAKING CHANGE.

Scope each "getAvatarUrl" to be instance methods on the entity it
relates to (Room and RoomMember respectively). By doing this, we
can actually pull out specific state such as the `m.room.avatar`
event more easily rather than keeping it in the global cesspit
of `MatrixClient`.

This was complicated by `getHttpUriForMxc` and `getIdenticonUri`
which were attached to the HTTP API to pull out the `baseUrl` when
crafting the URL. Pull out this dependency out and explicitly pass
it in when crafting the URL. This is trivial to get from
`MatrixClient.getHomeserverUrl()`.
2015-10-19 14:14:34 +01:00
Kegan Dougal 5ae87b7c95 Bug fixes and unit tests 2015-10-16 17:27:05 +01:00
Kegan Dougal 219103a4e2 Yank out invite event from initialSync. Set stripped state events when recalculating invited rooms. 2015-10-16 17:07:04 +01:00
Kegan Dougal 4ec7b9bb3f Add support for archived=true in initial sync
Make MatrixClient.startClient take 'opts' instead of 'historyLen' in
a backwards compatible way. Add 'includeArchivedRooms' as an option.
2015-10-16 15:00:26 +01:00
Kegan Dougal bad8b7fb76 Look for a canonical alias when determining the room name 2015-10-16 14:30:21 +01:00
Kegan Dougal a101857cb6 Add integration tests for read receipts 2015-10-16 13:51:44 +01:00
Kegan Dougal a52f92830a Implement unit tests for read receipts. 2015-10-16 13:37:53 +01:00
Kegan Dougal 40d113a423 Pass in receipts from initialSync 2015-10-16 11:54:47 +01:00
Kegan Dougal 7ec8421d19 Fix linting errors 2015-10-16 11:38:49 +01:00
Kegan Dougal 9048efeb65 Implement receipt handling and expose new Room functions
Add polyfills for Array.map/filter according to MDN because it looks much
better than the utils format.

Add stub tests for edge cases and implement test for the common case.
2015-10-16 11:32:27 +01:00
Kegan Dougal 43fc200dae Read receipt HTTP API tweaks 2015-10-16 09:36:13 +01:00
David Baker 6679e93afc Add untested read receipt sending method 2015-10-16 09:12:50 +01:00
19 changed files with 1784 additions and 336 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
"nonew": true,
"curly": true,
"forin": true,
"freeze": true,
"freeze": false,
"undef": true,
"unused": "vars"
}
+60 -7
View File
@@ -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
+3
View File
@@ -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
View File
@@ -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) {
+81
View File
@@ -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
View File
@@ -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'));
+2
View File
@@ -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
View File
@@ -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;
}
+43
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>&lt;video&gt;</code> DOM element
* to render video to.
* @param {Element} localVideoElement a <code>&lt;video&gt;</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>&lt;video&gt;</code> DOM element.
* @return {Element} The dom element
@@ -95,13 +138,23 @@ MatrixCall.prototype.getLocalVideoElement = function() {
};
/**
* Retrieve the remote <code>&lt;video&gt;</code> DOM element.
* Retrieve the remote <code>&lt;video&gt;</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>&lt;audio&gt;</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>&lt;video&gt;</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>&lt;video&gt;</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>&lt;video&gt;</code> DOM element.
*/
MatrixCall.prototype.setRemoteVideoElement = function(element) {
@@ -134,6 +187,16 @@ MatrixCall.prototype.setRemoteVideoElement = function(element) {
_tryPlayRemoteStream(this);
};
/**
* Set the remote <code>&lt;audio&gt;</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>&lt;video&gt;</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
View File
@@ -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": {
+44
View File
@@ -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();
});
});
});
});
+270 -5
View File
@@ -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() {
+1
View File
@@ -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);
};
}
+83
View File
@@ -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"
);
});
});
});
+37
View File
@@ -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
View File
@@ -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]);
});
});
});
});