1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-26 17:03:12 +03:00

Groups: Sync Stream, Accept Invite & Leave (#528)

* WIP support for reading groups from sync stream

Only does invites currently

* More support for parsing groups in the sync stream

* Fix jsdoc
This commit is contained in:
David Baker
2017-08-24 10:24:24 +01:00
committed by Luke Barnard
parent 15b77861ea
commit 033babfbfc
8 changed files with 333 additions and 4 deletions

View File

@@ -466,6 +466,32 @@ MatrixBaseApis.prototype.removeUserFromGroup = function(groupId, userId) {
return this._http.authedRequest(undefined, "PUT", path, undefined, {});
};
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.acceptGroupInvite = function(groupId) {
const path = utils.encodeUri(
"/groups/$groupId/self/accept_invite",
{$groupId: groupId},
);
return this._http.authedRequest(undefined, "PUT", path, undefined, {});
};
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.leaveGroup = function(groupId) {
const path = utils.encodeUri(
"/groups/$groupId/self/leave",
{$groupId: groupId},
);
return this._http.authedRequest(undefined, "PUT", path, undefined, {});
};
/**
* @return {module:client.Promise} Resolves: The groups to which the user is joined
* @return {module:http-api.MatrixError} Rejects: with an error response.

View File

@@ -659,6 +659,30 @@ MatrixClient.prototype.importRoomKeys = function(keys) {
return this._crypto.importRoomKeys(keys);
};
// Group ops
// =========
// Operations on groups that come down the sync stream (ie. ones the
// user is a member of or invited to)
/**
* Get the group for the given group ID.
* This function will return a valid group for any group for which a Group event
* has been emitted.
* @param {string} groupId The group ID
* @return {Group} The Group or null if the group is not known or there is no data store.
*/
MatrixClient.prototype.getGroup = function(groupId) {
return this.store.getGroup(groupId);
};
/**
* Retrieve all known groups.
* @return {Groups[]} A list of groups, or an empty list if there is no data store.
*/
MatrixClient.prototype.getGroups = function() {
return this.store.getGroups();
};
// Room ops
// ========
@@ -3423,6 +3447,17 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
* });
*/
/**
* Fires whenever the sdk learns about a new group. <strong>This event
* is experimental and may change.</strong>
* @event module:client~MatrixClient#"Group"
* @param {Group} group The newly created, fully populated group.
* @example
* matrixClient.on("Group", function(group){
* var groupId = group.groupId;
* });
*/
/**
* Fires whenever a new Room is added. This will fire when you are invited to a
* room, as well as when you join a room. <strong>This event is experimental and

95
src/models/group.js Normal file
View File

@@ -0,0 +1,95 @@
/*
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @module models/group
*/
const EventEmitter = require("events").EventEmitter;
const utils = require("../utils");
/**
* Construct a new Group.
*
* @param {string} groupId The ID of this group.
*
* @prop {string} groupId The ID of this group.
* @prop {string} name The human-readable display name for this group.
* @prop {string} avatarUrl The mxc URL for this group's avatar.
* @prop {string} myMembership The logged in user's membership of this group
* @prop {Object} inviter Infomation about the user who invited the logged in user
* to the group, if myMembership is 'invite'.
* @prop {string} inviter.userId The user ID of the inviter
*/
function Group(groupId) {
this.groupId = groupId;
this.name = null;
this.avatarUrl = null;
this.myMembership = null;
this.inviter = null;
}
utils.inherits(Group, EventEmitter);
Group.prototype.setProfile = function(name, avatarUrl) {
if (this.name === name && this.avatarUrl === avatarUrl) return;
this.name = name || this.groupId;
this.avatarUrl = avatarUrl;
this.emit("Group.profile", this);
};
Group.prototype.setMyMembership = function(membership) {
if (this.myMembership === membership) return;
this.myMembership = membership;
this.emit("Group.myMembership", this);
};
/**
* Sets the 'inviter' property. This does not emit an event (the inviter
* will only change when the user is revited / reinvited to a room),
* so set this before setting myMembership.
* @param {Object} inviter Infomation about who invited us to the room
*/
Group.prototype.setInviter = function(inviter) {
this.inviter = inviter;
};
module.exports = Group;
/**
* Fires whenever a group's profile information is updated.
* This means the 'name' and 'avatarUrl' properties.
* @event module:client~MatrixClient#"Group.profile"
* @param {Group} group The group whose profile was updated.
* @example
* matrixClient.on("Group.profile", function(group){
* var name = group.name;
* });
*/
/**
* Fires whenever the logged in user's membership status of
* the group is updated.
* @event module:client~MatrixClient#"Group.myMembership"
* @param {Group} group The group in which the user's membership changed
* @example
* matrixClient.on("Group.myMembership", function(group){
* var myMembership = group.myMembership;
* });
*/

View File

@@ -175,6 +175,7 @@ LocalIndexedDBStoreBackend.prototype = {
this._syncAccumulator.accumulate({
next_batch: syncData.nextBatch,
rooms: syncData.roomsData,
groups: syncData.groupsData,
account_data: {
events: accountData,
},
@@ -251,7 +252,9 @@ LocalIndexedDBStoreBackend.prototype = {
return Promise.all([
this._persistUserPresenceEvents(userTuples),
this._persistAccountData(syncData.accountData),
this._persistSyncData(syncData.nextBatch, syncData.roomsData),
this._persistSyncData(
syncData.nextBatch, syncData.roomsData, syncData.groupsData,
),
]);
},
@@ -259,9 +262,10 @@ LocalIndexedDBStoreBackend.prototype = {
* Persist rooms /sync data along with the next batch token.
* @param {string} nextBatch The next_batch /sync value.
* @param {Object} roomsData The 'rooms' /sync data from a SyncAccumulator
* @param {Object} groupsData The 'groups' /sync data from a SyncAccumulator
* @return {Promise} Resolves if the data was persisted.
*/
_persistSyncData: function(nextBatch, roomsData) {
_persistSyncData: function(nextBatch, roomsData, groupsData) {
console.log("Persisting sync data up to ", nextBatch);
return Promise.try(() => {
const txn = this.db.transaction(["sync"], "readwrite");
@@ -270,6 +274,7 @@ LocalIndexedDBStoreBackend.prototype = {
clobber: "-", // constant key so will always clobber
nextBatch: nextBatch,
roomsData: roomsData,
groupsData: groupsData,
}); // put == UPSERT
return promiseifyTxn(txn);
});

View File

@@ -35,6 +35,9 @@ module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) {
this.rooms = {
// roomId: Room
};
this.groups = {
// groupId: Group
};
this.users = {
// userId: User
};
@@ -69,6 +72,31 @@ module.exports.MatrixInMemoryStore.prototype = {
this.syncToken = token;
},
/**
* Store the given room.
* @param {Group} group The group to be stored
*/
storeGroup: function(group) {
this.groups[group.groupId] = group;
},
/**
* Retrieve a group by its group ID.
* @param {string} groupId The group ID.
* @return {Group} The group or null.
*/
getGroup: function(groupId) {
return this.groups[groupId] || null;
},
/**
* Retrieve all known groups.
* @return {Group[]} A list of groups, which may be empty.
*/
getGroups: function() {
return utils.values(this.groups);
},
/**
* Store the given room.
* @param {Room} room The room to be stored. All properties must be stored.

View File

@@ -47,6 +47,30 @@ StubStore.prototype = {
this.fromToken = token;
},
/**
* No-op.
* @param {Group} group
*/
storeGroup: function(group) {
},
/**
* No-op.
* @param {string} groupId
* @return {null}
*/
getGroup: function(groupId) {
return null;
},
/**
* No-op.
* @return {Array} An empty array.
*/
getGroups: function() {
return [];
},
/**
* No-op.
* @param {Room} room

View File

@@ -72,10 +72,18 @@ class SyncAccumulator {
// coherent /sync response and know at what point they should be
// streaming from without losing events.
this.nextBatch = null;
// { ('invite'|'join'|'leave'): $groupId: { ... sync 'group' data } }
this.groups = {
invite: {},
join: {},
leave: {},
};
}
accumulate(syncResponse) {
this._accumulateRooms(syncResponse);
this._accumulateGroups(syncResponse);
this._accumulateAccountData(syncResponse);
this.nextBatch = syncResponse.next_batch;
}
@@ -336,6 +344,44 @@ class SyncAccumulator {
}
}
/**
* Accumulate incremental /sync group data.
* @param {Object} syncResponse the complete /sync JSON
*/
_accumulateGroups(syncResponse) {
if (!syncResponse.groups) {
return;
}
if (syncResponse.groups.invite) {
Object.keys(syncResponse.groups.invite).forEach((groupId) => {
this._accumulateGroup(
groupId, "invite", syncResponse.groups.invite[groupId],
);
});
}
if (syncResponse.groups.join) {
Object.keys(syncResponse.groups.join).forEach((groupId) => {
this._accumulateGroup(
groupId, "join", syncResponse.groups.join[groupId],
);
});
}
if (syncResponse.groups.leave) {
Object.keys(syncResponse.groups.leave).forEach((groupId) => {
this._accumulateGroup(
groupId, "leave", syncResponse.groups.leave[groupId],
);
});
}
}
_accumulateGroup(groupId, category, data) {
for (const cat of ['invite', 'join', 'leave']) {
delete this.groups[cat][groupId];
}
this.groups[category][groupId] = data;
}
/**
* Return everything under the 'rooms' key from a /sync response which
* represents all room data that should be stored. This should be paired
@@ -470,6 +516,7 @@ class SyncAccumulator {
return {
nextBatch: this.nextBatch,
roomsData: data,
groupsData: this.groups,
accountData: accData,
};
}

View File

@@ -27,6 +27,7 @@ limitations under the License.
import Promise from 'bluebird';
const User = require("./models/user");
const Room = require("./models/room");
const Group = require('./models/group');
const utils = require("./utils");
const Filter = require("./filter");
const EventTimeline = require("./models/event-timeline");
@@ -124,6 +125,17 @@ SyncApi.prototype.createRoom = function(roomId) {
return room;
};
/**
* @param {string} groupId
* @return {Group}
*/
SyncApi.prototype.createGroup = function(groupId) {
const client = this.client;
const group = new Group(groupId);
reEmit(client, group, ["Group.profile", "Group.myMembership"]);
return group;
};
/**
* @param {Room} room
* @private
@@ -568,6 +580,7 @@ SyncApi.prototype._sync = function(syncOptions) {
return {
next_batch: savedSync.nextBatch,
rooms: savedSync.roomsData,
groups: savedSync.groupsData,
account_data: {
events: savedSync.accountData,
},
@@ -714,6 +727,19 @@ SyncApi.prototype._processSyncResponse = async function(syncToken, data) {
// }
// }
// },
// groups: {
// invite: {
// $groupId: {
// inviter: $inviter,
// profile: {
// avatar_url: $avatarUrl,
// name: $groupName,
// },
// },
// },
// join: {},
// leave: {},
// },
// }
// TODO-arch:
@@ -781,6 +807,20 @@ SyncApi.prototype._processSyncResponse = async function(syncToken, data) {
this._catchingUp = false;
}
if (data.groups) {
if (data.groups.invite) {
this._processGroupSyncEntry(data.groups.invite, 'invite');
}
if (data.groups.join) {
this._processGroupSyncEntry(data.groups.join, 'join');
}
if (data.groups.leave) {
this._processGroupSyncEntry(data.groups.leave, 'leave');
}
}
// the returned json structure is a bit crap, so make it into a
// nicer form (array) after applying sanity to make sure we don't fail
// on missing keys (on the off chance)
@@ -1070,6 +1110,35 @@ SyncApi.prototype._pokeKeepAlive = function() {
});
};
/**
* @param {Object} groupsSection Groups section object, eg. response.groups.invite
* @param {string} sectionName Which section this is ('invite', 'join' or 'leave')
*/
SyncApi.prototype._processGroupSyncEntry = function(groupsSection, sectionName) {
// Processes entries from 'groups' section of the sync stream
for (const groupId of Object.keys(groupsSection)) {
const groupInfo = groupsSection[groupId];
let group = this.client.store.getGroup(groupId);
const isBrandNew = group === null;
if (group === null) {
group = this.createGroup(groupId);
}
if (groupInfo.profile) {
group.setProfile(
groupInfo.profile.name, groupInfo.profile.avatar_url,
);
}
if (groupInfo.inviter) {
group.setInviter({userId: groupInfo.inviter});
}
group.setMyMembership(sectionName);
if (isBrandNew) {
this.client.store.storeGroup(group);
this.client.emit("Group", group);
}
}
};
/**
* @param {Object} obj
* @return {Object[]}