Prepare utils for local rooms (#9084)

* Prepare utils for local rooms

* Split up direct-messages module
This commit is contained in:
Michael Weimann 2022-07-25 10:17:40 +02:00 committed by GitHub
parent 0a8adb6bfe
commit c5eaeafe8e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1016 additions and 220 deletions

View file

@ -58,9 +58,9 @@ import IncomingCallToast, { getIncomingCallToastKey } from './toasts/IncomingCal
import ToastStore from './stores/ToastStore';
import Resend from './Resend';
import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
import { findDMForUser } from "./utils/direct-messages";
import { KIND_CALL_TRANSFER } from "./components/views/dialogs/InviteDialogTypes";
import { OpenInviteDialogPayload } from "./dispatcher/payloads/OpenInviteDialogPayload";
import { findDMForUser } from './utils/dm/findDMForUser';
export const PROTOCOL_PSTN = 'm.protocol.pstn';
export const PROTOCOL_PSTN_PREFIXED = 'im.vector.protocol.pstn';

View file

@ -23,7 +23,7 @@ import { MatrixClientPeg } from "./MatrixClientPeg";
import DMRoomMap from "./utils/DMRoomMap";
import CallHandler from './CallHandler';
import { VIRTUAL_ROOM_EVENT_TYPE } from "./call-types";
import { findDMForUser } from "./utils/direct-messages";
import { findDMForUser } from './utils/dm/findDMForUser';
// Functions for mapping virtual users & rooms. Currently the only lookup
// is sip virtual: there could be others in the future.

View file

@ -62,10 +62,11 @@ import CopyableText from "../elements/CopyableText";
import { ScreenName } from '../../../PosthogTrackers';
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { DirectoryMember, IDMUserTileProps, Member, startDm, ThreepidMember } from "../../../utils/direct-messages";
import { DirectoryMember, IDMUserTileProps, Member, ThreepidMember } from "../../../utils/direct-messages";
import { AnyInviteKind, KIND_CALL_TRANSFER, KIND_DM, KIND_INVITE } from './InviteDialogTypes';
import Modal from '../../../Modal';
import dis from "../../../dispatcher/dispatcher";
import { startDm } from '../../../utils/dm/startDm';
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
/* eslint-disable camelcase */

View file

@ -70,7 +70,7 @@ import { RecentAlgorithm } from "../../../../stores/room-list/algorithms/tag-sor
import { RoomViewStore } from "../../../../stores/RoomViewStore";
import { getMetaSpaceName } from "../../../../stores/spaces";
import SpaceStore from "../../../../stores/spaces/SpaceStore";
import { DirectoryMember, Member, startDm } from "../../../../utils/direct-messages";
import { DirectoryMember, Member } from "../../../../utils/direct-messages";
import DMRoomMap from "../../../../utils/DMRoomMap";
import { makeUserPermalink } from "../../../../utils/permalinks/Permalinks";
import { buildActivityScores, buildMemberScores, compareMembers } from "../../../../utils/SortMembers";
@ -92,6 +92,7 @@ import { RoomResultContextMenus } from "./RoomResultContextMenus";
import { RoomContextDetails } from "../../rooms/RoomContextDetails";
import { TooltipOption } from "./TooltipOption";
import { isLocalRoom } from "../../../../utils/localRoom/isLocalRoom";
import { startDm } from "../../../../utils/dm/startDm";
const MAX_RECENT_SEARCHES = 10;
const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons

View file

@ -78,8 +78,8 @@ import { IRightPanelCardState } from '../../../stores/right-panel/RightPanelStor
import UserIdentifierCustomisations from '../../../customisations/UserIdentifier';
import PosthogTrackers from "../../../PosthogTrackers";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { findDMForUser } from "../../../utils/direct-messages";
import { privateShouldBeEncrypted } from "../../../utils/rooms";
import { findDMForUser } from '../../../utils/dm/findDMForUser';
export interface IDevice {
deviceId: string;

View file

@ -42,7 +42,7 @@ import { Action } from "./dispatcher/actions";
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
import Spinner from "./components/views/elements/Spinner";
import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
import { findDMForUser } from "./utils/direct-messages";
import { findDMForUser } from "./utils/dm/findDMForUser";
import { privateShouldBeEncrypted } from "./utils/rooms";
import { waitForMember } from "./utils/membership";
import { PreferredRoomVersions } from "./utils/PreferredRoomVersions";

View file

@ -14,54 +14,26 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { IInvite3PID } from "matrix-js-sdk/src/@types/requests";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room";
import { logger } from "matrix-js-sdk/src/logger";
import createRoom, { canEncryptToAllUsers } from "../createRoom";
import { canEncryptToAllUsers } from "../createRoom";
import { Action } from "../dispatcher/actions";
import { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload";
import { getAddressType } from "../UserAddress";
import DMRoomMap from "./DMRoomMap";
import { isJoinedOrNearlyJoined } from "./membership";
import dis from "../dispatcher/dispatcher";
import { LocalRoom, LocalRoomState } from "../models/LocalRoom";
import { waitForRoomReadyAndApplyAfterCreateCallbacks } from "./local-room";
import { findDMRoom } from "./dm/findDMRoom";
import { privateShouldBeEncrypted } from "./rooms";
import { createDmLocalRoom } from "./dm/createDmLocalRoom";
import { startDm } from "./dm/startDm";
export function findDMForUser(client: MatrixClient, userId: string): Room {
const roomIds = DMRoomMap.shared().getDMRoomsForUserId(userId);
const rooms = roomIds.map(id => client.getRoom(id));
const suitableDMRooms = rooms.filter(r => {
// Validate that we are joined and the other person is also joined. We'll also make sure
// that the room also looks like a DM (until we have canonical DMs to tell us). For now,
// a DM is a room of two people that contains those two people exactly. This does mean
// that bots, assistants, etc will ruin a room's DM-ness, though this is a problem for
// canonical DMs to solve.
if (r && r.getMyMembership() === "join") {
const members = r.currentState.getMembers();
const joinedMembers = members.filter(m => isJoinedOrNearlyJoined(m.membership));
const otherMember = joinedMembers.find(m => m.userId === userId);
return otherMember && joinedMembers.length === 2;
}
return false;
}).sort((r1, r2) => {
return r2.getLastActiveTimestamp() -
r1.getLastActiveTimestamp();
});
if (suitableDMRooms.length) {
return suitableDMRooms[0];
}
}
export async function startDm(client: MatrixClient, targets: Member[]): Promise<void> {
const targetIds = targets.map(t => t.userId);
// Check if there is already a DM with these people and reuse it if possible.
let existingRoom: Room;
if (targetIds.length === 1) {
existingRoom = findDMForUser(client, targetIds[0]);
} else {
existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds);
}
export async function startDmOnFirstMessage(
client: MatrixClient,
targets: Member[],
): Promise<Room> {
const existingRoom = findDMRoom(client, targets);
if (existingRoom) {
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
@ -70,51 +42,47 @@ export async function startDm(client: MatrixClient, targets: Member[]): Promise<
joining: false,
metricsTrigger: "MessageUser",
});
return existingRoom;
}
const room = await createDmLocalRoom(client, targets);
dis.dispatch({
action: Action.ViewRoom,
room_id: room.roomId,
joining: false,
targets,
});
return room;
}
/**
* Starts a DM based on a local room.
*
* @async
* @param {MatrixClient} client
* @param {LocalRoom} localRoom
* @returns {Promise<string | void>} Resolves to the created room id
*/
export async function createRoomFromLocalRoom(client: MatrixClient, localRoom: LocalRoom): Promise<string | void> {
if (!localRoom.isNew) {
// This action only makes sense for new local rooms.
return;
}
const createRoomOptions = { inlineErrors: true } as any; // XXX: Type out `createRoomOptions`
localRoom.state = LocalRoomState.CREATING;
client.emit(ClientEvent.Room, localRoom);
if (privateShouldBeEncrypted()) {
// Check whether all users have uploaded device keys before.
// If so, enable encryption in the new room.
const has3PidMembers = targets.some(t => t instanceof ThreepidMember);
if (!has3PidMembers) {
const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds);
if (allHaveDeviceKeys) {
createRoomOptions.encryption = true;
}
}
}
// Check if it's a traditional DM and create the room if required.
// TODO: [Canonical DMs] Remove this check and instead just create the multi-person DM
const isSelf = targetIds.length === 1 && targetIds[0] === client.getUserId();
if (targetIds.length === 1 && !isSelf) {
createRoomOptions.dmUserId = targetIds[0];
}
if (targetIds.length > 1) {
createRoomOptions.createOpts = targetIds.reduce(
(roomOptions, address) => {
const type = getAddressType(address);
if (type === 'email') {
const invite: IInvite3PID = {
id_server: client.getIdentityServerUrl(true),
medium: 'email',
address,
};
roomOptions.invite_3pid.push(invite);
} else if (type === 'mx-user-id') {
roomOptions.invite.push(address);
}
return roomOptions;
},
{ invite: [], invite_3pid: [] },
);
}
await createRoom(createRoomOptions);
return startDm(client, localRoom.targets, false).then(
(roomId) => {
localRoom.actualRoomId = roomId;
return waitForRoomReadyAndApplyAfterCreateCallbacks(client, localRoom);
},
() => {
logger.warn(`Error creating DM for local room ${localRoom.roomId}`);
localRoom.state = LocalRoomState.ERROR;
client.emit(ClientEvent.Room, localRoom);
},
);
}
// This is the interface that is expected by various components in the Invite Dialog and RoomInvite.
@ -200,3 +168,28 @@ export interface IDMUserTileProps {
member: Member;
onRemove(member: Member): void;
}
/**
* Detects whether a room should be encrypted.
*
* @async
* @param {MatrixClient} client
* @param {Member[]} targets The members to which run the check against
* @returns {Promise<boolean>}
*/
export async function determineCreateRoomEncryptionOption(client: MatrixClient, targets: Member[]): Promise<boolean> {
if (privateShouldBeEncrypted()) {
// Check whether all users have uploaded device keys before.
// If so, enable encryption in the new room.
const has3PidMembers = targets.some(t => t instanceof ThreepidMember);
if (!has3PidMembers) {
const targetIds = targets.map(t => t.userId);
const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds);
if (allHaveDeviceKeys) {
return true;
}
}
}
return false;
}

View file

@ -0,0 +1,123 @@
/*
Copyright 2022 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 { MEGOLM_ALGORITHM } from "matrix-js-sdk/src/crypto/olmlib";
import { EventType, KNOWN_SAFE_ROOM_VERSION, MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../../src/models/LocalRoom";
import { determineCreateRoomEncryptionOption, Member } from "../../../src/utils/direct-messages";
/**
* Create a DM local room. This room will not be send to the server and only exists inside the client.
* It sets up the local room with some artificial state events
* so that can be used in most components instead of a real room.
*
* @async
* @param {MatrixClient} client
* @param {Member[]} targets DM partners
* @returns {Promise<LocalRoom>} Resolves to the new local room
*/
export async function createDmLocalRoom(
client: MatrixClient,
targets: Member[],
): Promise<LocalRoom> {
const userId = client.getUserId();
const localRoom = new LocalRoom(LOCAL_ROOM_ID_PREFIX + client.makeTxnId(), client, userId);
const events = [];
events.push(new MatrixEvent({
event_id: `~${localRoom.roomId}:${client.makeTxnId()}`,
type: EventType.RoomCreate,
content: {
creator: userId,
room_version: KNOWN_SAFE_ROOM_VERSION,
},
state_key: "",
user_id: userId,
sender: userId,
room_id: localRoom.roomId,
origin_server_ts: Date.now(),
}));
if (await determineCreateRoomEncryptionOption(client, targets)) {
localRoom.encrypted = true;
events.push(
new MatrixEvent({
event_id: `~${localRoom.roomId}:${client.makeTxnId()}`,
type: EventType.RoomEncryption,
content: {
algorithm: MEGOLM_ALGORITHM,
},
user_id: userId,
sender: userId,
state_key: "",
room_id: localRoom.roomId,
origin_server_ts: Date.now(),
}),
);
}
events.push(new MatrixEvent({
event_id: `~${localRoom.roomId}:${client.makeTxnId()}`,
type: EventType.RoomMember,
content: {
displayname: userId,
membership: "join",
},
state_key: userId,
user_id: userId,
sender: userId,
room_id: localRoom.roomId,
}));
targets.forEach((target: Member) => {
events.push(new MatrixEvent({
event_id: `~${localRoom.roomId}:${client.makeTxnId()}`,
type: EventType.RoomMember,
content: {
displayname: target.name,
avatar_url: target.getMxcAvatarUrl(),
membership: "invite",
isDirect: true,
},
state_key: target.userId,
sender: userId,
room_id: localRoom.roomId,
}));
events.push(new MatrixEvent({
event_id: `~${localRoom.roomId}:${client.makeTxnId()}`,
type: EventType.RoomMember,
content: {
displayname: target.name,
avatar_url: target.getMxcAvatarUrl(),
membership: "join",
},
state_key: target.userId,
sender: target.userId,
room_id: localRoom.roomId,
}));
});
localRoom.targets = targets;
localRoom.updateMyMembership("join");
localRoom.addLiveEvents(events);
localRoom.currentState.setStateEvents(events);
localRoom.name = localRoom.getDefaultRoomName(client.getUserId());
client.store.storeRoom(localRoom);
return localRoom;
}

View file

@ -0,0 +1,55 @@
/*
Copyright 2022 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 { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import DMRoomMap from "../DMRoomMap";
import { isLocalRoom } from "../localRoom/isLocalRoom";
import { isJoinedOrNearlyJoined } from "../membership";
/**
* Tries to find a DM room with a specific user.
*
* @param {MatrixClient} client
* @param {string} userId ID of the user to find the DM for
* @returns {Room} Room if found
*/
export function findDMForUser(client: MatrixClient, userId: string): Room {
const roomIds = DMRoomMap.shared().getDMRoomsForUserId(userId);
const rooms = roomIds.map(id => client.getRoom(id));
const suitableDMRooms = rooms.filter(r => {
// Validate that we are joined and the other person is also joined. We'll also make sure
// that the room also looks like a DM (until we have canonical DMs to tell us). For now,
// a DM is a room of two people that contains those two people exactly. This does mean
// that bots, assistants, etc will ruin a room's DM-ness, though this is a problem for
// canonical DMs to solve.
if (r && r.getMyMembership() === "join") {
if (isLocalRoom(r)) return false;
const members = r.currentState.getMembers();
const joinedMembers = members.filter(m => isJoinedOrNearlyJoined(m.membership));
const otherMember = joinedMembers.find(m => m.userId === userId);
return otherMember && joinedMembers.length === 2;
}
return false;
}).sort((r1, r2) => {
return r2.getLastActiveTimestamp() -
r1.getLastActiveTimestamp();
});
if (suitableDMRooms.length) {
return suitableDMRooms[0];
}
}

View file

@ -0,0 +1,42 @@
/*
Copyright 2022 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 { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import { Member } from "../direct-messages";
import DMRoomMap from "../DMRoomMap";
import { findDMForUser } from "./findDMForUser";
/**
* Tries to find a DM room with some other users.
*
* @param {MatrixClient} client
* @param {Member[]} targets The Members to try to find the room for
* @returns {Room | null} Resolved so the room if found, else null
*/
export function findDMRoom(client: MatrixClient, targets: Member[]): Room | null {
const targetIds = targets.map(t => t.userId);
let existingRoom: Room;
if (targetIds.length === 1) {
existingRoom = findDMForUser(client, targetIds[0]);
} else {
existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds);
}
if (existingRoom) {
return existingRoom;
}
return null;
}

90
src/utils/dm/startDm.ts Normal file
View file

@ -0,0 +1,90 @@
/*
Copyright 2022 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 { IInvite3PID, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import { Action } from "../../dispatcher/actions";
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { determineCreateRoomEncryptionOption, Member } from "../direct-messages";
import DMRoomMap from "../DMRoomMap";
import { isLocalRoom } from "../localRoom/isLocalRoom";
import { findDMForUser } from "./findDMForUser";
import dis from "../../dispatcher/dispatcher";
import { getAddressType } from "../../UserAddress";
import createRoom from "../../createRoom";
/**
* Start a DM.
*
* @returns {Promise<string | null} Resolves to the room id.
*/
export async function startDm(client: MatrixClient, targets: Member[], showSpinner = true): Promise<string | null> {
const targetIds = targets.map(t => t.userId);
// Check if there is already a DM with these people and reuse it if possible.
let existingRoom: Room;
if (targetIds.length === 1) {
existingRoom = findDMForUser(client, targetIds[0]);
} else {
existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds);
}
if (existingRoom && !isLocalRoom(existingRoom)) {
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: existingRoom.roomId,
should_peek: false,
joining: false,
metricsTrigger: "MessageUser",
});
return Promise.resolve(existingRoom.roomId);
}
const createRoomOptions = { inlineErrors: true } as any; // XXX: Type out `createRoomOptions`
if (await determineCreateRoomEncryptionOption(client, targets)) {
createRoomOptions.encryption = true;
}
// Check if it's a traditional DM and create the room if required.
// TODO: [Canonical DMs] Remove this check and instead just create the multi-person DM
const isSelf = targetIds.length === 1 && targetIds[0] === client.getUserId();
if (targetIds.length === 1 && !isSelf) {
createRoomOptions.dmUserId = targetIds[0];
}
if (targetIds.length > 1) {
createRoomOptions.createOpts = targetIds.reduce(
(roomOptions, address) => {
const type = getAddressType(address);
if (type === 'email') {
const invite: IInvite3PID = {
id_server: client.getIdentityServerUrl(true),
medium: 'email',
address,
};
roomOptions.invite_3pid.push(invite);
} else if (type === 'mx-user-id') {
roomOptions.invite.push(address);
}
return roomOptions;
},
{ invite: [], invite_3pid: [] },
);
}
createRoomOptions.spinner = showSpinner;
return createRoom(createRoomOptions);
}

View file

@ -15,12 +15,13 @@ limitations under the License.
*/
import { logger } from "matrix-js-sdk/src/logger";
import { ClientEvent, EventType, MatrixClient } from "matrix-js-sdk/src/matrix";
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/matrix";
import defaultDispatcher from "../dispatcher/dispatcher";
import { MatrixClientPeg } from "../MatrixClientPeg";
import { LocalRoom, LocalRoomState, LOCAL_ROOM_ID_PREFIX } from "../models/LocalRoom";
import * as thisModule from "./local-room";
import { LocalRoom, LocalRoomState } from "../models/LocalRoom";
import { isLocalRoom } from "./localRoom/isLocalRoom";
import { isRoomReady } from "./localRoom/isRoomReady";
/**
* Does a room action:
@ -40,7 +41,7 @@ export async function doMaybeLocalRoomAction<T>(
fn: (actualRoomId: string) => Promise<T>,
client?: MatrixClient,
): Promise<T> {
if (roomId.startsWith(LOCAL_ROOM_ID_PREFIX)) {
if (isLocalRoom(roomId)) {
client = client ?? MatrixClientPeg.get();
const room = client.getRoom(roomId) as LocalRoom;
@ -62,34 +63,6 @@ export async function doMaybeLocalRoomAction<T>(
return fn(roomId);
}
/**
* Tests whether a room created based on a local room is ready.
*/
export function isRoomReady(
client: MatrixClient,
localRoom: LocalRoom,
): boolean {
// not ready if no actual room id exists
if (!localRoom.actualRoomId) return false;
const room = client.getRoom(localRoom.actualRoomId);
// not ready if the room does not exist
if (!room) return false;
// not ready if not all members joined/invited
if (room.getInvitedAndJoinedMemberCount() !== 1 + localRoom.targets?.length) return false;
const roomHistoryVisibilityEvents = room.currentState.getStateEvents(EventType.RoomHistoryVisibility);
// not ready if the room history has not been configured
if (roomHistoryVisibilityEvents.length === 0) return false;
const roomEncryptionEvents = room.currentState.getStateEvents(EventType.RoomEncryption);
// not ready if encryption has not been configured (applies only to encrypted rooms)
if (localRoom.encrypted === true && roomEncryptionEvents.length === 0) return false;
return true;
}
/**
* Waits until a room is ready and then applies the after-create local room callbacks.
* Also implements a stopgap timeout after that a room is assumed to be ready.
@ -104,7 +77,7 @@ export async function waitForRoomReadyAndApplyAfterCreateCallbacks(
client: MatrixClient,
localRoom: LocalRoom,
): Promise<string> {
if (thisModule.isRoomReady(client, localRoom)) {
if (isRoomReady(client, localRoom)) {
return applyAfterCreateCallbacks(localRoom, localRoom.actualRoomId).then(() => {
localRoom.state = LocalRoomState.CREATED;
client.emit(ClientEvent.Room, localRoom);
@ -130,7 +103,7 @@ export async function waitForRoomReadyAndApplyAfterCreateCallbacks(
};
const checkRoomStateIntervalHandle = setInterval(() => {
if (thisModule.isRoomReady(client, localRoom)) finish();
if (isRoomReady(client, localRoom)) finish();
}, 500);
const stopgapTimeoutHandle = setTimeout(stopgapFinish, 5000);
});

View file

@ -0,0 +1,47 @@
/*
Copyright 2022 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 { EventType, MatrixClient } from "matrix-js-sdk/src/matrix";
import { LocalRoom } from "../../models/LocalRoom";
/**
* Tests whether a room created based on a local room is ready.
*/
export function isRoomReady(
client: MatrixClient,
localRoom: LocalRoom,
): boolean {
// not ready if no actual room id exists
if (!localRoom.actualRoomId) return false;
const room = client.getRoom(localRoom.actualRoomId);
// not ready if the room does not exist
if (!room) return false;
// not ready if not all members joined/invited
if (room.getInvitedAndJoinedMemberCount() !== 1 + localRoom.targets?.length) return false;
const roomHistoryVisibilityEvents = room.currentState.getStateEvents(EventType.RoomHistoryVisibility);
// not ready if the room history has not been configured
if (roomHistoryVisibilityEvents.length === 0) return false;
const roomEncryptionEvents = room.currentState.getStateEvents(EventType.RoomEncryption);
// not ready if encryption has not been configured (applies only to encrypted rooms)
if (localRoom.encrypted === true && roomEncryptionEvents.length === 0) return false;
return true;
}

View file

@ -28,7 +28,7 @@ import { IDevice } from "./components/views/right_panel/UserInfo";
import ManualDeviceKeyVerificationDialog from "./components/views/dialogs/ManualDeviceKeyVerificationDialog";
import RightPanelStore from "./stores/right-panel/RightPanelStore";
import { IRightPanelCardState } from "./stores/right-panel/RightPanelStoreIPanelState";
import { findDMForUser } from "./utils/direct-messages";
import { findDMForUser } from "./utils/dm/findDMForUser";
async function enable4SIfNeeded() {
const cli = MatrixClientPeg.get();

View file

@ -0,0 +1,175 @@
/*
Copyright 2022 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 { mocked } from "jest-mock";
import { ClientEvent, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import DMRoomMap from "../../src/utils/DMRoomMap";
import { createTestClient } from "../test-utils";
import { LocalRoom, LocalRoomState, LOCAL_ROOM_ID_PREFIX } from "../../src/models/LocalRoom";
import * as dmModule from "../../src/utils/direct-messages";
import dis from "../../src/dispatcher/dispatcher";
import { Action } from "../../src/dispatcher/actions";
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
import { waitForRoomReadyAndApplyAfterCreateCallbacks } from "../../src/utils/local-room";
import { findDMRoom } from "../../src/utils/dm/findDMRoom";
import { createDmLocalRoom } from "../../src/utils/dm/createDmLocalRoom";
import { startDm } from "../../src/utils/dm/startDm";
jest.mock("../../src/utils/rooms", () => ({
...(jest.requireActual("../../src/utils/rooms") as object),
privateShouldBeEncrypted: jest.fn(),
}));
jest.mock("../../src/createRoom", () => ({
...(jest.requireActual("../../src/createRoom") as object),
canEncryptToAllUsers: jest.fn(),
}));
jest.mock("../../src/utils/local-room", () => ({
waitForRoomReadyAndApplyAfterCreateCallbacks: jest.fn(),
}));
jest.mock("../../src/utils/dm/findDMForUser", () => ({
findDMForUser: jest.fn(),
}));
jest.mock("../../src/utils/dm/findDMRoom", () => ({
findDMRoom: jest.fn(),
}));
jest.mock("../../src/utils/dm/createDmLocalRoom", () => ({
createDmLocalRoom: jest.fn(),
}));
jest.mock("../../src/utils/dm/startDm", () => ({
startDm: jest.fn(),
}));
describe("direct-messages", () => {
const userId1 = "@user1:example.com";
const member1 = new dmModule.DirectoryMember({ user_id: userId1 });
let room1: Room;
let localRoom: LocalRoom;
let dmRoomMap: DMRoomMap;
let mockClient: MatrixClient;
let roomEvents: Room[];
beforeEach(() => {
jest.restoreAllMocks();
mockClient = createTestClient();
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
roomEvents = [];
mockClient.on(ClientEvent.Room, (room: Room) => {
roomEvents.push(room);
});
room1 = new Room("!room1:example.com", mockClient, userId1);
room1.getMyMembership = () => "join";
localRoom = new LocalRoom(LOCAL_ROOM_ID_PREFIX + "test", mockClient, userId1);
dmRoomMap = {
getDMRoomForIdentifiers: jest.fn(),
getDMRoomsForUserId: jest.fn(),
} as unknown as DMRoomMap;
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
jest.spyOn(dis, "dispatch");
jest.setSystemTime(new Date(2022, 7, 4, 11, 12, 30, 42));
});
describe("startDmOnFirstMessage", () => {
describe("if no room exists", () => {
beforeEach(() => {
mocked(findDMRoom).mockReturnValue(null);
});
it("should create a local room and dispatch a view room event", async () => {
mocked(createDmLocalRoom).mockResolvedValue(localRoom);
const room = await dmModule.startDmOnFirstMessage(mockClient, [member1]);
expect(room).toBe(localRoom);
expect(dis.dispatch).toHaveBeenCalledWith({
action: Action.ViewRoom,
room_id: room.roomId,
joining: false,
targets: [member1],
});
});
});
describe("if a room exists", () => {
beforeEach(() => {
mocked(findDMRoom).mockReturnValue(room1);
});
it("should return the room and dispatch a view room event", async () => {
const room = await dmModule.startDmOnFirstMessage(mockClient, [member1]);
expect(room).toBe(room1);
expect(dis.dispatch).toHaveBeenCalledWith({
action: Action.ViewRoom,
room_id: room1.roomId,
should_peek: false,
joining: false,
metricsTrigger: "MessageUser",
});
});
});
});
describe("createRoomFromLocalRoom", () => {
[LocalRoomState.CREATING, LocalRoomState.CREATED, LocalRoomState.ERROR].forEach((state: LocalRoomState) => {
it(`should do nothing for room in state ${state}`, async () => {
localRoom.state = state;
await dmModule.createRoomFromLocalRoom(mockClient, localRoom);
expect(localRoom.state).toBe(state);
expect(startDm).not.toHaveBeenCalled();
});
});
describe("on startDm error", () => {
beforeEach(() => {
mocked(startDm).mockRejectedValue(true);
});
it("should set the room state to error", async () => {
await dmModule.createRoomFromLocalRoom(mockClient, localRoom);
expect(localRoom.state).toBe(LocalRoomState.ERROR);
});
});
describe("on startDm success", () => {
beforeEach(() => {
mocked(waitForRoomReadyAndApplyAfterCreateCallbacks).mockResolvedValue(room1.roomId);
mocked(startDm).mockResolvedValue(room1.roomId);
});
it(
"should set the room into creating state and call waitForRoomReadyAndApplyAfterCreateCallbacks",
async () => {
const result = await dmModule.createRoomFromLocalRoom(mockClient, localRoom);
expect(result).toBe(room1.roomId);
expect(localRoom.state).toBe(LocalRoomState.CREATING);
expect(waitForRoomReadyAndApplyAfterCreateCallbacks).toHaveBeenCalledWith(
mockClient,
localRoom,
);
},
);
});
});
});

View file

@ -0,0 +1,114 @@
/*
Copyright 2022 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 { mocked } from "jest-mock";
import { EventType, KNOWN_SAFE_ROOM_VERSION, MatrixClient } from "matrix-js-sdk/src/matrix";
import { canEncryptToAllUsers } from "../../../src/createRoom";
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../../src/models/LocalRoom";
import { DirectoryMember, Member, ThreepidMember } from "../../../src/utils/direct-messages";
import { createDmLocalRoom } from "../../../src/utils/dm/createDmLocalRoom";
import { privateShouldBeEncrypted } from "../../../src/utils/rooms";
import { createTestClient } from "../../test-utils";
jest.mock("../../../src/utils/rooms", () => ({
privateShouldBeEncrypted: jest.fn(),
}));
jest.mock("../../../src/createRoom", () => ({
canEncryptToAllUsers: jest.fn(),
}));
function assertLocalRoom(room: LocalRoom, targets: Member[], encrypted: boolean) {
expect(room.roomId).toBe(LOCAL_ROOM_ID_PREFIX + "t1");
expect(room.name).toBe(targets.length ? targets[0].name : "Empty Room");
expect(room.encrypted).toBe(encrypted);
expect(room.targets).toEqual(targets);
expect(room.getMyMembership()).toBe("join");
const roomCreateEvent = room.currentState.getStateEvents(EventType.RoomCreate)[0];
expect(roomCreateEvent).toBeDefined();
expect(roomCreateEvent.getContent()["room_version"]).toBe(KNOWN_SAFE_ROOM_VERSION);
// check that the user and all targets are joined
expect(room.getMember("@userId:matrix.org").membership).toBe("join");
targets.forEach((target: Member) => {
expect(room.getMember(target.userId).membership).toBe("join");
});
if (encrypted) {
const encryptionEvent = room.currentState.getStateEvents(EventType.RoomEncryption)[0];
expect(encryptionEvent).toBeDefined();
}
}
describe("createDmLocalRoom", () => {
let mockClient: MatrixClient;
const userId1 = "@user1:example.com";
const member1 = new DirectoryMember({ user_id: userId1 });
const member2 = new ThreepidMember("user2");
beforeEach(() => {
mockClient = createTestClient();
});
describe("when rooms should be encrypted", () => {
beforeEach(() => {
mocked(privateShouldBeEncrypted).mockReturnValue(true);
});
it("should create an unencrypted room for 3PID targets", async () => {
const room = await createDmLocalRoom(mockClient, [member2]);
expect(mockClient.store.storeRoom).toHaveBeenCalledWith(room);
assertLocalRoom(room, [member2], false);
});
describe("for MXID targets with encryption available", () => {
beforeEach(() => {
mocked(canEncryptToAllUsers).mockResolvedValue(true);
});
it("should create an encrypted room", async () => {
const room = await createDmLocalRoom(mockClient, [member1]);
expect(mockClient.store.storeRoom).toHaveBeenCalledWith(room);
assertLocalRoom(room, [member1], true);
});
});
describe("for MXID targets with encryption unavailable", () => {
beforeEach(() => {
mocked(canEncryptToAllUsers).mockResolvedValue(false);
});
it("should create an unencrypted room", async () => {
const room = await createDmLocalRoom(mockClient, [member1]);
expect(mockClient.store.storeRoom).toHaveBeenCalledWith(room);
assertLocalRoom(room, [member1], false);
});
});
});
describe("if rooms should not be encrypted", () => {
beforeEach(() => {
mocked(privateShouldBeEncrypted).mockReturnValue(false);
});
it("should create an unencrypted room", async () => {
const room = await createDmLocalRoom(mockClient, [member1]);
assertLocalRoom(room, [member1], false);
});
});
});

View file

@ -0,0 +1,72 @@
/*
Copyright 2022 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 { mocked } from "jest-mock";
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import { DirectoryMember, ThreepidMember } from "../../../src/utils/direct-messages";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import DMRoomMap from "../../../src/utils/DMRoomMap";
import { createTestClient } from "../../test-utils";
import { findDMRoom } from "../../../src/utils/dm/findDMRoom";
import { findDMForUser } from "../../../src/utils/dm/findDMForUser";
jest.mock("../../../src/utils/dm/findDMForUser", () => ({
findDMForUser: jest.fn(),
}));
describe("findDMRoom", () => {
const userId1 = "@user1:example.com";
const member1 = new DirectoryMember({ user_id: userId1 });
const member2 = new ThreepidMember("user2");
let mockClient: MatrixClient;
let room1: Room;
let dmRoomMap: DMRoomMap;
beforeEach(() => {
mockClient = createTestClient();
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
room1 = new Room("!room1:example.com", mockClient, userId1);
room1.getMyMembership = () => "join";
dmRoomMap = {
getDMRoomForIdentifiers: jest.fn(),
getDMRoomsForUserId: jest.fn(),
} as unknown as DMRoomMap;
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
});
it("should return the room for a single target with a room", () => {
mocked(findDMForUser).mockReturnValue(room1);
expect(findDMRoom(mockClient, [member1])).toBe(room1);
});
it("should return null for a single target without a room", () => {
mocked(findDMForUser).mockReturnValue(null);
expect(findDMRoom(mockClient, [member1])).toBeNull();
});
it("should return the room for 2 targets with a room", () => {
mocked(dmRoomMap.getDMRoomForIdentifiers).mockReturnValue(room1);
expect(findDMRoom(mockClient, [member1, member2])).toBe(room1);
});
it("should return null for 2 targets without a room", () => {
mocked(dmRoomMap.getDMRoomForIdentifiers).mockReturnValue(null);
expect(findDMRoom(mockClient, [member1, member2])).toBeNull();
});
});

View file

@ -0,0 +1,70 @@
/*
Copyright 2022 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 { mocked } from "jest-mock";
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import { DirectoryMember, ThreepidMember } from "../../../src/utils/direct-messages";
import { findDMForUser } from "../../../src/utils/dm/findDMForUser";
import { findDMRoom } from "../../../src/utils/dm/findDMRoom";
import DMRoomMap from "../../../src/utils/DMRoomMap";
import { createTestClient } from "../../test-utils";
jest.mock("../../../src/utils/dm/findDMForUser", () => ({
findDMForUser: jest.fn(),
}));
describe("findDMRoom", () => {
const userId1 = "@user1:example.com";
const member1 = new DirectoryMember({ user_id: userId1 });
const member2 = new ThreepidMember("user2");
let mockClient: MatrixClient;
let room1: Room;
let dmRoomMap: DMRoomMap;
beforeEach(() => {
mockClient = createTestClient();
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
room1 = new Room("!room1:example.com", mockClient, userId1);
dmRoomMap = {
getDMRoomForIdentifiers: jest.fn(),
getDMRoomsForUserId: jest.fn(),
} as unknown as DMRoomMap;
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
});
it("should return the room for a single target with a room", () => {
mocked(findDMForUser).mockReturnValue(room1);
expect(findDMRoom(mockClient, [member1])).toBe(room1);
});
it("should return null for a single target without a room", () => {
mocked(findDMForUser).mockReturnValue(null);
expect(findDMRoom(mockClient, [member1])).toBeNull();
});
it("should return the room for 2 targets with a room", () => {
mocked(dmRoomMap.getDMRoomForIdentifiers).mockReturnValue(room1);
expect(findDMRoom(mockClient, [member1, member2])).toBe(room1);
});
it("should return null for 2 targets without a room", () => {
mocked(dmRoomMap.getDMRoomForIdentifiers).mockReturnValue(null);
expect(findDMRoom(mockClient, [member1, member2])).toBeNull();
});
});

View file

@ -15,18 +15,20 @@ limitations under the License.
*/
import { mocked } from "jest-mock";
import { EventType, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import { LocalRoom, LocalRoomState, LOCAL_ROOM_ID_PREFIX } from "../../src/models/LocalRoom";
import * as localRoomModule from "../../src/utils/local-room";
import defaultDispatcher from "../../src/dispatcher/dispatcher";
import { createTestClient, makeMembershipEvent, mkEvent } from "../test-utils";
import { DirectoryMember } from "../../src/utils/direct-messages";
import { createTestClient } from "../test-utils";
import { isRoomReady } from "../../src/utils/localRoom/isRoomReady";
jest.mock("../../src/utils/localRoom/isRoomReady", () => ({
isRoomReady: jest.fn(),
}));
describe("local-room", () => {
const userId1 = "@user1:example.com";
const member1 = new DirectoryMember({ user_id: userId1 });
const userId2 = "@user2:example.com";
let room1: Room;
let localRoom: LocalRoom;
let client: MatrixClient;
@ -88,94 +90,6 @@ describe("local-room", () => {
});
});
describe("isRoomReady", () => {
beforeEach(() => {
localRoom.targets = [member1];
});
it("should return false if the room has no actual room id", () => {
expect(localRoomModule.isRoomReady(client, localRoom)).toBe(false);
});
describe("for a room with an actual room id", () => {
beforeEach(() => {
localRoom.actualRoomId = room1.roomId;
mocked(client.getRoom).mockReturnValue(null);
});
it("it should return false", () => {
expect(localRoomModule.isRoomReady(client, localRoom)).toBe(false);
});
describe("and the room is known to the client", () => {
beforeEach(() => {
mocked(client.getRoom).mockImplementation((roomId: string) => {
if (roomId === room1.roomId) return room1;
});
});
it("it should return false", () => {
expect(localRoomModule.isRoomReady(client, localRoom)).toBe(false);
});
describe("and all members have been invited or joined", () => {
beforeEach(() => {
room1.currentState.setStateEvents([
makeMembershipEvent(room1.roomId, userId1, "join"),
makeMembershipEvent(room1.roomId, userId2, "invite"),
]);
});
it("it should return false", () => {
expect(localRoomModule.isRoomReady(client, localRoom)).toBe(false);
});
describe("and a RoomHistoryVisibility event", () => {
beforeEach(() => {
room1.currentState.setStateEvents([mkEvent({
user: userId1,
event: true,
type: EventType.RoomHistoryVisibility,
room: room1.roomId,
content: {},
})]);
});
it("it should return true", () => {
expect(localRoomModule.isRoomReady(client, localRoom)).toBe(true);
});
describe("and an encrypted room", () => {
beforeEach(() => {
localRoom.encrypted = true;
});
it("it should return false", () => {
expect(localRoomModule.isRoomReady(client, localRoom)).toBe(false);
});
describe("and a room encryption state event", () => {
beforeEach(() => {
room1.currentState.setStateEvents([mkEvent({
user: userId1,
event: true,
type: EventType.RoomEncryption,
room: room1.roomId,
content: {},
})]);
});
it("it should return true", () => {
expect(localRoomModule.isRoomReady(client, localRoom)).toBe(true);
});
});
});
});
});
});
});
});
describe("waitForRoomReadyAndApplyAfterCreateCallbacks", () => {
let localRoomCallbackRoomId: string;
@ -190,7 +104,7 @@ describe("local-room", () => {
describe("for an immediate ready room", () => {
beforeEach(() => {
jest.spyOn(localRoomModule, "isRoomReady").mockReturnValue(true);
mocked(isRoomReady).mockReturnValue(true);
});
it("should invoke the callbacks, set the room state to created and return the actual room id", async () => {
@ -203,7 +117,7 @@ describe("local-room", () => {
describe("for a room running into the create timeout", () => {
beforeEach(() => {
jest.spyOn(localRoomModule, "isRoomReady").mockReturnValue(false);
mocked(isRoomReady).mockReturnValue(false);
});
it("should invoke the callbacks, set the room state to created and return the actual room id", (done) => {
@ -221,12 +135,12 @@ describe("local-room", () => {
describe("for a room that is ready after a while", () => {
beforeEach(() => {
jest.spyOn(localRoomModule, "isRoomReady").mockReturnValue(false);
mocked(isRoomReady).mockReturnValue(false);
});
it("should invoke the callbacks, set the room state to created and return the actual room id", (done) => {
const prom = localRoomModule.waitForRoomReadyAndApplyAfterCreateCallbacks(client, localRoom);
jest.spyOn(localRoomModule, "isRoomReady").mockReturnValue(true);
mocked(isRoomReady).mockReturnValue(true);
jest.advanceTimersByTime(500);
prom.then((roomId: string) => {
expect(localRoom.state).toBe(LocalRoomState.CREATED);

View file

@ -0,0 +1,126 @@
/*
Copyright 2022 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 { mocked } from "jest-mock";
import { EventType, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../../src/models/LocalRoom";
import { DirectoryMember } from "../../../src/utils/direct-messages";
import { isRoomReady } from "../../../src/utils/localRoom/isRoomReady";
import { createTestClient, makeMembershipEvent, mkEvent } from "../../test-utils";
describe("isRoomReady", () => {
const userId1 = "@user1:example.com";
const member1 = new DirectoryMember({ user_id: userId1 });
const userId2 = "@user2:example.com";
let room1: Room;
let localRoom: LocalRoom;
let client: MatrixClient;
beforeEach(() => {
client = createTestClient();
room1 = new Room("!room1:example.com", client, userId1);
room1.getMyMembership = () => "join";
localRoom = new LocalRoom(LOCAL_ROOM_ID_PREFIX + "test", client, "@test:example.com");
});
beforeEach(() => {
localRoom.targets = [member1];
});
it("should return false if the room has no actual room id", () => {
expect(isRoomReady(client, localRoom)).toBe(false);
});
describe("for a room with an actual room id", () => {
beforeEach(() => {
localRoom.actualRoomId = room1.roomId;
mocked(client.getRoom).mockReturnValue(null);
});
it("it should return false", () => {
expect(isRoomReady(client, localRoom)).toBe(false);
});
describe("and the room is known to the client", () => {
beforeEach(() => {
mocked(client.getRoom).mockImplementation((roomId: string) => {
if (roomId === room1.roomId) return room1;
});
});
it("it should return false", () => {
expect(isRoomReady(client, localRoom)).toBe(false);
});
describe("and all members have been invited or joined", () => {
beforeEach(() => {
room1.currentState.setStateEvents([
makeMembershipEvent(room1.roomId, userId1, "join"),
makeMembershipEvent(room1.roomId, userId2, "invite"),
]);
});
it("it should return false", () => {
expect(isRoomReady(client, localRoom)).toBe(false);
});
describe("and a RoomHistoryVisibility event", () => {
beforeEach(() => {
room1.currentState.setStateEvents([mkEvent({
user: userId1,
event: true,
type: EventType.RoomHistoryVisibility,
room: room1.roomId,
content: {},
})]);
});
it("it should return true", () => {
expect(isRoomReady(client, localRoom)).toBe(true);
});
describe("and an encrypted room", () => {
beforeEach(() => {
localRoom.encrypted = true;
});
it("it should return false", () => {
expect(isRoomReady(client, localRoom)).toBe(false);
});
describe("and a room encryption state event", () => {
beforeEach(() => {
room1.currentState.setStateEvents([mkEvent({
user: userId1,
event: true,
type: EventType.RoomEncryption,
room: room1.roomId,
content: {},
})]);
});
it("it should return true", () => {
expect(isRoomReady(client, localRoom)).toBe(true);
});
});
});
});
});
});
});
});