diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.tsx similarity index 79% rename from src/components/views/elements/Pill.js rename to src/components/views/elements/Pill.tsx index 95d29fc9ae8..6a25a0ce103 100644 --- a/src/components/views/elements/Pill.js +++ b/src/components/views/elements/Pill.tsx @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ import React from 'react'; +import { MatrixClient } from 'matrix-js-sdk/src/client'; import * as sdk from '../../../index'; import dis from '../../../dispatcher/dispatcher'; import classNames from 'classnames'; import { Room } from 'matrix-js-sdk/src/models/room'; import { RoomMember } from 'matrix-js-sdk/src/models/room-member'; -import PropTypes from 'prop-types'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import FlairStore from "../../../stores/FlairStore"; import { getPrimaryPermalinkEntity, parseAppLocalLink } from "../../../utils/permalinks/Permalinks"; @@ -28,38 +28,67 @@ import { Action } from "../../../dispatcher/actions"; import { mediaFromMxc } from "../../../customisations/Media"; import Tooltip from './Tooltip'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { MatrixEvent } from 'matrix-js-sdk'; + +interface IProps { + // The Type of this Pill. If url is given, this is auto-detected. + type?: string; + // The URL to pillify (no validation is done) + url?: string; + // Whether the pill is in a message + inMessage?: boolean; + // The room in which this pill is being rendered + room?: Room; + // Whether to include an avatar in the pill + shouldShowPillAvatar?: boolean; + // Whether to render this pill as if it were highlit by a selection + isSelected?: boolean; + + yOffset?: number; +} + +interface IState { + // ID/alias of the room/user + resourceId: string; + // Type of pill + pillType: null; + + // The member related to the user pill + member: RoomMember; + // The group related to the group pill + group: any; // TODO: Remove after communities are deprecated + // The room related to the room pill + room: Room; + // Is the user hovering the pill + hover: boolean; +} + +export enum PillType { + UserMention = "TYPE_USER_MENTION", + RoomMention = "TYPE_ROOM_MENTION", + GroupMention = "TYPE_GROUP_MENTION", + AtRoomMention = "TYPE_AT_ROOM_MENTION", // '@room' mention +} @replaceableComponent("views.elements.Pill") -class Pill extends React.Component { - static roomNotifPos(text) { +class Pill extends React.Component { + public static roomNotifPos(text: string): number { return text.indexOf("@room"); } - static roomNotifLen() { + public static roomNotifLen(): number { return "@room".length; } - static TYPE_USER_MENTION = 'TYPE_USER_MENTION'; - static TYPE_ROOM_MENTION = 'TYPE_ROOM_MENTION'; - static TYPE_GROUP_MENTION = 'TYPE_GROUP_MENTION'; - static TYPE_AT_ROOM_MENTION = 'TYPE_AT_ROOM_MENTION'; // '@room' mention - - static propTypes = { - // The Type of this Pill. If url is given, this is auto-detected. - type: PropTypes.string, - // The URL to pillify (no validation is done) - url: PropTypes.string, - // Whether the pill is in a message - inMessage: PropTypes.bool, - // The room in which this pill is being rendered - room: PropTypes.instanceOf(Room), - // Whether to include an avatar in the pill - shouldShowPillAvatar: PropTypes.bool, - // Whether to render this pill as if it were highlit by a selection - isSelected: PropTypes.bool, - }; + public static readonly TYPE_USER_MENTION = PillType.UserMention; + public static readonly TYPE_ROOM_MENTION = PillType.RoomMention; + public static readonly TYPE_GROUP_MENTION = PillType.GroupMention; + public static readonly TYPE_AT_ROOM_MENTION = PillType.AtRoomMention; // '@room' mention + + private unmounted = false; + private matrixClient: MatrixClient = null; - state = { + public state: IState = { // ID/alias of the room/user resourceId: null, // Type of pill @@ -76,8 +105,8 @@ class Pill extends React.Component { }; // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - // eslint-disable-next-line camelcase - async UNSAFE_componentWillReceiveProps(nextProps) { + // eslint-disable-next-line + public async UNSAFE_componentWillReceiveProps(nextProps: IProps): Promise { let resourceId; let prefix; @@ -147,33 +176,33 @@ class Pill extends React.Component { this.setState({ resourceId, pillType, member, group, room }); } - componentDidMount() { - this._unmounted = false; - this._matrixClient = MatrixClientPeg.get(); + public componentDidMount(): void { + this.unmounted = false; + this.matrixClient = MatrixClientPeg.get(); // eslint-disable-next-line new-cap this.UNSAFE_componentWillReceiveProps(this.props); // HACK: We shouldn't be calling lifecycle functions ourselves. } - componentWillUnmount() { - this._unmounted = true; + public componentWillUnmount(): void { + this.unmounted = true; } - onMouseOver = () => { + private onMouseOver = (): void => { this.setState({ hover: true, }); }; - onMouseLeave = () => { + private onMouseLeave = (): void => { this.setState({ hover: false, }); }; - doProfileLookup(userId, member) { + private doProfileLookup(userId: string, member: RoomMember): void { MatrixClientPeg.get().getProfileInfo(userId).then((resp) => { - if (this._unmounted) { + if (this.unmounted) { return; } member.name = resp.displayname; @@ -185,14 +214,14 @@ class Pill extends React.Component { getDirectionalContent: function() { return this.getContent(); }, - }; + } as MatrixEvent; this.setState({ member }); }).catch((err) => { console.error('Could not retrieve profile data for ' + userId + ':', err); }); } - onUserPillClicked = (e) => { + private onUserPillClicked = (e: React.MouseEvent): void => { e.preventDefault(); dis.dispatch({ action: Action.ViewUser, @@ -200,7 +229,7 @@ class Pill extends React.Component { }); }; - render() { + public render(): JSX.Element { const BaseAvatar = sdk.getComponent('views.avatars.BaseAvatar'); const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); const RoomAvatar = sdk.getComponent('avatars.RoomAvatar'); @@ -284,13 +313,12 @@ class Pill extends React.Component { tip = ; } - return + return { this.props.inMessage ? @@ -300,7 +328,6 @@ class Pill extends React.Component { : diff --git a/src/components/views/elements/PowerSelector.js b/src/components/views/elements/PowerSelector.tsx similarity index 69% rename from src/components/views/elements/PowerSelector.js rename to src/components/views/elements/PowerSelector.tsx index 42386ca5c11..a16ba6827b5 100644 --- a/src/components/views/elements/PowerSelector.js +++ b/src/components/views/elements/PowerSelector.tsx @@ -15,34 +15,39 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import * as Roles from '../../../Roles'; import { _t } from '../../../languageHandler'; import Field from "./Field"; import { Key } from "../../../Keyboard"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -@replaceableComponent("views.elements.PowerSelector") -export default class PowerSelector extends React.Component { - static propTypes = { - value: PropTypes.number.isRequired, - // The maximum value that can be set with the power selector - maxValue: PropTypes.number.isRequired, - - // Default user power level for the room - usersDefault: PropTypes.number.isRequired, - - // should the user be able to change the value? false by default. - disabled: PropTypes.bool, - onChange: PropTypes.func, - - // Optional key to pass as the second argument to `onChange` - powerLevelKey: PropTypes.string, +interface IProps { + value: string | number; + // The maximum value that can be set with the power selector + maxValue: number; + // Default user power level for the room + usersDefault: number; + // should the user be able to change the value? false by default. + disabled?: boolean; + onChange?: (value: string | number, powerLevel: string) => void; + // Optional key to pass as the second argument to `onChange` + powerLevelKey?: string; + // The name to annotate the selector with + label?: string; +} - // The name to annotate the selector with - label: PropTypes.string, - } +interface IState { + levelRoleMap: any; // TODO: typings in Roles.ts + // List of power levels to show in the drop-down + options: any[]; // TODO: Typings in Roles.ts + custom?: boolean; + customLevel?: string | number; + customValue: string | number; + selectValue: string | number; +} +@replaceableComponent("views.elements.PowerSelector") +export default class PowerSelector extends React.Component { static defaultProps = { maxValue: Infinity, usersDefault: 0, @@ -62,24 +67,25 @@ export default class PowerSelector extends React.Component { } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - // eslint-disable-next-line camelcase - UNSAFE_componentWillMount() { - this._initStateFromProps(this.props); + // eslint-disable-next-line + public UNSAFE_componentWillMount(): void { + this.initStateFromProps(this.props); } - // eslint-disable-next-line camelcase - UNSAFE_componentWillReceiveProps(newProps) { - this._initStateFromProps(newProps); + // eslint-disable-next-line + public UNSAFE_componentWillReceiveProps(newProps: IProps): void { + this.initStateFromProps(newProps); } - _initStateFromProps(newProps) { + private initStateFromProps(newProps: IProps): void { // This needs to be done now because levelRoleMap has translated strings const levelRoleMap = Roles.levelRoleMap(newProps.usersDefault); const options = Object.keys(levelRoleMap).filter(level => { + const levelNumber = parseInt(level, 10); return ( - level === undefined || - level <= newProps.maxValue || - level == newProps.value + levelNumber === undefined || + levelNumber <= newProps.maxValue || + levelNumber == newProps.value ); }); @@ -94,7 +100,7 @@ export default class PowerSelector extends React.Component { }); } - onSelectChange = event => { + private onSelectChange = (event: React.ChangeEvent): void => { const isCustom = event.target.value === "SELECT_VALUE_CUSTOM"; if (isCustom) { this.setState({ custom: true }); @@ -104,18 +110,19 @@ export default class PowerSelector extends React.Component { } }; - onCustomChange = event => { + private onCustomChange = (event: React.ChangeEvent): void => { this.setState({ customValue: event.target.value }); }; - onCustomBlur = event => { + private onCustomBlur = (event: React.FocusEvent): void => { event.preventDefault(); event.stopPropagation(); - this.props.onChange(parseInt(this.state.customValue), this.props.powerLevelKey); + const value = this.state.customValue.toString(); + this.props.onChange(parseInt(value, 10), this.props.powerLevelKey); }; - onCustomKeyDown = event => { + private onCustomKeyDown = (event: React.KeyboardEvent): void => { if (event.key === Key.ENTER) { event.preventDefault(); event.stopPropagation(); @@ -125,11 +132,11 @@ export default class PowerSelector extends React.Component { // raising a dialog which causes a blur which causes a dialog which causes a blur and // so on. By not causing the onChange to be called here, we avoid the loop because we // handle the onBlur safely. - event.target.blur(); + event.currentTarget.blur(); } }; - render() { + public render(): JSX.Element { let picker; const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label; if (this.state.custom) { @@ -147,7 +154,7 @@ export default class PowerSelector extends React.Component { ); } else { // Each level must have a definition in this.state.levelRoleMap - let options = this.state.options.map((level) => { + let options: any = this.state.options.map((level) => { return { value: level, text: Roles.textualPowerLevel(level, this.props.usersDefault),