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) {
|
wrap(_, callback) {
|
||||||
return callback(this);
|
return callback(this);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Room} from "./room/Room.js";
|
import {Room} from "./room/Room.js";
|
||||||
|
import {Invite} from "./room/Invite.js";
|
||||||
import {Pusher} from "./push/Pusher.js";
|
import {Pusher} from "./push/Pusher.js";
|
||||||
import { ObservableMap } from "../observable/index.js";
|
import { ObservableMap } from "../observable/index.js";
|
||||||
import {User} from "./User.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;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
function processStateEvent(data, event) {
|
export function processStateEvent(data, event) {
|
||||||
if (event.type === "m.room.encryption") {
|
if (event.type === "m.room.encryption") {
|
||||||
const algorithm = event.content?.algorithm;
|
const algorithm = event.content?.algorithm;
|
||||||
if (!data.encryption && algorithm === MEGOLM_ALGORITHM) {
|
if (!data.encryption && algorithm === MEGOLM_ALGORITHM) {
|
||||||
|
@ -148,7 +148,7 @@ function updateSummary(data, summary) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SummaryData {
|
export class SummaryData {
|
||||||
constructor(copy, roomId) {
|
constructor(copy, roomId) {
|
||||||
this.roomId = copy ? copy.roomId : roomId;
|
this.roomId = copy ? copy.roomId : roomId;
|
||||||
this.name = copy ? copy.name : null;
|
this.name = copy ? copy.name : null;
|
||||||
|
|
Loading…
Reference in a new issue