forked from mystiq/hydrogen-web
add Invite class
calculating the room name, avatar, etc ... with empty accept and reject methods for now
This commit is contained in:
parent
7c4a6fbe4b
commit
81a35639ba
6 changed files with 367 additions and 3 deletions
52
src/fixtures/matrix/invites/dm.js
Normal file
52
src/fixtures/matrix/invites/dm.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
const inviteFixture = {
|
||||
"invite_state": {
|
||||
"events": [
|
||||
{
|
||||
"type": "m.room.create",
|
||||
"state_key": "",
|
||||
"content": {
|
||||
"creator": "@alice:hs.tld",
|
||||
},
|
||||
"sender": "@alice:hs.tld"
|
||||
},
|
||||
{
|
||||
"type": "m.room.encryption",
|
||||
"state_key": "",
|
||||
"content": {
|
||||
"algorithm": "m.megolm.v1.aes-sha2"
|
||||
},
|
||||
"sender": "@alice:hs.tld"
|
||||
},
|
||||
{
|
||||
"type": "m.room.join_rules",
|
||||
"state_key": "",
|
||||
"content": {
|
||||
"join_rule": "invite"
|
||||
},
|
||||
"sender": "@alice:hs.tld"
|
||||
},
|
||||
{
|
||||
"type": "m.room.member",
|
||||
"state_key": "@alice:hs.tld",
|
||||
"content": {
|
||||
"avatar_url": "mxc://hs.tld/def456",
|
||||
"displayname": "Alice",
|
||||
"membership": "join"
|
||||
},
|
||||
"sender": "@alice:hs.tld"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"avatar_url": "mxc://hs.tld/abc123",
|
||||
"displayname": "Bob",
|
||||
"is_direct": true,
|
||||
"membership": "invite"
|
||||
},
|
||||
"sender": "@alice:hs.tld",
|
||||
"state_key": "@bob:hs.tld",
|
||||
"type": "m.room.member",
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
export default inviteFixture;
|
59
src/fixtures/matrix/invites/room.js
Normal file
59
src/fixtures/matrix/invites/room.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
const inviteFixture = {
|
||||
"invite_state": {
|
||||
"events": [
|
||||
{
|
||||
"type": "m.room.create",
|
||||
"state_key": "",
|
||||
"content": {
|
||||
"creator": "@alice:hs.tld",
|
||||
},
|
||||
"sender": "@alice:hs.tld"
|
||||
},
|
||||
{
|
||||
"type": "m.room.join_rules",
|
||||
"state_key": "",
|
||||
"content": {
|
||||
"join_rule": "invite"
|
||||
},
|
||||
"sender": "@alice:hs.tld"
|
||||
},
|
||||
{
|
||||
"type": "m.room.member",
|
||||
"state_key": "@alice:hs.tld",
|
||||
"content": {
|
||||
"avatar_url": "mxc://hs.tld/def456",
|
||||
"displayname": "Alice",
|
||||
"membership": "join"
|
||||
},
|
||||
"sender": "@alice:hs.tld"
|
||||
},
|
||||
{
|
||||
"type": "m.room.name",
|
||||
"state_key": "",
|
||||
"content": {
|
||||
"name": "Invite example"
|
||||
},
|
||||
"sender": "@alice:hs.tld"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"avatar_url": "mxc://hs.tld/abc123",
|
||||
"displayname": "Bob",
|
||||
"membership": "invite"
|
||||
},
|
||||
"sender": "@alice:hs.tld",
|
||||
"state_key": "@bob:hs.tld",
|
||||
"type": "m.room.member",
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"url": "mxc://hs.tld/roomavatar"
|
||||
},
|
||||
"sender": "@alice:hs.tld",
|
||||
"state_key": "",
|
||||
"type": "m.room.avatar",
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
export default inviteFixture;
|
|
@ -50,7 +50,7 @@ export class NullLogger {
|
|||
}
|
||||
}
|
||||
|
||||
class NullLogItem {
|
||||
export class NullLogItem {
|
||||
wrap(_, callback) {
|
||||
return callback(this);
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import {Room} from "./room/Room.js";
|
||||
import {Invite} from "./room/Invite.js";
|
||||
import {Pusher} from "./push/Pusher.js";
|
||||
import { ObservableMap } from "../observable/index.js";
|
||||
import {User} from "./User.js";
|
||||
|
|
252
src/matrix/room/Invite.js
Normal file
252
src/matrix/room/Invite.js
Normal file
|
@ -0,0 +1,252 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
import {EventEmitter} from "../../utils/EventEmitter.js";
|
||||
import {SummaryData, processStateEvent} from "./RoomSummary.js";
|
||||
import {Heroes} from "./members/Heroes.js";
|
||||
import {MemberChange, RoomMember, EVENT_TYPE as MEMBER_EVENT_TYPE} from "./members/RoomMember.js";
|
||||
|
||||
export class Invite extends EventEmitter {
|
||||
constructor({roomId, user, hsApi, emitCollectionRemove, clock}) {
|
||||
super();
|
||||
this._roomId = roomId;
|
||||
this._user = user;
|
||||
this._hsApi = hsApi;
|
||||
this._emitCollectionRemove = emitCollectionRemove;
|
||||
this._clock = clock;
|
||||
this._inviteData = null;
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this._roomId;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this._inviteData.name || this._inviteData.canonicalAlias;
|
||||
}
|
||||
|
||||
get isDirectMessage() {
|
||||
return this._inviteData.isDirectMessage;
|
||||
}
|
||||
|
||||
get avatarUrl() {
|
||||
return this._inviteData.avatarUrl;
|
||||
}
|
||||
|
||||
get timestamp() {
|
||||
return this._inviteData.timestamp;
|
||||
}
|
||||
|
||||
get isEncrypted() {
|
||||
return this._inviteData.isEncrypted;
|
||||
}
|
||||
|
||||
get inviter() {
|
||||
return this._inviter;
|
||||
}
|
||||
|
||||
get joinRule() {
|
||||
return this._inviteData.joinRule;
|
||||
}
|
||||
|
||||
async accept() {
|
||||
|
||||
}
|
||||
|
||||
async reject() {
|
||||
|
||||
}
|
||||
|
||||
load(inviteData) {
|
||||
this._inviteData = inviteData;
|
||||
this._inviter = inviteData.inviter ? new RoomMember(inviteData.inviter) : null;
|
||||
}
|
||||
|
||||
async writeSync(membership, roomResponse, txn, log) {
|
||||
if (membership === "invite") {
|
||||
return log.wrap("new invite", async log => {
|
||||
log.set("id", this.id);
|
||||
const inviteState = roomResponse["invite_state"]?.events;
|
||||
if (!Array.isArray(inviteState)) {
|
||||
return null;
|
||||
}
|
||||
const summaryData = this._createSummaryData(inviteState);
|
||||
let heroes;
|
||||
if (!summaryData.name && !summaryData.canonicalAlias) {
|
||||
heroes = await this._createHeroes(inviteState);
|
||||
}
|
||||
const myInvite = this._getMyInvite(inviteState);
|
||||
if (!myInvite) {
|
||||
return null;
|
||||
}
|
||||
const inviter = this._getInviter(myInvite, inviteState);
|
||||
const inviteData = this._createData(inviteState, myInvite, inviter, summaryData, heroes);
|
||||
txn.invites.set(inviteData);
|
||||
return {inviteData, inviter};
|
||||
});
|
||||
} else {
|
||||
return log.wrap("remove invite", log => {
|
||||
log.set("id", this.id);
|
||||
log.set("membership", membership);
|
||||
txn.invites.remove(this.id);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
afterSync(changes, room) {
|
||||
if (changes) {
|
||||
this._inviteData = changes.inviteData;
|
||||
this._inviter = changes.inviter;
|
||||
// emit update/add
|
||||
} else {
|
||||
this._emitCollectionRemove(this);
|
||||
this.emit("change");
|
||||
}
|
||||
}
|
||||
|
||||
_createData(inviteState, myInvite, inviter, summaryData, heroes) {
|
||||
const name = heroes ? heroes.roomName : summaryData.name;
|
||||
const avatarUrl = heroes ? heroes.roomAvatarUrl : summaryData.avatarUrl;
|
||||
return {
|
||||
roomId: this.id,
|
||||
isEncrypted: !!summaryData.encryption,
|
||||
isDirectMessage: this._isDirectMessage(myInvite),
|
||||
// type:
|
||||
name,
|
||||
avatarUrl,
|
||||
canonicalAlias: summaryData.canonicalAlias,
|
||||
timestamp: this._clock.now(),
|
||||
joinRule: this._getJoinRule(inviteState),
|
||||
inviter: inviter?.serialize(),
|
||||
};
|
||||
}
|
||||
|
||||
_isDirectMessage(myInvite) {
|
||||
return !!(myInvite?.content?.is_direct);
|
||||
}
|
||||
|
||||
_createSummaryData(inviteState) {
|
||||
return inviteState.reduce(processStateEvent, new SummaryData(null, this.id));
|
||||
}
|
||||
|
||||
async _createHeroes(inviteState) {
|
||||
const members = inviteState.filter(e => e.type === MEMBER_EVENT_TYPE);
|
||||
const otherMembers = members.filter(e => e.state_key !== this._user.id);
|
||||
const memberChanges = otherMembers.reduce((map, e) => {
|
||||
const member = RoomMember.fromMemberEvent(this.id, e);
|
||||
map.set(member.userId, new MemberChange(member, null));
|
||||
return map;
|
||||
}, new Map());
|
||||
const otherUserIds = otherMembers.map(e => e.state_key);
|
||||
const heroes = new Heroes(this.id);
|
||||
const changes = await heroes.calculateChanges(otherUserIds, memberChanges, null);
|
||||
// we don't get an actual lazy-loading m.heroes summary on invites,
|
||||
// so just count the members by hand
|
||||
const countSummary = new SummaryData(null, this.id);
|
||||
countSummary.joinCount = members.reduce((sum, e) => sum + (e.content?.membership === "join" ? 1 : 0), 0);
|
||||
countSummary.inviteCount = members.reduce((sum, e) => sum + (e.content?.membership === "invite" ? 1 : 0), 0);
|
||||
heroes.applyChanges(changes, countSummary);
|
||||
return heroes;
|
||||
}
|
||||
|
||||
_getMyInvite(inviteState) {
|
||||
return inviteState.find(e => e.type === MEMBER_EVENT_TYPE && e.state_key === this._user.id);
|
||||
}
|
||||
|
||||
_getInviter(myInvite, inviteState) {
|
||||
const inviterMemberEvent = inviteState.find(e => e.type === MEMBER_EVENT_TYPE && e.state_key === myInvite.sender);
|
||||
if (inviterMemberEvent) {
|
||||
return RoomMember.fromMemberEvent(this.id, inviterMemberEvent);
|
||||
}
|
||||
}
|
||||
|
||||
_getJoinRule(inviteState) {
|
||||
const event = inviteState.find(e => e.type === "m.room.join_rules");
|
||||
if (event) {
|
||||
return event.content?.join_rule;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
import {NullLogItem} from "../../logging/NullLogger.js";
|
||||
import {Clock as MockClock} from "../../mocks/Clock.js";
|
||||
import {default as roomInviteFixture} from "../../fixtures/matrix/invites/room.js";
|
||||
import {default as dmInviteFixture} from "../../fixtures/matrix/invites/dm.js";
|
||||
|
||||
export function tests() {
|
||||
|
||||
function createStorage() {
|
||||
const invitesMap = new Map();
|
||||
return {
|
||||
invitesMap,
|
||||
invites: {
|
||||
set(invite) {
|
||||
invitesMap.set(invite.roomId, invite);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const roomId = "!123:hs.tld";
|
||||
const aliceAvatarUrl = "mxc://hs.tld/def456";
|
||||
const roomAvatarUrl = "mxc://hs.tld/roomavatar";
|
||||
|
||||
return {
|
||||
"invite for room has correct fields": async assert => {
|
||||
const invite = new Invite({
|
||||
roomId,
|
||||
clock: new MockClock(1001),
|
||||
user: {id: "@bob:hs.tld"}
|
||||
});
|
||||
const txn = createStorage();
|
||||
const changes = await invite.writeSync("invite", roomInviteFixture, txn, new NullLogItem());
|
||||
assert.equal(txn.invitesMap.get(roomId).roomId, roomId);
|
||||
invite.afterSync(changes);
|
||||
assert.equal(invite.name, "Invite example");
|
||||
assert.equal(invite.avatarUrl, roomAvatarUrl);
|
||||
assert.equal(invite.joinRule, "invite");
|
||||
assert.equal(invite.timestamp, 1001);
|
||||
assert.equal(invite.isEncrypted, false);
|
||||
assert.equal(invite.isDirectMessage, false);
|
||||
assert(invite.inviter);
|
||||
assert.equal(invite.inviter.userId, "@alice:hs.tld");
|
||||
assert.equal(invite.inviter.displayName, "Alice");
|
||||
assert.equal(invite.inviter.avatarUrl, aliceAvatarUrl);
|
||||
},
|
||||
"invite for encrypted DM has correct fields": async assert => {
|
||||
const invite = new Invite({
|
||||
roomId,
|
||||
clock: new MockClock(1003),
|
||||
user: {id: "@bob:hs.tld"}
|
||||
});
|
||||
const txn = createStorage();
|
||||
const changes = await invite.writeSync("invite", dmInviteFixture, txn, new NullLogItem());
|
||||
assert.equal(txn.invitesMap.get(roomId).roomId, roomId);
|
||||
invite.afterSync(changes);
|
||||
assert.equal(invite.name, "Alice");
|
||||
assert.equal(invite.avatarUrl, aliceAvatarUrl);
|
||||
assert.equal(invite.timestamp, 1003);
|
||||
assert.equal(invite.isEncrypted, true);
|
||||
assert.equal(invite.isDirectMessage, true);
|
||||
assert(invite.inviter);
|
||||
assert.equal(invite.inviter.userId, "@alice:hs.tld");
|
||||
assert.equal(invite.inviter.displayName, "Alice");
|
||||
assert.equal(invite.inviter.avatarUrl, aliceAvatarUrl);
|
||||
},
|
||||
}
|
||||
}
|
|
@ -85,7 +85,7 @@ function processRoomAccountData(data, event) {
|
|||
return data;
|
||||
}
|
||||
|
||||
function processStateEvent(data, event) {
|
||||
export function processStateEvent(data, event) {
|
||||
if (event.type === "m.room.encryption") {
|
||||
const algorithm = event.content?.algorithm;
|
||||
if (!data.encryption && algorithm === MEGOLM_ALGORITHM) {
|
||||
|
@ -148,7 +148,7 @@ function updateSummary(data, summary) {
|
|||
return data;
|
||||
}
|
||||
|
||||
class SummaryData {
|
||||
export class SummaryData {
|
||||
constructor(copy, roomId) {
|
||||
this.roomId = copy ? copy.roomId : roomId;
|
||||
this.name = copy ? copy.name : null;
|
||||
|
|
Loading…
Reference in a new issue