From a63449acddfcc169ac27743b9def67bd0cdc449a Mon Sep 17 00:00:00 2001
From: Germain <germains@element.io>
Date: Thu, 14 Apr 2022 16:52:12 +0100
Subject: [PATCH] Extract start DM logic to a helper file (#8317)

* Extract start DM logic to a helper file

* Fix incorrect import
---
 src/RoomInvite.tsx                            |   3 +-
 src/components/views/dialogs/InviteDialog.tsx | 146 ++--------------
 .../views/dialogs/InviteDialogTypes.ts        |  23 ---
 src/utils/direct-messages.ts                  | 156 ++++++++++++++++++
 4 files changed, 168 insertions(+), 160 deletions(-)

diff --git a/src/RoomInvite.tsx b/src/RoomInvite.tsx
index eb0c4f6ba8..200da2f7cf 100644
--- a/src/RoomInvite.tsx
+++ b/src/RoomInvite.tsx
@@ -28,7 +28,8 @@ import InviteDialog from "./components/views/dialogs/InviteDialog";
 import BaseAvatar from "./components/views/avatars/BaseAvatar";
 import { mediaFromMxc } from "./customisations/Media";
 import ErrorDialog from "./components/views/dialogs/ErrorDialog";
-import { KIND_DM, KIND_INVITE, Member } from "./components/views/dialogs/InviteDialogTypes";
+import { KIND_DM, KIND_INVITE } from "./components/views/dialogs/InviteDialogTypes";
+import { Member } from "./utils/direct-messages";
 
 export interface IInviteResult {
     states: CompletionStates;
diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx
index ed79958e3c..978c176ab0 100644
--- a/src/components/views/dialogs/InviteDialog.tsx
+++ b/src/components/views/dialogs/InviteDialog.tsx
@@ -1,5 +1,5 @@
 /*
-Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
+Copyright 2019 - 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.
@@ -19,7 +19,6 @@ import classNames from 'classnames';
 import { RoomMember } from "matrix-js-sdk/src/models/room-member";
 import { Room } from "matrix-js-sdk/src/models/room";
 import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
-import { IInvite3PID } from "matrix-js-sdk/src/@types/requests";
 import { logger } from "matrix-js-sdk/src/logger";
 
 import { _t, _td } from "../../../languageHandler";
@@ -30,11 +29,8 @@ import SdkConfig from "../../../SdkConfig";
 import * as Email from "../../../email";
 import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from "../../../utils/IdentityServerUtils";
 import { abbreviateUrl } from "../../../utils/UrlUtils";
-import dis from "../../../dispatcher/dispatcher";
 import IdentityAuthClient from "../../../IdentityAuthClient";
-import Modal from "../../../Modal";
 import { humanizeTime } from "../../../utils/humanize";
-import createRoom, { canEncryptToAllUsers } from "../../../createRoom";
 import {
     IInviteResult,
     inviteMultipleToRoom,
@@ -46,7 +42,6 @@ import RoomListStore from "../../../stores/room-list/RoomListStore";
 import SettingsStore from "../../../settings/SettingsStore";
 import { UIFeature } from "../../../settings/UIFeature";
 import { mediaFromMxc } from "../../../customisations/Media";
-import { getAddressType } from "../../../UserAddress";
 import BaseAvatar from '../avatars/BaseAvatar';
 import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
 import { compare, selectText } from '../../../utils/strings';
@@ -61,12 +56,12 @@ import CallHandler from "../../../CallHandler";
 import UserIdentifierCustomisations from '../../../customisations/UserIdentifier';
 import CopyableText from "../elements/CopyableText";
 import { ScreenName } from '../../../PosthogTrackers';
-import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
 import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
 import { getKeyBindingsManager } from "../../../KeyBindingsManager";
-import { privateShouldBeEncrypted } from "../../../utils/rooms";
-import { findDMForUser } from "../../../utils/direct-messages";
-import { AnyInviteKind, KIND_CALL_TRANSFER, KIND_DM, KIND_INVITE, Member } from './InviteDialogTypes';
+import { DirectoryMember, IDMUserTileProps, Member, startDm, 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";
 
 // we have a number of types defined from the Matrix spec which can't reasonably be altered here.
 /* eslint-disable camelcase */
@@ -85,67 +80,6 @@ enum TabId {
     DialPad = 'dialpad',
 }
 
-class DirectoryMember extends Member {
-    private readonly _userId: string;
-    private readonly displayName?: string;
-    private readonly avatarUrl?: string;
-
-    // eslint-disable-next-line camelcase
-    constructor(userDirResult: { user_id: string, display_name?: string, avatar_url?: string }) {
-        super();
-        this._userId = userDirResult.user_id;
-        this.displayName = userDirResult.display_name;
-        this.avatarUrl = userDirResult.avatar_url;
-    }
-
-    // These next class members are for the Member interface
-    get name(): string {
-        return this.displayName || this._userId;
-    }
-
-    get userId(): string {
-        return this._userId;
-    }
-
-    getMxcAvatarUrl(): string {
-        return this.avatarUrl;
-    }
-}
-
-class ThreepidMember extends Member {
-    private readonly id: string;
-
-    constructor(id: string) {
-        super();
-        this.id = id;
-    }
-
-    // This is a getter that would be falsey on all other implementations. Until we have
-    // better type support in the react-sdk we can use this trick to determine the kind
-    // of 3PID we're dealing with, if any.
-    get isEmail(): boolean {
-        return this.id.includes('@');
-    }
-
-    // These next class members are for the Member interface
-    get name(): string {
-        return this.id;
-    }
-
-    get userId(): string {
-        return this.id;
-    }
-
-    getMxcAvatarUrl(): string {
-        return null;
-    }
-}
-
-interface IDMUserTileProps {
-    member: Member;
-    onRemove(member: Member): void;
-}
-
 class DMUserTile extends React.PureComponent<IDMUserTileProps> {
     private onRemove = (e) => {
         // Stop the browser from highlighting text
@@ -630,72 +564,10 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
 
     private startDm = async () => {
         this.setState({ busy: true });
-        const client = MatrixClientPeg.get();
-        const targets = this.convertFilter();
-        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) {
-            dis.dispatch<ViewRoomPayload>({
-                action: Action.ViewRoom,
-                room_id: existingRoom.roomId,
-                should_peek: false,
-                joining: false,
-                metricsTrigger: "MessageUser",
-            });
-            this.props.onFinished(true);
-            return;
-        }
-
-        const createRoomOptions = { inlineErrors: true } as any; // XXX: Type out `createRoomOptions`
-
-        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
         try {
-            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);
+            const cli = MatrixClientPeg.get();
+            const targets = this.convertFilter();
+            await startDm(cli, targets);
             this.props.onFinished(true);
         } catch (err) {
             logger.error(err);
@@ -703,6 +575,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
                 busy: false,
                 errorText: _t("We couldn't create your DM."),
             });
+        } finally {
+            this.setState({ busy: false });
         }
     };
 
diff --git a/src/components/views/dialogs/InviteDialogTypes.ts b/src/components/views/dialogs/InviteDialogTypes.ts
index e9ec7f4927..7eed739250 100644
--- a/src/components/views/dialogs/InviteDialogTypes.ts
+++ b/src/components/views/dialogs/InviteDialogTypes.ts
@@ -22,26 +22,3 @@ export const KIND_INVITE = "invite";
 export const KIND_CALL_TRANSFER = "call_transfer";
 
 export type AnyInviteKind = typeof KIND_INVITE | typeof KIND_DM | typeof KIND_CALL_TRANSFER;
-
-// This is the interface that is expected by various components in the Invite Dialog and RoomInvite.
-// It is a bit awkward because it also matches the RoomMember class from the js-sdk with some extra support
-// for 3PIDs/email addresses.
-export abstract class Member {
-    /**
-     * The display name of this Member. For users this should be their profile's display
-     * name or user ID if none set. For 3PIDs this should be the 3PID address (email).
-     */
-    public abstract get name(): string;
-
-    /**
-     * The ID of this Member. For users this should be their user ID. For 3PIDs this should
-     * be the 3PID address (email).
-     */
-    public abstract get userId(): string;
-
-    /**
-     * Gets the MXC URL of this Member's avatar. For users this should be their profile's
-     * avatar MXC URL or null if none set. For 3PIDs this should always be null.
-     */
-    public abstract getMxcAvatarUrl(): string;
-}
diff --git a/src/utils/direct-messages.ts b/src/utils/direct-messages.ts
index 6d187b8b7a..0932cc9aaf 100644
--- a/src/utils/direct-messages.ts
+++ b/src/utils/direct-messages.ts
@@ -14,11 +14,18 @@ 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 { Room } from "matrix-js-sdk/src/models/room";
 
+import createRoom, { 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 { privateShouldBeEncrypted } from "./rooms";
 
 export function findDMForUser(client: MatrixClient, userId: string): Room {
     const roomIds = DMRoomMap.shared().getDMRoomsForUserId(userId);
@@ -44,3 +51,152 @@ export function findDMForUser(client: MatrixClient, userId: string): Room {
         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);
+    }
+    if (existingRoom) {
+        dis.dispatch<ViewRoomPayload>({
+            action: Action.ViewRoom,
+            room_id: existingRoom.roomId,
+            should_peek: false,
+            joining: false,
+            metricsTrigger: "MessageUser",
+        });
+        return;
+    }
+
+    const createRoomOptions = { inlineErrors: true } as any; // XXX: Type out `createRoomOptions`
+
+    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);
+}
+
+// This is the interface that is expected by various components in the Invite Dialog and RoomInvite.
+// It is a bit awkward because it also matches the RoomMember class from the js-sdk with some extra support
+// for 3PIDs/email addresses.
+export abstract class Member {
+    /**
+     * The display name of this Member. For users this should be their profile's display
+     * name or user ID if none set. For 3PIDs this should be the 3PID address (email).
+     */
+    public abstract get name(): string;
+
+    /**
+     * The ID of this Member. For users this should be their user ID. For 3PIDs this should
+     * be the 3PID address (email).
+     */
+    public abstract get userId(): string;
+
+    /**
+     * Gets the MXC URL of this Member's avatar. For users this should be their profile's
+     * avatar MXC URL or null if none set. For 3PIDs this should always be null.
+     */
+    public abstract getMxcAvatarUrl(): string;
+}
+
+export class DirectoryMember extends Member {
+    private readonly _userId: string;
+    private readonly displayName?: string;
+    private readonly avatarUrl?: string;
+
+    // eslint-disable-next-line camelcase
+    constructor(userDirResult: { user_id: string, display_name?: string, avatar_url?: string }) {
+        super();
+        this._userId = userDirResult.user_id;
+        this.displayName = userDirResult.display_name;
+        this.avatarUrl = userDirResult.avatar_url;
+    }
+
+    // These next class members are for the Member interface
+    get name(): string {
+        return this.displayName || this._userId;
+    }
+
+    get userId(): string {
+        return this._userId;
+    }
+
+    getMxcAvatarUrl(): string {
+        return this.avatarUrl;
+    }
+}
+
+export class ThreepidMember extends Member {
+    private readonly id: string;
+
+    constructor(id: string) {
+        super();
+        this.id = id;
+    }
+
+    // This is a getter that would be falsey on all other implementations. Until we have
+    // better type support in the react-sdk we can use this trick to determine the kind
+    // of 3PID we're dealing with, if any.
+    get isEmail(): boolean {
+        return this.id.includes('@');
+    }
+
+    // These next class members are for the Member interface
+    get name(): string {
+        return this.id;
+    }
+
+    get userId(): string {
+        return this.id;
+    }
+
+    getMxcAvatarUrl(): string {
+        return null;
+    }
+}
+
+export interface IDMUserTileProps {
+    member: Member;
+    onRemove(member: Member): void;
+}