diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx index 8d8609d1cf..3c5f99cc7d 100644 --- a/src/components/structures/RoomDirectory.tsx +++ b/src/components/structures/RoomDirectory.tsx @@ -347,7 +347,7 @@ export default class RoomDirectory extends React.Component { }); } - private onRoomClicked = (room: IPublicRoomsChunkRoom, ev: ButtonEvent) => { + private onRoomClicked = (room: IPublicRoomsChunkRoom, ev: React.MouseEvent) => { // If room was shift-clicked, remove it from the room directory if (ev.shiftKey && !this.state.selectedCommunityId) { ev.preventDefault(); diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index baf557ee18..8223c12e77 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -1867,7 +1867,7 @@ export default class RoomView extends React.Component { isRoomEncrypted={this.context.isRoomEncrypted(this.state.room.roomId)} />; } else if (showRoomUpgradeBar) { - aux = ; + aux = ; } else if (myMembership !== "join") { // We do have a room object for this room, but we're not currently in it. // We may have a 3rd party invite to it. @@ -2042,7 +2042,6 @@ export default class RoomView extends React.Component { highlight={this.state.room.getUnreadNotificationCount(NotificationCountType.Highlight) > 0} numUnreadMessages={this.state.numUnreadMessages} onScrollToBottomClick={this.jumpToLiveTimeline} - roomId={this.state.roomId} />); } diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx index 11c24a5981..3c734705b7 100644 --- a/src/components/views/avatars/MemberAvatar.tsx +++ b/src/components/views/avatars/MemberAvatar.tsx @@ -36,6 +36,7 @@ interface IProps extends Omit, "name" | // Whether the onClick of the avatar should be overriden to dispatch `Action.ViewUser` viewUserOnClick?: boolean; title?: string; + style?: any; } interface IState { diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index 0ce9a3a030..75b6890112 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -19,7 +19,7 @@ import React, { ReactHTML } from 'react'; import { Key } from '../../../Keyboard'; import classnames from 'classnames'; -export type ButtonEvent = React.MouseEvent | React.KeyboardEvent; +export type ButtonEvent = React.MouseEvent | React.KeyboardEvent | React.FormEvent; /** * children: React's magic prop. Represents all children given to the element. @@ -39,7 +39,7 @@ interface IProps extends React.InputHTMLAttributes { tabIndex?: number; disabled?: boolean; className?: string; - onClick(e?: ButtonEvent): void; + onClick(e?: ButtonEvent): void | Promise; } interface IAccessibleButtonProps extends React.InputHTMLAttributes { diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 138f5bf9fe..d15f349d62 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -428,7 +428,7 @@ const UserOptionsSection: React.FC<{ let directMessageButton; if (!isMe) { directMessageButton = ( - openDMForUser(cli, member.userId)} className="mx_UserInfo_field"> + { openDMForUser(cli, member.userId); }} className="mx_UserInfo_field"> { _t('Direct message') } ); diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 935a349b10..315241c074 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -243,6 +243,7 @@ interface IProps { // opaque readreceipt info for each userId; used by ReadReceiptMarker // to manage its animations. Should be an empty object when the room // first loads + // TODO: Proper typing for RR info readReceiptMap?: any; // A function which is used to check if the parent panel is being diff --git a/src/components/views/rooms/JumpToBottomButton.js b/src/components/views/rooms/JumpToBottomButton.tsx similarity index 83% rename from src/components/views/rooms/JumpToBottomButton.js rename to src/components/views/rooms/JumpToBottomButton.tsx index d2e2a391a6..0b680d093d 100644 --- a/src/components/views/rooms/JumpToBottomButton.js +++ b/src/components/views/rooms/JumpToBottomButton.tsx @@ -14,11 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React from "react"; import { _t } from '../../../languageHandler'; import AccessibleButton from '../elements/AccessibleButton'; import classNames from 'classnames'; -export default (props) => { +interface IProps { + numUnreadMessages: number; + highlight: boolean; + onScrollToBottomClick: (e: React.MouseEvent) => void; +} + +const JumpToBottomButton: React.FC = (props) => { const className = classNames({ 'mx_JumpToBottomButton': true, 'mx_JumpToBottomButton_highlight': props.highlight, @@ -36,3 +43,5 @@ export default (props) => { { badge } ); }; + +export default JumpToBottomButton; diff --git a/src/components/views/rooms/ReadReceiptMarker.js b/src/components/views/rooms/ReadReceiptMarker.tsx similarity index 71% rename from src/components/views/rooms/ReadReceiptMarker.js rename to src/components/views/rooms/ReadReceiptMarker.tsx index c9688b4d29..cfc535b23d 100644 --- a/src/components/views/rooms/ReadReceiptMarker.js +++ b/src/components/views/rooms/ReadReceiptMarker.tsx @@ -15,62 +15,75 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef } from 'react'; -import PropTypes from 'prop-types'; +import React, { createRef, RefObject } from 'react'; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; + import { _t } from '../../../languageHandler'; import { formatDate } from '../../../DateUtils'; import NodeAnimator from "../../../NodeAnimator"; -import * as sdk from "../../../index"; import { toPx } from "../../../utils/units"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import MemberAvatar from '../avatars/MemberAvatar'; + +interface IProps { + // the RoomMember to show the RR for + member?: RoomMember; + // userId to fallback the avatar to + // if the member hasn't been loaded yet + fallbackUserId: string; + + // number of pixels to offset the avatar from the right of its parent; + // typically a negative value. + leftOffset?: number; + + // true to hide the avatar (it will still be animated) + hidden?: boolean; + + // don't animate this RR into position + suppressAnimation?: boolean; + + // an opaque object for storing information about this user's RR in + // this room + // TODO: proper typing for RR info + readReceiptInfo: any; + + // A function which is used to check if the parent panel is being + // unmounted, to avoid unnecessary work. Should return true if we + // are being unmounted. + checkUnmounting?: () => boolean; + + // callback for clicks on this RR + onClick?: (e: React.MouseEvent) => void; + + // Timestamp when the receipt was read + timestamp?: number; + + // True to show twelve hour format, false otherwise + showTwelveHour?: boolean; +} + +interface IState { + suppressDisplay: boolean; + startStyles?: IReadReceiptMarkerStyle[]; +} + +interface IReadReceiptMarkerStyle { + top: number; + left: number; +} + @replaceableComponent("views.rooms.ReadReceiptMarker") -export default class ReadReceiptMarker extends React.PureComponent { - static propTypes = { - // the RoomMember to show the RR for - member: PropTypes.object, - // userId to fallback the avatar to - // if the member hasn't been loaded yet - fallbackUserId: PropTypes.string.isRequired, - - // number of pixels to offset the avatar from the right of its parent; - // typically a negative value. - leftOffset: PropTypes.number, - - // true to hide the avatar (it will still be animated) - hidden: PropTypes.bool, - - // don't animate this RR into position - suppressAnimation: PropTypes.bool, - - // an opaque object for storing information about this user's RR in - // this room - readReceiptInfo: PropTypes.object, - - // A function which is used to check if the parent panel is being - // unmounted, to avoid unnecessary work. Should return true if we - // are being unmounted. - checkUnmounting: PropTypes.func, - - // callback for clicks on this RR - onClick: PropTypes.func, - - // Timestamp when the receipt was read - timestamp: PropTypes.number, - - // True to show twelve hour format, false otherwise - showTwelveHour: PropTypes.bool, - }; +export default class ReadReceiptMarker extends React.PureComponent { + private avatar: React.RefObject = createRef(); static defaultProps = { leftOffset: 0, }; - constructor(props) { + constructor(props: IProps) { super(props); - this._avatar = createRef(); - this.state = { // if we are going to animate the RR, we don't show it on first render, // and instead just add a placeholder to the DOM; once we've been @@ -80,7 +93,7 @@ export default class ReadReceiptMarker extends React.PureComponent { }; } - componentWillUnmount() { + public componentWillUnmount(): void { // before we remove the rr, store its location in the map, so that if // it reappears, it can be animated from the right place. const rrInfo = this.props.readReceiptInfo; @@ -95,29 +108,29 @@ export default class ReadReceiptMarker extends React.PureComponent { return; } - const avatarNode = this._avatar.current; + const avatarNode = this.avatar.current; rrInfo.top = avatarNode.offsetTop; rrInfo.left = avatarNode.offsetLeft; rrInfo.parent = avatarNode.offsetParent; } - componentDidMount() { + public componentDidMount(): void { if (!this.state.suppressDisplay) { // we've already done our display - nothing more to do. return; } - this._animateMarker(); + this.animateMarker(); } - componentDidUpdate(prevProps) { + public componentDidUpdate(prevProps: IProps): void { const differentLeftOffset = prevProps.leftOffset !== this.props.leftOffset; const visibilityChanged = prevProps.hidden !== this.props.hidden; if (differentLeftOffset || visibilityChanged) { - this._animateMarker(); + this.animateMarker(); } } - _animateMarker() { + private animateMarker(): void { // treat new RRs as though they were off the top of the screen let oldTop = -15; @@ -126,7 +139,7 @@ export default class ReadReceiptMarker extends React.PureComponent { oldTop = oldInfo.top + oldInfo.parent.getBoundingClientRect().top; } - const newElement = this._avatar.current; + const newElement = this.avatar.current; let startTopOffset; if (!newElement.offsetParent) { // this seems to happen sometimes for reasons I don't understand @@ -156,10 +169,9 @@ export default class ReadReceiptMarker extends React.PureComponent { }); } - render() { - const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); + public render(): JSX.Element { if (this.state.suppressDisplay) { - return
; + return
} />; } const style = { @@ -198,7 +210,7 @@ export default class ReadReceiptMarker extends React.PureComponent { style={style} title={title} onClick={this.props.onClick} - inputRef={this._avatar} + inputRef={this.avatar as RefObject} /> ); diff --git a/src/components/views/rooms/RoomDetailList.js b/src/components/views/rooms/RoomDetailList.tsx similarity index 76% rename from src/components/views/rooms/RoomDetailList.js rename to src/components/views/rooms/RoomDetailList.tsx index bf2f5418c9..869ab9e8f3 100644 --- a/src/components/views/rooms/RoomDetailList.js +++ b/src/components/views/rooms/RoomDetailList.tsx @@ -14,41 +14,38 @@ See the License for the specific language governing permissions and limitations under the License. */ -import * as sdk from '../../../index'; -import dis from '../../../dispatcher/dispatcher'; import React from 'react'; -import { _t } from '../../../languageHandler'; -import PropTypes from 'prop-types'; +import { Room } from 'matrix-js-sdk/src'; import classNames from 'classnames'; +import dis from '../../../dispatcher/dispatcher'; +import { _t } from '../../../languageHandler'; -import { roomShape } from './RoomDetailRow'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import RoomDetailRow from "./RoomDetailRow"; + +interface IProps { + rooms?: Room[]; + className?: string; +} @replaceableComponent("views.rooms.RoomDetailList") -export default class RoomDetailList extends React.Component { - static propTypes = { - rooms: PropTypes.arrayOf(roomShape), - className: PropTypes.string, - }; - - getRows() { +export default class RoomDetailList extends React.Component { + private getRows(): JSX.Element[] { if (!this.props.rooms) return []; - - const RoomDetailRow = sdk.getComponent('rooms.RoomDetailRow'); return this.props.rooms.map((room, index) => { return ; }); } - onDetailsClick = (ev, room) => { + private onDetailsClick = (ev: React.MouseEvent, room: Room): void => { dis.dispatch({ action: 'view_room', room_id: room.roomId, - room_alias: room.canonicalAlias || (room.aliases || [])[0], + room_alias: room.getCanonicalAlias() || (room.getAltAliases() || [])[0], }); }; - render() { + public render(): JSX.Element { const rows = this.getRows(); let rooms; if (rows.length === 0) { diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index 15b25ed64b..d0e438bcda 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -195,7 +195,7 @@ export default class RoomHeader extends React.Component { videoCallButton = ev.shiftKey ? + onClick={(ev: React.MouseEvent) => ev.shiftKey ? this.displayInfoDialogAboutScreensharing() : this.props.onCallPlaced(PlaceCallType.Video)} title={_t("Video call")} />; } diff --git a/src/components/views/rooms/RoomUpgradeWarningBar.js b/src/components/views/rooms/RoomUpgradeWarningBar.tsx similarity index 83% rename from src/components/views/rooms/RoomUpgradeWarningBar.js rename to src/components/views/rooms/RoomUpgradeWarningBar.tsx index 384845cdf9..eb334ab825 100644 --- a/src/components/views/rooms/RoomUpgradeWarningBar.js +++ b/src/components/views/rooms/RoomUpgradeWarningBar.tsx @@ -1,5 +1,5 @@ /* -Copyright 2018-2020 New Vector Ltd +Copyright 2018-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,41 +15,43 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; +import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import { RoomState } from 'matrix-js-sdk/src/models/room-state'; + import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import RoomUpgradeDialog from '../dialogs/RoomUpgradeDialog'; +import AccessibleButton from '../elements/AccessibleButton'; + +interface IProps { + room: Room; +} + +interface IState { + upgraded?: boolean; +} @replaceableComponent("views.rooms.RoomUpgradeWarningBar") -export default class RoomUpgradeWarningBar extends React.PureComponent { - static propTypes = { - room: PropTypes.object.isRequired, - recommendation: PropTypes.object.isRequired, - }; - - constructor(props) { - super(props); - this.state = {}; - } - - componentDidMount() { +export default class RoomUpgradeWarningBar extends React.PureComponent { + public componentDidMount(): void { const tombstone = this.props.room.currentState.getStateEvents("m.room.tombstone", ""); this.setState({ upgraded: tombstone && tombstone.getContent().replacement_room }); - MatrixClientPeg.get().on("RoomState.events", this._onStateEvents); + MatrixClientPeg.get().on("RoomState.events", this.onStateEvents); } - componentWillUnmount() { + public componentWillUnmount(): void { const cli = MatrixClientPeg.get(); if (cli) { - cli.removeListener("RoomState.events", this._onStateEvents); + cli.removeListener("RoomState.events", this.onStateEvents); } } - _onStateEvents = (event, state) => { + private onStateEvents = (event: MatrixEvent, state: RoomState): void => { if (!this.props.room || event.getRoomId() !== this.props.room.roomId) { return; } @@ -60,14 +62,11 @@ export default class RoomUpgradeWarningBar extends React.PureComponent { this.setState({ upgraded: tombstone && tombstone.getContent().replacement_room }); }; - onUpgradeClick = () => { - const RoomUpgradeDialog = sdk.getComponent('dialogs.RoomUpgradeDialog'); + private onUpgradeClick = (): void => { Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, { room: this.props.room }); }; - render() { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - + public render(): JSX.Element { let doUpgradeWarnings = (
diff --git a/src/components/views/rooms/SimpleRoomHeader.js b/src/components/views/rooms/SimpleRoomHeader.tsx similarity index 82% rename from src/components/views/rooms/SimpleRoomHeader.js rename to src/components/views/rooms/SimpleRoomHeader.tsx index a2b5566e39..d6effaceb4 100644 --- a/src/components/views/rooms/SimpleRoomHeader.js +++ b/src/components/views/rooms/SimpleRoomHeader.tsx @@ -1,5 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd +Copyright 2016-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,23 +15,21 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +interface IProps { + title?: string; + // `src` to an image. Optional. + icon?: string; +} + /* * A stripped-down room header used for things like the user settings * and room directory. */ @replaceableComponent("views.rooms.SimpleRoomHeader") -export default class SimpleRoomHeader extends React.Component { - static propTypes = { - title: PropTypes.string, - - // `src` to an image. Optional. - icon: PropTypes.string, - }; - - render() { +export default class SimpleRoomHeader extends React.PureComponent { + public render(): JSX.Element { let icon; if (this.props.icon) { icon = void; + onCloseClick?: (e: React.MouseEvent) => void; +} - render() { +@replaceableComponent("views.rooms.TopUnreadMessagesBar") +export default class TopUnreadMessagesBar extends React.PureComponent { + public render(): JSX.Element { return (
{ +interface IProps { + avatarUrl?: string; + avatarName: string; // name of user/room the avatar belongs to + uploadAvatar?: (e: React.MouseEvent) => void; + removeAvatar?: (e: React.MouseEvent) => void; + avatarAltText: string; +} + +const AvatarSetting: React.FC = ({ avatarUrl, avatarAltText, avatarName, uploadAvatar, removeAvatar }) => { const [isHovering, setIsHovering] = useState(false); const hoveringProps = { onMouseEnter: () => setIsHovering(true), @@ -78,12 +85,4 @@ const AvatarSetting = ({ avatarUrl, avatarAltText, avatarName, uploadAvatar, rem
; }; -AvatarSetting.propTypes = { - avatarUrl: PropTypes.string, - avatarName: PropTypes.string.isRequired, // name of user/room the avatar belongs to - uploadAvatar: PropTypes.func, - removeAvatar: PropTypes.func, - avatarAltText: PropTypes.string.isRequired, -}; - export default AvatarSetting; diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.tsx similarity index 71% rename from src/components/views/settings/ChangeAvatar.js rename to src/components/views/settings/ChangeAvatar.tsx index c3a1544cdc..36178540f7 100644 --- a/src/components/views/settings/ChangeAvatar.js +++ b/src/components/views/settings/ChangeAvatar.tsx @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd +Copyright 2015-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,54 +15,65 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; +import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; +import { Room } from 'matrix-js-sdk/src/models/room'; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import Spinner from '../elements/Spinner'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromMxc } from "../../../customisations/Media"; +import RoomAvatar from '../avatars/RoomAvatar'; +import BaseAvatar from '../avatars/BaseAvatar'; + +interface IProps { + initialAvatarUrl?: string; + room?: Room; + // if false, you need to call changeAvatar.onFileSelected yourself. + showUploadSection?: boolean; + width?: number; + height?: number; + className?: string; +} + +interface IState { + avatarUrl?: string; + errorText?: string; + phase?: Phases; +} + +enum Phases { + Display = "display", + Uploading = "uploading", + Error = "error", +} @replaceableComponent("views.settings.ChangeAvatar") -export default class ChangeAvatar extends React.Component { - static propTypes = { - initialAvatarUrl: PropTypes.string, - room: PropTypes.object, - // if false, you need to call changeAvatar.onFileSelected yourself. - showUploadSection: PropTypes.bool, - width: PropTypes.number, - height: PropTypes.number, - className: PropTypes.string, - }; - - static Phases = { - Display: "display", - Uploading: "uploading", - Error: "error", - }; - - static defaultProps = { +export default class ChangeAvatar extends React.Component { + public static defaultProps = { showUploadSection: true, className: "", width: 80, height: 80, }; - constructor(props) { + private avatarSet = false; + + constructor(props: IProps) { super(props); this.state = { avatarUrl: this.props.initialAvatarUrl, - phase: ChangeAvatar.Phases.Display, + phase: Phases.Display, }; } - componentDidMount() { + public componentDidMount(): void { MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents); } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase + // eslint-disable-next-line + public UNSAFE_componentWillReceiveProps(newProps: IProps): void { if (this.avatarSet) { // don't clobber what the user has just set return; @@ -72,13 +83,13 @@ export default class ChangeAvatar extends React.Component { }); } - componentWillUnmount() { + public componentWillUnmount(): void { if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents); } } - onRoomStateEvents = (ev) => { + private onRoomStateEvents = (ev: MatrixEvent) => { if (!this.props.room) { return; } @@ -94,18 +105,17 @@ export default class ChangeAvatar extends React.Component { } }; - setAvatarFromFile(file) { + private setAvatarFromFile(file: File): Promise<{}> { let newUrl = null; this.setState({ - phase: ChangeAvatar.Phases.Uploading, + phase: Phases.Uploading, }); - const self = this; - const httpPromise = MatrixClientPeg.get().uploadContent(file).then(function(url) { + const httpPromise = MatrixClientPeg.get().uploadContent(file).then((url) => { newUrl = url; - if (self.props.room) { + if (this.props.room) { return MatrixClientPeg.get().sendStateEvent( - self.props.room.roomId, + this.props.room.roomId, 'm.room.avatar', { url: url }, '', @@ -115,38 +125,37 @@ export default class ChangeAvatar extends React.Component { } }); - httpPromise.then(function() { - self.setState({ - phase: ChangeAvatar.Phases.Display, + httpPromise.then(() => { + this.setState({ + phase: Phases.Display, avatarUrl: mediaFromMxc(newUrl).srcHttp, }); - }, function(error) { - self.setState({ - phase: ChangeAvatar.Phases.Error, + }, () => { + this.setState({ + phase: Phases.Error, }); - self.onError(error); + this.onError(); }); return httpPromise; } - onFileSelected = (ev) => { + private onFileSelected = (ev: React.ChangeEvent) => { this.avatarSet = true; return this.setAvatarFromFile(ev.target.files[0]); }; - onError = (error) => { + private onError = (): void => { this.setState({ errorText: _t("Failed to upload profile picture!"), }); }; - render() { + public render(): JSX.Element { let avatarImg; // Having just set an avatar we just display that since it will take a little // time to propagate through to the RoomAvatar. if (this.props.room && !this.avatarSet) { - const RoomAvatar = sdk.getComponent('avatars.RoomAvatar'); avatarImg = ; } else { - const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); // XXX: FIXME: once we track in the JS what our own displayname is(!) then use it here rather than ? avatarImg =
@@ -188,7 +196,7 @@ export default class ChangeAvatar extends React.Component { { uploadSection }
); - case ChangeAvatar.Phases.Uploading: + case Phases.Uploading: return ( ); diff --git a/src/components/views/settings/ChangeDisplayName.js b/src/components/views/settings/ChangeDisplayName.tsx similarity index 73% rename from src/components/views/settings/ChangeDisplayName.js rename to src/components/views/settings/ChangeDisplayName.tsx index 2f336e18c6..9f0f813ec6 100644 --- a/src/components/views/settings/ChangeDisplayName.js +++ b/src/components/views/settings/ChangeDisplayName.tsx @@ -1,7 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2018 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2015 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,14 +15,14 @@ limitations under the License. */ import React from 'react'; -import * as sdk from '../../../index'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import EditableTextContainer from "../elements/EditableTextContainer"; @replaceableComponent("views.settings.ChangeDisplayName") export default class ChangeDisplayName extends React.Component { - _getDisplayName = async () => { + private getDisplayName = async (): Promise => { const cli = MatrixClientPeg.get(); try { const res = await cli.getProfileInfo(cli.getUserId()); @@ -34,21 +32,20 @@ export default class ChangeDisplayName extends React.Component { } }; - _changeDisplayName = (newDisplayname) => { + private changeDisplayName = (newDisplayname: string): Promise<{}> => { const cli = MatrixClientPeg.get(); - return cli.setDisplayName(newDisplayname).catch(function(e) { - throw new Error("Failed to set display name", e); + return cli.setDisplayName(newDisplayname).catch(function() { + throw new Error("Failed to set display name"); }); }; - render() { - const EditableTextContainer = sdk.getComponent('elements.EditableTextContainer'); + public render(): JSX.Element { return ( + onSubmit={this.changeDisplayName} /> ); } } diff --git a/src/components/views/settings/DevicesPanel.js b/src/components/views/settings/DevicesPanel.tsx similarity index 76% rename from src/components/views/settings/DevicesPanel.js rename to src/components/views/settings/DevicesPanel.tsx index 0f052332ee..5e297bbea6 100644 --- a/src/components/views/settings/DevicesPanel.js +++ b/src/components/views/settings/DevicesPanel.tsx @@ -1,6 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2016 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,52 +15,50 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import classNames from 'classnames'; +import { IMyDevice } from "matrix-js-sdk/src/client"; -import * as sdk from '../../../index'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; import { SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import InteractiveAuthDialog from "../dialogs/InteractiveAuthDialog"; +import DevicesPanelEntry from "./DevicesPanelEntry"; +import Spinner from "../elements/Spinner"; +import AccessibleButton from "../elements/AccessibleButton"; + +interface IProps { + className?: string; +} + +interface IState { + devices: IMyDevice[]; + deviceLoadError?: string; + selectedDevices?: string[]; + deleting?: boolean; +} @replaceableComponent("views.settings.DevicesPanel") -export default class DevicesPanel extends React.Component { - constructor(props) { - super(props); +export default class DevicesPanel extends React.Component { + private unmounted = false; - this.state = { - devices: undefined, - deviceLoadError: undefined, - - selectedDevices: [], - deleting: false, - }; - - this._unmounted = false; - - this._renderDevice = this._renderDevice.bind(this); - this._onDeviceSelectionToggled = this._onDeviceSelectionToggled.bind(this); - this._onDeleteClick = this._onDeleteClick.bind(this); + public componentDidMount(): void { + this.loadDevices(); } - componentDidMount() { - this._loadDevices(); + public componentWillUnmount(): void { + this.unmounted = true; } - componentWillUnmount() { - this._unmounted = true; - } - - _loadDevices() { + private loadDevices(): void { MatrixClientPeg.get().getDevices().then( (resp) => { - if (this._unmounted) { return; } + if (this.unmounted) { return; } this.setState({ devices: resp.devices || [] }); }, (error) => { - if (this._unmounted) { return; } + if (this.unmounted) { return; } let errtxt; if (error.httpStatus == 404) { // 404 probably means the HS doesn't yet support the API. @@ -79,7 +76,7 @@ export default class DevicesPanel extends React.Component { * compare two devices, sorting from most-recently-seen to least-recently-seen * (and then, for stability, by device id) */ - _deviceCompare(a, b) { + private deviceCompare(a: IMyDevice, b: IMyDevice): number { // return < 0 if a comes before b, > 0 if a comes after b. const lastSeenDelta = (b.last_seen_ts || 0) - (a.last_seen_ts || 0); @@ -91,8 +88,8 @@ export default class DevicesPanel extends React.Component { return (idA < idB) ? -1 : (idA > idB) ? 1 : 0; } - _onDeviceSelectionToggled(device) { - if (this._unmounted) { return; } + private onDeviceSelectionToggled = (device: IMyDevice): void => { + if (this.unmounted) { return; } const deviceId = device.device_id; this.setState((state, props) => { @@ -108,22 +105,21 @@ export default class DevicesPanel extends React.Component { return { selectedDevices }; }); - } + }; - _onDeleteClick() { + private onDeleteClick = (): void => { this.setState({ deleting: true, }); - this._makeDeleteRequest(null).catch((error) => { - if (this._unmounted) { return; } + this.makeDeleteRequest(null).catch((error) => { + if (this.unmounted) { return; } if (error.httpStatus !== 401 || !error.data || !error.data.flows) { // doesn't look like an interactive-auth failure throw error; } // pop up an interactive auth dialog - const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); const numDevices = this.state.selectedDevices.length; const dialogAesthetics = { @@ -148,7 +144,7 @@ export default class DevicesPanel extends React.Component { title: _t("Authentication"), matrixClient: MatrixClientPeg.get(), authData: error.data, - makeRequest: this._makeDeleteRequest.bind(this), + makeRequest: this.makeDeleteRequest.bind(this), aestheticsForStagePhases: { [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics, [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics, @@ -156,15 +152,16 @@ export default class DevicesPanel extends React.Component { }); }).catch((e) => { console.error("Error deleting sessions", e); - if (this._unmounted) { return; } + if (this.unmounted) { return; } }).finally(() => { this.setState({ deleting: false, }); }); - } + }; - _makeDeleteRequest(auth) { + // TODO: proper typing for auth + private makeDeleteRequest(auth?: any): Promise { return MatrixClientPeg.get().deleteMultipleDevices(this.state.selectedDevices, auth).then( () => { // Remove the deleted devices from `devices`, reset selection to [] @@ -178,20 +175,16 @@ export default class DevicesPanel extends React.Component { ); } - _renderDevice(device) { - const DevicesPanelEntry = sdk.getComponent('settings.DevicesPanelEntry'); + private renderDevice = (device: IMyDevice): JSX.Element => { return ; - } - - render() { - const Spinner = sdk.getComponent("elements.Spinner"); - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); + }; + public render(): JSX.Element { if (this.state.deviceLoadError !== undefined) { const classes = classNames(this.props.className, "error"); return ( @@ -204,15 +197,14 @@ export default class DevicesPanel extends React.Component { const devices = this.state.devices; if (devices === undefined) { // still loading - const classes = this.props.className; - return ; + return ; } - devices.sort(this._deviceCompare); + devices.sort(this.deviceCompare); const deleteButton = this.state.deleting ? : - + { _t("Delete %(count)s sessions", { count: this.state.selectedDevices.length }) } ; @@ -227,12 +219,8 @@ export default class DevicesPanel extends React.Component { { this.state.selectedDevices.length > 0 ? deleteButton : null }
- { devices.map(this._renderDevice) } + { devices.map(this.renderDevice) }
); } } - -DevicesPanel.propTypes = { - className: PropTypes.string, -}; diff --git a/src/components/views/settings/DevicesPanelEntry.js b/src/components/views/settings/DevicesPanelEntry.tsx similarity index 74% rename from src/components/views/settings/DevicesPanelEntry.js rename to src/components/views/settings/DevicesPanelEntry.tsx index a5b674b8f6..d033bc41a9 100644 --- a/src/components/views/settings/DevicesPanelEntry.js +++ b/src/components/views/settings/DevicesPanelEntry.tsx @@ -1,5 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd +Copyright 2016 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,30 +15,28 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; +import { IMyDevice } from 'matrix-js-sdk/src/client'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { formatDate } from '../../../DateUtils'; import StyledCheckbox from '../elements/StyledCheckbox'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import EditableTextContainer from "../elements/EditableTextContainer"; + +interface IProps { + device?: IMyDevice; + onDeviceToggled?: (device: IMyDevice) => void; + selected?: boolean; +} @replaceableComponent("views.settings.DevicesPanelEntry") -export default class DevicesPanelEntry extends React.Component { - constructor(props) { - super(props); +export default class DevicesPanelEntry extends React.Component { + public static defaultProps = { + onDeviceToggled: () => {}, + }; - this._unmounted = false; - this.onDeviceToggled = this.onDeviceToggled.bind(this); - this._onDisplayNameChanged = this._onDisplayNameChanged.bind(this); - } - - componentWillUnmount() { - this._unmounted = true; - } - - _onDisplayNameChanged(value) { + private onDisplayNameChanged = (value: string): Promise<{}> => { const device = this.props.device; return MatrixClientPeg.get().setDeviceDetails(device.device_id, { display_name: value, @@ -46,15 +44,13 @@ export default class DevicesPanelEntry extends React.Component { console.error("Error setting session display name", e); throw new Error(_t("Failed to set display name")); }); - } + }; - onDeviceToggled() { + private onDeviceToggled = (): void => { this.props.onDeviceToggled(this.props.device); - } - - render() { - const EditableTextContainer = sdk.getComponent('elements.EditableTextContainer'); + }; + public render(): JSX.Element { const device = this.props.device; let lastSeen = ""; @@ -76,7 +72,7 @@ export default class DevicesPanelEntry extends React.Component {
@@ -90,12 +86,3 @@ export default class DevicesPanelEntry extends React.Component { ); } } - -DevicesPanelEntry.propTypes = { - device: PropTypes.object.isRequired, - onDeviceToggled: PropTypes.func, -}; - -DevicesPanelEntry.defaultProps = { - onDeviceToggled: function() {}, -}; diff --git a/src/components/views/settings/IntegrationManager.js b/src/components/views/settings/IntegrationManager.tsx similarity index 68% rename from src/components/views/settings/IntegrationManager.js rename to src/components/views/settings/IntegrationManager.tsx index 9f2985df14..0b880c019f 100644 --- a/src/components/views/settings/IntegrationManager.js +++ b/src/components/views/settings/IntegrationManager.tsx @@ -1,6 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2015 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,53 +15,55 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import dis from '../../../dispatcher/dispatcher'; import { Key } from "../../../Keyboard"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { ActionPayload } from '../../../dispatcher/payloads'; +import Spinner from "../elements/Spinner"; + +interface IProps { + // false to display an error saying that we couldn't connect to the integration manager + connected: boolean; + + // true to display a loading spinner + loading: boolean; + + // The source URL to load + url?: string; + + // callback when the manager is dismissed + onFinished: () => void; +} + +interface IState { + errored: boolean; +} @replaceableComponent("views.settings.IntegrationManager") -export default class IntegrationManager extends React.Component { - static propTypes = { - // false to display an error saying that we couldn't connect to the integration manager - connected: PropTypes.bool.isRequired, +export default class IntegrationManager extends React.Component { + private dispatcherRef: string; - // true to display a loading spinner - loading: PropTypes.bool.isRequired, - - // The source URL to load - url: PropTypes.string, - - // callback when the manager is dismissed - onFinished: PropTypes.func.isRequired, - }; - - static defaultProps = { + public static defaultProps = { connected: true, loading: false, }; - constructor(props) { - super(props); + public state = { + errored: false, + }; - this.state = { - errored: false, - }; - } - - componentDidMount() { + public componentDidMount(): void { this.dispatcherRef = dis.register(this.onAction); document.addEventListener("keydown", this.onKeyDown); } - componentWillUnmount() { + public componentWillUnmount(): void { dis.unregister(this.dispatcherRef); document.removeEventListener("keydown", this.onKeyDown); } - onKeyDown = (ev) => { + private onKeyDown = (ev: KeyboardEvent): void => { if (ev.key === Key.ESCAPE) { ev.stopPropagation(); ev.preventDefault(); @@ -70,19 +71,18 @@ export default class IntegrationManager extends React.Component { } }; - onAction = (payload) => { + private onAction = (payload: ActionPayload): void => { if (payload.action === 'close_scalar') { this.props.onFinished(); } }; - onError = () => { + private onError = (): void => { this.setState({ errored: true }); }; - render() { + public render(): JSX.Element { if (this.props.loading) { - const Spinner = sdk.getComponent("elements.Spinner"); return (

{ _t("Connecting to integration manager...") }

diff --git a/src/components/views/settings/ProfileSettings.js b/src/components/views/settings/ProfileSettings.tsx similarity index 81% rename from src/components/views/settings/ProfileSettings.js rename to src/components/views/settings/ProfileSettings.tsx index d05fca983c..9563280550 100644 --- a/src/components/views/settings/ProfileSettings.js +++ b/src/components/views/settings/ProfileSettings.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,17 +19,30 @@ import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import Field from "../elements/Field"; import { getHostingLink } from '../../../utils/HostingLink'; -import * as sdk from "../../../index"; import { OwnProfileStore } from "../../../stores/OwnProfileStore"; import Modal from "../../../Modal"; import ErrorDialog from "../dialogs/ErrorDialog"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromMxc } from "../../../customisations/Media"; +import AccessibleButton from '../elements/AccessibleButton'; +import AvatarSetting from './AvatarSetting'; + +interface IState { + userId?: string; + originalDisplayName?: string; + displayName?: string; + originalAvatarUrl?: string; + avatarUrl?: string | ArrayBuffer; + avatarFile?: File; + enableProfileSave?: boolean; +} @replaceableComponent("views.settings.ProfileSettings") -export default class ProfileSettings extends React.Component { - constructor() { - super(); +export default class ProfileSettings extends React.Component<{}, IState> { + private avatarUpload: React.RefObject = createRef(); + + constructor(props: {}) { + super(props); const client = MatrixClientPeg.get(); let avatarUrl = OwnProfileStore.instance.avatarMxc; @@ -43,17 +56,15 @@ export default class ProfileSettings extends React.Component { avatarFile: null, enableProfileSave: false, }; - - this._avatarUpload = createRef(); } - _uploadAvatar = () => { - this._avatarUpload.current.click(); + private uploadAvatar = (): void => { + this.avatarUpload.current.click(); }; - _removeAvatar = () => { + private removeAvatar = (): void => { // clear file upload field so same file can be selected - this._avatarUpload.current.value = ""; + this.avatarUpload.current.value = ""; this.setState({ avatarUrl: null, avatarFile: null, @@ -61,7 +72,7 @@ export default class ProfileSettings extends React.Component { }); }; - _cancelProfileChanges = async (e) => { + private cancelProfileChanges = async (e: React.MouseEvent): Promise => { e.stopPropagation(); e.preventDefault(); @@ -74,7 +85,7 @@ export default class ProfileSettings extends React.Component { }); }; - _saveProfile = async (e) => { + private saveProfile = async (e: React.FormEvent): Promise => { e.stopPropagation(); e.preventDefault(); @@ -82,7 +93,7 @@ export default class ProfileSettings extends React.Component { this.setState({ enableProfileSave: false }); const client = MatrixClientPeg.get(); - const newState = {}; + const newState: IState = {}; const displayName = this.state.displayName.trim(); try { @@ -115,14 +126,14 @@ export default class ProfileSettings extends React.Component { this.setState(newState); }; - _onDisplayNameChanged = (e) => { + private onDisplayNameChanged = (e: React.ChangeEvent): void => { this.setState({ displayName: e.target.value, enableProfileSave: true, }); }; - _onAvatarChanged = (e) => { + private onAvatarChanged = (e: React.ChangeEvent): void => { if (!e.target.files || !e.target.files.length) { this.setState({ avatarUrl: this.state.originalAvatarUrl, @@ -144,7 +155,7 @@ export default class ProfileSettings extends React.Component { reader.readAsDataURL(file); }; - render() { + public render(): JSX.Element { const hostingSignupLink = getHostingLink('user-settings'); let hostingSignup = null; if (hostingSignupLink) { @@ -161,20 +172,18 @@ export default class ProfileSettings extends React.Component { ; } - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - const AvatarSetting = sdk.getComponent('settings.AvatarSetting'); return (
@@ -185,7 +194,7 @@ export default class ProfileSettings extends React.Component { type="text" value={this.state.displayName} autoComplete="off" - onChange={this._onDisplayNameChanged} + onChange={this.onDisplayNameChanged} />

{ this.state.userId } @@ -193,22 +202,22 @@ export default class ProfileSettings extends React.Component {

+ uploadAvatar={this.uploadAvatar} + removeAvatar={this.removeAvatar} />
{ _t("Cancel") }