diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.tsx similarity index 80% rename from src/components/views/dialogs/AddressPickerDialog.js rename to src/components/views/dialogs/AddressPickerDialog.tsx index 09714e24e3..1802f8ced2 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.tsx @@ -18,7 +18,6 @@ limitations under the License. */ import React, { createRef } from 'react'; -import PropTypes from 'prop-types'; import { sleep } from "matrix-js-sdk/src/utils"; import { _t, _td } from '../../../languageHandler'; @@ -34,6 +33,7 @@ import { abbreviateUrl } from '../../../utils/UrlUtils'; import { Key } from "../../../Keyboard"; import { Action } from "../../../dispatcher/actions"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import AddressSelector from '../elements/AddressSelector'; const TRUNCATE_QUERY_LIST = 40; const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200; @@ -44,27 +44,54 @@ const addressTypeName = { 'email': _td("email address"), }; +interface IProps { + title: string; + description?: JSX.Element; + // Extra node inserted after picker input, dropdown and errors + extraNode?: JSX.Element; + value?: string; + placeholder?: ((validAddressTypes: any) => string) | string; + roomId?: string; + button?: string; + focus?: boolean; + validAddressTypes?; // FIXME: UserAddressType should be an interface + onFinished: (success: boolean, list?) => void; // FIXME: UserAddressType should be an interface + groupId?: string; + // The type of entity to search for. Default: 'user'. + pickerType?: 'user' | 'room'; + // Whether the current user should be included in the addresses returned. Only + // applicable when pickerType is `user`. Default: false. + includeSelf?: boolean; +} + +interface IState { + // Whether to show an error message because of an invalid address + invalidAddressError: boolean; + // List of UserAddressType objects representing + // the list of addresses we're going to invite + selectedList; // FIXME: UserAddressType should be an interface + // Whether a search is ongoing + busy: boolean; + // An error message generated during the user directory search + searchError: string; + // Whether the server supports the user_directory API + serverSupportsUserDirectory: boolean; + // The query being searched for + query: string; + // List of UserAddressType objects representing the set of + // auto-completion results for the current search query. + suggestedList; // FIXME: UserAddressType should be an interface + // List of address types initialised from props, but may change while the + // dialog is open and represents the supported list of address types at this time. + validAddressTypes; +} + @replaceableComponent("views.dialogs.AddressPickerDialog") -export default class AddressPickerDialog extends React.Component { - static propTypes = { - title: PropTypes.string.isRequired, - description: PropTypes.node, - // Extra node inserted after picker input, dropdown and errors - extraNode: PropTypes.node, - value: PropTypes.string, - placeholder: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), - roomId: PropTypes.string, - button: PropTypes.string, - focus: PropTypes.bool, - validAddressTypes: PropTypes.arrayOf(PropTypes.oneOf(addressTypes)), - onFinished: PropTypes.func.isRequired, - groupId: PropTypes.string, - // The type of entity to search for. Default: 'user'. - pickerType: PropTypes.oneOf(['user', 'room']), - // Whether the current user should be included in the addresses returned. Only - // applicable when pickerType is `user`. Default: false. - includeSelf: PropTypes.bool, - }; +export default class AddressPickerDialog extends React.Component { + private textinput = createRef(); + private addressSelector = createRef(); + private queryChangedDebouncer: NodeJS.Timeout; + private cancelThreepidLookup: () => void; static defaultProps = { value: "", @@ -74,11 +101,9 @@ export default class AddressPickerDialog extends React.Component { includeSelf: false, }; - constructor(props) { + constructor(props: IProps) { super(props); - this._textinput = createRef(); - let validAddressTypes = this.props.validAddressTypes; // Remove email from validAddressTypes if no IS is configured. It may be added at a later stage by the user if (!MatrixClientPeg.get().getIdentityServerUrl() && validAddressTypes.includes("email")) { @@ -86,24 +111,13 @@ export default class AddressPickerDialog extends React.Component { } this.state = { - // Whether to show an error message because of an invalid address invalidAddressError: false, - // List of UserAddressType objects representing - // the list of addresses we're going to invite selectedList: [], - // Whether a search is ongoing busy: false, - // An error message generated during the user directory search searchError: null, - // Whether the server supports the user_directory API serverSupportsUserDirectory: true, - // The query being searched for query: "", - // List of UserAddressType objects representing the set of - // auto-completion results for the current search query. suggestedList: [], - // List of address types initialised from props, but may change while the - // dialog is open and represents the supported list of address types at this time. validAddressTypes, }; } @@ -111,11 +125,11 @@ export default class AddressPickerDialog extends React.Component { componentDidMount() { if (this.props.focus) { // Set the cursor at the end of the text input - this._textinput.current.value = this.props.value; + this.textinput.current.value = this.props.value; } } - getPlaceholder() { + private getPlaceholder(): string { const { placeholder } = this.props; if (typeof placeholder === "string") { return placeholder; @@ -124,23 +138,23 @@ export default class AddressPickerDialog extends React.Component { return placeholder(this.state.validAddressTypes); } - onButtonClick = () => { + private onButtonClick = (): void => { let selectedList = this.state.selectedList.slice(); // Check the text input field to see if user has an unconverted address // If there is and it's valid add it to the local selectedList - if (this._textinput.current.value !== '') { - selectedList = this._addAddressesToList([this._textinput.current.value]); + if (this.textinput.current.value !== '') { + selectedList = this.addAddressesToList([this.textinput.current.value]); if (selectedList === null) return; } this.props.onFinished(true, selectedList); }; - onCancel = () => { + private onCancel = (): void => { this.props.onFinished(false); }; - onKeyDown = e => { - const textInput = this._textinput.current ? this._textinput.current.value : undefined; + private onKeyDown = (e: React.KeyboardEvent): void => { + const textInput = this.textinput.current ? this.textinput.current.value : undefined; if (e.key === Key.ESCAPE) { e.stopPropagation(); @@ -149,15 +163,15 @@ export default class AddressPickerDialog extends React.Component { } else if (e.key === Key.ARROW_UP) { e.stopPropagation(); e.preventDefault(); - if (this.addressSelector) this.addressSelector.moveSelectionUp(); + if (this.addressSelector.current) this.addressSelector.current.moveSelectionUp(); } else if (e.key === Key.ARROW_DOWN) { e.stopPropagation(); e.preventDefault(); - if (this.addressSelector) this.addressSelector.moveSelectionDown(); + if (this.addressSelector.current) this.addressSelector.current.moveSelectionDown(); } else if (this.state.suggestedList.length > 0 && [Key.COMMA, Key.ENTER, Key.TAB].includes(e.key)) { e.stopPropagation(); e.preventDefault(); - if (this.addressSelector) this.addressSelector.chooseSelection(); + if (this.addressSelector.current) this.addressSelector.current.chooseSelection(); } else if (textInput.length === 0 && this.state.selectedList.length && e.key === Key.BACKSPACE) { e.stopPropagation(); e.preventDefault(); @@ -169,17 +183,17 @@ export default class AddressPickerDialog extends React.Component { // if there's nothing in the input box, submit the form this.onButtonClick(); } else { - this._addAddressesToList([textInput]); + this.addAddressesToList([textInput]); } } else if (textInput && (e.key === Key.COMMA || e.key === Key.TAB)) { e.stopPropagation(); e.preventDefault(); - this._addAddressesToList([textInput]); + this.addAddressesToList([textInput]); } }; - onQueryChanged = ev => { - const query = ev.target.value; + private onQueryChanged = (ev: React.ChangeEvent): void => { + const query = (ev.target as HTMLTextAreaElement).value; if (this.queryChangedDebouncer) { clearTimeout(this.queryChangedDebouncer); } @@ -188,17 +202,17 @@ export default class AddressPickerDialog extends React.Component { this.queryChangedDebouncer = setTimeout(() => { if (this.props.pickerType === 'user') { if (this.props.groupId) { - this._doNaiveGroupSearch(query); + this.doNaiveGroupSearch(query); } else if (this.state.serverSupportsUserDirectory) { - this._doUserDirectorySearch(query); + this.doUserDirectorySearch(query); } else { - this._doLocalSearch(query); + this.doLocalSearch(query); } } else if (this.props.pickerType === 'room') { if (this.props.groupId) { - this._doNaiveGroupRoomSearch(query); + this.doNaiveGroupRoomSearch(query); } else { - this._doRoomSearch(query); + this.doRoomSearch(query); } } else { console.error('Unknown pickerType', this.props.pickerType); @@ -213,7 +227,7 @@ export default class AddressPickerDialog extends React.Component { } }; - onDismissed = index => () => { + private onDismissed = (index: number) => () => { const selectedList = this.state.selectedList.slice(); selectedList.splice(index, 1); this.setState({ @@ -221,25 +235,21 @@ export default class AddressPickerDialog extends React.Component { suggestedList: [], query: "", }); - if (this._cancelThreepidLookup) this._cancelThreepidLookup(); + if (this.cancelThreepidLookup) this.cancelThreepidLookup(); }; - onClick = index => () => { - this.onSelected(index); - }; - - onSelected = index => { + private onSelected = (index: number): void => { const selectedList = this.state.selectedList.slice(); - selectedList.push(this._getFilteredSuggestions()[index]); + selectedList.push(this.getFilteredSuggestions()[index]); this.setState({ selectedList, suggestedList: [], query: "", }); - if (this._cancelThreepidLookup) this._cancelThreepidLookup(); + if (this.cancelThreepidLookup) this.cancelThreepidLookup(); }; - _doNaiveGroupSearch(query) { + private doNaiveGroupSearch(query: string): void { const lowerCaseQuery = query.toLowerCase(); this.setState({ busy: true, @@ -260,7 +270,7 @@ export default class AddressPickerDialog extends React.Component { display_name: u.displayname, }); }); - this._processResults(results, query); + this.processResults(results, query); }).catch((err) => { console.error('Error whilst searching group rooms: ', err); this.setState({ @@ -273,7 +283,7 @@ export default class AddressPickerDialog extends React.Component { }); } - _doNaiveGroupRoomSearch(query) { + private doNaiveGroupRoomSearch(query: string): void { const lowerCaseQuery = query.toLowerCase(); const results = []; GroupStore.getGroupRooms(this.props.groupId).forEach((r) => { @@ -289,13 +299,13 @@ export default class AddressPickerDialog extends React.Component { name: r.name || r.canonical_alias, }); }); - this._processResults(results, query); + this.processResults(results, query); this.setState({ busy: false, }); } - _doRoomSearch(query) { + private doRoomSearch(query: string): void { const lowerCaseQuery = query.toLowerCase(); const rooms = MatrixClientPeg.get().getRooms(); const results = []; @@ -346,13 +356,13 @@ export default class AddressPickerDialog extends React.Component { return a.rank - b.rank; }); - this._processResults(sortedResults, query); + this.processResults(sortedResults, query); this.setState({ busy: false, }); } - _doUserDirectorySearch(query) { + private doUserDirectorySearch(query: string): void { this.setState({ busy: true, query, @@ -366,7 +376,7 @@ export default class AddressPickerDialog extends React.Component { if (this.state.query !== query) { return; } - this._processResults(resp.results, query); + this.processResults(resp.results, query); }).catch((err) => { console.error('Error whilst searching user directory: ', err); this.setState({ @@ -377,7 +387,7 @@ export default class AddressPickerDialog extends React.Component { serverSupportsUserDirectory: false, }); // Do a local search immediately - this._doLocalSearch(query); + this.doLocalSearch(query); } }).then(() => { this.setState({ @@ -386,7 +396,7 @@ export default class AddressPickerDialog extends React.Component { }); } - _doLocalSearch(query) { + private doLocalSearch(query: string): void { this.setState({ query, searchError: null, @@ -407,10 +417,10 @@ export default class AddressPickerDialog extends React.Component { avatar_url: user.avatarUrl, }); }); - this._processResults(results, query); + this.processResults(results, query); } - _processResults(results, query) { + private processResults(results, query: string): void { const suggestedList = []; results.forEach((result) => { if (result.room_id) { @@ -465,20 +475,20 @@ export default class AddressPickerDialog extends React.Component { address: query, isKnown: false, }); - if (this._cancelThreepidLookup) this._cancelThreepidLookup(); + if (this.cancelThreepidLookup) this.cancelThreepidLookup(); if (addrType === 'email') { - this._lookupThreepid(addrType, query); + this.lookupThreepid(addrType, query); } } this.setState({ suggestedList, invalidAddressError: false, }, () => { - if (this.addressSelector) this.addressSelector.moveSelectionTop(); + if (this.addressSelector.current) this.addressSelector.current.moveSelectionTop(); }); } - _addAddressesToList(addressTexts) { + private addAddressesToList(addressTexts): void { const selectedList = this.state.selectedList.slice(); let hasError = false; @@ -518,17 +528,17 @@ export default class AddressPickerDialog extends React.Component { query: "", invalidAddressError: hasError ? true : this.state.invalidAddressError, }); - if (this._cancelThreepidLookup) this._cancelThreepidLookup(); + if (this.cancelThreepidLookup) this.cancelThreepidLookup(); return hasError ? null : selectedList; } - async _lookupThreepid(medium, address) { + private async lookupThreepid(medium, address): Promise { let cancelled = false; // Note that we can't safely remove this after we're done // because we don't know that it's the same one, so we just // leave it: it's replacing the old one each time so it's // not like they leak. - this._cancelThreepidLookup = function() { + this.cancelThreepidLookup = function() { cancelled = true; }; @@ -570,7 +580,7 @@ export default class AddressPickerDialog extends React.Component { } } - _getFilteredSuggestions() { + private getFilteredSuggestions() { // FIXME: UserAddressType should be an interface // map addressType => set of addresses to avoid O(n*m) operation const selectedAddresses = {}; this.state.selectedList.forEach(({ address, addressType }) => { @@ -584,15 +594,15 @@ export default class AddressPickerDialog extends React.Component { }); } - _onPaste = e => { + private onPaste = (e: React.ClipboardEvent): void => { // Prevent the text being pasted into the textarea e.preventDefault(); const text = e.clipboardData.getData("text"); // Process it as a list of addresses to add instead - this._addAddressesToList(text.split(/[\s,]+/)); + this.addAddressesToList(text.split(/[\s,]+/)); }; - onUseDefaultIdentityServerClick = e => { + private onUseDefaultIdentityServerClick = (e: React.MouseEvent): void => { e.preventDefault(); // Update the IS in account data. Actually using it may trigger terms. @@ -605,7 +615,7 @@ export default class AddressPickerDialog extends React.Component { this.setState({ validAddressTypes }); }; - onManageSettingsClick = e => { + private onManageSettingsClick = (e: React.MouseEvent): void => { e.preventDefault(); dis.fire(Action.ViewUserSettings); this.onCancel(); @@ -615,12 +625,11 @@ export default class AddressPickerDialog extends React.Component { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const AddressSelector = sdk.getComponent("elements.AddressSelector"); - this.scrollElement = null; let inputLabel; if (this.props.description) { inputLabel =
- +
; } @@ -644,10 +653,10 @@ export default class AddressPickerDialog extends React.Component { query.push(