Start DM on first message (#8612)

pull/28788/head^2
Michael Weimann 2022-08-04 08:19:52 +02:00 committed by GitHub
parent 0e0be08781
commit ed8ccb5d80
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 482 additions and 65 deletions

View File

@ -57,7 +57,7 @@ const startDMWithBob = function(this: CryptoTestContext) {
};
const testMessages = function(this: CryptoTestContext) {
cy.get(".mx_BasicMessageComposer_input").should("have.focus").type("Hey!{enter}");
// check the invite message
cy.contains(".mx_EventTile_body", "Hey!")
.closest(".mx_EventTile")
.should("not.have.descendants", ".mx_EventTile_e2eIcon_warning")
@ -150,6 +150,11 @@ describe("Cryptography", function() {
it("creating a DM should work, being e2e-encrypted / user verification", function(this: CryptoTestContext) {
cy.bootstrapCrossSigning();
startDMWithBob.call(this);
// send first message
cy.get(".mx_BasicMessageComposer_input")
.click()
.should("have.focus")
.type("Hey!{enter}");
checkDMRoom();
bobJoin.call(this);
testMessages.call(this);

View File

@ -118,10 +118,13 @@ Cypress.Commands.add("startDM", (name: string) => {
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", name);
cy.spotlightResults().eq(0).click();
}).then(() => {
cy.roomHeaderName().should("contain", name);
cy.get(".mx_RoomSublist[aria-label=People]").should("contain", name);
});
// send first message to start DM
cy.get(".mx_BasicMessageComposer_input")
.should("have.focus")
.type("Hey!{enter}");
cy.contains(".mx_EventTile_body", "Hey!");
cy.get(".mx_RoomSublist[aria-label=People]").should("contain", name);
});
describe("Spotlight", () => {
@ -324,11 +327,19 @@ describe("Spotlight", () => {
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", bot2Name);
cy.spotlightResults().eq(0).click();
}).then(() => {
cy.roomHeaderName().should("contain", bot2Name);
cy.get(".mx_RoomSublist[aria-label=People]").should("contain", bot2Name);
});
// Send first message to actually start DM
cy.roomHeaderName().should("contain", bot2Name);
cy.get(".mx_BasicMessageComposer_input")
.click()
.should("have.focus")
.type("Hey!{enter}");
// Assert DM exists by checking for the first message and the room being in the room list
cy.contains(".mx_EventTile_body", "Hey!");
cy.get(".mx_RoomSublist[aria-label=People]").should("contain", bot2Name);
// Invite BotBob into existing DM with ByteBot
cy.getDmRooms(bot2.getUserId()).then(dmRooms => dmRooms[0])
.then(groupDmId => cy.inviteUser(groupDmId, bot1.getUserId()))

View File

@ -38,6 +38,7 @@
@import "./structures/_GenericErrorPage.pcss";
@import "./structures/_HeaderButtons.pcss";
@import "./structures/_HomePage.pcss";
@import "./structures/_LargeLoader.pcss";
@import "./structures/_LeftPanel.pcss";
@import "./structures/_MainSplit.pcss";
@import "./structures/_MatrixChat.pcss";

View File

@ -0,0 +1,37 @@
/*
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.
*/
.mx_LargeLoader {
align-items: center;
display: flex;
flex-direction: column;
height: 100%;
.mx_Spinner {
flex: unset;
height: auto;
margin-bottom: 32px;
margin-top: 33vh;
}
.mx_LargeLoader_text {
font-size: 24px;
font-weight: 600;
padding: 0 16px;
position: relative;
text-align: center;
}
}

View File

@ -32,6 +32,13 @@ limitations under the License.
position: relative;
}
.mx_MainSplit_timeline,
.mx_RoomView--local {
.mx_MessageComposer_wrapper {
margin: $spacing-8 $spacing-16;
}
}
.mx_RoomView_auxPanel {
min-width: 0px;
width: 100%;
@ -183,6 +190,10 @@ limitations under the License.
box-sizing: border-box;
}
.mx_RoomView--local .mx_ScrollPanel .mx_RoomView_MessageList {
justify-content: center;
}
.mx_RoomView_MessageList li {
clear: both;
}

View File

@ -0,0 +1,37 @@
/*
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 React from "react";
import Spinner from "../views/elements/Spinner";
interface LargeLoaderProps {
text: string;
}
/**
* Loader component that displays a (almost centered) spinner and loading message.
*/
export const LargeLoader: React.FC<LargeLoaderProps> = ({ text }) => {
return (
<div className="mx_LargeLoader">
<Spinner w={45} h={45} />
<div className="mx_LargeLoader_text">
{ text }
</div>
</div>
);
};

View File

@ -20,7 +20,7 @@ limitations under the License.
// TODO: This component is enormous! There's several things which could stand-alone:
// - Search results component
import React, { createRef } from 'react';
import React, { createRef, ReactElement, ReactNode, RefObject, useContext } from 'react';
import classNames from 'classnames';
import { IRecommendedVersion, NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { IThreadBundledRelationship, MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
@ -46,7 +46,7 @@ import ResizeNotifier from '../../utils/ResizeNotifier';
import ContentMessages from '../../ContentMessages';
import Modal from '../../Modal';
import CallHandler, { CallHandlerEvent } from '../../CallHandler';
import dis from '../../dispatcher/dispatcher';
import dis, { defaultDispatcher } from '../../dispatcher/dispatcher';
import * as Rooms from '../../Rooms';
import eventSearch, { searchPagination } from '../../Searching';
import MainSplit from './MainSplit';
@ -110,7 +110,15 @@ import FileDropTarget from './FileDropTarget';
import Measured from '../views/elements/Measured';
import { FocusComposerPayload } from '../../dispatcher/payloads/FocusComposerPayload';
import { haveRendererForEvent } from "../../events/EventTileFactory";
import { LocalRoom, LocalRoomState } from '../../models/LocalRoom';
import { createRoomFromLocalRoom } from '../../utils/direct-messages';
import NewRoomIntro from '../views/rooms/NewRoomIntro';
import EncryptionEvent from '../views/messages/EncryptionEvent';
import { StaticNotificationState } from '../../stores/notifications/StaticNotificationState';
import { isLocalRoom } from '../../utils/localRoom/isLocalRoom';
import { ShowThreadPayload } from "../../dispatcher/payloads/ShowThreadPayload";
import { RoomStatusBarUnsentMessages } from './RoomStatusBarUnsentMessages';
import { LargeLoader } from './LargeLoader';
const DEBUG = false;
let debuglog = function(msg: string) {};
@ -222,6 +230,137 @@ export interface IRoomState {
narrow: boolean;
}
interface LocalRoomViewProps {
resizeNotifier: ResizeNotifier;
permalinkCreator: RoomPermalinkCreator;
roomView: RefObject<HTMLElement>;
onFileDrop: (dataTransfer: DataTransfer) => Promise<void>;
}
/**
* Local room view. Uses only the bits necessary to display a local room view like room header or composer.
*
* @param {LocalRoomViewProps} props Room view props
* @returns {ReactElement}
*/
function LocalRoomView(props: LocalRoomViewProps): ReactElement {
const context = useContext(RoomContext);
const room = context.room as LocalRoom;
const encryptionEvent = context.room.currentState.getStateEvents(EventType.RoomEncryption)[0];
let encryptionTile: ReactNode;
if (encryptionEvent) {
encryptionTile = <EncryptionEvent mxEvent={encryptionEvent} />;
}
const onRetryClicked = () => {
room.state = LocalRoomState.NEW;
defaultDispatcher.dispatch({
action: "local_room_event",
roomId: room.roomId,
});
};
let statusBar: ReactElement;
let composer: ReactElement;
if (room.isError) {
const buttons = (
<AccessibleButton onClick={onRetryClicked} className="mx_RoomStatusBar_unsentRetry">
{ _t("Retry") }
</AccessibleButton>
);
statusBar = <RoomStatusBarUnsentMessages
title={_t("Some of your messages have not been sent")}
notificationState={StaticNotificationState.RED_EXCLAMATION}
buttons={buttons}
/>;
} else {
composer = <MessageComposer
room={context.room}
resizeNotifier={props.resizeNotifier}
permalinkCreator={props.permalinkCreator}
/>;
}
return (
<div className="mx_RoomView mx_RoomView--local">
<ErrorBoundary>
<RoomHeader
room={context.room}
searchInfo={null}
inRoom={true}
onSearchClick={null}
onInviteClick={null}
onForgetClick={null}
e2eStatus={E2EStatus.Normal}
onAppsClick={null}
appsShown={false}
onCallPlaced={null}
excludedRightPanelPhaseButtons={[]}
showButtons={false}
enableRoomOptionsMenu={false}
/>
<main className="mx_RoomView_body" ref={props.roomView}>
<FileDropTarget parent={props.roomView.current} onFileDrop={props.onFileDrop} />
<div className="mx_RoomView_timeline">
<ScrollPanel
className="mx_RoomView_messagePanel"
resizeNotifier={props.resizeNotifier}
>
{ encryptionTile }
<NewRoomIntro />
</ScrollPanel>
</div>
{ statusBar }
{ composer }
</main>
</ErrorBoundary>
</div>
);
}
interface ILocalRoomCreateLoaderProps {
names: string;
resizeNotifier: ResizeNotifier;
}
/**
* Room create loader view displaying a message and a spinner.
*
* @param {ILocalRoomCreateLoaderProps} props Room view props
* @return {ReactElement}
*/
function LocalRoomCreateLoader(props: ILocalRoomCreateLoaderProps): ReactElement {
const context = useContext(RoomContext);
const text = _t("We're creating a room with %(names)s", { names: props.names });
return (
<div className="mx_RoomView mx_RoomView--local">
<ErrorBoundary>
<RoomHeader
room={context.room}
searchInfo={null}
inRoom={true}
onSearchClick={null}
onInviteClick={null}
onForgetClick={null}
e2eStatus={E2EStatus.Normal}
onAppsClick={null}
appsShown={false}
onCallPlaced={null}
excludedRightPanelPhaseButtons={[]}
showButtons={false}
enableRoomOptionsMenu={false}
/>
<div className="mx_RoomView_body">
<LargeLoader text={text} />
</div>
</ErrorBoundary>
</div>
);
}
export class RoomView extends React.Component<IRoomProps, IRoomState> {
private readonly dispatcherRef: string;
private readonly roomStoreToken: EventSubscription;
@ -765,6 +904,11 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
for (const watcher of this.settingWatchers) {
SettingsStore.unwatchSetting(watcher);
}
if (this.viewsLocalRoom) {
// clean up if this was a local room
this.props.mxClient.store.removeRoom(this.state.room.roomId);
}
}
private onRightPanelStoreUpdate = () => {
@ -873,6 +1017,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
this.onSearchClick();
break;
case 'local_room_event':
this.onLocalRoomEvent(payload.roomId);
break;
case Action.EditEvent: {
// Quit early if we're trying to edit events in wrong rendering context
if (payload.timelineRenderingType !== this.state.timelineRenderingType) return;
@ -925,6 +1073,11 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}
};
private onLocalRoomEvent(roomId: string) {
if (roomId !== this.state.room.roomId) return;
createRoomFromLocalRoom(this.props.mxClient, this.state.room as LocalRoom);
}
private onRoomTimeline = (ev: MatrixEvent, room: Room | null, toStartOfTimeline: boolean, removed, data) => {
if (this.unmounted) return;
@ -1494,7 +1647,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
searchResult={result}
searchHighlights={this.state.searchHighlights}
resultLink={resultLink}
permalinkCreator={this.getPermalinkCreatorForRoom(room)}
permalinkCreator={this.permalinkCreator}
onHeightChanged={onHeightChanged}
/>);
}
@ -1769,7 +1922,44 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
this.setState({ narrow });
};
private get viewsLocalRoom(): boolean {
return isLocalRoom(this.state.room);
}
private get permalinkCreator(): RoomPermalinkCreator {
return this.getPermalinkCreatorForRoom(this.state.room);
}
private renderLocalRoomCreateLoader(): ReactElement {
const names = this.state.room.getDefaultRoomName(this.props.mxClient.getUserId());
return <RoomContext.Provider value={this.state}>
<LocalRoomCreateLoader
names={names}
resizeNotifier={this.props.resizeNotifier}
/>
</RoomContext.Provider>;
}
private renderLocalRoomView(): ReactElement {
return <RoomContext.Provider value={this.state}>
<LocalRoomView
resizeNotifier={this.props.resizeNotifier}
permalinkCreator={this.permalinkCreator}
roomView={this.roomView}
onFileDrop={this.onFileDrop}
/>
</RoomContext.Provider>;
}
render() {
if (this.state.room instanceof LocalRoom) {
if (this.state.room.state === LocalRoomState.CREATING) {
return this.renderLocalRoomCreateLoader();
}
return this.renderLocalRoomView();
}
if (!this.state.room) {
const loading = !this.state.matrixClientIsReady || this.state.roomLoading || this.state.peekLoading;
if (loading) {
@ -2027,7 +2217,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
e2eStatus={this.state.e2eStatus}
resizeNotifier={this.props.resizeNotifier}
replyToEvent={this.state.replyToEvent}
permalinkCreator={this.getPermalinkCreatorForRoom(this.state.room)}
permalinkCreator={this.permalinkCreator}
/>;
}
@ -2093,7 +2283,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
showUrlPreview={this.state.showUrlPreview}
className={this.messagePanelClassNames}
membersLoaded={this.state.membersLoaded}
permalinkCreator={this.getPermalinkCreatorForRoom(this.state.room)}
permalinkCreator={this.permalinkCreator}
resizeNotifier={this.props.resizeNotifier}
showReactions={true}
layout={this.state.layout}
@ -2123,7 +2313,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
? <RightPanel
room={this.state.room}
resizeNotifier={this.props.resizeNotifier}
permalinkCreator={this.getPermalinkCreatorForRoom(this.state.room)}
permalinkCreator={this.permalinkCreator}
e2eStatus={this.state.e2eStatus} />
: null;
@ -2237,6 +2427,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
appsShown={this.state.showApps}
onCallPlaced={onCallPlaced}
excludedRightPanelPhaseButtons={excludedRightPanelPhaseButtons}
showButtons={!this.viewsLocalRoom}
enableRoomOptionsMenu={!this.viewsLocalRoom}
/>
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
<div className={mainSplitContentClasses} ref={this.roomViewBody} data-layout={this.state.layout}>

View File

@ -62,11 +62,16 @@ import CopyableText from "../elements/CopyableText";
import { ScreenName } from '../../../PosthogTrackers';
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { DirectoryMember, IDMUserTileProps, Member, ThreepidMember } from "../../../utils/direct-messages";
import {
DirectoryMember,
IDMUserTileProps,
Member,
startDmOnFirstMessage,
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 */
@ -446,11 +451,10 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
}
private startDm = async () => {
this.setState({ busy: true });
try {
const cli = MatrixClientPeg.get();
const targets = this.convertFilter();
await startDm(cli, targets);
startDmOnFirstMessage(cli, targets);
this.props.onFinished(true);
} catch (err) {
logger.error(err);
@ -458,8 +462,6 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
busy: false,
errorText: _t("We couldn't create your DM."),
});
} finally {
this.setState({ busy: false });
}
};

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 } from "../../../../utils/direct-messages";
import { DirectoryMember, Member, startDmOnFirstMessage } from "../../../../utils/direct-messages";
import DMRoomMap from "../../../../utils/DMRoomMap";
import { makeUserPermalink } from "../../../../utils/permalinks/Permalinks";
import { buildActivityScores, buildMemberScores, compareMembers } from "../../../../utils/SortMembers";
@ -92,7 +92,6 @@ 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
@ -593,7 +592,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
id={`mx_SpotlightDialog_button_result_${result.member.userId}`}
key={`${Section[result.section]}-${result.member.userId}`}
onClick={() => {
startDm(cli, [result.member]);
startDmOnFirstMessage(cli, [result.member]);
onFinished();
}}
aria-label={result.member instanceof RoomMember

View File

@ -33,7 +33,6 @@ import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import dis from '../../../dispatcher/dispatcher';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import createRoom from '../../../createRoom';
import DMRoomMap from '../../../utils/DMRoomMap';
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
import SdkConfig from '../../../SdkConfig';
@ -78,8 +77,7 @@ import { IRightPanelCardState } from '../../../stores/right-panel/RightPanelStor
import UserIdentifierCustomisations from '../../../customisations/UserIdentifier';
import PosthogTrackers from "../../../PosthogTrackers";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { privateShouldBeEncrypted } from "../../../utils/rooms";
import { findDMForUser } from '../../../utils/dm/findDMForUser';
import { DirectoryMember, startDmOnFirstMessage } from '../../../utils/direct-messages';
export interface IDevice {
deviceId: string;
@ -124,38 +122,13 @@ export const getE2EStatus = (cli: MatrixClient, userId: string, devices: IDevice
return anyDeviceUnverified ? E2EStatus.Warning : E2EStatus.Verified;
};
async function openDMForUser(matrixClient: MatrixClient, userId: string, viaKeyboard = false): Promise<void> {
const lastActiveRoom = findDMForUser(matrixClient, userId);
if (lastActiveRoom) {
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: lastActiveRoom.roomId,
metricsTrigger: "MessageUser",
metricsViaKeyboard: viaKeyboard,
});
return;
}
const createRoomOptions = {
dmUserId: userId,
encryption: undefined,
};
if (privateShouldBeEncrypted()) {
// Check whether all users have uploaded device keys before.
// If so, enable encryption in the new room.
const usersToDevicesMap = await matrixClient.downloadKeys([userId]);
const allHaveDeviceKeys = Object.values(usersToDevicesMap).every(devices => {
// `devices` is an object of the form { deviceId: deviceInfo, ... }.
return Object.keys(devices).length > 0;
});
if (allHaveDeviceKeys) {
createRoomOptions.encryption = true;
}
}
await createRoom(createRoomOptions);
async function openDMForUser(matrixClient: MatrixClient, user: RoomMember): Promise<void> {
const startDMUser = new DirectoryMember({
user_id: user.userId,
display_name: user.rawDisplayName,
avatar_url: user.getMxcAvatarUrl(),
});
startDmOnFirstMessage(matrixClient, [startDMUser]);
}
type SetUpdating = (updating: boolean) => void;
@ -328,17 +301,17 @@ function DevicesSection({ devices, userId, loading }: { devices: IDevice[], user
);
}
const MessageButton = ({ userId }: { userId: string }) => {
const MessageButton = ({ member }: { member: RoomMember }) => {
const cli = useContext(MatrixClientContext);
const [busy, setBusy] = useState(false);
return (
<AccessibleButton
kind="link"
onClick={async (ev) => {
onClick={async () => {
if (busy) return;
setBusy(true);
await openDMForUser(cli, userId, ev.type !== "click");
await openDMForUser(cli, member);
setBusy(false);
}}
className="mx_UserInfo_field"
@ -484,7 +457,7 @@ const UserOptionsSection: React.FC<{
let directMessageButton: JSX.Element;
if (!isMe) {
directMessageButton = <MessageButton userId={member.userId} />;
directMessageButton = <MessageButton member={member} />;
}
return (

View File

@ -948,7 +948,6 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
isSeeingThroughMessageHiddenForModeration,
} = getEventDisplayInfo(this.props.mxEvent, this.context.showHiddenEvents, this.shouldHideEvent());
const { isQuoteExpanded } = this.state;
// This shouldn't happen: the caller should check we support this type
// before trying to instantiate us
if (!hasRenderer) {

View File

@ -3156,6 +3156,7 @@
"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.",
"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?",
"Search failed": "Search failed",

View File

@ -0,0 +1,32 @@
/*
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 React from "react";
import { render, screen } from "@testing-library/react";
import { LargeLoader } from "../../../src/components/structures/LargeLoader";
describe("LargeLoader", () => {
const text = "test loading text";
beforeEach(() => {
render(<LargeLoader text={text} />);
});
it("should render the text", () => {
screen.getByText(text);
});
});

View File

@ -22,11 +22,13 @@ import { mocked, MockedObject } from "jest-mock";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { EventType } from "matrix-js-sdk/src/matrix";
import { MEGOLM_ALGORITHM } from "matrix-js-sdk/src/crypto/olmlib";
import { stubClient, mockPlatformPeg, unmockPlatformPeg, wrapInMatrixClientContext } from "../../test-utils";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import { Action } from "../../../src/dispatcher/actions";
import dis from "../../../src/dispatcher/dispatcher";
import { defaultDispatcher } from "../../../src/dispatcher/dispatcher";
import { ViewRoomPayload } from "../../../src/dispatcher/payloads/ViewRoomPayload";
import { RoomView as _RoomView } from "../../../src/components/structures/RoomView";
import ResizeNotifier from "../../../src/utils/ResizeNotifier";
@ -37,6 +39,9 @@ import DMRoomMap from "../../../src/utils/DMRoomMap";
import { NotificationState } from "../../../src/stores/notifications/NotificationState";
import RightPanelStore from "../../../src/stores/right-panel/RightPanelStore";
import { RightPanelPhases } from "../../../src/stores/right-panel/RightPanelStorePhases";
import { LocalRoom, LocalRoomState } from "../../../src/models/LocalRoom";
import { DirectoryMember } from "../../../src/utils/direct-messages";
import { createDmLocalRoom } from "../../../src/utils/dm/createDmLocalRoom";
const RoomView = wrapInMatrixClientContext(_RoomView);
@ -44,6 +49,7 @@ describe("RoomView", () => {
let cli: MockedObject<MatrixClient>;
let room: Room;
let roomCount = 0;
beforeEach(async () => {
mockPlatformPeg({ reload: () => {} });
stubClient();
@ -51,7 +57,7 @@ describe("RoomView", () => {
room = new Room(`!${roomCount++}:example.org`, cli, "@alice:example.org");
room.getPendingEvents = () => [];
cli.getRoom.mockReturnValue(room);
cli.getRoom.mockImplementation(() => room);
// Re-emit certain events on the mocked client
room.on(RoomEvent.Timeline, (...args) => cli.emit(RoomEvent.Timeline, ...args));
room.on(RoomEvent.TimelineReset, (...args) => cli.emit(RoomEvent.TimelineReset, ...args));
@ -76,7 +82,7 @@ describe("RoomView", () => {
});
});
dis.dispatch<ViewRoomPayload>({
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: room.roomId,
metricsTrigger: null,
@ -164,4 +170,82 @@ describe("RoomView", () => {
expect(RightPanelStore.instance.currentCard.phase).toEqual(RightPanelPhases.Timeline);
});
});
describe("for a local room", () => {
let localRoom: LocalRoom;
let roomView: ReactWrapper;
beforeEach(async () => {
localRoom = room = await createDmLocalRoom(cli, [new DirectoryMember({ user_id: "@user:example.com" })]);
cli.store.storeRoom(room);
});
it("should remove the room from the store on unmount", async () => {
roomView = await mountRoomView();
roomView.unmount();
expect(cli.store.removeRoom).toHaveBeenCalledWith(room.roomId);
});
describe("in state NEW", () => {
it("should match the snapshot", async () => {
roomView = await mountRoomView();
expect(roomView.html()).toMatchSnapshot();
});
describe("that is encrypted", () => {
beforeEach(() => {
mocked(cli.isRoomEncrypted).mockReturnValue(true);
localRoom.encrypted = true;
localRoom.currentState.setStateEvents([
new MatrixEvent({
event_id: `~${localRoom.roomId}:${cli.makeTxnId()}`,
type: EventType.RoomEncryption,
content: {
algorithm: MEGOLM_ALGORITHM,
},
user_id: cli.getUserId(),
sender: cli.getUserId(),
state_key: "",
room_id: localRoom.roomId,
origin_server_ts: Date.now(),
}),
]);
});
it("should match the snapshot", async () => {
const roomView = await mountRoomView();
expect(roomView.html()).toMatchSnapshot();
});
});
});
it("in state CREATING should match the snapshot", async () => {
localRoom.state = LocalRoomState.CREATING;
roomView = await mountRoomView();
expect(roomView.html()).toMatchSnapshot();
});
describe("in state ERROR", () => {
beforeEach(async () => {
localRoom.state = LocalRoomState.ERROR;
roomView = await mountRoomView();
});
it("should match the snapshot", async () => {
expect(roomView.html()).toMatchSnapshot();
});
it("clicking retry should set the room state to new dispatch a local room event", () => {
jest.spyOn(defaultDispatcher, "dispatch");
roomView.findWhere((w: ReactWrapper) => {
return w.hasClass("mx_RoomStatusBar_unsentRetry") && w.text() === "Retry";
}).first().simulate("click");
expect(localRoom.state).toBe(LocalRoomState.NEW);
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({
action: "local_room_event",
roomId: room.roomId,
});
});
});
});
});

View File

@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RoomView for a local room in state CREATING should match the snapshot 1`] = `"<div class=\\"mx_RoomView mx_RoomView--local\\"><header class=\\"mx_RoomHeader light-panel\\"><div class=\\"mx_RoomHeader_wrapper\\"><div class=\\"mx_RoomHeader_avatar\\"><div class=\\"mx_DecoratedRoomAvatar\\"><span class=\\"mx_BaseAvatar\\" role=\\"presentation\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 15.600000000000001px; width: 24px; line-height: 24px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 24px; height: 24px;\\" aria-hidden=\\"true\\"></span></div></div><div class=\\"mx_RoomHeader_e2eIcon\\"><div class=\\"mx_E2EIcon mx_E2EIcon_normal\\"></div></div><div class=\\"mx_RoomHeader_name mx_RoomHeader_name--textonly\\"><div dir=\\"auto\\" class=\\"mx_RoomHeader_nametext\\" title=\\"@user:example.com\\" role=\\"heading\\" aria-level=\\"1\\">@user:example.com</div></div><div class=\\"mx_RoomHeader_topic mx_RoomTopic\\" dir=\\"auto\\"><div tabindex=\\"0\\"><div><span dir=\\"auto\\"></span></div></div></div></div></header><div class=\\"mx_RoomView_body\\"><div class=\\"mx_LargeLoader\\"><div class=\\"mx_Spinner\\"><div class=\\"mx_Spinner_icon\\" style=\\"width: 45px; height: 45px;\\" aria-label=\\"Loading...\\" role=\\"progressbar\\"></div></div><div class=\\"mx_LargeLoader_text\\">We're creating a room with @user:example.com</div></div></div></div>"`;
exports[`RoomView for a local room in state ERROR should match the snapshot 1`] = `"<div class=\\"mx_RoomView mx_RoomView--local\\"><header class=\\"mx_RoomHeader light-panel\\"><div class=\\"mx_RoomHeader_wrapper\\"><div class=\\"mx_RoomHeader_avatar\\"><div class=\\"mx_DecoratedRoomAvatar\\"><span class=\\"mx_BaseAvatar\\" role=\\"presentation\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 15.600000000000001px; width: 24px; line-height: 24px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 24px; height: 24px;\\" aria-hidden=\\"true\\"></span></div></div><div class=\\"mx_RoomHeader_e2eIcon\\"><div class=\\"mx_E2EIcon mx_E2EIcon_normal\\"></div></div><div class=\\"mx_RoomHeader_name mx_RoomHeader_name--textonly\\"><div dir=\\"auto\\" class=\\"mx_RoomHeader_nametext\\" title=\\"@user:example.com\\" role=\\"heading\\" aria-level=\\"1\\">@user:example.com</div></div><div class=\\"mx_RoomHeader_topic mx_RoomTopic\\" dir=\\"auto\\"><div tabindex=\\"0\\"><div><span dir=\\"auto\\"></span></div></div></div></div></header><main class=\\"mx_RoomView_body\\"><div class=\\"mx_RoomView_timeline\\"><div class=\\"mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel\\" tabindex=\\"-1\\"><div class=\\"mx_RoomView_messageListWrapper\\"><ol class=\\"mx_RoomView_MessageList\\" aria-live=\\"polite\\" style=\\"height: 400px;\\"><li class=\\"mx_NewRoomIntro\\"><div class=\\"mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon_warning\\"><div class=\\"mx_EventTileBubble_title\\">End-to-end encryption isn't enabled</div><div class=\\"mx_EventTileBubble_subtitle\\"><span> Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. </span></div></div><span aria-label=\\"Avatar\\" aria-live=\\"off\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_BaseAvatar\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 33.800000000000004px; width: 52px; line-height: 52px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 52px; height: 52px;\\" aria-hidden=\\"true\\"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class=\\"mx_RoomStatusBar mx_RoomStatusBar_unsentMessages\\"><div role=\\"alert\\"><div class=\\"mx_RoomStatusBar_unsentBadge\\"><div class=\\"mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_highlighted mx_NotificationBadge_2char\\"><span class=\\"mx_NotificationBadge_count\\">!</span></div></div><div><div class=\\"mx_RoomStatusBar_unsentTitle\\">Some of your messages have not been sent</div></div><div class=\\"mx_RoomStatusBar_unsentButtonBar\\"><div role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_RoomStatusBar_unsentRetry\\">Retry</div></div></div></div></main></div>"`;
exports[`RoomView for a local room in state NEW should match the snapshot 1`] = `"<div class=\\"mx_RoomView mx_RoomView--local\\"><header class=\\"mx_RoomHeader light-panel\\"><div class=\\"mx_RoomHeader_wrapper\\"><div class=\\"mx_RoomHeader_avatar\\"><div class=\\"mx_DecoratedRoomAvatar\\"><span class=\\"mx_BaseAvatar\\" role=\\"presentation\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 15.600000000000001px; width: 24px; line-height: 24px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 24px; height: 24px;\\" aria-hidden=\\"true\\"></span></div></div><div class=\\"mx_RoomHeader_e2eIcon\\"><div class=\\"mx_E2EIcon mx_E2EIcon_normal\\"></div></div><div class=\\"mx_RoomHeader_name mx_RoomHeader_name--textonly\\"><div dir=\\"auto\\" class=\\"mx_RoomHeader_nametext\\" title=\\"@user:example.com\\" role=\\"heading\\" aria-level=\\"1\\">@user:example.com</div></div><div class=\\"mx_RoomHeader_topic mx_RoomTopic\\" dir=\\"auto\\"><div tabindex=\\"0\\"><div><span dir=\\"auto\\"></span></div></div></div></div></header><main class=\\"mx_RoomView_body\\"><div class=\\"mx_RoomView_timeline\\"><div class=\\"mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel\\" tabindex=\\"-1\\"><div class=\\"mx_RoomView_messageListWrapper\\"><ol class=\\"mx_RoomView_MessageList\\" aria-live=\\"polite\\" style=\\"height: 400px;\\"><li class=\\"mx_NewRoomIntro\\"><div class=\\"mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon_warning\\"><div class=\\"mx_EventTileBubble_title\\">End-to-end encryption isn't enabled</div><div class=\\"mx_EventTileBubble_subtitle\\"><span> Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. </span></div></div><span aria-label=\\"Avatar\\" aria-live=\\"off\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_BaseAvatar\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 33.800000000000004px; width: 52px; line-height: 52px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 52px; height: 52px;\\" aria-hidden=\\"true\\"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class=\\"mx_MessageComposer\\"><div class=\\"mx_MessageComposer_wrapper\\"><div class=\\"mx_MessageComposer_row\\"><div class=\\"mx_SendMessageComposer\\"><div class=\\"mx_BasicMessageComposer\\"><div class=\\"mx_MessageComposerFormatBar\\"><button type=\\"button\\" aria-label=\\"Bold\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconBold\\"></button><button type=\\"button\\" aria-label=\\"Italics\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconItalic\\"></button><button type=\\"button\\" aria-label=\\"Strikethrough\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconStrikethrough\\"></button><button type=\\"button\\" aria-label=\\"Code block\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconCode\\"></button><button type=\\"button\\" aria-label=\\"Quote\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconQuote\\"></button><button type=\\"button\\" aria-label=\\"Insert link\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconInsertLink\\"></button></div><div class=\\"mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty\\" contenteditable=\\"true\\" tabindex=\\"0\\" aria-label=\\"Send a message…\\" role=\\"textbox\\" aria-multiline=\\"true\\" aria-autocomplete=\\"list\\" aria-haspopup=\\"listbox\\" dir=\\"auto\\" aria-disabled=\\"false\\" style=\\"--placeholder: 'Send a message…';\\"><div><br></div></div></div></div><div aria-label=\\"Emoji\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_emoji\\"></div><div aria-label=\\"Attachment\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_upload\\"></div><div aria-label=\\"More options\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu\\"></div><input type=\\"file\\" style=\\"display: none;\\" multiple=\\"\\"></div></div></div></main></div>"`;
exports[`RoomView for a local room in state NEW that is encrypted should match the snapshot 1`] = `"<div class=\\"mx_RoomView mx_RoomView--local\\"><header class=\\"mx_RoomHeader light-panel\\"><div class=\\"mx_RoomHeader_wrapper\\"><div class=\\"mx_RoomHeader_avatar\\"><div class=\\"mx_DecoratedRoomAvatar\\"><span class=\\"mx_BaseAvatar\\" role=\\"presentation\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 15.600000000000001px; width: 24px; line-height: 24px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 24px; height: 24px;\\" aria-hidden=\\"true\\"></span></div></div><div class=\\"mx_RoomHeader_e2eIcon\\"><div class=\\"mx_E2EIcon mx_E2EIcon_normal\\"></div></div><div class=\\"mx_RoomHeader_name mx_RoomHeader_name--textonly\\"><div dir=\\"auto\\" class=\\"mx_RoomHeader_nametext\\" title=\\"@user:example.com\\" role=\\"heading\\" aria-level=\\"1\\">@user:example.com</div></div><div class=\\"mx_RoomHeader_topic mx_RoomTopic\\" dir=\\"auto\\"><div tabindex=\\"0\\"><div><span dir=\\"auto\\"></span></div></div></div></div></header><main class=\\"mx_RoomView_body\\"><div class=\\"mx_RoomView_timeline\\"><div class=\\"mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel\\" tabindex=\\"-1\\"><div class=\\"mx_RoomView_messageListWrapper\\"><ol class=\\"mx_RoomView_MessageList\\" aria-live=\\"polite\\" style=\\"height: 400px;\\"><div class=\\"mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon\\"><div class=\\"mx_EventTileBubble_title\\">Encryption enabled</div><div class=\\"mx_EventTileBubble_subtitle\\">Messages in this chat will be end-to-end encrypted.</div></div><li class=\\"mx_NewRoomIntro\\"><span aria-label=\\"Avatar\\" aria-live=\\"off\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_BaseAvatar\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 33.800000000000004px; width: 52px; line-height: 52px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 52px; height: 52px;\\" aria-hidden=\\"true\\"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class=\\"mx_MessageComposer\\"><div class=\\"mx_MessageComposer_wrapper\\"><div class=\\"mx_MessageComposer_row\\"><div class=\\"mx_SendMessageComposer\\"><div class=\\"mx_BasicMessageComposer\\"><div class=\\"mx_MessageComposerFormatBar\\"><button type=\\"button\\" aria-label=\\"Bold\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconBold\\"></button><button type=\\"button\\" aria-label=\\"Italics\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconItalic\\"></button><button type=\\"button\\" aria-label=\\"Strikethrough\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconStrikethrough\\"></button><button type=\\"button\\" aria-label=\\"Code block\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconCode\\"></button><button type=\\"button\\" aria-label=\\"Quote\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconQuote\\"></button><button type=\\"button\\" aria-label=\\"Insert link\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconInsertLink\\"></button></div><div class=\\"mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty\\" contenteditable=\\"true\\" tabindex=\\"0\\" aria-label=\\"Send a message…\\" role=\\"textbox\\" aria-multiline=\\"true\\" aria-autocomplete=\\"list\\" aria-haspopup=\\"listbox\\" dir=\\"auto\\" aria-disabled=\\"false\\" style=\\"--placeholder: 'Send a message…';\\"><div><br></div></div></div></div><div aria-label=\\"Emoji\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_emoji\\"></div><div aria-label=\\"Attachment\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_upload\\"></div><div aria-label=\\"More options\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu\\"></div><input type=\\"file\\" style=\\"display: none;\\" multiple=\\"\\"></div></div></div></main></div>"`;

View File

@ -26,6 +26,7 @@ import sanitizeHtml from "sanitize-html";
import SpotlightDialog, { Filter } from "../../../../src/components/views/dialogs/spotlight/SpotlightDialog";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../../../src/models/LocalRoom";
import { DirectoryMember, startDmOnFirstMessage } from "../../../../src/utils/direct-messages";
import DMRoomMap from "../../../../src/utils/DMRoomMap";
import { mkRoom, stubClient } from "../../../test-utils";
@ -346,4 +347,27 @@ describe("Spotlight Dialog", () => {
expect(options.first().text()).not.toContain(testLocalRoom.name);
});
});
it("should start a DM when clicking a person", async () => {
const wrapper = mount(
<SpotlightDialog
initialFilter={Filter.People}
initialText={testPerson.display_name}
onFinished={() => null} />,
);
await act(async () => {
await sleep(200);
});
wrapper.update();
const options = wrapper.find("div.mx_SpotlightDialog_option");
expect(options.length).toBeGreaterThanOrEqual(1);
expect(options.first().text()).toContain(testPerson.display_name);
options.first().simulate("click");
expect(startDmOnFirstMessage).toHaveBeenCalledWith(mockedClient, [new DirectoryMember(testPerson)]);
wrapper.unmount();
});
});