Fix soft crash around inviting invalid MXIDs in start DM on first message flow (#9281)

* Fix soft crash around inviting invalid MXIDs

* Make ts --strict happier

* Prevent suggesting invalid MXIDs

* Add tests

* Fix coverage

* Fix coverage

* Make tsc --strict happier

* Fix test

* Add tests
This commit is contained in:
Michael Telatynski 2022-09-16 09:03:17 +01:00 committed by GitHub
parent 4fec436883
commit 4a23630e06
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 194 additions and 57 deletions

View file

@ -25,7 +25,12 @@ import { Icon as InfoIcon } from "../../../../res/img/element-icons/info.svg";
import { Icon as EmailPillAvatarIcon } from "../../../../res/img/icon-email-pill-avatar.svg";
import { _t, _td } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { makeRoomPermalink, makeUserPermalink } from "../../../utils/permalinks/Permalinks";
import {
getHostnameFromMatrixServerName,
getServerName,
makeRoomPermalink,
makeUserPermalink,
} from "../../../utils/permalinks/Permalinks";
import DMRoomMap from "../../../utils/DMRoomMap";
import SdkConfig from "../../../SdkConfig";
import * as Email from "../../../email";
@ -69,7 +74,7 @@ import {
startDmOnFirstMessage,
ThreepidMember,
} from "../../../utils/direct-messages";
import { AnyInviteKind, KIND_CALL_TRANSFER, KIND_DM, KIND_INVITE } from './InviteDialogTypes';
import { KIND_CALL_TRANSFER, KIND_DM, KIND_INVITE } from './InviteDialogTypes';
import Modal from '../../../Modal';
import dis from "../../../dispatcher/dispatcher";
@ -243,26 +248,37 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
}
}
interface IInviteDialogProps {
interface BaseProps {
// Takes a boolean which is true if a user / users were invited /
// a call transfer was initiated or false if the dialog was cancelled
// with no action taken.
onFinished: (success: boolean) => void;
// The kind of invite being performed. Assumed to be KIND_DM if
// not provided.
kind: AnyInviteKind;
// Initial value to populate the filter with
initialText?: string;
}
interface InviteDMProps extends BaseProps {
// The kind of invite being performed. Assumed to be KIND_DM if not provided.
kind?: typeof KIND_DM;
}
interface InviteRoomProps extends BaseProps {
kind: typeof KIND_INVITE;
// The room ID this dialog is for. Only required for KIND_INVITE.
roomId: string;
}
interface InviteCallProps extends BaseProps {
kind: typeof KIND_CALL_TRANSFER;
// The call to transfer. Only required for KIND_CALL_TRANSFER.
call: MatrixCall;
// Initial value to populate the filter with
initialText: string;
}
type Props = InviteDMProps | InviteRoomProps | InviteCallProps;
interface IInviteDialogState {
targets: Member[]; // array of Member objects (see interface above)
filterText: string;
@ -283,14 +299,13 @@ interface IInviteDialogState {
errorText: string;
}
export default class InviteDialog extends React.PureComponent<IInviteDialogProps, IInviteDialogState> {
export default class InviteDialog extends React.PureComponent<Props, IInviteDialogState> {
static defaultProps = {
kind: KIND_DM,
initialText: "",
};
private closeCopiedTooltip: () => void;
private debounceTimer: number = null; // actually number because we're in the browser
private debounceTimer: number | null = null; // actually number because we're in the browser
private editorRef = createRef<HTMLInputElement>();
private numberEntryFieldRef: React.RefObject<Field> = createRef();
private unmounted = false;
@ -316,7 +331,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
this.state = {
targets: [], // array of Member objects (see interface above)
filterText: this.props.initialText,
filterText: this.props.initialText || "",
recents: InviteDialog.buildRecents(alreadyInvited),
numRecentsShown: INITIAL_ROOMS_SHOWN,
suggestions: this.buildSuggestions(alreadyInvited),
@ -343,9 +358,6 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
componentWillUnmount() {
this.unmounted = true;
// if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close
// the tooltip otherwise, such as pressing Escape or clicking X really quickly
if (this.closeCopiedTooltip) this.closeCopiedTooltip();
}
private onConsultFirstChange = (ev) => {
@ -466,6 +478,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
};
private inviteUsers = async () => {
if (this.props.kind !== KIND_INVITE) return;
this.setState({ busy: true });
this.convertFilter();
const targets = this.convertFilter();
@ -499,6 +512,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
};
private transferCall = async () => {
if (this.props.kind !== KIND_CALL_TRANSFER) return;
if (this.state.currentTabId == TabId.UserDirectory) {
this.convertFilter();
const targets = this.convertFilter();
@ -565,7 +579,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
this.props.onFinished(false);
};
private updateSuggestions = async (term) => {
private updateSuggestions = async (term: string) => {
MatrixClientPeg.get().searchUserDirectory({ term }).then(async r => {
if (term !== this.state.filterText) {
// Discard the results - we were probably too slow on the server-side to make
@ -595,13 +609,17 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
logger.warn("Non-fatal error trying to make an invite for a user ID");
logger.warn(e);
// Add a result anyways, just without a profile. We stick it at the
// top so it is most obviously presented to the user.
r.results.splice(0, 0, {
user_id: term,
display_name: term,
avatar_url: null,
});
// Reuse logic from Permalinks as a basic MXID validity check
const serverName = getServerName(term);
const domain = getHostnameFromMatrixServerName(serverName);
if (domain) {
// Add a result anyways, just without a profile. We stick it at the
// top so it is most obviously presented to the user.
r.results.splice(0, 0, {
user_id: term,
display_name: term,
});
}
}
}
@ -940,7 +958,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
disabled={this.state.busy || (this.props.kind == KIND_CALL_TRANSFER && this.state.targets.length > 0)}
autoComplete="off"
placeholder={hasPlaceholder ? _t("Search") : null}
data-test-id="invite-dialog-input"
data-testid="invite-dialog-input"
/>
);
return (
@ -1107,14 +1125,15 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
</CopyableText>
</div>;
} else if (this.props.kind === KIND_INVITE) {
const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
const roomId = this.props.roomId;
const room = MatrixClientPeg.get()?.getRoom(roomId);
const isSpace = room?.isSpaceRoom();
title = isSpace
? _t("Invite to %(spaceName)s", {
spaceName: room.name || _t("Unnamed Space"),
spaceName: room?.name || _t("Unnamed Space"),
})
: _t("Invite to %(roomName)s", {
roomName: room.name || _t("Unnamed Room"),
roomName: room?.name || _t("Unnamed Room"),
});
let helpTextUntranslated;
@ -1140,14 +1159,14 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
userId: () =>
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{ userId }</a>,
a: (sub) =>
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{ sub }</a>,
<a href={makeRoomPermalink(roomId)} rel="noreferrer noopener" target="_blank">{ sub }</a>,
});
buttonText = _t("Invite");
goButtonFn = this.inviteUsers;
if (cli.isRoomEncrypted(this.props.roomId)) {
const room = cli.getRoom(this.props.roomId);
const room = cli.getRoom(this.props.roomId)!;
const visibilityEvent = room.currentState.getStateEvents(
"m.room.history_visibility", "",
);
@ -1185,8 +1204,6 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
{ _t("Transfer") }
</AccessibleButton>
</div>;
} else {
logger.error("Unknown kind of InviteDialog: " + this.props.kind);
}
const goButton = this.props.kind == KIND_CALL_TRANSFER ? null : <AccessibleButton