Start DM on first message (#8612)

This commit is contained in:
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

@ -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",