From 8d6b4f0342b9966a572474d4dec47a5c72218161 Mon Sep 17 00:00:00 2001 From: John Tore Simonsen Date: Thu, 20 Jun 2024 16:16:22 +0200 Subject: [PATCH] Adjustments needed for onboarding-module --- src/MatrixClientPeg.ts | 18 + src/components/structures/MatrixChat.tsx | 31 +- src/components/views/dialogs/InviteDialog.tsx | 504 +++++++++++++++--- src/utils/direct-messages.ts | 5 +- 4 files changed, 483 insertions(+), 75 deletions(-) diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index e0a5bf45a04..a0f2088ccdd 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -151,6 +151,10 @@ export interface IMatrixClientPeg { * see {@link ICreateClientOpts.tokenRefreshFunction} */ replaceUsingCreds(creds: IMatrixClientCreds, tokenRefreshFunction?: TokenRefreshFunction): void; + + /*VERJI Custom Functions */ + getCredentials(): IMatrixClientCreds | undefined; + /*END VERJI Custom Functions*/ } /** @@ -499,6 +503,20 @@ class MatrixClientPegClass implements IMatrixClientPeg { notifTimelineSet.getLiveTimeline().setPaginationToken("", EventTimeline.BACKWARDS); this.matrixClient.setNotifTimelineSet(notifTimelineSet); } + /* VERJI Custom Functions */ + public getCredentials(): IMatrixClientCreds | undefined { + if (!this.matrixClient) return; + + return { + homeserverUrl: this.matrixClient.baseUrl, + identityServerUrl: this.matrixClient.idBaseUrl, + userId: this.matrixClient.credentials.userId ?? "", + deviceId: this.matrixClient.getDeviceId() ?? "", + accessToken: this.matrixClient.getAccessToken() ?? "", + guest: this.matrixClient.isGuest(), + }; + } + /* END VERJI Custom Functions*/ } /** diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 4e177924c85..e35f6ca4b7f 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -32,9 +32,12 @@ import { logger } from "matrix-js-sdk/src/logger"; import { throttle } from "lodash"; import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; - // what-input helps improve keyboard accessibility import "what-input"; +import { + CustomComponentLifecycle, + CustomComponentOpts, +} from "@matrix-org/react-sdk-module-api/lib/lifecycles/CustomComponentLifecycle"; import type NewRecoveryMethodDialog from "../../async-components/views/dialogs/security/NewRecoveryMethodDialog"; import type RecoveryMethodRemovedDialog from "../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog"; @@ -942,6 +945,32 @@ export default class MatrixChat extends React.PureComponent { true, ); break; + // VERJI + case Action.OpenInviteExternalUsersDialog: { + const customOnboardingOpts = { CustomComponent: React.Fragment }; + ModuleRunner.instance.invoke( + CustomComponentLifecycle.Experimental, + customOnboardingOpts as CustomComponentOpts, + ); + const Props = (props: any): React.JSX.Element =>
{props}
; + const customOnboardingDialog = (props: any): React.JSX.Element => ( + + + + ); + console.log("[Verji.Onboarding - Action.OpenInviteExternalUsers]", payload.data); + Modal.createDialog( + customOnboardingDialog, + { + initialText: payload.initialText, + data: payload?.data, + }, + "mx_RoomDirectory_dialogWrapper", + false, + true, + ); + } + // END VERJI } }; diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index bb81d7a05f0..fbfcd8f2668 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -16,7 +16,7 @@ limitations under the License. import React, { createRef, ReactNode, SyntheticEvent } from "react"; import classNames from "classnames"; -import { RoomMember, Room, MatrixError, EventType } from "matrix-js-sdk/src/matrix"; +import { RoomMember, Room } from "matrix-js-sdk/src/matrix"; //VERJI remove: MatrixError, EventType import { KnownMembership } from "matrix-js-sdk/src/types"; import { MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import { logger } from "matrix-js-sdk/src/logger"; @@ -70,37 +70,39 @@ import Modal from "../../../Modal"; import dis from "../../../dispatcher/dispatcher"; import { privateShouldBeEncrypted } from "../../../utils/rooms"; import { NonEmptyArray } from "../../../@types/common"; -import { UNKNOWN_PROFILE_ERRORS } from "../../../utils/MultiInviter"; -import AskInviteAnywayDialog, { UnknownProfiles } from "./AskInviteAnywayDialog"; +// VERJI Remove: import { UNKNOWN_PROFILE_ERRORS } from "../../../utils/MultiInviter"; +// VERJI Remove: import AskInviteAnywayDialog, { UnknownProfiles } from "./AskInviteAnywayDialog"; import { SdkContextClass } from "../../../contexts/SDKContext"; import { UserProfilesStore } from "../../../stores/UserProfilesStore"; +import { Key } from "../../../Keyboard"; // we have a number of types defined from the Matrix spec which can't reasonably be altered here. /* eslint-disable camelcase */ -const extractTargetUnknownProfiles = async ( - targets: Member[], - profilesStores: UserProfilesStore, -): Promise => { - const directoryMembers = targets.filter((t): t is DirectoryMember => t instanceof DirectoryMember); - await Promise.all(directoryMembers.map((t) => profilesStores.getOrFetchProfile(t.userId))); - return directoryMembers.reduce((unknownProfiles: UnknownProfiles, target: DirectoryMember) => { - const lookupError = profilesStores.getProfileLookupError(target.userId); - - if ( - lookupError instanceof MatrixError && - lookupError.errcode && - UNKNOWN_PROFILE_ERRORS.includes(lookupError.errcode) - ) { - unknownProfiles.push({ - userId: target.userId, - errorText: lookupError.data.error || "", - }); - } - - return unknownProfiles; - }, []); -}; +// const extractTargetUnknownProfiles = async ( +// targets: Member[], +// targetEmails: string[], // Verji +// profilesStores: UserProfilesStore, +// ): Promise => { +// const directoryMembers = targets.filter((t): t is DirectoryMember => t instanceof DirectoryMember); +// await Promise.all(directoryMembers.map((t) => profilesStores.getOrFetchProfile(t.userId))); +// return directoryMembers.reduce((unknownProfiles: UnknownProfiles, target: DirectoryMember) => { +// const lookupError = profilesStores.getProfileLookupError(target.userId); + +// if ( +// lookupError instanceof MatrixError && +// lookupError.errcode && +// UNKNOWN_PROFILE_ERRORS.includes(lookupError.errcode) +// ) { +// unknownProfiles.push({ +// userId: target.userId, +// errorText: lookupError.data.error || "", +// }); +// } + +// return unknownProfiles; +// }, []); +// }; interface Result { userId: string; @@ -154,7 +156,72 @@ class DMUserTile extends React.PureComponent { ); } } +/* VERJI START */ +class DMEmailTile extends React.PureComponent { + private onRemove = (e: any): void => { + // Stop the browser from highlighting text + e.preventDefault(); + e.stopPropagation(); + console.log(e); + const asMember: Member = { + name: this.props.email, + userId: this.props.email, + getMxcAvatarUrl: () => { + return ""; + }, + }; + if (this.props.onRemove) { + this.props.onRemove(asMember); + } + }; + + public render(): any { + let closeButton; + if (this.props.onRemove) { + closeButton = ( + + + + + + + + + + + + + + + + + + + ); + } + return ( + + + {this.props?.email ?? ""} + + {closeButton} + + ); + } +} +/* VERJI END */ /** * Converts a RoomMember to a Member. * Returns the Member if it is already a Member. @@ -326,6 +393,7 @@ type Props = InviteDMProps | InviteRoomProps | InviteCallProps; interface IInviteDialogState { targets: Member[]; // array of Member objects (see interface above) + targetEmails: string[]; // Verji filterText: string; recents: Result[]; numRecentsShown: number; @@ -356,6 +424,7 @@ export default class InviteDialog extends React.PureComponent { + // rosberg start + this.setState({ busy: true }); + + let foundUser = false; + await MatrixClientPeg.get() + ?.searchUserDirectory({ term: this.state.filterText.trim().split(":")[0] ?? this.state.filterText }) + .then(async (r) => { + this.setState({ busy: false }); + + if (r.results.find((e) => e.user_id == this.state.filterText.trim())) { + foundUser = true; + } + }); + + const currentUserId: string | undefined = MatrixClientPeg.getCredentials()?.userId.trim(); + + if (currentUserId && currentUserId == this.state.filterText.trim()) { + this.setState({ busy: false }); + return; + } + + if (foundUser == false) { + // Look in other stores for user if search might have failed unexpectedly + const possibleMembers = [ + ...this.state.recents, + ...this.state.suggestions, + ...this.state.serverResultsMixin, + ...this.state.threepidResultsMixin, + ]; + + const toAdd = []; + const potentialAddresses = this.state.filterText + .split(/[\s,]+/) + .map((p) => p.trim()) + .filter((p) => !!p); // filter empty strings + for (const address of potentialAddresses) { + const member = possibleMembers.find((m) => m.userId === address); + if (member) { + toAdd.push(member.user); + continue; + } + } + + if (!toAdd?.length || toAdd?.length == 0) { + return; + } + } // Check to see if there's anything to convert first if (!this.state.filterText || !this.state.filterText.includes("@")) return this.state.targets || []; @@ -536,38 +673,73 @@ export default class InviteDialog extends React.PureComponent { + if (_email == newEmail) { + this.setState({ filterText: text ? this.state.filterText : "" }); + return this.state.targetEmails; + } + }); + const newTargetsToInvite = [...(this.state.targetEmails || []), newEmail]; + + this.setState({ targets: [], targetEmails: newTargetsToInvite, filterText: text ? this.state.filterText : "" }); + + console.log("[Verji.InviteDialog] - Onboarding: " + newTargetsToInvite); + return newTargetsToInvite; + } + private startInviteByEmail = async (): void => { + this.props.onFinished(false); + + let _externals = this.state.targetEmails; + if (_externals == null) _externals = []; + if (Email.looksValid(this.state.filterText)) { + _externals.push(this.state.filterText); + } + dis.dispatch({ + action: Action.OpenInviteExternalUsersDialog, + data: { + externals: _externals, + }, + }); + }; + /* VERJI END */ /** * Check if there are unknown profiles if promptBeforeInviteUnknownUsers setting is enabled. * If so show the "invite anyway?" dialog. Otherwise directly create the DM local room. */ - private checkProfileAndStartDm = async (): Promise => { - this.setBusy(true); - const targets = this.convertFilter(); + // VERJI COMMENT OUT CheckProfileAndStartDM + // private checkProfileAndStartDm = async (): Promise => { + // this.setBusy(true); + // const targets = this.convertFilter(); - if (SettingsStore.getValue("promptBeforeInviteUnknownUsers")) { - const unknownProfileUsers = await extractTargetUnknownProfiles(targets, this.profilesStore); + // if (SettingsStore.getValue("promptBeforeInviteUnknownUsers")) { + // const unknownProfileUsers = await extractTargetUnknownProfiles(targets, this.profilesStore); - if (unknownProfileUsers.length) { - this.showAskInviteAnywayDialog(unknownProfileUsers); - return; - } - } + // if (unknownProfileUsers.length) { + // this.showAskInviteAnywayDialog(unknownProfileUsers); + // return; + // } + // } - await this.startDm(); - }; + // await this.startDm(); + // }; private startDm = async (): Promise => { this.setBusy(true); @@ -591,19 +763,19 @@ export default class InviteDialog extends React.PureComponent this.startDm(), - onGiveUp: () => { - this.setBusy(false); - }, - description: _t("invite|ask_anyway_description"), - inviteNeverWarnLabel: _t("invite|ask_anyway_never_warn_label"), - inviteLabel: _t("invite|ask_anyway_label"), - }); - } + // VERJI COMMENT OUT showAskInviteAnywayDialog + // private showAskInviteAnywayDialog(unknownProfileUsers: { userId: string; errorText: string }[]): void { + // Modal.createDialog(AskInviteAnywayDialog, { + // unknownProfileUsers, + // onInviteAnyways: () => this.startDm(), + // onGiveUp: () => { + // this.setBusy(false); + // }, + // description: _t("invite|ask_anyway_description"), + // inviteNeverWarnLabel: _t("invite|ask_anyway_never_warn_label"), + // inviteLabel: _t("invite|ask_anyway_label"), + // }); + // } private inviteUsers = async (): Promise => { if (this.props.kind !== InviteKind.Invite) return; @@ -662,33 +834,81 @@ export default class InviteDialog extends React.PureComponent): void => { + private onKeyDown = async (e: React.KeyboardEvent | any): Promise => { if (this.state.busy) return; let handled = false; const value = e.currentTarget.value.trim(); const action = getKeyBindingsManager().getAccessibilityAction(e); - + console.log("[Verji.inviteDialog.tsx] - onKeyDown, allowOndboardingFlag: ", this.allowOnboardingFlag); + // VERJI START + if (!this.allowOnboardingFlag) { + if (this.state.busy) return; + const value = e.target.value.trim(); + const hasModifiers = e.ctrlKey || e.shiftKey || e.metaKey; + if (!value && this.state.targets.length > 0 && e.key === Key.BACKSPACE && !hasModifiers) { + // when the field is empty and the user hits backspace remove the right-most target + e.preventDefault(); + this.removeMember(this.state.targets[this.state.targets.length - 1]); + } else if (value && e.key === Key.ENTER && !hasModifiers) { + // when the user hits enter with something in their field try to convert it + e.preventDefault(); + await this.convertFilter(); + } else if (value && e.key === Key.SPACE && !hasModifiers && value.includes("@") && !value.includes(" ")) { + // when the user hits space and their input looks like an e-mail/MXID then try to convert it + e.preventDefault(); + await this.convertFilter(); + } + return; + } + // VERJI END switch (action) { case KeyBindingAction.Backspace: - if (value || this.state.targets.length <= 0) break; - + /* ROSBERG VERJI */ + if (value || (this.state.targets.length <= 0 && this.state.targetEmails?.length <= 0)) break; + if (this.allowOnboardingFlag) { + if (this.state.targetEmails?.length > 0) { + this.removeEmailInvite(this.state.targetEmails[this.state.targetEmails.length - 1]); + return; + } + } + /* VERJI END*/ // when the field is empty and the user hits backspace remove the right-most target this.removeMember(this.state.targets[this.state.targets.length - 1]); handled = true; break; case KeyBindingAction.Space: + /* VERJI START */ + if (this.allowOnboardingFlag) { + if (value && Email.looksValid(value)) { + this.convertFilterOnboarding(); + break; + } else { + this.setState({ targetEmails: [] }); // dont allow combination of members + } + } + /* VERJI END*/ if (!value || !value.includes("@") || value.includes(" ")) break; // when the user hits space and their input looks like an e-mail/MXID then try to convert it - this.convertFilter(); + await this.convertFilter(); // VERJI ADD await handled = true; break; case KeyBindingAction.Enter: if (!value) break; + /* VERJI START */ + if (this.allowOnboardingFlag) { + if (value && Email.looksValid(value)) { + this.convertFilterOnboarding(); + break; + } else { + this.setState({ targetEmails: [] }); // dont allow combination of members + } + } + /* VERJI END*/ // when the user hits enter with something in their field try to convert it - this.convertFilter(); + await this.convertFilter(); // VERJI add await handled = true; break; } @@ -832,6 +1052,11 @@ export default class InviteDialog extends React.PureComponent { + // VERJI START + if (this.allowOnboardingFlag) { + this.setState({ targetEmails: [] }); + } + // VERJI END if (!this.state.busy) { let filterText = this.state.filterText; let targets = this.state.targets.map((t) => t); // cheap clone for mutation @@ -853,6 +1078,22 @@ export default class InviteDialog extends React.PureComponent { + const _email = (email as Member)?.name ?? (email as string); + + const targets = this.state.targetEmails.map((t) => t); // cheap clone for mutation + const idx = targets.indexOf(_email); + if (idx >= 0) { + targets.splice(idx, 1); + this.setState({ targetEmails: targets }); + } + + if (this.editorRef && this.editorRef.current) { + this.editorRef.current.focus(); + } + }; + /* VERJI END */ private removeMember = (member: Member): void => { const targets = this.state.targets.map((t) => t); // cheap clone for mutation const idx = targets.indexOf(member); @@ -874,6 +1115,11 @@ export default class InviteDialog extends React.PureComponent => { + // VERJI START + if (this.allowOnboardingFlag) { + return; + } + // VERJI END if (this.state.filterText) { // if the user has already typed something, just let them // paste normally. @@ -881,6 +1127,26 @@ export default class InviteDialog extends React.PureComponent { + this.setState({ busy: false }); + + if (r.results.find((e) => e.user_id == this.state.filterText.trim())) { + directoryUsers = r.results.map((u) => { + return { + userId: u.user_id, + user: null, + }; + }); + } + }); + + // ROSBERG END const potentialAddresses = this.parseFilter(text); // one search term which is not a mxid or email address if (potentialAddresses.length === 1 && !potentialAddresses[0].includes("@")) { @@ -897,6 +1163,7 @@ export default class InviteDialog extends React.PureComponent m.userId === address); if (member) { + // ROSBERG start + if (member.userId.trim() == MatrixClientPeg.getCredentials()?.userId.trim()) { + failed.push(text); // ROSBERG + continue; + } + //ROSBERG END if (this.canInviteMore([...this.state.targets, ...toAdd])) { toAdd.push(member.user); } else { @@ -919,7 +1198,7 @@ export default class InviteDialog extends React.PureComponent ( - - )); + // const targets = this.state.targets.map((t) => ( + // + // )); + /* ROSBERG START */ + let targets; + if (this.allowOnboardingFlag) { + if (this.state.targetEmails?.length > 0) { + targets = this.state.targetEmails.map((t) => ( + // ROSBERG + )); + } else { + targets = this.state.targets.map((t) => + t?.userId && t?.name ? ( + + ) : null, + ); + } + } else { + targets = this.state.targets.map((t) => + t?.userId && t?.name ? ( + + ) : null, + ); + } + /* ROSBERG END */ const input = ( 0 || (this.state.filterText && this.state.filterText.includes("@")); + //const hasSelection = this.state.targets.length > 0 || (this.state.filterText && this.state.filterText.includes("@")); + // VERJI HACK (&& !Email.looksValid(this.state.filterText)) + let hasSelection = false; + //let emailInvite = false; + if (this.allowOnboardingFlag) { + hasSelection = this.state.targets.length > 0 || this.state.targetEmails?.length > 0; + // emailInvite = (this.state.filterText && this.state.filterText.includes('@') && Email.looksValid(this.state.filterText)); + } else { + hasSelection = this.state.targets.length > 0; + } + // VERJI END const cli = MatrixClientPeg.safeGet(); const userId = cli.getUserId()!; if (this.props.kind === InviteKind.Dm) { @@ -1320,9 +1638,15 @@ export default class InviteDialog extends React.PureComponent 0) { + buttonText = _t("action|go"); + } else { + buttonText = _t("action|go"); + } + /* VERJI END */ buttonText = _t("action|go"); - goButtonFn = this.checkProfileAndStartDm; + goButtonFn = this.startDm; //this.checkProfileAndStartDm; extraSection = (
{_t("invite|suggestions_disclaimer")} @@ -1433,7 +1757,7 @@ export default class InviteDialog extends React.PureComponent ); } + /* VERJI start */ + if (this.allowOnboardingFlag) { + goButton = + this.props.kind == InviteKind.CallTransfer ? null : ( + 0 || Email.looksValid(this.state.filterText)) + ? this.startInviteByEmail + : goButtonFn + } + className="mx_InviteDialog_goButton" + disabled={ + (this.state.busy || !hasSelection) && + this.state.targetEmails?.length <= 0 && + !Email.looksValid(this.state.filterText) + } + > + {buttonText} + + ); + } else { + goButton = + this.props.kind == InviteKind.CallTransfer ? null : ( + + {buttonText} + + ); + } + /* VERJI END */ const usersSection = ( diff --git a/src/utils/direct-messages.ts b/src/utils/direct-messages.ts index 13c028652e8..0f4c4f9134e 100644 --- a/src/utils/direct-messages.ts +++ b/src/utils/direct-messages.ts @@ -179,8 +179,9 @@ export class ThreepidMember extends Member { } export interface IDMUserTileProps { - member: Member; - onRemove?(member: Member): void; + member: Member; // VERJI + email: string; // VERJI + onRemove?(member: Member | string): void; // VERJI } /**