1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-12-23 22:42:10 +03:00

Merge branch 'develop' into rav/context

Conflicts:
	lib/models/room.js
This commit is contained in:
Richard van der Hoff
2016-01-25 10:45:22 +00:00
9 changed files with 373 additions and 71 deletions

View File

@@ -287,7 +287,7 @@ MatrixClient.prototype.retryImmediately = function() {
// stop waiting
clearTimeout(this._syncingRetry.timeoutId);
// invoke immediately
this._syncingRetry.fn();
this._syncingRetry.fn(2); // FIXME: It shouldn't need to know about attempts :/
this._syncingRetry = null;
return true;
};
@@ -616,6 +616,14 @@ MatrixClient.prototype.getUser = function(userId) {
return this.store.getUser(userId);
};
/**
* Retrieve all known users.
* @return {User[]} A list of users, or an empty list if there is no data store.
*/
MatrixClient.prototype.getUsers = function() {
return this.store.getUsers();
};
// Room operations
// ===============
@@ -1466,6 +1474,23 @@ MatrixClient.prototype.createAlias = function(alias, roomId, callback) {
);
};
/**
* Delete an alias to room ID mapping. This alias must be on your local server
* and you must have sufficient access to do this operation.
* @param {string} alias The room alias to delete.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.deleteAlias = function(alias, callback) {
var path = utils.encodeUri("/directory/room/$alias", {
$alias: alias
});
return this._http.authedRequest(
callback, "DELETE", path, undefined, undefined
);
};
/**
* Get room info for the given alias.
* @param {string} alias The room alias to resolve.
@@ -2384,7 +2409,7 @@ MatrixClient.prototype.loginWithToken = function(token, callback) {
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.pushRules = function(callback) {
MatrixClient.prototype.getPushRules = function(callback) {
return this._http.authedRequest(callback, "GET", "/pushrules/");
};
@@ -2425,6 +2450,129 @@ MatrixClient.prototype.deletePushRule = function(scope, kind, ruleId, callback)
return this._http.authedRequest(callback, "DELETE", path);
};
/**
* Enable or disable a push notification rule.
* @param {string} scope
* @param {string} kind
* @param {string} ruleId
* @param {boolean} enabled
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: result object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.setPushRuleEnabled = function(scope, kind,
ruleId, enabled, callback) {
var path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/enabled", {
$kind: kind,
$ruleId: ruleId
});
return this._http.authedRequest(
callback, "PUT", path, undefined, {"enabled": enabled}
);
};
/**
* Get the room-kind push rule associated with a room.
* @param {string} scope "global" or device-specific.
* @param {string} roomId the id of the room.
* @return {object} the rule or undefined.
*/
MatrixClient.prototype.getRoomPushRule = function(scope, roomId) {
// There can be only room-kind push rule per room
// and its id is the room id.
if (this.pushRules) {
for (var i = 0; i < this.pushRules[scope].room.length; i++) {
var rule = this.pushRules[scope].room[i];
if (rule.rule_id === roomId) {
return rule;
}
}
}
else {
throw new Error(
"SyncApi.sync() must be done before accessing to push rules."
);
}
};
/**
* Set a room-kind muting push rule in a room.
* The operation also updates MatrixClient.pushRules at the end.
* @param {string} scope "global" or device-specific.
* @param {string} roomId the id of the room.
* @param {string} mute the mute state.
* @return {module:client.Promise} Resolves: result object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.setRoomMutePushRule = function(scope, roomId, mute) {
var self = this;
var deferred, hasDontNotifyRule;
// Get the existing room-kind push rule if any
var roomPushRule = this.getRoomPushRule(scope, roomId);
if (roomPushRule) {
if (0 <= roomPushRule.actions.indexOf("dont_notify")) {
hasDontNotifyRule = true;
}
}
if (!mute) {
// Remove the rule only if it is a muting rule
if (hasDontNotifyRule) {
deferred = this.deletePushRule(scope, "room", roomPushRule.rule_id);
}
}
else {
if (!roomPushRule) {
deferred = this.addPushRule(scope, "room", roomId, {
actions: ["dont_notify"]
});
}
else if (!hasDontNotifyRule) {
// Remove the existing one before setting the mute push rule
// This is a workaround to SYN-590 (Push rule update fails)
deferred = q.defer();
this.deletePushRule(scope, "room", roomPushRule.rule_id)
.done(function() {
self.addPushRule(scope, "room", roomId, {
actions: ["dont_notify"]
}).done(function() {
deferred.resolve();
}, function(err) {
deferred.reject(err);
});
}, function(err) {
deferred.reject(err);
});
deferred = deferred.promise;
}
}
if (deferred) {
// Update this.pushRules when the operation completes
var ruleRefreshDeferred = q.defer();
deferred.done(function() {
self.getPushRules().done(function(result) {
self.pushRules = result;
ruleRefreshDeferred.resolve();
}, function(err) {
ruleRefreshDeferred.reject(err);
});
}, function(err) {
// Update it even if the previous operation fails. This can help the
// app to recover when push settings has been modifed from another client
self.getPushRules().done(function(result) {
self.pushRules = result;
ruleRefreshDeferred.reject(err);
}, function(err2) {
ruleRefreshDeferred.reject(err);
});
});
return ruleRefreshDeferred.promise;
}
};
/**
* Perform a server-side search for messages containing the given text.
* @param {Object} opts Options for the search.

View File

@@ -43,6 +43,11 @@ module.exports.PREFIX_V2_ALPHA = "/_matrix/client/v2_alpha";
*/
module.exports.PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1";
/**
* A constant representing the URI path for release 0 of the Client-Server HTTP API.
*/
module.exports.PREFIX_R0 = "/_matrix/client/r0";
/**
* Construct a MatrixHttpApi.
* @constructor
@@ -416,8 +421,10 @@ module.exports.MatrixHttpApi.prototype = {
}, localTimeoutMs);
}
var reqPromise = defer.promise;
try {
this.opts.request(
var req = this.opts.request(
{
uri: uri,
method: method,
@@ -425,6 +432,7 @@ module.exports.MatrixHttpApi.prototype = {
qs: queryParams,
body: data,
json: true,
timeout: localTimeoutMs,
_matrix_opts: this.opts
},
function(err, response, body) {
@@ -438,6 +446,11 @@ module.exports.MatrixHttpApi.prototype = {
handlerFn(err, response, body);
}
);
if (req && req.abort) {
// FIXME: This is EVIL, but I can't think of a better way to expose
// abort() operations on underlying HTTP requests :(
reqPromise.abort = req.abort.bind(req);
}
}
catch (ex) {
defer.reject(ex);
@@ -445,7 +458,7 @@ module.exports.MatrixHttpApi.prototype = {
callback(ex);
}
}
return defer.promise;
return reqPromise;
}
};

View File

@@ -139,6 +139,8 @@ function Room(roomId, opts) {
// }]
};
this._notificationCounts = {};
// just a list - *not* ordered.
this._timelines = [];
this._eventIdToTimeline = {};
@@ -200,6 +202,26 @@ Room.prototype.getTimelineForEvent = function(eventId) {
return this._eventIdToTimeline[eventId];
};
/*
* Get one of the notification counts for this room
* @param {String} type The type of notification count to get. default: 'total'
* @return {Number} The notification count, or undefined if there is no count
* for this type.
*/
Room.prototype.getUnreadNotificationCount = function(type) {
type = type || 'total';
return this._notificationCounts[type];
};
/**
* Set one of the notification counts for this room
* @param {String} type The type of notification count to set.
* @param {Number} count The new count
*/
Room.prototype.setUnreadNotificationCount = function(type, count) {
this._notificationCounts[type] = count;
};
/**
* Get the avatar URL for a room if one was set.
* @param {String} baseUrl The homeserver base URL. See
@@ -286,9 +308,11 @@ Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod,
* @return {boolean} True if this user_id has the given membership state.
*/
Room.prototype.hasMembershipState = function(userId, membership) {
return utils.filter(this.currentState.getMembers(), function(m) {
return m.membership === membership && m.userId === userId;
}).length > 0;
var member = this.getMember(userId);
if (!member) {
return false;
}
return member.membership === membership;
};
/**
@@ -941,7 +965,7 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) {
return (m.membership !== "leave");
});
if (memberList.length === 1) {
// we exist, but no one else... self-chat or invite.
// self-chat, peeked room with 1 participant, or invite.
if (memberList[0].membership === "invite") {
if (memberList[0].events.member) {
// extract who invited us to the room
@@ -952,12 +976,17 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) {
}
}
else {
return userId;
if (memberList[0].userId === userId) {
return "Empty room";
}
else {
return memberList[0].name;
}
}
}
else {
// there really isn't anyone in this room...
return "?";
return "Empty room";
}
}
else if (members.length === 1) {

View File

@@ -89,6 +89,32 @@ User.prototype.setPresenceEvent = function(event) {
}
};
/**
* Manually set this user's display name. No event is emitted in response to this
* as there is no underlying MatrixEvent to emit with.
* @param {string} name The new display name.
*/
User.prototype.setDisplayName = function(name) {
var oldName = this.displayName;
this.displayName = name;
if (name !== oldName) {
this._updateModifiedTime();
}
};
/**
* Manually set this user's avatar URL. No event is emitted in response to this
* as there is no underlying MatrixEvent to emit with.
* @param {string} url The new avatar URL.
*/
User.prototype.setAvatarUrl = function(url) {
var oldUrl = this.avatarUrl;
this.avatarUrl = url;
if (url !== oldUrl) {
this._updateModifiedTime();
}
};
/**
* Update the last modified time to the current time.
*/

View File

@@ -19,6 +19,7 @@ limitations under the License.
* @module store/memory
*/
var utils = require("../utils");
var User = require("../models/user");
/**
* Construct a new in-memory data store for the Matrix Client.
@@ -55,6 +56,7 @@ module.exports.MatrixInMemoryStore.prototype = {
return this.syncToken;
},
/**
* Set the token to stream from.
* @param {string} token The token to stream from.
@@ -69,6 +71,44 @@ module.exports.MatrixInMemoryStore.prototype = {
*/
storeRoom: function(room) {
this.rooms[room.roomId] = room;
// add listeners for room member changes so we can keep the room member
// map up-to-date.
room.currentState.on("RoomState.members", this._onRoomMember.bind(this));
// add existing members
var self = this;
room.currentState.getMembers().forEach(function(m) {
self._onRoomMember(null, room.currentState, m);
});
},
/**
* Called when a room member in a room being tracked by this store has been
* updated.
* @param {MatrixEvent} event
* @param {RoomState} state
* @param {RoomMember} member
*/
_onRoomMember: function(event, state, member) {
if (member.membership === "invite") {
// We do NOT add invited members because people love to typo user IDs
// which would then show up in these lists (!)
return;
}
// We don't clobber any existing entry in the user map which has presence
// so user entries with presence info are preferred. This does mean we will
// clobber room member entries constantly, which is desirable to keep things
// like display names and avatar URLs up-to-date.
if (this.users[member.userId] && this.users[member.userId].events.presence) {
return;
}
var user = new User(member.userId);
user.setDisplayName(member.name);
var rawUrl = (
member.events.member ? member.events.member.getContent().avatar_url : null
);
user.setAvatarUrl(rawUrl);
this.users[user.userId] = user;
},
/**
@@ -93,6 +133,9 @@ module.exports.MatrixInMemoryStore.prototype = {
* @param {string} roomId
*/
removeRoom: function(roomId) {
if (this.rooms[roomId]) {
this.rooms[roomId].removeListener("RoomState.members", this._onRoomMember);
}
delete this.rooms[roomId];
},
@@ -123,6 +166,14 @@ module.exports.MatrixInMemoryStore.prototype = {
return this.users[userId] || null;
},
/**
* Retrieve all known users.
* @return {User[]} A list of users, which may be empty.
*/
getUsers: function() {
return utils.values(this.users);
},
/**
* Retrieve scrollback for this room.
* @param {Room} room The matrix room

View File

@@ -101,6 +101,14 @@ StubStore.prototype = {
return null;
},
/**
* No-op.
* @return {User[]}
*/
getUsers: function() {
return [];
},
/**
* No-op.
* @param {Room} room

View File

@@ -66,7 +66,8 @@ function SyncApi(client, opts) {
opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
this.opts = opts;
this._peekRoomId = null;
this._lowClientTimeouts = false;
this._syncConnectionLost = false;
this._currentSyncRequest = null;
}
/**
@@ -296,7 +297,7 @@ SyncApi.prototype.sync = function() {
attempt = attempt || 0;
attempt += 1;
client.pushRules().done(function(result) {
client.getPushRules().done(function(result) {
debuglog("Got push rules");
client.pushRules = result;
getFilter(); // Now get the filter
@@ -370,25 +371,13 @@ SyncApi.prototype._sync = function(syncOptions, attempt) {
// normal timeout= plus buffer time
var clientSideTimeoutMs = this.opts.pollTimeout + BUFFER_PERIOD_MS;
if (self._lowClientTimeouts) {
debuglog("_lowClientTimeouts flag set.");
clientSideTimeoutMs = this.opts.pollTimeout;
}
var dateStamp = new Date();
dateStamp = dateStamp.getHours() + ":" + dateStamp.getMinutes() + ":" +
dateStamp.getSeconds() + "." + dateStamp.getMilliseconds();
debuglog("DEBUG[%s]: NEW _sync attempt=%s qp_timeout=%s cli_timeout=%s",
dateStamp, attempt, qps.timeout, clientSideTimeoutMs);
client._http.authedRequestWithPrefix(
this._currentSyncRequest = client._http.authedRequestWithPrefix(
undefined, "GET", "/sync", qps, undefined, httpApi.PREFIX_V2_ALPHA,
clientSideTimeoutMs
).done(function(data) {
debuglog("DEBUG[%s]: _sync RECV", dateStamp);
self._lowClientTimeouts = false;
);
this._currentSyncRequest.done(function(data) {
self._syncConnectionLost = false;
// data looks like:
// {
// next_batch: $token,
@@ -405,7 +394,10 @@ SyncApi.prototype._sync = function(syncOptions, attempt) {
// timeline: { events: [], prev_batch: $token, limited: true },
// ephemeral: { events: [] },
// account_data: { events: [] },
// unread_notification_count: 0
// unread_notifications: {
// highlight_count: 0,
// notification_count: 0,
// }
// }
// },
// leave: {
@@ -487,7 +479,14 @@ SyncApi.prototype._sync = function(syncOptions, attempt) {
var accountDataEvents = self._mapSyncEventsFormat(joinObj.account_data);
// we do this first so it's correct when any of the events fire
room.unread_notification_count = joinObj.unread_notification_count;
if (joinObj.unread_notifications) {
room.setUnreadNotificationCount(
'total', joinObj.unread_notifications.notification_count
);
room.setUnreadNotificationCount(
'highlight', joinObj.unread_notifications.highlight_count
);
}
joinObj.timeline = joinObj.timeline || {};
@@ -554,22 +553,47 @@ SyncApi.prototype._sync = function(syncOptions, attempt) {
self._sync(syncOptions);
}, function(err) {
debuglog("DEBUG[%s]: RECV FAIL %s", dateStamp, require("util").inspect(err));
if (!self._syncConnectionLost) {
debuglog("Starting keep-alive");
self._syncConnectionLost = true;
retryPromise(self._pokeKeepAlive.bind(self), 2000).done(function() {
debuglog("Keep-alive successful.");
// blow away the current /sync request if the connection is still
// dead. It may be black-holed.
if (!self._syncConnectionLost) {
return;
}
if (self._currentSyncRequest.abort) {
// kill the current sync request
debuglog("Aborting current /sync.");
self._currentSyncRequest.abort();
}
// immediately retry if we were waiting
debuglog(
"Interrupted /sync backoff: %s", self.client.retryImmediately()
);
});
}
console.error("/sync error (%s attempts): %s", attempt, err);
console.error(err);
attempt += 1;
startSyncingRetryTimer(client, attempt, function(newAttempt, extendedWait) {
debuglog("DEBUG[%s]: Init new _sync new_attempt=%s extended_wait=%s",
dateStamp, newAttempt, extendedWait);
if (extendedWait) {
self._lowClientTimeouts = true;
}
startSyncingRetryTimer(client, attempt, function(newAttempt) {
self._sync(syncOptions, newAttempt);
});
updateSyncState(client, "ERROR", { error: err });
});
};
/**
* @return {Promise}
*/
SyncApi.prototype._pokeKeepAlive = function() {
return this.client._http.requestWithPrefix(
undefined, "GET", "/_matrix/client/versions", undefined,
undefined, "", 5 * 1000
);
};
/**
* @param {string} filterName
* @param {Filter} filter
@@ -749,6 +773,14 @@ function retryTimeMsForAttempt(attempt) {
return Math.pow(2, Math.min(attempt, 5)) * 1000;
}
function retryPromise(promiseFn, delay) {
delay = delay || 0;
return promiseFn().catch(function(reason) { // if it fails
// retry after waiting the delay time
return q.delay(delay).then(retryPromise.bind(null, promiseFn, delay));
});
}
function startSyncingRetryTimer(client, attempt, fn) {
client._syncingRetry = {};
client._syncingRetry.fn = fn;
@@ -758,19 +790,17 @@ function startSyncingRetryTimer(client, attempt, fn) {
client._syncingRetry.timeoutId = setTimeout(function() {
var timeAfterWaitingMs = Date.now();
var timeDeltaMs = timeAfterWaitingMs - timeBeforeWaitingMs;
var extendedWait = false;
if (timeDeltaMs > (2 * timeToWaitMs)) {
// we've waited more than twice what we were supposed to. Reset the
// attempt number back to 1. This can happen when the comp goes to
// sleep whilst the timer is running.
newAttempt = 1;
extendedWait = true;
console.warn(
"Sync retry timer: Tried to wait %s ms but actually waited %s ms",
timeToWaitMs, timeDeltaMs
);
}
fn(newAttempt, extendedWait);
fn(newAttempt);
}, timeToWaitMs);
}

View File

@@ -10,6 +10,8 @@ describe("MatrixClient", function() {
var identityServerDomain = "identity.server";
var client, store, scheduler;
var KEEP_ALIVE_PATH = "/_matrix/client/versions";
var PUSH_RULES_RESPONSE = {
method: "GET",
path: "/pushrules/",
@@ -51,6 +53,9 @@ describe("MatrixClient", function() {
];
var pendingLookup = null;
function httpReq(cb, method, path, qp, data, prefix) {
if (path === KEEP_ALIVE_PATH) {
return q();
}
var next = httpLookups.shift();
var logLine = (
"MatrixClient[UT] RECV " + method + " " + path + " " +
@@ -137,6 +142,7 @@ describe("MatrixClient", function() {
]);
client._http.authedRequest.andCallFake(httpReq);
client._http.authedRequestWithPrefix.andCallFake(httpReq);
client._http.requestWithPrefix.andCallFake(httpReq);
// set reasonable working defaults
pendingLookup = null;

View File

@@ -343,50 +343,40 @@ describe("Room", function() {
it("should return true for a matching userId and membership",
function() {
room.currentState.getMembers.andCallFake(function() {
return [
{ userId: "@alice:bar", membership: "join" },
{ userId: "@bob:bar", membership: "invite" }
];
});
room.currentState.members = {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
"@bob:bar": { userId: "@bob:bar", membership: "invite" }
};
expect(room.hasMembershipState("@bob:bar", "invite")).toBe(true);
});
it("should return false if match membership but no match userId",
function() {
room.currentState.getMembers.andCallFake(function() {
return [
{ userId: "@alice:bar", membership: "join" }
];
});
room.currentState.members = {
"@alice:bar": { userId: "@alice:bar", membership: "join" }
};
expect(room.hasMembershipState("@bob:bar", "join")).toBe(false);
});
it("should return false if match userId but no match membership",
function() {
room.currentState.getMembers.andCallFake(function() {
return [
{ userId: "@alice:bar", membership: "join" }
];
});
room.currentState.members = {
"@alice:bar": { userId: "@alice:bar", membership: "join" }
};
expect(room.hasMembershipState("@alice:bar", "ban")).toBe(false);
});
it("should return false if no match membership or userId",
function() {
room.currentState.getMembers.andCallFake(function() {
return [
{ userId: "@alice:bar", membership: "join" }
];
});
room.currentState.members = {
"@alice:bar": { userId: "@alice:bar", membership: "join" }
};
expect(room.hasMembershipState("@bob:bar", "invite")).toBe(false);
});
it("should return false if no members exist",
function() {
room.currentState.getMembers.andCallFake(function() {
return [];
});
room.currentState.members = {};
expect(room.hasMembershipState("@foo:bar", "join")).toBe(false);
});
});
@@ -622,29 +612,30 @@ describe("Room", function() {
expect(name).toEqual(roomName);
});
it("should show your name for private (invite join_rules) rooms if" +
it("should return 'Empty room' 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);
expect(name).toEqual("Empty room");
});
it("should show your name for public (public join_rules) rooms if a" +
it("should return 'Empty room' 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);
expect(name).toEqual("Empty room");
});
it("should return '?' if there is no name, alias or members in the room.",
it("should return 'Empty room' if there is no name, " +
"alias or members in the room.",
function() {
room.recalculate(userA);
var name = room.name;
expect(name).toEqual("?");
expect(name).toEqual("Empty room");
});
});