From e5c488a8900f09b2d87e5f1225a0ef4cecfa1233 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Mon, 16 May 2022 18:17:57 +0200 Subject: [PATCH] Start DM on first message Signed-off-by: Michael Weimann --- src/PageTypes.ts | 1 + src/PosthogTrackers.ts | 1 + src/components/structures/LoggedInView.tsx | 24 ++++ src/components/structures/MatrixChat.tsx | 9 +- src/components/structures/RoomView.tsx | 7 +- src/components/views/dialogs/InviteDialog.tsx | 21 +++- .../views/rooms/MessageComposer.tsx | 12 ++ .../views/rooms/SendMessageComposer.tsx | 7 +- src/dispatcher/actions.ts | 2 + .../payloads/ViewLocalRoomPayload.ts | 22 ++++ src/models/LocalRoom.ts | 23 ++++ src/stores/RoomViewStore.tsx | 1 + src/utils/direct-messages.ts | 110 +++++++++++++++++- 13 files changed, 223 insertions(+), 17 deletions(-) create mode 100644 src/dispatcher/payloads/ViewLocalRoomPayload.ts create mode 100644 src/models/LocalRoom.ts diff --git a/src/PageTypes.ts b/src/PageTypes.ts index fb0424f6e055..447a34799cf5 100644 --- a/src/PageTypes.ts +++ b/src/PageTypes.ts @@ -19,6 +19,7 @@ limitations under the License. enum PageType { HomePage = "home_page", RoomView = "room_view", + LocalRoomView = "local_room_view", UserView = "user_view", LegacyGroupView = "legacy_group_view", } diff --git a/src/PosthogTrackers.ts b/src/PosthogTrackers.ts index 434d142c8cd6..a2b8d3798084 100644 --- a/src/PosthogTrackers.ts +++ b/src/PosthogTrackers.ts @@ -39,6 +39,7 @@ const notLoggedInMap: Record, ScreenName> = { const loggedInPageTypeMap: Record = { [PageType.HomePage]: "Home", [PageType.RoomView]: "Room", + [PageType.LocalRoomView]: "Room", [PageType.UserView]: "User", [PageType.LegacyGroupView]: "Group", }; diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index de60ca71fa17..da293ef3cd7a 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -22,6 +22,8 @@ import classNames from 'classnames'; import { ISyncStateData, SyncState } from 'matrix-js-sdk/src/sync'; import { IUsageLimit } from 'matrix-js-sdk/src/@types/partials'; import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; +import { ISendEventResponse } from "matrix-js-sdk/src/@types/requests"; +import { IContent } from "matrix-js-sdk/src/models/event"; import { isOnlyCtrlOrCmdKeyEvent, Key } from '../../Keyboard'; import PageTypes from '../../PageTypes'; @@ -71,6 +73,8 @@ import { SwitchSpacePayload } from "../../dispatcher/payloads/SwitchSpacePayload import LegacyGroupView from "./LegacyGroupView"; import { IConfigOptions } from "../../IConfigOptions"; import LeftPanelLiveShareWarning from '../views/beacon/LeftPanelLiveShareWarning'; +import { startDm } from '../../utils/direct-messages'; +import { LocalRoom } from '../../models/LocalRoom'; // We need to fetch each pinned message individually (if we don't already have it) // so each pinned message may trigger a request. Limit the number per room for sanity. @@ -619,8 +623,27 @@ class LoggedInView extends React.Component { render() { let pageElement; + let messageComposerHandlers; switch (this.props.page_type) { + case PageTypes.LocalRoomView: + messageComposerHandlers = { + sendMessage: async ( + localRoomId: string, + threadId: string | null, + content: IContent, + ): Promise => { + const room = this._matrixClient.store.getRoom(localRoomId); + + if (!(room instanceof LocalRoom)) { + return; + } + + const rooomId = await startDm(this._matrixClient, room.targets); + return this._matrixClient.sendMessage(rooomId, threadId, content); + }, + }; + // fallthrough case PageTypes.RoomView: pageElement = { resizeNotifier={this.props.resizeNotifier} justCreatedOpts={this.props.roomJustCreatedOpts} forceTimeline={this.props.forceTimeline} + messageComposerHandlers={messageComposerHandlers} />; break; diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index b7e89b08a40a..89e179bed3f7 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -651,12 +651,15 @@ export default class MatrixChat extends React.PureComponent { case 'view_user_info': this.viewUser(payload.userId, payload.subAction); break; + case Action.ViewLocalRoom: + this.viewRoom(payload as ViewRoomPayload, PageType.LocalRoomView); + break; case Action.ViewRoom: { // Takes either a room ID or room alias: if switching to a room the client is already // known to be in (eg. user clicks on a room in the recents panel), supply the ID // If the user is clicking on a room in the context of the alias being presented // to them, supply the room alias. If both are supplied, the room ID will be ignored. - const promise = this.viewRoom(payload as ViewRoomPayload); + const promise = this.viewRoom(payload as ViewRoomPayload, PageType.RoomView); if (payload.deferred_action) { promise.then(() => { dis.dispatch(payload.deferred_action); @@ -854,7 +857,7 @@ export default class MatrixChat extends React.PureComponent { } // switch view to the given room - private async viewRoom(roomInfo: ViewRoomPayload) { + private async viewRoom(roomInfo: ViewRoomPayload, pageType: PageType) { this.focusComposer = true; if (roomInfo.room_alias) { @@ -913,7 +916,7 @@ export default class MatrixChat extends React.PureComponent { this.setState({ view: Views.LOGGED_IN, currentRoomId: roomInfo.room_id || null, - page_type: PageType.RoomView, + page_type: pageType, threepidInvite: roomInfo.threepid_invite, roomOobData: roomInfo.oob_data, forceTimeline: roomInfo.forceTimeline, diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 1539037cf894..b36d31c10634 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -93,7 +93,7 @@ import SearchResultTile from '../views/rooms/SearchResultTile'; import Spinner from "../views/elements/Spinner"; import UploadBar from './UploadBar'; import RoomStatusBar from "./RoomStatusBar"; -import MessageComposer from '../views/rooms/MessageComposer'; +import MessageComposer, { IMessageComposerHandlers } from '../views/rooms/MessageComposer'; import JumpToBottomButton from "../views/rooms/JumpToBottomButton"; import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar"; import { showThread } from '../../dispatcher/dispatch-actions/threads'; @@ -132,6 +132,8 @@ interface IRoomProps extends MatrixClientProps { // Called with the credentials of a registered user (if they were a ROU that transitioned to PWLU) onRegistered?(credentials: IMatrixClientCreds): void; + + messageComposerHandlers?: IMessageComposerHandlers; } // This defines the content of the mainSplit. @@ -2013,6 +2015,7 @@ export class RoomView extends React.Component { resizeNotifier={this.props.resizeNotifier} replyToEvent={this.state.replyToEvent} permalinkCreator={this.getPermalinkCreatorForRoom(this.state.room)} + handlers={this.props.messageComposerHandlers} />; } @@ -2066,7 +2069,7 @@ export class RoomView extends React.Component { showReadReceipts={this.state.showReadReceipts} manageReadReceipts={!this.state.isPeeking} sendReadReceiptOnLoad={!this.state.wasContextSwitch} - manageReadMarkers={!this.state.isPeeking} + manageReadMarkers={false} hidden={hideMessagePanel} highlightedEventId={highlightedEventId} eventId={this.state.initialEventId} diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 978c176ab0a3..7816c708514b 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -58,7 +58,13 @@ import CopyableText from "../elements/CopyableText"; import { ScreenName } from '../../../PosthogTrackers'; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; import { getKeyBindingsManager } from "../../../KeyBindingsManager"; -import { DirectoryMember, IDMUserTileProps, Member, startDm, ThreepidMember } from "../../../utils/direct-messages"; +import { + createDmLocalRoom, + DirectoryMember, + IDMUserTileProps, + Member, + 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"; @@ -563,11 +569,16 @@ export default class InviteDialog extends React.PureComponent { - this.setState({ busy: true }); try { - const cli = MatrixClientPeg.get(); const targets = this.convertFilter(); - await startDm(cli, targets); + const client = MatrixClientPeg.get(); + createDmLocalRoom(client, targets); + dis.dispatch({ + action: Action.ViewLocalRoom, + room_id: 'local_room', + joining: false, + targets, + }); this.props.onFinished(true); } catch (err) { logger.error(err); @@ -575,8 +586,6 @@ export default class InviteDialog extends React.PureComponent Promise; +} + export default class MessageComposer extends React.Component { private dispatcherRef: string; private messageComposerInput = createRef(); @@ -377,6 +388,7 @@ export default class MessageComposer extends React.Component { onChange={this.onChange} disabled={this.state.haveRecording} toggleStickerPickerOpen={this.toggleStickerPickerOpen} + handlers={this.props.handlers} />, ); diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index c309e0a16c1c..32faa75d2aa6 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -58,6 +58,7 @@ import { getSlashCommand, isSlashCommand, runSlashCommand, shouldSendAnyway } fr import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; import { PosthogAnalytics } from "../../../PosthogAnalytics"; import { addReplyToMessageContent } from '../../../utils/Reply'; +import { IMessageComposerHandlers } from './MessageComposer'; // Merges favouring the given relation export function attachRelation(content: IContent, relation?: IEventRelation): void { @@ -139,6 +140,7 @@ interface ISendMessageComposerProps extends MatrixClientProps { onChange?(model: EditorModel): void; includeReplyLegacyFallback?: boolean; toggleStickerPickerOpen: () => void; + handlers?: IMessageComposerHandlers; } export class SendMessageComposer extends React.Component { @@ -401,7 +403,10 @@ export class SendMessageComposer extends React.Component { // - event_offset: 100 // - highlighted: true case Action.ViewRoom: + case Action.ViewLocalRoom: this.viewRoom(payload); break; // for these events blank out the roomId as we are no longer in the RoomView diff --git a/src/utils/direct-messages.ts b/src/utils/direct-messages.ts index e67c01c7cad8..e8b94207eaa0 100644 --- a/src/utils/direct-messages.ts +++ b/src/utils/direct-messages.ts @@ -15,7 +15,9 @@ limitations under the License. */ import { IInvite3PID } from "matrix-js-sdk/src/@types/requests"; -import { MatrixClient } from "matrix-js-sdk/src/client"; +import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client"; +import { EventType } from "matrix-js-sdk/src/matrix"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { Room } from "matrix-js-sdk/src/models/room"; import createRoom, { canEncryptToAllUsers } from "../createRoom"; @@ -26,6 +28,8 @@ import DMRoomMap from "./DMRoomMap"; import { isJoinedOrNearlyJoined } from "./membership"; import dis from "../dispatcher/dispatcher"; import { privateShouldBeEncrypted } from "./rooms"; +import * as Rooms from '../Rooms'; +import { LocalRoom } from '../models/LocalRoom'; export function findDMForUser(client: MatrixClient, userId: string): Room { const roomIds = DMRoomMap.shared().getDMRoomsForUserId(userId); @@ -52,7 +56,103 @@ export function findDMForUser(client: MatrixClient, userId: string): Room { } } -export async function startDm(client: MatrixClient, targets: Member[]): Promise { +export async function createDmLocalRoom( + client: MatrixClient, + targets: Member[], +) { + const userId = client.getUserId(); + const other = targets[0]; + + const roomId = `!${client.makeTxnId()}:local`; + Rooms.setDMRoom(roomId, userId); + + const roomCreateEvent = new MatrixEvent({ + event_id: `~${roomId}:${client.makeTxnId()}`, + type: EventType.RoomCreate, + content: { + creator: userId, + room_version: "9", + }, + state_key: "", + user_id: userId, + sender: userId, + room_id: 'local_room', + origin_server_ts: new Date().getTime(), + }); + + const roomMembershipEvent = new MatrixEvent({ + event_id: `~${roomId}:${client.makeTxnId()}`, + type: EventType.RoomMember, + content: { + displayname: userId, + membership: "join", + }, + state_key: userId, + user_id: userId, + sender: userId, + room_id: 'local_room', + origin_server_ts: new Date().getTime(), + }); + + const roomMembership2Event = new MatrixEvent({ + event_id: `~${roomId}:${client.makeTxnId()}`, + type: EventType.RoomMember, + content: { + displayname: other.name, + membership: "join", + }, + state_key: other.userId, + user_id: other.userId, + sender: other.userId, + room_id: 'local_room', + origin_server_ts: new Date().getTime(), + }); + + const encryptionEvent = new MatrixEvent({ + event_id: `~${roomId}:${client.makeTxnId()}`, + type: "m.room.encryption", + content: { + algorithm: "m.megolm.v1.aes-sha2", + }, + user_id: userId, + sender: userId, + state_key: "", + room_id: 'local_room', + origin_server_ts: new Date().getTime(), + }); + + const localEvents = [ + roomCreateEvent, + encryptionEvent, + roomMembershipEvent, + roomMembership2Event, + ]; + + const localRoom = new LocalRoom( + 'local_room', + client, + userId, + { + pendingEventOrdering: PendingEventOrdering.Detached, + unstableClientRelationAggregation: true, + }, + ); + localRoom.name = other.name; + localRoom.targets = targets; + localRoom.updateMyMembership("join"); + localRoom.addLiveEvents(localEvents); + localRoom.currentState.setStateEvents(localEvents); + + client.store.storeRoom(localRoom); + client.sessionStore.store.setItem('mx_pending_events_local_room', []); +} + +/** + * Start a DM. + * + * @returns {Promise { const targetIds = targets.map(t => t.userId); // Check if there is already a DM with these people and reuse it if possible. @@ -62,7 +162,7 @@ export async function startDm(client: MatrixClient, targets: Member[]): Promise< } else { existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds); } - if (existingRoom) { + if (existingRoom && existingRoom.roomId !== 'local_room') { dis.dispatch({ action: Action.ViewRoom, room_id: existingRoom.roomId, @@ -70,7 +170,7 @@ export async function startDm(client: MatrixClient, targets: Member[]): Promise< joining: false, metricsTrigger: "MessageUser", }); - return; + return Promise.resolve(existingRoom.roomId); } const createRoomOptions = { inlineErrors: true } as any; // XXX: Type out `createRoomOptions` @@ -114,7 +214,7 @@ export async function startDm(client: MatrixClient, targets: Member[]): Promise< ); } - await createRoom(createRoomOptions); + return createRoom(createRoomOptions); } // This is the interface that is expected by various components in the Invite Dialog and RoomInvite.