From 91b1c8b8173b2e83b97602ac6a0eb5fa587cb86a Mon Sep 17 00:00:00 2001
From: Travis Ralston <travpc@gmail.com>
Date: Fri, 13 Nov 2020 15:19:34 -0700
Subject: [PATCH] Fix DM logic to always pick a more reliable DM room

Fixes https://github.com/vector-im/element-web/issues/15605
---
 src/components/views/dialogs/InviteDialog.js  | 10 ++++++++--
 src/components/views/right_panel/UserInfo.tsx | 14 ++-----------
 src/createRoom.ts                             | 20 +++++++++++++------
 src/utils/membership.ts                       |  5 +++++
 4 files changed, 29 insertions(+), 20 deletions(-)

diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js
index 99878569d3..e29ef143f2 100644
--- a/src/components/views/dialogs/InviteDialog.js
+++ b/src/components/views/dialogs/InviteDialog.js
@@ -31,7 +31,7 @@ import dis from "../../../dispatcher/dispatcher";
 import IdentityAuthClient from "../../../IdentityAuthClient";
 import Modal from "../../../Modal";
 import {humanizeTime} from "../../../utils/humanize";
-import createRoom, {canEncryptToAllUsers, privateShouldBeEncrypted} from "../../../createRoom";
+import createRoom, {canEncryptToAllUsers, findDMForUser, privateShouldBeEncrypted} from "../../../createRoom";
 import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
 import {Key} from "../../../Keyboard";
 import {Action} from "../../../dispatcher/actions";
@@ -41,6 +41,7 @@ import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
 import SettingsStore from "../../../settings/SettingsStore";
 import {UIFeature} from "../../../settings/UIFeature";
 import CountlyAnalytics from "../../../CountlyAnalytics";
+import {Room} from "matrix-js-sdk/src/models/room";
 
 // we have a number of types defined from the Matrix spec which can't reasonably be altered here.
 /* eslint-disable camelcase */
@@ -575,7 +576,12 @@ export default class InviteDialog extends React.PureComponent {
         const targetIds = targets.map(t => t.userId);
 
         // Check if there is already a DM with these people and reuse it if possible.
-        const existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds);
+        let existingRoom: Room;
+        if (targetIds.length === 1) {
+            existingRoom = findDMForUser(MatrixClientPeg.get(), targetIds[0]);
+        } else {
+            existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds);
+        }
         if (existingRoom) {
             dis.dispatch({
                 action: 'view_room',
diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx
index 66b689ddb9..cdb4c43b09 100644
--- a/src/components/views/right_panel/UserInfo.tsx
+++ b/src/components/views/right_panel/UserInfo.tsx
@@ -28,7 +28,7 @@ import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline';
 import dis from '../../../dispatcher/dispatcher';
 import Modal from '../../../Modal';
 import {_t} from '../../../languageHandler';
-import createRoom, {privateShouldBeEncrypted} from '../../../createRoom';
+import createRoom, { findDMForUser, privateShouldBeEncrypted } from '../../../createRoom';
 import DMRoomMap from '../../../utils/DMRoomMap';
 import AccessibleButton from '../elements/AccessibleButton';
 import SdkConfig from '../../../SdkConfig';
@@ -105,17 +105,7 @@ export const getE2EStatus = (cli: MatrixClient, userId: string, devices: IDevice
 };
 
 async function openDMForUser(matrixClient: MatrixClient, userId: string) {
-    const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId);
-    const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => {
-        const room = matrixClient.getRoom(roomId);
-        if (!room || room.getMyMembership() === "leave") {
-            return lastActiveRoom;
-        }
-        if (!lastActiveRoom || lastActiveRoom.getLastActiveTimestamp() < room.getLastActiveTimestamp()) {
-            return room;
-        }
-        return lastActiveRoom;
-    }, null);
+    const lastActiveRoom = findDMForUser(matrixClient, userId);
 
     if (lastActiveRoom) {
         dis.dispatch({
diff --git a/src/createRoom.ts b/src/createRoom.ts
index a42fcc5e7b..699df0d799 100644
--- a/src/createRoom.ts
+++ b/src/createRoom.ts
@@ -15,20 +15,21 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import {MatrixClient} from "matrix-js-sdk/src/client";
-import {Room} from "matrix-js-sdk/src/models/room";
+import { MatrixClient } from "matrix-js-sdk/src/client";
+import { Room } from "matrix-js-sdk/src/models/room";
 
-import {MatrixClientPeg} from './MatrixClientPeg';
+import { MatrixClientPeg } from './MatrixClientPeg';
 import Modal from './Modal';
 import * as sdk from './index';
 import { _t } from './languageHandler';
 import dis from "./dispatcher/dispatcher";
 import * as Rooms from "./Rooms";
 import DMRoomMap from "./utils/DMRoomMap";
-import {getAddressType} from "./UserAddress";
+import { getAddressType } from "./UserAddress";
 import { getE2EEWellKnown } from "./utils/WellKnownUtils";
 import GroupStore from "./stores/GroupStore";
 import CountlyAnalytics from "./CountlyAnalytics";
+import { isJoinedOrNearlyJoined } from "./utils/membership";
 
 // we define a number of interfaces which take their names from the js-sdk
 /* eslint-disable camelcase */
@@ -236,9 +237,16 @@ 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 member = r.getMember(userId);
-            return member && (member.membership === "invite" || member.membership === "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) => {
diff --git a/src/utils/membership.ts b/src/utils/membership.ts
index 68ac958490..696bd57880 100644
--- a/src/utils/membership.ts
+++ b/src/utils/membership.ts
@@ -78,6 +78,11 @@ export function getEffectiveMembership(membership: string): EffectiveMembership
     }
 }
 
+export function isJoinedOrNearlyJoined(membership: string): boolean {
+    const effective = getEffectiveMembership(membership);
+    return effective === EffectiveMembership.Join || effective === EffectiveMembership.Invite;
+}
+
 export async function leaveRoomBehaviour(roomId: string) {
     let leavingAllVersions = true;
     const history = await MatrixClientPeg.get().getRoomUpgradeHistory(roomId);