Implement third-party invite waiting room (#10229)
This commit is contained in:
parent
94950c6987
commit
db6ee53535
12 changed files with 388 additions and 40 deletions
src
components
i18n/strings
utils
test
components
utils
|
@ -116,6 +116,8 @@ import VoipUserMapper from "../../VoipUserMapper";
|
|||
import { isCallEvent } from "./LegacyCallEventGrouper";
|
||||
import { WidgetType } from "../../widgets/WidgetType";
|
||||
import WidgetUtils from "../../utils/WidgetUtils";
|
||||
import { shouldEncryptRoomWithSingle3rdPartyInvite } from "../../utils/room/shouldEncryptRoomWithSingle3rdPartyInvite";
|
||||
import { WaitingForThirdPartyRoomView } from "./WaitingForThirdPartyRoomView";
|
||||
|
||||
const DEBUG = false;
|
||||
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
|
||||
|
@ -231,6 +233,7 @@ export interface IRoomState {
|
|||
}
|
||||
|
||||
interface LocalRoomViewProps {
|
||||
localRoom: LocalRoom;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
roomView: RefObject<HTMLElement>;
|
||||
|
@ -246,7 +249,7 @@ interface LocalRoomViewProps {
|
|||
function LocalRoomView(props: LocalRoomViewProps): ReactElement {
|
||||
const context = useContext(RoomContext);
|
||||
const room = context.room as LocalRoom;
|
||||
const encryptionEvent = context.room.currentState.getStateEvents(EventType.RoomEncryption)[0];
|
||||
const encryptionEvent = props.localRoom.currentState.getStateEvents(EventType.RoomEncryption)[0];
|
||||
let encryptionTile: ReactNode;
|
||||
|
||||
if (encryptionEvent) {
|
||||
|
@ -261,8 +264,8 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement {
|
|||
});
|
||||
};
|
||||
|
||||
let statusBar: ReactElement;
|
||||
let composer: ReactElement;
|
||||
let statusBar: ReactElement | null = null;
|
||||
let composer: ReactElement | null = null;
|
||||
|
||||
if (room.isError) {
|
||||
const buttons = (
|
||||
|
@ -281,7 +284,7 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement {
|
|||
} else {
|
||||
composer = (
|
||||
<MessageComposer
|
||||
room={context.room}
|
||||
room={props.localRoom}
|
||||
resizeNotifier={props.resizeNotifier}
|
||||
permalinkCreator={props.permalinkCreator}
|
||||
/>
|
||||
|
@ -293,7 +296,7 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement {
|
|||
<ErrorBoundary>
|
||||
<RoomHeader
|
||||
room={context.room}
|
||||
searchInfo={null}
|
||||
searchInfo={undefined}
|
||||
inRoom={true}
|
||||
onSearchClick={null}
|
||||
onInviteClick={null}
|
||||
|
@ -342,7 +345,7 @@ function LocalRoomCreateLoader(props: ILocalRoomCreateLoaderProps): ReactElement
|
|||
<ErrorBoundary>
|
||||
<RoomHeader
|
||||
room={context.room}
|
||||
searchInfo={null}
|
||||
searchInfo={undefined}
|
||||
inRoom={true}
|
||||
onSearchClick={null}
|
||||
onInviteClick={null}
|
||||
|
@ -373,7 +376,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
|
||||
private roomView = createRef<HTMLElement>();
|
||||
private searchResultsPanel = createRef<ScrollPanel>();
|
||||
private messagePanel: TimelinePanel;
|
||||
private messagePanel?: TimelinePanel;
|
||||
private roomViewBody = createRef<HTMLDivElement>();
|
||||
|
||||
public static contextType = SDKContext;
|
||||
|
@ -382,15 +385,19 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
public constructor(props: IRoomProps, context: React.ContextType<typeof SDKContext>) {
|
||||
super(props, context);
|
||||
|
||||
if (!context.client) {
|
||||
throw new Error("Unable to create RoomView without MatrixClient");
|
||||
}
|
||||
|
||||
const llMembers = context.client.hasLazyLoadMembersEnabled();
|
||||
this.state = {
|
||||
roomId: null,
|
||||
roomId: undefined,
|
||||
roomLoading: true,
|
||||
peekLoading: false,
|
||||
shouldPeek: true,
|
||||
membersLoaded: !llMembers,
|
||||
numUnreadMessages: 0,
|
||||
callState: null,
|
||||
callState: undefined,
|
||||
activeCall: null,
|
||||
canPeek: false,
|
||||
canSelfRedact: false,
|
||||
|
@ -1920,10 +1927,11 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
);
|
||||
}
|
||||
|
||||
private renderLocalRoomView(): ReactElement {
|
||||
private renderLocalRoomView(localRoom: LocalRoom): ReactElement {
|
||||
return (
|
||||
<RoomContext.Provider value={this.state}>
|
||||
<LocalRoomView
|
||||
localRoom={localRoom}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
permalinkCreator={this.permalinkCreator}
|
||||
roomView={this.roomView}
|
||||
|
@ -1933,13 +1941,33 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
);
|
||||
}
|
||||
|
||||
private renderWaitingForThirdPartyRoomView(inviteEvent: MatrixEvent): ReactElement {
|
||||
return (
|
||||
<RoomContext.Provider value={this.state}>
|
||||
<WaitingForThirdPartyRoomView
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
roomView={this.roomView}
|
||||
inviteEvent={inviteEvent}
|
||||
/>
|
||||
</RoomContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
if (this.state.room instanceof LocalRoom) {
|
||||
if (this.state.room.state === LocalRoomState.CREATING) {
|
||||
return this.renderLocalRoomCreateLoader();
|
||||
}
|
||||
|
||||
return this.renderLocalRoomView();
|
||||
return this.renderLocalRoomView(this.state.room);
|
||||
}
|
||||
|
||||
if (this.state.room) {
|
||||
const { shouldEncrypt, inviteEvent } = shouldEncryptRoomWithSingle3rdPartyInvite(this.state.room);
|
||||
|
||||
if (shouldEncrypt) {
|
||||
return this.renderWaitingForThirdPartyRoomView(inviteEvent);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.state.room) {
|
||||
|
|
82
src/components/structures/WaitingForThirdPartyRoomView.tsx
Normal file
82
src/components/structures/WaitingForThirdPartyRoomView.tsx
Normal file
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
Copyright 2023 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 React from "react";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { RefObject } from "react";
|
||||
|
||||
import { useRoomContext } from "../../contexts/RoomContext";
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import { E2EStatus } from "../../utils/ShieldUtils";
|
||||
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||
import RoomHeader from "../views/rooms/RoomHeader";
|
||||
import ScrollPanel from "./ScrollPanel";
|
||||
import EventTileBubble from "../views/messages/EventTileBubble";
|
||||
import NewRoomIntro from "../views/rooms/NewRoomIntro";
|
||||
import { UnwrappedEventTile } from "../views/rooms/EventTile";
|
||||
import { _t } from "../../languageHandler";
|
||||
|
||||
interface Props {
|
||||
roomView: RefObject<HTMLElement>;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
inviteEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that displays a waiting room for an encrypted DM with a third party invite.
|
||||
* If encryption by default is enabled, DMs with a third party invite should be encrypted as well.
|
||||
* To avoid UTDs, users are shown a waiting room until the others have joined.
|
||||
*/
|
||||
export const WaitingForThirdPartyRoomView: React.FC<Props> = ({ roomView, resizeNotifier, inviteEvent }) => {
|
||||
const context = useRoomContext();
|
||||
|
||||
return (
|
||||
<div className="mx_RoomView mx_RoomView--local">
|
||||
<ErrorBoundary>
|
||||
<RoomHeader
|
||||
room={context.room}
|
||||
inRoom={true}
|
||||
onSearchClick={null}
|
||||
onInviteClick={null}
|
||||
onForgetClick={null}
|
||||
e2eStatus={E2EStatus.Normal}
|
||||
onAppsClick={null}
|
||||
appsShown={false}
|
||||
excludedRightPanelPhaseButtons={[]}
|
||||
showButtons={false}
|
||||
enableRoomOptionsMenu={false}
|
||||
viewingCall={false}
|
||||
activeCall={null}
|
||||
/>
|
||||
<main className="mx_RoomView_body" ref={roomView}>
|
||||
<div className="mx_RoomView_timeline">
|
||||
<ScrollPanel className="mx_RoomView_messagePanel" resizeNotifier={resizeNotifier}>
|
||||
<EventTileBubble
|
||||
className="mx_cryptoEvent mx_cryptoEvent_icon"
|
||||
title={_t("Waiting for users to join Element")}
|
||||
subtitle={_t(
|
||||
"Once invited users have joined Element, you will be able to chat and the room will be end-to-end encrypted",
|
||||
)}
|
||||
/>
|
||||
<NewRoomIntro />
|
||||
<UnwrappedEventTile mxEvent={inviteEvent} />
|
||||
</ScrollPanel>
|
||||
</div>
|
||||
</main>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -39,6 +39,7 @@ import { shouldShowComponent } from "../../../customisations/helpers/UIComponent
|
|||
import { UIComponent } from "../../../settings/UIFeature";
|
||||
import { privateShouldBeEncrypted } from "../../../utils/rooms";
|
||||
import { LocalRoom } from "../../../models/LocalRoom";
|
||||
import { shouldEncryptRoomWithSingle3rdPartyInvite } from "../../../utils/room/shouldEncryptRoomWithSingle3rdPartyInvite";
|
||||
|
||||
function hasExpectedEncryptionSettings(matrixClient: MatrixClient, room: Room): boolean {
|
||||
const isEncrypted: boolean = matrixClient.isRoomEncrypted(room.roomId);
|
||||
|
@ -46,21 +47,40 @@ function hasExpectedEncryptionSettings(matrixClient: MatrixClient, room: Room):
|
|||
return isPublic || !privateShouldBeEncrypted() || isEncrypted;
|
||||
}
|
||||
|
||||
const determineIntroMessage = (room: Room, encryptedSingle3rdPartyInvite: boolean): string => {
|
||||
if (room instanceof LocalRoom) {
|
||||
return _td("Send your first message to invite <displayName/> to chat");
|
||||
}
|
||||
|
||||
if (encryptedSingle3rdPartyInvite) {
|
||||
return _td("Once everyone has joined, you’ll be able to chat");
|
||||
}
|
||||
|
||||
return _td("This is the beginning of your direct message history with <displayName/>.");
|
||||
};
|
||||
|
||||
const NewRoomIntro: React.FC = () => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const { room, roomId } = useContext(RoomContext);
|
||||
|
||||
if (!room || !roomId) {
|
||||
throw new Error("Unable to create a NewRoomIntro without room and roomId");
|
||||
}
|
||||
|
||||
const isLocalRoom = room instanceof LocalRoom;
|
||||
const dmPartner = isLocalRoom ? room.targets[0]?.userId : DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||
|
||||
let body: JSX.Element;
|
||||
if (dmPartner) {
|
||||
let introMessage = _td("This is the beginning of your direct message history with <displayName/>.");
|
||||
const { shouldEncrypt: encryptedSingle3rdPartyInvite } = shouldEncryptRoomWithSingle3rdPartyInvite(room);
|
||||
const introMessage = determineIntroMessage(room, encryptedSingle3rdPartyInvite);
|
||||
let caption: string | undefined;
|
||||
|
||||
if (isLocalRoom) {
|
||||
introMessage = _td("Send your first message to invite <displayName/> to chat");
|
||||
} else if (room.getJoinedMemberCount() + room.getInvitedMemberCount() === 2) {
|
||||
if (
|
||||
!(room instanceof LocalRoom) &&
|
||||
!encryptedSingle3rdPartyInvite &&
|
||||
room.getJoinedMemberCount() + room.getInvitedMemberCount() === 2
|
||||
) {
|
||||
caption = _t("Only the two of you are in this conversation, unless either of you invites anyone to join.");
|
||||
}
|
||||
|
||||
|
@ -98,7 +118,7 @@ const NewRoomIntro: React.FC = () => {
|
|||
} else {
|
||||
const inRoom = room && room.getMyMembership() === "join";
|
||||
const topic = room.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic;
|
||||
const canAddTopic = inRoom && room.currentState.maySendStateEvent(EventType.RoomTopic, cli.getUserId());
|
||||
const canAddTopic = inRoom && room.currentState.maySendStateEvent(EventType.RoomTopic, cli.getSafeUserId());
|
||||
|
||||
const onTopicClick = (): void => {
|
||||
defaultDispatcher.dispatch(
|
||||
|
@ -110,7 +130,7 @@ const NewRoomIntro: React.FC = () => {
|
|||
);
|
||||
// focus the topic field to help the user find it as it'll gain an outline
|
||||
setImmediate(() => {
|
||||
window.document.getElementById("profileTopic").focus();
|
||||
window.document.getElementById("profileTopic")?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -1951,8 +1951,9 @@
|
|||
"Code block": "Code block",
|
||||
"Quote": "Quote",
|
||||
"Insert link": "Insert link",
|
||||
"This is the beginning of your direct message history with <displayName/>.": "This is the beginning of your direct message history with <displayName/>.",
|
||||
"Send your first message to invite <displayName/> to chat": "Send your first message to invite <displayName/> to chat",
|
||||
"Once everyone has joined, you’ll be able to chat": "Once everyone has joined, you’ll be able to chat",
|
||||
"This is the beginning of your direct message history with <displayName/>.": "This is the beginning of your direct message history with <displayName/>.",
|
||||
"Only the two of you are in this conversation, unless either of you invites anyone to join.": "Only the two of you are in this conversation, unless either of you invites anyone to join.",
|
||||
"Topic: %(topic)s (<a>edit</a>)": "Topic: %(topic)s (<a>edit</a>)",
|
||||
"Topic: %(topic)s ": "Topic: %(topic)s ",
|
||||
|
@ -3487,6 +3488,8 @@
|
|||
"Original event source": "Original event source",
|
||||
"Event ID: %(eventId)s": "Event ID: %(eventId)s",
|
||||
"Thread root ID: %(threadRootId)s": "Thread root ID: %(threadRootId)s",
|
||||
"Waiting for users to join Element": "Waiting for users to join Element",
|
||||
"Once invited users have joined Element, you will be able to chat and the room will be end-to-end encrypted": "Once invited users have joined Element, you will be able to chat and the room will be end-to-end encrypted",
|
||||
"Unable to verify this device": "Unable to verify this device",
|
||||
"Verify this device": "Verify this device",
|
||||
"Device verified": "Device verified",
|
||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
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 { canEncryptToAllUsers } from "../createRoom";
|
||||
|
@ -30,7 +29,7 @@ import { createDmLocalRoom } from "./dm/createDmLocalRoom";
|
|||
import { startDm } from "./dm/startDm";
|
||||
import { resolveThreePids } from "./threepids";
|
||||
|
||||
export async function startDmOnFirstMessage(client: MatrixClient, targets: Member[]): Promise<Room> {
|
||||
export async function startDmOnFirstMessage(client: MatrixClient, targets: Member[]): Promise<string | null> {
|
||||
let resolvedTargets = targets;
|
||||
|
||||
try {
|
||||
|
@ -49,7 +48,13 @@ export async function startDmOnFirstMessage(client: MatrixClient, targets: Membe
|
|||
joining: false,
|
||||
metricsTrigger: "MessageUser",
|
||||
});
|
||||
return existingRoom;
|
||||
return existingRoom.roomId;
|
||||
}
|
||||
|
||||
if (targets.length === 1 && targets[0] instanceof ThreepidMember && privateShouldBeEncrypted()) {
|
||||
// Single 3rd-party invite and well-known promotes encryption:
|
||||
// Directly create a room and invite the other.
|
||||
return await startDm(client, targets);
|
||||
}
|
||||
|
||||
const room = await createDmLocalRoom(client, resolvedTargets);
|
||||
|
@ -59,7 +64,7 @@ export async function startDmOnFirstMessage(client: MatrixClient, targets: Membe
|
|||
joining: false,
|
||||
targets: resolvedTargets,
|
||||
});
|
||||
return room;
|
||||
return room.roomId;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -81,6 +86,8 @@ export async function createRoomFromLocalRoom(client: MatrixClient, localRoom: L
|
|||
|
||||
return startDm(client, localRoom.targets, false).then(
|
||||
(roomId) => {
|
||||
if (!roomId) throw new Error(`startDm for local room ${localRoom.roomId} didn't return a room Id`);
|
||||
|
||||
localRoom.actualRoomId = roomId;
|
||||
return waitForRoomReadyAndApplyAfterCreateCallbacks(client, localRoom);
|
||||
},
|
||||
|
@ -186,6 +193,9 @@ export interface IDMUserTileProps {
|
|||
*/
|
||||
export async function determineCreateRoomEncryptionOption(client: MatrixClient, targets: Member[]): Promise<boolean> {
|
||||
if (privateShouldBeEncrypted()) {
|
||||
// Enable encryption for a single 3rd party invite.
|
||||
if (targets.length === 1 && targets[0] instanceof ThreepidMember) return true;
|
||||
|
||||
// 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);
|
||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { IInvite3PID, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { Optional } from "matrix-events-sdk";
|
||||
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
||||
|
@ -35,7 +36,7 @@ export async function startDm(client: MatrixClient, targets: Member[], showSpinn
|
|||
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 | undefined;
|
||||
let existingRoom: Optional<Room>;
|
||||
if (targetIds.length === 1) {
|
||||
existingRoom = findDMForUser(client, targetIds[0]);
|
||||
} else {
|
||||
|
|
48
src/utils/room/shouldEncryptRoomWithSingle3rdPartyInvite.ts
Normal file
48
src/utils/room/shouldEncryptRoomWithSingle3rdPartyInvite.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
Copyright 2023 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 { MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import DMRoomMap from "../DMRoomMap";
|
||||
import { privateShouldBeEncrypted } from "../rooms";
|
||||
|
||||
/**
|
||||
* Tests whether a DM room with exactly one third-party invite should be encrypted.
|
||||
* If it should be encrypted, the third-party invitation event is also returned.
|
||||
*/
|
||||
export const shouldEncryptRoomWithSingle3rdPartyInvite = (
|
||||
room: Room,
|
||||
): { shouldEncrypt: true; inviteEvent: MatrixEvent } | { shouldEncrypt: false; inviteEvent?: undefined } => {
|
||||
// encryption not promoted via .well-known
|
||||
if (!privateShouldBeEncrypted()) return { shouldEncrypt: false };
|
||||
|
||||
// not a DM room
|
||||
if (!DMRoomMap.shared().getRoomIds().has(room.roomId)) return { shouldEncrypt: false };
|
||||
|
||||
// more than one room member / invite
|
||||
if (room.getInvitedAndJoinedMemberCount() !== 1) return { shouldEncrypt: false };
|
||||
|
||||
const thirdPartyInvites = room.currentState.getStateEvents("m.room.third_party_invite") || [];
|
||||
|
||||
if (thirdPartyInvites.length === 1) {
|
||||
return {
|
||||
shouldEncrypt: true,
|
||||
inviteEvent: thirdPartyInvites[0],
|
||||
};
|
||||
}
|
||||
|
||||
return { shouldEncrypt: false };
|
||||
};
|
|
@ -23,7 +23,7 @@ import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
|||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { EventType, RoomStateEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { MEGOLM_ALGORITHM } from "matrix-js-sdk/src/crypto/olmlib";
|
||||
import { fireEvent, render } from "@testing-library/react";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
|
||||
import {
|
||||
stubClient,
|
||||
|
@ -34,6 +34,8 @@ import {
|
|||
mkEvent,
|
||||
setupAsyncStoreWithClient,
|
||||
filterConsole,
|
||||
mkRoomMemberJoinEvent,
|
||||
mkThirdPartyInviteEvent,
|
||||
} from "../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||
import { Action } from "../../../src/dispatcher/actions";
|
||||
|
@ -68,7 +70,7 @@ describe("RoomView", () => {
|
|||
// mute some noise
|
||||
filterConsole("RVS update", "does not have an m.room.create event", "Current version: 1", "Version capability");
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
mockPlatformPeg({ reload: () => {} });
|
||||
stubClient();
|
||||
cli = mocked(MatrixClientPeg.get());
|
||||
|
@ -91,7 +93,7 @@ describe("RoomView", () => {
|
|||
jest.spyOn(VoipUserMapper.sharedInstance(), "getVirtualRoomForRoom").mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
afterEach(() => {
|
||||
unmockPlatformPeg();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
@ -369,6 +371,32 @@ describe("RoomView", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("when rendering a DM room with a single third-party invite", () => {
|
||||
beforeEach(async () => {
|
||||
room.currentState.setStateEvents([
|
||||
mkRoomMemberJoinEvent(cli.getSafeUserId(), room.roomId),
|
||||
mkThirdPartyInviteEvent(cli.getSafeUserId(), "user@example.com", room.roomId),
|
||||
]);
|
||||
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(cli.getSafeUserId());
|
||||
jest.spyOn(DMRoomMap.shared(), "getRoomIds").mockReturnValue(new Set([room.roomId]));
|
||||
mocked(cli).isRoomEncrypted.mockReturnValue(true);
|
||||
await renderRoomView();
|
||||
});
|
||||
|
||||
it("should render the »waiting for third-party« view", () => {
|
||||
expect(screen.getByText("Waiting for users to join Element")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Once invited users have joined Element, you will be able to chat and the room will be end-to-end encrypted",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// no message composer
|
||||
expect(screen.queryByText("Send an encrypted message…")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Send a message…")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there is a RoomView", () => {
|
||||
const widget1Id = "widget1";
|
||||
const widget2Id = "widget2";
|
||||
|
|
|
@ -20,13 +20,12 @@ import { render, screen } from "@testing-library/react";
|
|||
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { LocalRoom } from "../../../../src/models/LocalRoom";
|
||||
import { createTestClient } from "../../../test-utils";
|
||||
import { filterConsole, mkRoomMemberJoinEvent, mkThirdPartyInviteEvent, stubClient } from "../../../test-utils";
|
||||
import RoomContext from "../../../../src/contexts/RoomContext";
|
||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||
import NewRoomIntro from "../../../../src/components/views/rooms/NewRoomIntro";
|
||||
import { IRoomState } from "../../../../src/components/structures/RoomView";
|
||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import { DirectoryMember } from "../../../../src/utils/direct-messages";
|
||||
|
||||
const renderNewRoomIntro = (client: MatrixClient, room: Room | LocalRoom) => {
|
||||
|
@ -44,9 +43,10 @@ describe("NewRoomIntro", () => {
|
|||
const roomId = "!room:example.com";
|
||||
const userId = "@user:example.com";
|
||||
|
||||
beforeEach(() => {
|
||||
client = createTestClient();
|
||||
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(client);
|
||||
filterConsole("Room !room:example.com does not have an m.room.create event");
|
||||
|
||||
beforeAll(() => {
|
||||
client = stubClient();
|
||||
DMRoomMap.makeShared();
|
||||
});
|
||||
|
||||
|
@ -64,6 +64,24 @@ describe("NewRoomIntro", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("should render as expected for a DM room with a single third-party invite", () => {
|
||||
const room = new Room(roomId, client, client.getSafeUserId());
|
||||
room.currentState.setStateEvents([
|
||||
mkRoomMemberJoinEvent(client.getSafeUserId(), room.roomId),
|
||||
mkThirdPartyInviteEvent(client.getSafeUserId(), "user@example.com", room.roomId),
|
||||
]);
|
||||
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(userId);
|
||||
jest.spyOn(DMRoomMap.shared(), "getRoomIds").mockReturnValue(new Set([room.roomId]));
|
||||
renderNewRoomIntro(client, room);
|
||||
|
||||
expect(screen.getByText("Once everyone has joined, you’ll be able to chat")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(
|
||||
"Only the two of you are in this conversation, unless either of you invites anyone to join.",
|
||||
),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("for a DM LocalRoom", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(userId);
|
||||
|
|
|
@ -116,11 +116,11 @@ describe("direct-messages", () => {
|
|||
it("should create a local room and dispatch a view room event", async () => {
|
||||
mocked(createDmLocalRoom).mockResolvedValue(localRoom);
|
||||
const members = [member1];
|
||||
const room = await dmModule.startDmOnFirstMessage(mockClient, members);
|
||||
expect(room).toBe(localRoom);
|
||||
const roomId = await dmModule.startDmOnFirstMessage(mockClient, members);
|
||||
expect(roomId).toBe(localRoom.roomId);
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
room_id: roomId,
|
||||
joining: false,
|
||||
targets: [member1],
|
||||
});
|
||||
|
@ -135,8 +135,8 @@ describe("direct-messages", () => {
|
|||
|
||||
mocked(createDmLocalRoom).mockResolvedValue(localRoom);
|
||||
const members = [member1];
|
||||
const room = await dmModule.startDmOnFirstMessage(mockClient, members);
|
||||
expect(room).toBe(localRoom);
|
||||
const roomId = await dmModule.startDmOnFirstMessage(mockClient, members);
|
||||
expect(roomId).toBe(localRoom.roomId);
|
||||
|
||||
// ensure that startDmOnFirstMessage tries to resolve 3rd-party IDs
|
||||
expect(resolveThreePids).toHaveBeenCalledWith(members, mockClient);
|
||||
|
@ -152,8 +152,8 @@ describe("direct-messages", () => {
|
|||
});
|
||||
|
||||
it("should return the room and dispatch a view room event", async () => {
|
||||
const room = await dmModule.startDmOnFirstMessage(mockClient, [member1]);
|
||||
expect(room).toBe(room1);
|
||||
const roomId = await dmModule.startDmOnFirstMessage(mockClient, [member1]);
|
||||
expect(roomId).toBe(room1.roomId);
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room1.roomId,
|
||||
|
|
|
@ -70,10 +70,10 @@ describe("createDmLocalRoom", () => {
|
|||
mocked(privateShouldBeEncrypted).mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("should create an unencrypted room for 3PID targets", async () => {
|
||||
it("should create an encrytped room for 3PID targets", async () => {
|
||||
const room = await createDmLocalRoom(mockClient, [member2]);
|
||||
expect(mockClient.store.storeRoom).toHaveBeenCalledWith(room);
|
||||
assertLocalRoom(room, [member2], false);
|
||||
assertLocalRoom(room, [member2], true);
|
||||
});
|
||||
|
||||
describe("for MXID targets with encryption available", () => {
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
Copyright 2023 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, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import DMRoomMap from "../../../src/utils/DMRoomMap";
|
||||
import { shouldEncryptRoomWithSingle3rdPartyInvite } from "../../../src/utils/room/shouldEncryptRoomWithSingle3rdPartyInvite";
|
||||
import { privateShouldBeEncrypted } from "../../../src/utils/rooms";
|
||||
import { mkRoomMemberJoinEvent, mkThirdPartyInviteEvent, stubClient } from "../../test-utils";
|
||||
|
||||
jest.mock("../../../src/utils/rooms", () => ({
|
||||
privateShouldBeEncrypted: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("shouldEncryptRoomWithSingle3rdPartyInvite", () => {
|
||||
let client: MatrixClient;
|
||||
let thirdPartyInviteEvent: MatrixEvent;
|
||||
let roomWithOneThirdPartyInvite: Room;
|
||||
|
||||
beforeAll(() => {
|
||||
client = stubClient();
|
||||
DMRoomMap.makeShared();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
roomWithOneThirdPartyInvite = new Room("!room1:example.com", client, client.getSafeUserId());
|
||||
thirdPartyInviteEvent = mkThirdPartyInviteEvent(
|
||||
client.getSafeUserId(),
|
||||
"user@example.com",
|
||||
roomWithOneThirdPartyInvite.roomId,
|
||||
);
|
||||
|
||||
roomWithOneThirdPartyInvite.currentState.setStateEvents([
|
||||
mkRoomMemberJoinEvent(client.getSafeUserId(), roomWithOneThirdPartyInvite.roomId),
|
||||
thirdPartyInviteEvent,
|
||||
]);
|
||||
jest.spyOn(DMRoomMap.shared(), "getRoomIds").mockReturnValue(new Set([roomWithOneThirdPartyInvite.roomId]));
|
||||
});
|
||||
|
||||
describe("when well-known promotes encryption", () => {
|
||||
beforeEach(() => {
|
||||
mocked(privateShouldBeEncrypted).mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("should return true + invite event for a DM room with one third-party invite", () => {
|
||||
expect(shouldEncryptRoomWithSingle3rdPartyInvite(roomWithOneThirdPartyInvite)).toEqual({
|
||||
shouldEncrypt: true,
|
||||
inviteEvent: thirdPartyInviteEvent,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return false for a non-DM room with one third-party invite", () => {
|
||||
mocked(DMRoomMap.shared().getRoomIds).mockReturnValue(new Set());
|
||||
|
||||
expect(shouldEncryptRoomWithSingle3rdPartyInvite(roomWithOneThirdPartyInvite)).toEqual({
|
||||
shouldEncrypt: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return false for a DM room with two members", () => {
|
||||
roomWithOneThirdPartyInvite.currentState.setStateEvents([
|
||||
mkRoomMemberJoinEvent("@user2:example.com", roomWithOneThirdPartyInvite.roomId),
|
||||
]);
|
||||
|
||||
expect(shouldEncryptRoomWithSingle3rdPartyInvite(roomWithOneThirdPartyInvite)).toEqual({
|
||||
shouldEncrypt: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return false for a DM room with two third-party invites", () => {
|
||||
roomWithOneThirdPartyInvite.currentState.setStateEvents([
|
||||
mkThirdPartyInviteEvent(
|
||||
client.getSafeUserId(),
|
||||
"user2@example.com",
|
||||
roomWithOneThirdPartyInvite.roomId,
|
||||
),
|
||||
]);
|
||||
|
||||
expect(shouldEncryptRoomWithSingle3rdPartyInvite(roomWithOneThirdPartyInvite)).toEqual({
|
||||
shouldEncrypt: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when well-known does not promote encryption", () => {
|
||||
beforeEach(() => {
|
||||
mocked(privateShouldBeEncrypted).mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("should return false for a DM room with one third-party invite", () => {
|
||||
expect(shouldEncryptRoomWithSingle3rdPartyInvite(roomWithOneThirdPartyInvite)).toEqual({
|
||||
shouldEncrypt: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue