diff --git a/src/@types/svg.d.ts b/src/@types/svg.d.ts new file mode 100644 index 0000000000..96f671c52f --- /dev/null +++ b/src/@types/svg.d.ts @@ -0,0 +1,20 @@ +/* +Copyright 2021 Šimon Brandner + +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. +*/ + +declare module "*.svg" { + const path: string; + export default path; +} diff --git a/src/UserAddress.ts b/src/UserAddress.ts index a2c546deb7..248814aa01 100644 --- a/src/UserAddress.ts +++ b/src/UserAddress.ts @@ -14,35 +14,33 @@ See the License for the specific language governing permissions and limitations under the License. */ -import PropTypes from "prop-types"; - const emailRegex = /^\S+@\S+\.\S+$/; const mxUserIdRegex = /^@\S+:\S+$/; const mxRoomIdRegex = /^!\S+:\S+$/; -export const addressTypes = ['mx-user-id', 'mx-room-id', 'email']; - export enum AddressType { Email = "email", MatrixUserId = "mx-user-id", MatrixRoomId = "mx-room-id", } +export const addressTypes = [AddressType.Email, AddressType.MatrixRoomId, AddressType.MatrixUserId]; + // PropType definition for an object describing // an address that can be invited to a room (which // could be a third party identifier or a matrix ID) // along with some additional information about the // address / target. -export const UserAddressType = PropTypes.shape({ - addressType: PropTypes.oneOf(addressTypes).isRequired, - address: PropTypes.string.isRequired, - displayName: PropTypes.string, - avatarMxc: PropTypes.string, +export interface IUserAddress { + addressType: AddressType; + address: string; + displayName?: string; + avatarMxc?: string; // true if the address is known to be a valid address (eg. is a real // user we've seen) or false otherwise (eg. is just an address the // user has entered) - isKnown: PropTypes.bool, -}); + isKnown?: boolean; +} export function getAddressType(inputText: string): AddressType | null { if (emailRegex.test(inputText)) { diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.tsx similarity index 79% rename from src/components/views/dialogs/AddressPickerDialog.js rename to src/components/views/dialogs/AddressPickerDialog.tsx index 61215c33a1..1a976918bd 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.tsx @@ -18,14 +18,12 @@ 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'; -import * as sdk from '../../../index'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import dis from '../../../dispatcher/dispatcher'; -import { addressTypes, getAddressType } from '../../../UserAddress'; +import { AddressType, addressTypes, getAddressType, IUserAddress } from '../../../UserAddress'; import GroupStore from '../../../stores/GroupStore'; import * as Email from '../../../email'; import IdentityAuthClient from '../../../IdentityAuthClient'; @@ -34,6 +32,10 @@ import { abbreviateUrl } from '../../../utils/UrlUtils'; import { Key } from "../../../Keyboard"; import { Action } from "../../../dispatcher/actions"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import AddressSelector from '../elements/AddressSelector'; +import AddressTile from '../elements/AddressTile'; +import BaseDialog from "./BaseDialog"; +import DialogButtons from "../elements/DialogButtons"; const TRUNCATE_QUERY_LIST = 40; const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200; @@ -44,29 +46,64 @@ const addressTypeName = { 'email': _td("email address"), }; -@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, - }; +interface IResult { + user_id: string; // eslint-disable-line camelcase + room_id?: string; // eslint-disable-line camelcase + name?: string; + display_name?: string; // eslint-disable-line camelcase + avatar_url?: string;// eslint-disable-line camelcase +} - static defaultProps = { +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?: AddressType[]; + onFinished: (success: boolean, list?: IUserAddress[]) => void; + 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: IUserAddress[]; + // 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: IUserAddress[]; + // 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: AddressType[]; +} + +@replaceableComponent("views.dialogs.AddressPickerDialog") +export default class AddressPickerDialog extends React.Component { + private textinput = createRef(); + private addressSelector = createRef(); + private queryChangedDebouncer: number; + private cancelThreepidLookup: () => void; + + static defaultProps: Partial = { value: "", focus: true, validAddressTypes: addressTypes, @@ -74,36 +111,23 @@ 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")) { - validAddressTypes = validAddressTypes.filter(type => type !== "email"); + if (!MatrixClientPeg.get().getIdentityServerUrl() && validAddressTypes.includes(AddressType.Email)) { + validAddressTypes = validAddressTypes.filter(type => type !== AddressType.Email); } 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 +135,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 +148,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 +173,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 +193,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 +212,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 +237,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 +245,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 +280,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 +293,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 +309,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 +366,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 +386,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 +397,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 +406,7 @@ export default class AddressPickerDialog extends React.Component { }); } - _doLocalSearch(query) { + private doLocalSearch(query: string): void { this.setState({ query, searchError: null, @@ -407,10 +427,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: IResult[], query: string): void { const suggestedList = []; results.forEach((result) => { if (result.room_id) { @@ -465,27 +485,27 @@ 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: string[]): IUserAddress[] { const selectedList = this.state.selectedList.slice(); let hasError = false; addressTexts.forEach((addressText) => { addressText = addressText.trim(); const addrType = getAddressType(addressText); - const addrObj = { + const addrObj: IUserAddress = { addressType: addrType, address: addressText, isKnown: false, @@ -504,7 +524,6 @@ export default class AddressPickerDialog extends React.Component { const room = MatrixClientPeg.get().getRoom(addrObj.address); if (room) { addrObj.displayName = room.name; - addrObj.avatarMxc = room.avatarUrl; addrObj.isKnown = true; } } @@ -518,17 +537,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: AddressType, address: string): 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 +589,7 @@ export default class AddressPickerDialog extends React.Component { } } - _getFilteredSuggestions() { + private getFilteredSuggestions(): IUserAddress[] { // map addressType => set of addresses to avoid O(n*m) operation const selectedAddresses = {}; this.state.selectedList.forEach(({ address, addressType }) => { @@ -584,15 +603,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. @@ -601,22 +620,17 @@ export default class AddressPickerDialog extends React.Component { // Add email as a valid address type. const { validAddressTypes } = this.state; - validAddressTypes.push('email'); + validAddressTypes.push(AddressType.Email); this.setState({ validAddressTypes }); }; - onManageSettingsClick = e => { + private onManageSettingsClick = (e: React.MouseEvent): void => { e.preventDefault(); dis.fire(Action.ViewUserSettings); this.onCancel(); }; render() { - 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 =
@@ -627,7 +641,6 @@ export default class AddressPickerDialog extends React.Component { const query = []; // create the invite list if (this.state.selectedList.length > 0) { - const AddressTile = sdk.getComponent("elements.AddressTile"); for (let i = 0; i < this.state.selectedList.length; i++) { query.push( , ); - const filteredSuggestedList = this._getFilteredSuggestions(); + const filteredSuggestedList = this.getFilteredSuggestions(); let error; let addressSelector; @@ -675,7 +688,7 @@ export default class AddressPickerDialog extends React.Component { error =
{ _t("No results") }
; } else { addressSelector = ( - {this.addressSelector = ref;}} + { _t( diff --git a/src/components/views/elements/ActionButton.js b/src/components/views/elements/ActionButton.tsx similarity index 71% rename from src/components/views/elements/ActionButton.js rename to src/components/views/elements/ActionButton.tsx index 9c9e9663e7..390e84be77 100644 --- a/src/components/views/elements/ActionButton.js +++ b/src/components/views/elements/ActionButton.tsx @@ -15,56 +15,62 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import AccessibleButton from './AccessibleButton'; import dis from '../../../dispatcher/dispatcher'; -import * as sdk from '../../../index'; import Analytics from '../../../Analytics'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import Tooltip from './Tooltip'; + +interface IProps { + size?: string; + tooltip?: boolean; + action: string; + mouseOverAction?: string; + label: string; + iconPath?: string; + className?: string; + children?: JSX.Element; +} + +interface IState { + showTooltip: boolean; +} @replaceableComponent("views.elements.ActionButton") -export default class ActionButton extends React.Component { - static propTypes = { - size: PropTypes.string, - tooltip: PropTypes.bool, - action: PropTypes.string.isRequired, - mouseOverAction: PropTypes.string, - label: PropTypes.string.isRequired, - iconPath: PropTypes.string, - className: PropTypes.string, - children: PropTypes.node, - }; - - static defaultProps = { +export default class ActionButton extends React.Component { + static defaultProps: Partial = { size: "25", tooltip: false, }; - state = { - showTooltip: false, - }; + constructor(props: IProps) { + super(props); - _onClick = (ev) => { + this.state = { + showTooltip: false, + }; + } + + private onClick = (ev: React.MouseEvent): void => { ev.stopPropagation(); Analytics.trackEvent('Action Button', 'click', this.props.action); dis.dispatch({ action: this.props.action }); }; - _onMouseEnter = () => { + private onMouseEnter = (): void => { if (this.props.tooltip) this.setState({ showTooltip: true }); if (this.props.mouseOverAction) { dis.dispatch({ action: this.props.mouseOverAction }); } }; - _onMouseLeave = () => { + private onMouseLeave = (): void => { this.setState({ showTooltip: false }); }; render() { let tooltip; if (this.state.showTooltip) { - const Tooltip = sdk.getComponent("elements.Tooltip"); tooltip = ; } @@ -80,9 +86,9 @@ export default class ActionButton extends React.Component { return ( { icon } diff --git a/src/components/views/elements/AddressSelector.js b/src/components/views/elements/AddressSelector.tsx similarity index 68% rename from src/components/views/elements/AddressSelector.js rename to src/components/views/elements/AddressSelector.tsx index b7c9124438..eae82142da 100644 --- a/src/components/views/elements/AddressSelector.js +++ b/src/components/views/elements/AddressSelector.tsx @@ -15,30 +15,37 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; +import React, { createRef } from 'react'; import classNames from 'classnames'; -import { UserAddressType } from '../../../UserAddress'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { IUserAddress } from '../../../UserAddress'; +import AddressTile from './AddressTile'; + +interface IProps { + onSelected: (index: number) => void; + + // List of the addresses to display + addressList: IUserAddress[]; + // Whether to show the address on the address tiles + showAddress?: boolean; + truncateAt: number; + selected?: number; + + // Element to put as a header on top of the list + header?: JSX.Element; +} + +interface IState { + selected: number; + hover: boolean; +} @replaceableComponent("views.elements.AddressSelector") -export default class AddressSelector extends React.Component { - static propTypes = { - onSelected: PropTypes.func.isRequired, +export default class AddressSelector extends React.Component { + private scrollElement = createRef(); + private addressListElement = createRef(); - // List of the addresses to display - addressList: PropTypes.arrayOf(UserAddressType).isRequired, - // Whether to show the address on the address tiles - showAddress: PropTypes.bool, - truncateAt: PropTypes.number.isRequired, - selected: PropTypes.number, - - // Element to put as a header on top of the list - header: PropTypes.node, - }; - - constructor(props) { + constructor(props: IProps) { super(props); this.state = { @@ -48,10 +55,10 @@ export default class AddressSelector extends React.Component { } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - UNSAFE_componentWillReceiveProps(props) { // eslint-disable-line camelcase + UNSAFE_componentWillReceiveProps(props: IProps) { // eslint-disable-line // Make sure the selected item isn't outside the list bounds const selected = this.state.selected; - const maxSelected = this._maxSelected(props.addressList); + const maxSelected = this.maxSelected(props.addressList); if (selected > maxSelected) { this.setState({ selected: maxSelected }); } @@ -60,13 +67,13 @@ export default class AddressSelector extends React.Component { componentDidUpdate() { // As the user scrolls with the arrow keys keep the selected item // at the top of the window. - if (this.scrollElement && this.props.addressList.length > 0 && !this.state.hover) { - const elementHeight = this.addressListElement.getBoundingClientRect().height; - this.scrollElement.scrollTop = (this.state.selected * elementHeight) - elementHeight; + if (this.scrollElement.current && this.props.addressList.length > 0 && !this.state.hover) { + const elementHeight = this.addressListElement.current.getBoundingClientRect().height; + this.scrollElement.current.scrollTop = (this.state.selected * elementHeight) - elementHeight; } } - moveSelectionTop = () => { + public moveSelectionTop = (): void => { if (this.state.selected > 0) { this.setState({ selected: 0, @@ -75,7 +82,7 @@ export default class AddressSelector extends React.Component { } }; - moveSelectionUp = () => { + public moveSelectionUp = (): void => { if (this.state.selected > 0) { this.setState({ selected: this.state.selected - 1, @@ -84,8 +91,8 @@ export default class AddressSelector extends React.Component { } }; - moveSelectionDown = () => { - if (this.state.selected < this._maxSelected(this.props.addressList)) { + public moveSelectionDown = (): void => { + if (this.state.selected < this.maxSelected(this.props.addressList)) { this.setState({ selected: this.state.selected + 1, hover: false, @@ -93,26 +100,26 @@ export default class AddressSelector extends React.Component { } }; - chooseSelection = () => { + public chooseSelection = (): void => { this.selectAddress(this.state.selected); }; - onClick = index => { + private onClick = (index: number): void => { this.selectAddress(index); }; - onMouseEnter = index => { + private onMouseEnter = (index: number): void => { this.setState({ selected: index, hover: true, }); }; - onMouseLeave = () => { + private onMouseLeave = (): void => { this.setState({ hover: false }); }; - selectAddress = index => { + private selectAddress = (index: number): void => { // Only try to select an address if one exists if (this.props.addressList.length !== 0) { this.props.onSelected(index); @@ -120,9 +127,8 @@ export default class AddressSelector extends React.Component { } }; - createAddressListTiles() { - const AddressTile = sdk.getComponent("elements.AddressTile"); - const maxSelected = this._maxSelected(this.props.addressList); + private createAddressListTiles(): JSX.Element[] { + const maxSelected = this.maxSelected(this.props.addressList); const addressList = []; // Only create the address elements if there are address @@ -143,14 +149,12 @@ export default class AddressSelector extends React.Component { onMouseEnter={this.onMouseEnter.bind(this, i)} onMouseLeave={this.onMouseLeave} key={this.props.addressList[i].addressType + "/" + this.props.addressList[i].address} - ref={(ref) => { this.addressListElement = ref; }} + ref={this.addressListElement} >
, ); @@ -159,7 +163,7 @@ export default class AddressSelector extends React.Component { return addressList; } - _maxSelected(list) { + private maxSelected(list: IUserAddress[]): number { const listSize = list.length === 0 ? 0 : list.length - 1; const maxSelected = listSize > (this.props.truncateAt - 1) ? (this.props.truncateAt - 1) : listSize; return maxSelected; @@ -172,7 +176,7 @@ export default class AddressSelector extends React.Component { }); return ( -
{this.scrollElement = ref;}}> +
{ this.props.header } { this.createAddressListTiles() }
diff --git a/src/components/views/elements/AddressTile.js b/src/components/views/elements/AddressTile.tsx similarity index 85% rename from src/components/views/elements/AddressTile.js rename to src/components/views/elements/AddressTile.tsx index ca85d73a11..cdeb7cacd6 100644 --- a/src/components/views/elements/AddressTile.js +++ b/src/components/views/elements/AddressTile.tsx @@ -16,24 +16,25 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import classNames from 'classnames'; -import * as sdk from "../../../index"; import { _t } from '../../../languageHandler'; -import { UserAddressType } from '../../../UserAddress'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromMxc } from "../../../customisations/Media"; +import { IUserAddress } from '../../../UserAddress'; +import BaseAvatar from '../avatars/BaseAvatar'; +import EmailUserIcon from "../../../../res/img/icon-email-user.svg"; + +interface IProps { + address: IUserAddress; + canDismiss?: boolean; + onDismissed?: () => void; + justified?: boolean; + showAddress?: boolean; +} @replaceableComponent("views.elements.AddressTile") -export default class AddressTile extends React.Component { - static propTypes = { - address: UserAddressType.isRequired, - canDismiss: PropTypes.bool, - onDismissed: PropTypes.func, - justified: PropTypes.bool, - }; - - static defaultProps = { +export default class AddressTile extends React.Component { + static defaultProps: Partial = { canDismiss: false, onDismissed: function() {}, // NOP justified: false, @@ -49,11 +50,9 @@ export default class AddressTile extends React.Component { if (isMatrixAddress && address.avatarMxc) { imgUrls.push(mediaFromMxc(address.avatarMxc).getSquareThumbnailHttp(25)); } else if (address.addressType === 'email') { - imgUrls.push(require("../../../../res/img/icon-email-user.svg")); + imgUrls.push(EmailUserIcon); } - const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); - const nameClasses = classNames({ "mx_AddressTile_name": true, "mx_AddressTile_justified": this.props.justified, @@ -70,9 +69,10 @@ export default class AddressTile extends React.Component { info = (
{ name }
- { this.props.showAddress ? -
{ address.address }
: -
+ { + this.props.showAddress + ?
{ address.address }
+ :
}
); diff --git a/src/components/views/elements/AppPermission.js b/src/components/views/elements/AppPermission.tsx similarity index 85% rename from src/components/views/elements/AppPermission.js rename to src/components/views/elements/AppPermission.tsx index a7d249164b..8dc874381a 100644 --- a/src/components/views/elements/AppPermission.js +++ b/src/components/views/elements/AppPermission.tsx @@ -17,30 +17,39 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import url from 'url'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import WidgetUtils from "../../../utils/WidgetUtils"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { RoomMember } from 'matrix-js-sdk/src/models/room-member'; +import MemberAvatar from '../avatars/MemberAvatar'; +import BaseAvatar from '../avatars/BaseAvatar'; +import AccessibleButton from './AccessibleButton'; +import TextWithTooltip from "./TextWithTooltip"; + +interface IProps { + url: string; + creatorUserId: string; + roomId: string; + onPermissionGranted: () => void; + isRoomEncrypted?: boolean; +} + +interface IState { + roomMember: RoomMember; + isWrapped: boolean; + widgetDomain: string; +} @replaceableComponent("views.elements.AppPermission") -export default class AppPermission extends React.Component { - static propTypes = { - url: PropTypes.string.isRequired, - creatorUserId: PropTypes.string.isRequired, - roomId: PropTypes.string.isRequired, - onPermissionGranted: PropTypes.func.isRequired, - isRoomEncrypted: PropTypes.bool, - }; - - static defaultProps = { +export default class AppPermission extends React.Component { + static defaultProps: Partial = { onPermissionGranted: () => {}, }; - constructor(props) { + constructor(props: IProps) { super(props); // The first step is to pick apart the widget so we can render information about it @@ -55,16 +64,18 @@ export default class AppPermission extends React.Component { this.state = { ...urlInfo, roomMember, + isWrapped: null, + widgetDomain: null, }; } - parseWidgetUrl() { + private parseWidgetUrl(): { isWrapped: boolean, widgetDomain: string } { const widgetUrl = url.parse(this.props.url); const params = new URLSearchParams(widgetUrl.search); // HACK: We're relying on the query params when we should be relying on the widget's `data`. // This is a workaround for Scalar. - if (WidgetUtils.isScalarUrl(widgetUrl) && params && params.get('url')) { + if (WidgetUtils.isScalarUrl(this.props.url) && params && params.get('url')) { const unwrappedUrl = url.parse(params.get('url')); return { widgetDomain: unwrappedUrl.host || unwrappedUrl.hostname, @@ -80,10 +91,6 @@ export default class AppPermission extends React.Component { render() { const brand = SdkConfig.get().brand; - const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton"); - const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar"); - const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar"); - const TextWithTooltip = sdk.getComponent("views.elements.TextWithTooltip"); const displayName = this.state.roomMember ? this.state.roomMember.name : this.props.creatorUserId; const userId = displayName === this.props.creatorUserId ? null : this.props.creatorUserId;