Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Implement third-party invite waiting room #10229

Merged
merged 12 commits into from
Mar 6, 2023
103 changes: 92 additions & 11 deletions src/components/structures/RoomView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ import VoipUserMapper from "../../VoipUserMapper";
import { isCallEvent } from "./LegacyCallEventGrouper";
import { WidgetType } from "../../widgets/WidgetType";
import WidgetUtils from "../../utils/WidgetUtils";
import EventTileBubble from "../views/messages/EventTileBubble";
import { UnwrappedEventTile } from "../views/rooms/EventTile";
import { shouldEncryptRoomWithSingle3rdPartyInvite } from "../../utils/room/shouldEncryptRoomWithSingle3rdPartyInvite";

const DEBUG = false;
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
Expand Down Expand Up @@ -231,12 +234,65 @@ export interface IRoomState {
}

interface LocalRoomViewProps {
localRoom: LocalRoom;
resizeNotifier: ResizeNotifier;
permalinkCreator: RoomPermalinkCreator;
roomView: RefObject<HTMLElement>;
onFileDrop: (dataTransfer: DataTransfer) => Promise<void>;
}

interface WaitingForThirdPartyRoomViewProps {
roomView: RefObject<HTMLElement>;
resizeNotifier: ResizeNotifier;
inviteEvent: MatrixEvent;
}

const WaitingForThirdPartyRoomView: React.FC<WaitingForThirdPartyRoomViewProps> = ({
weeman1337 marked this conversation as resolved.
Show resolved Hide resolved
roomView,
resizeNotifier,
inviteEvent,
}) => {
const context = useContext(RoomContext);
weeman1337 marked this conversation as resolved.
Show resolved Hide resolved

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 users invited have joined Element, " +
weeman1337 marked this conversation as resolved.
Show resolved Hide resolved
"you will be able to chat and the room will be end-to end encrypted",
)}
/>
<NewRoomIntro />
<UnwrappedEventTile mxEvent={inviteEvent} />
</ScrollPanel>
</div>
</main>
</ErrorBoundary>
</div>
);
};

/**
* Local room view. Uses only the bits necessary to display a local room view like room header or composer.
*
Expand All @@ -246,7 +302,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) {
Expand All @@ -261,8 +317,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 = (
Expand All @@ -281,7 +337,7 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement {
} else {
composer = (
<MessageComposer
room={context.room}
room={props.localRoom}
resizeNotifier={props.resizeNotifier}
permalinkCreator={props.permalinkCreator}
/>
Expand All @@ -293,7 +349,7 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement {
<ErrorBoundary>
<RoomHeader
room={context.room}
searchInfo={null}
searchInfo={undefined}
inRoom={true}
onSearchClick={null}
onInviteClick={null}
Expand Down Expand Up @@ -342,7 +398,7 @@ function LocalRoomCreateLoader(props: ILocalRoomCreateLoaderProps): ReactElement
<ErrorBoundary>
<RoomHeader
room={context.room}
searchInfo={null}
searchInfo={undefined}
inRoom={true}
onSearchClick={null}
onInviteClick={null}
Expand Down Expand Up @@ -373,7 +429,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;
Expand All @@ -382,15 +438,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,
Expand Down Expand Up @@ -1920,10 +1980,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}
Expand All @@ -1933,13 +1994,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) {
Expand Down
32 changes: 26 additions & 6 deletions src/components/views/rooms/NewRoomIntro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,28 +39,48 @@ 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);
const isPublic: boolean = room.getJoinRule() === "public";
return isPublic || !privateShouldBeEncrypted() || isEncrypted;
}

const determineIntroMessage = (room: Room, encryptedSingle3rdPartyInvite: boolean): string => {
if (room instanceof LocalRoom) {
return _t("Send your first message to invite <displayName/> to chat");
}

if (encryptedSingle3rdPartyInvite) {
return _t("Once everyone has joined, you’ll be able to chat");
}

return _t("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 = _t("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 = _t("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.");
}

Expand Down Expand Up @@ -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(
Expand All @@ -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();
});
};

Expand Down
5 changes: 4 additions & 1 deletion src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1949,8 +1949,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 ",
Expand Down Expand Up @@ -3406,6 +3407,8 @@
"You can select all or individual messages to retry or delete": "You can select all or individual messages to retry or delete",
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
"Waiting for users to join Element": "Waiting for users to join Element",
"Once users invited have joined Element, you will be able to chat and the room will be end-to end encrypted": "Once users invited have joined Element, you will be able to chat and the room will be end-to end encrypted",
"We're creating a room with %(names)s": "We're creating a room with %(names)s",
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
Expand Down
18 changes: 14 additions & 4 deletions src/utils/direct-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 {
Expand All @@ -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);
Expand All @@ -59,7 +64,7 @@ export async function startDmOnFirstMessage(client: MatrixClient, targets: Membe
joining: false,
targets: resolvedTargets,
});
return room;
return room.roomId;
}

/**
Expand All @@ -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);
},
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion src/utils/dm/startDm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 {
Expand Down
Loading