Merge pull request #6619 from matrix-org/gsouquet/ts-components-migration

This commit is contained in:
Germain 2021-09-03 08:47:12 +01:00 committed by GitHub
commit e16921e1f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 347 additions and 346 deletions

View file

@ -347,7 +347,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
}); });
} }
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 room was shift-clicked, remove it from the room directory
if (ev.shiftKey && !this.state.selectedCommunityId) { if (ev.shiftKey && !this.state.selectedCommunityId) {
ev.preventDefault(); ev.preventDefault();

View file

@ -1867,7 +1867,7 @@ export default class RoomView extends React.Component<IProps, IState> {
isRoomEncrypted={this.context.isRoomEncrypted(this.state.room.roomId)} isRoomEncrypted={this.context.isRoomEncrypted(this.state.room.roomId)}
/>; />;
} else if (showRoomUpgradeBar) { } else if (showRoomUpgradeBar) {
aux = <RoomUpgradeWarningBar room={this.state.room} recommendation={roomVersionRecommendation} />; aux = <RoomUpgradeWarningBar room={this.state.room} />;
} else if (myMembership !== "join") { } else if (myMembership !== "join") {
// We do have a room object for this room, but we're not currently in it. // 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. // We may have a 3rd party invite to it.
@ -2042,7 +2042,6 @@ export default class RoomView extends React.Component<IProps, IState> {
highlight={this.state.room.getUnreadNotificationCount(NotificationCountType.Highlight) > 0} highlight={this.state.room.getUnreadNotificationCount(NotificationCountType.Highlight) > 0}
numUnreadMessages={this.state.numUnreadMessages} numUnreadMessages={this.state.numUnreadMessages}
onScrollToBottomClick={this.jumpToLiveTimeline} onScrollToBottomClick={this.jumpToLiveTimeline}
roomId={this.state.roomId}
/>); />);
} }

View file

@ -36,6 +36,7 @@ interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" |
// Whether the onClick of the avatar should be overriden to dispatch `Action.ViewUser` // Whether the onClick of the avatar should be overriden to dispatch `Action.ViewUser`
viewUserOnClick?: boolean; viewUserOnClick?: boolean;
title?: string; title?: string;
style?: any;
} }
interface IState { interface IState {

View file

@ -19,7 +19,7 @@ import React, { ReactHTML } from 'react';
import { Key } from '../../../Keyboard'; import { Key } from '../../../Keyboard';
import classnames from 'classnames'; import classnames from 'classnames';
export type ButtonEvent = React.MouseEvent<Element> | React.KeyboardEvent<Element>; export type ButtonEvent = React.MouseEvent<Element> | React.KeyboardEvent<Element> | React.FormEvent<Element>;
/** /**
* children: React's magic prop. Represents all children given to the element. * children: React's magic prop. Represents all children given to the element.
@ -39,7 +39,7 @@ interface IProps extends React.InputHTMLAttributes<Element> {
tabIndex?: number; tabIndex?: number;
disabled?: boolean; disabled?: boolean;
className?: string; className?: string;
onClick(e?: ButtonEvent): void; onClick(e?: ButtonEvent): void | Promise<void>;
} }
interface IAccessibleButtonProps extends React.InputHTMLAttributes<Element> { interface IAccessibleButtonProps extends React.InputHTMLAttributes<Element> {

View file

@ -428,7 +428,7 @@ const UserOptionsSection: React.FC<{
let directMessageButton; let directMessageButton;
if (!isMe) { if (!isMe) {
directMessageButton = ( directMessageButton = (
<AccessibleButton onClick={() => openDMForUser(cli, member.userId)} className="mx_UserInfo_field"> <AccessibleButton onClick={() => { openDMForUser(cli, member.userId); }} className="mx_UserInfo_field">
{ _t('Direct message') } { _t('Direct message') }
</AccessibleButton> </AccessibleButton>
); );

View file

@ -243,6 +243,7 @@ interface IProps {
// opaque readreceipt info for each userId; used by ReadReceiptMarker // opaque readreceipt info for each userId; used by ReadReceiptMarker
// to manage its animations. Should be an empty object when the room // to manage its animations. Should be an empty object when the room
// first loads // first loads
// TODO: Proper typing for RR info
readReceiptMap?: any; readReceiptMap?: any;
// A function which is used to check if the parent panel is being // A function which is used to check if the parent panel is being

View file

@ -14,11 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from "react";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import classNames from 'classnames'; import classNames from 'classnames';
export default (props) => { interface IProps {
numUnreadMessages: number;
highlight: boolean;
onScrollToBottomClick: (e: React.MouseEvent) => void;
}
const JumpToBottomButton: React.FC<IProps> = (props) => {
const className = classNames({ const className = classNames({
'mx_JumpToBottomButton': true, 'mx_JumpToBottomButton': true,
'mx_JumpToBottomButton_highlight': props.highlight, 'mx_JumpToBottomButton_highlight': props.highlight,
@ -36,3 +43,5 @@ export default (props) => {
{ badge } { badge }
</div>); </div>);
}; };
export default JumpToBottomButton;

View file

@ -15,62 +15,75 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { createRef } from 'react'; import React, { createRef, RefObject } from 'react';
import PropTypes from 'prop-types'; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { formatDate } from '../../../DateUtils'; import { formatDate } from '../../../DateUtils';
import NodeAnimator from "../../../NodeAnimator"; import NodeAnimator from "../../../NodeAnimator";
import * as sdk from "../../../index";
import { toPx } from "../../../utils/units"; import { toPx } from "../../../utils/units";
import { replaceableComponent } from "../../../utils/replaceableComponent"; 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") @replaceableComponent("views.rooms.ReadReceiptMarker")
export default class ReadReceiptMarker extends React.PureComponent { export default class ReadReceiptMarker extends React.PureComponent<IProps, IState> {
static propTypes = { private avatar: React.RefObject<HTMLDivElement | HTMLImageElement | HTMLSpanElement> = createRef();
// 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,
};
static defaultProps = { static defaultProps = {
leftOffset: 0, leftOffset: 0,
}; };
constructor(props) { constructor(props: IProps) {
super(props); super(props);
this._avatar = createRef();
this.state = { this.state = {
// if we are going to animate the RR, we don't show it on first render, // 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 // 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 // before we remove the rr, store its location in the map, so that if
// it reappears, it can be animated from the right place. // it reappears, it can be animated from the right place.
const rrInfo = this.props.readReceiptInfo; const rrInfo = this.props.readReceiptInfo;
@ -95,29 +108,29 @@ export default class ReadReceiptMarker extends React.PureComponent {
return; return;
} }
const avatarNode = this._avatar.current; const avatarNode = this.avatar.current;
rrInfo.top = avatarNode.offsetTop; rrInfo.top = avatarNode.offsetTop;
rrInfo.left = avatarNode.offsetLeft; rrInfo.left = avatarNode.offsetLeft;
rrInfo.parent = avatarNode.offsetParent; rrInfo.parent = avatarNode.offsetParent;
} }
componentDidMount() { public componentDidMount(): void {
if (!this.state.suppressDisplay) { if (!this.state.suppressDisplay) {
// we've already done our display - nothing more to do. // we've already done our display - nothing more to do.
return; return;
} }
this._animateMarker(); this.animateMarker();
} }
componentDidUpdate(prevProps) { public componentDidUpdate(prevProps: IProps): void {
const differentLeftOffset = prevProps.leftOffset !== this.props.leftOffset; const differentLeftOffset = prevProps.leftOffset !== this.props.leftOffset;
const visibilityChanged = prevProps.hidden !== this.props.hidden; const visibilityChanged = prevProps.hidden !== this.props.hidden;
if (differentLeftOffset || visibilityChanged) { if (differentLeftOffset || visibilityChanged) {
this._animateMarker(); this.animateMarker();
} }
} }
_animateMarker() { private animateMarker(): void {
// treat new RRs as though they were off the top of the screen // treat new RRs as though they were off the top of the screen
let oldTop = -15; let oldTop = -15;
@ -126,7 +139,7 @@ export default class ReadReceiptMarker extends React.PureComponent {
oldTop = oldInfo.top + oldInfo.parent.getBoundingClientRect().top; oldTop = oldInfo.top + oldInfo.parent.getBoundingClientRect().top;
} }
const newElement = this._avatar.current; const newElement = this.avatar.current;
let startTopOffset; let startTopOffset;
if (!newElement.offsetParent) { if (!newElement.offsetParent) {
// this seems to happen sometimes for reasons I don't understand // this seems to happen sometimes for reasons I don't understand
@ -156,10 +169,9 @@ export default class ReadReceiptMarker extends React.PureComponent {
}); });
} }
render() { public render(): JSX.Element {
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
if (this.state.suppressDisplay) { if (this.state.suppressDisplay) {
return <div ref={this._avatar} />; return <div ref={this.avatar as RefObject<HTMLDivElement>} />;
} }
const style = { const style = {
@ -198,7 +210,7 @@ export default class ReadReceiptMarker extends React.PureComponent {
style={style} style={style}
title={title} title={title}
onClick={this.props.onClick} onClick={this.props.onClick}
inputRef={this._avatar} inputRef={this.avatar as RefObject<HTMLImageElement>}
/> />
</NodeAnimator> </NodeAnimator>
); );

View file

@ -14,41 +14,38 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import React from 'react'; import React from 'react';
import { _t } from '../../../languageHandler'; import { Room } from 'matrix-js-sdk/src';
import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import { roomShape } from './RoomDetailRow';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import RoomDetailRow from "./RoomDetailRow";
interface IProps {
rooms?: Room[];
className?: string;
}
@replaceableComponent("views.rooms.RoomDetailList") @replaceableComponent("views.rooms.RoomDetailList")
export default class RoomDetailList extends React.Component { export default class RoomDetailList extends React.Component<IProps> {
static propTypes = { private getRows(): JSX.Element[] {
rooms: PropTypes.arrayOf(roomShape),
className: PropTypes.string,
};
getRows() {
if (!this.props.rooms) return []; if (!this.props.rooms) return [];
const RoomDetailRow = sdk.getComponent('rooms.RoomDetailRow');
return this.props.rooms.map((room, index) => { return this.props.rooms.map((room, index) => {
return <RoomDetailRow key={index} room={room} onClick={this.onDetailsClick} />; return <RoomDetailRow key={index} room={room} onClick={this.onDetailsClick} />;
}); });
} }
onDetailsClick = (ev, room) => { private onDetailsClick = (ev: React.MouseEvent, room: Room): void => {
dis.dispatch({ dis.dispatch({
action: 'view_room', action: 'view_room',
room_id: room.roomId, 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(); const rows = this.getRows();
let rooms; let rooms;
if (rows.length === 0) { if (rows.length === 0) {

View file

@ -195,7 +195,7 @@ export default class RoomHeader extends React.Component<IProps> {
videoCallButton = videoCallButton =
<AccessibleTooltipButton <AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_videoCallButton" className="mx_RoomHeader_button mx_RoomHeader_videoCallButton"
onClick={(ev) => ev.shiftKey ? onClick={(ev: React.MouseEvent<Element>) => ev.shiftKey ?
this.displayInfoDialogAboutScreensharing() : this.props.onCallPlaced(PlaceCallType.Video)} this.displayInfoDialogAboutScreensharing() : this.props.onCallPlaced(PlaceCallType.Video)}
title={_t("Video call")} />; title={_t("Video call")} />;
} }

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 React from 'react';
import PropTypes from 'prop-types'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import * as sdk from '../../../index'; import { Room } from 'matrix-js-sdk/src/models/room';
import { RoomState } from 'matrix-js-sdk/src/models/room-state';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { replaceableComponent } from "../../../utils/replaceableComponent"; 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") @replaceableComponent("views.rooms.RoomUpgradeWarningBar")
export default class RoomUpgradeWarningBar extends React.PureComponent { export default class RoomUpgradeWarningBar extends React.PureComponent<IProps, IState> {
static propTypes = { public componentDidMount(): void {
room: PropTypes.object.isRequired,
recommendation: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
const tombstone = this.props.room.currentState.getStateEvents("m.room.tombstone", ""); const tombstone = this.props.room.currentState.getStateEvents("m.room.tombstone", "");
this.setState({ upgraded: tombstone && tombstone.getContent().replacement_room }); 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(); const cli = MatrixClientPeg.get();
if (cli) { 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) { if (!this.props.room || event.getRoomId() !== this.props.room.roomId) {
return; return;
} }
@ -60,14 +62,11 @@ export default class RoomUpgradeWarningBar extends React.PureComponent {
this.setState({ upgraded: tombstone && tombstone.getContent().replacement_room }); this.setState({ upgraded: tombstone && tombstone.getContent().replacement_room });
}; };
onUpgradeClick = () => { private onUpgradeClick = (): void => {
const RoomUpgradeDialog = sdk.getComponent('dialogs.RoomUpgradeDialog');
Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, { room: this.props.room }); Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, { room: this.props.room });
}; };
render() { public render(): JSX.Element {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let doUpgradeWarnings = ( let doUpgradeWarnings = (
<div> <div>
<div className="mx_RoomUpgradeWarningBar_body"> <div className="mx_RoomUpgradeWarningBar_body">

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 React from 'react';
import PropTypes from 'prop-types';
import { replaceableComponent } from "../../../utils/replaceableComponent"; 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 * A stripped-down room header used for things like the user settings
* and room directory. * and room directory.
*/ */
@replaceableComponent("views.rooms.SimpleRoomHeader") @replaceableComponent("views.rooms.SimpleRoomHeader")
export default class SimpleRoomHeader extends React.Component { export default class SimpleRoomHeader extends React.PureComponent<IProps> {
static propTypes = { public render(): JSX.Element {
title: PropTypes.string,
// `src` to an image. Optional.
icon: PropTypes.string,
};
render() {
let icon; let icon;
if (this.props.icon) { if (this.props.icon) {
icon = <img icon = <img

View file

@ -1,7 +1,5 @@
/* /*
Copyright 2016 OpenMarket Ltd Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2017 Vector Creations Ltd
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -17,19 +15,18 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.rooms.TopUnreadMessagesBar") interface IProps {
export default class TopUnreadMessagesBar extends React.Component { onScrollUpClick?: (e: React.MouseEvent) => void;
static propTypes = { onCloseClick?: (e: React.MouseEvent) => void;
onScrollUpClick: PropTypes.func, }
onCloseClick: PropTypes.func,
};
render() { @replaceableComponent("views.rooms.TopUnreadMessagesBar")
export default class TopUnreadMessagesBar extends React.PureComponent<IProps> {
public render(): JSX.Element {
return ( return (
<div className="mx_TopUnreadMessagesBar"> <div className="mx_TopUnreadMessagesBar">
<AccessibleButton <AccessibleButton

View file

@ -15,12 +15,19 @@ limitations under the License.
*/ */
import React, { useState } from "react"; import React, { useState } from "react";
import PropTypes from "prop-types";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import classNames from "classnames"; import classNames from "classnames";
const AvatarSetting = ({ avatarUrl, avatarAltText, avatarName, uploadAvatar, removeAvatar }) => { 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<IProps> = ({ avatarUrl, avatarAltText, avatarName, uploadAvatar, removeAvatar }) => {
const [isHovering, setIsHovering] = useState(false); const [isHovering, setIsHovering] = useState(false);
const hoveringProps = { const hoveringProps = {
onMouseEnter: () => setIsHovering(true), onMouseEnter: () => setIsHovering(true),
@ -78,12 +85,4 @@ const AvatarSetting = ({ avatarUrl, avatarAltText, avatarName, uploadAvatar, rem
</div>; </div>;
}; };
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; export default AvatarSetting;

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 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 { MatrixClientPeg } from "../../../MatrixClientPeg";
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Spinner from '../elements/Spinner'; import Spinner from '../elements/Spinner';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media"; 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") @replaceableComponent("views.settings.ChangeAvatar")
export default class ChangeAvatar extends React.Component { export default class ChangeAvatar extends React.Component<IProps, IState> {
static propTypes = { public static defaultProps = {
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 = {
showUploadSection: true, showUploadSection: true,
className: "", className: "",
width: 80, width: 80,
height: 80, height: 80,
}; };
constructor(props) { private avatarSet = false;
constructor(props: IProps) {
super(props); super(props);
this.state = { this.state = {
avatarUrl: this.props.initialAvatarUrl, avatarUrl: this.props.initialAvatarUrl,
phase: ChangeAvatar.Phases.Display, phase: Phases.Display,
}; };
} }
componentDidMount() { public componentDidMount(): void {
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents); MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
} }
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // 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) { if (this.avatarSet) {
// don't clobber what the user has just set // don't clobber what the user has just set
return; return;
@ -72,13 +83,13 @@ export default class ChangeAvatar extends React.Component {
}); });
} }
componentWillUnmount() { public componentWillUnmount(): void {
if (MatrixClientPeg.get()) { if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents); MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
} }
} }
onRoomStateEvents = (ev) => { private onRoomStateEvents = (ev: MatrixEvent) => {
if (!this.props.room) { if (!this.props.room) {
return; return;
} }
@ -94,18 +105,17 @@ export default class ChangeAvatar extends React.Component {
} }
}; };
setAvatarFromFile(file) { private setAvatarFromFile(file: File): Promise<{}> {
let newUrl = null; let newUrl = null;
this.setState({ this.setState({
phase: ChangeAvatar.Phases.Uploading, phase: Phases.Uploading,
}); });
const self = this; const httpPromise = MatrixClientPeg.get().uploadContent(file).then((url) => {
const httpPromise = MatrixClientPeg.get().uploadContent(file).then(function(url) {
newUrl = url; newUrl = url;
if (self.props.room) { if (this.props.room) {
return MatrixClientPeg.get().sendStateEvent( return MatrixClientPeg.get().sendStateEvent(
self.props.room.roomId, this.props.room.roomId,
'm.room.avatar', 'm.room.avatar',
{ url: url }, { url: url },
'', '',
@ -115,38 +125,37 @@ export default class ChangeAvatar extends React.Component {
} }
}); });
httpPromise.then(function() { httpPromise.then(() => {
self.setState({ this.setState({
phase: ChangeAvatar.Phases.Display, phase: Phases.Display,
avatarUrl: mediaFromMxc(newUrl).srcHttp, avatarUrl: mediaFromMxc(newUrl).srcHttp,
}); });
}, function(error) { }, () => {
self.setState({ this.setState({
phase: ChangeAvatar.Phases.Error, phase: Phases.Error,
}); });
self.onError(error); this.onError();
}); });
return httpPromise; return httpPromise;
} }
onFileSelected = (ev) => { private onFileSelected = (ev: React.ChangeEvent<HTMLInputElement>) => {
this.avatarSet = true; this.avatarSet = true;
return this.setAvatarFromFile(ev.target.files[0]); return this.setAvatarFromFile(ev.target.files[0]);
}; };
onError = (error) => { private onError = (): void => {
this.setState({ this.setState({
errorText: _t("Failed to upload profile picture!"), errorText: _t("Failed to upload profile picture!"),
}); });
}; };
render() { public render(): JSX.Element {
let avatarImg; let avatarImg;
// Having just set an avatar we just display that since it will take a little // Having just set an avatar we just display that since it will take a little
// time to propagate through to the RoomAvatar. // time to propagate through to the RoomAvatar.
if (this.props.room && !this.avatarSet) { if (this.props.room && !this.avatarSet) {
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
avatarImg = <RoomAvatar avatarImg = <RoomAvatar
room={this.props.room} room={this.props.room}
width={this.props.width} width={this.props.width}
@ -154,7 +163,6 @@ export default class ChangeAvatar extends React.Component {
resizeMethod='crop' resizeMethod='crop'
/>; />;
} else { } 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 ? // XXX: FIXME: once we track in the JS what our own displayname is(!) then use it here rather than ?
avatarImg = <BaseAvatar avatarImg = <BaseAvatar
width={this.props.width} width={this.props.width}
@ -178,8 +186,8 @@ export default class ChangeAvatar extends React.Component {
} }
switch (this.state.phase) { switch (this.state.phase) {
case ChangeAvatar.Phases.Display: case Phases.Display:
case ChangeAvatar.Phases.Error: case Phases.Error:
return ( return (
<div> <div>
<div className={this.props.className}> <div className={this.props.className}>
@ -188,7 +196,7 @@ export default class ChangeAvatar extends React.Component {
{ uploadSection } { uploadSection }
</div> </div>
); );
case ChangeAvatar.Phases.Uploading: case Phases.Uploading:
return ( return (
<Spinner /> <Spinner />
); );

View file

@ -1,7 +1,5 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 React from 'react';
import * as sdk from '../../../index';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import EditableTextContainer from "../elements/EditableTextContainer";
@replaceableComponent("views.settings.ChangeDisplayName") @replaceableComponent("views.settings.ChangeDisplayName")
export default class ChangeDisplayName extends React.Component { export default class ChangeDisplayName extends React.Component {
_getDisplayName = async () => { private getDisplayName = async (): Promise<string> => {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
try { try {
const res = await cli.getProfileInfo(cli.getUserId()); 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(); const cli = MatrixClientPeg.get();
return cli.setDisplayName(newDisplayname).catch(function(e) { return cli.setDisplayName(newDisplayname).catch(function() {
throw new Error("Failed to set display name", e); throw new Error("Failed to set display name");
}); });
}; };
render() { public render(): JSX.Element {
const EditableTextContainer = sdk.getComponent('elements.EditableTextContainer');
return ( return (
<EditableTextContainer <EditableTextContainer
getInitialValue={this._getDisplayName} getInitialValue={this.getDisplayName}
placeholder={_t("No display name")} placeholder={_t("No display name")}
blurToSubmit={true} blurToSubmit={true}
onSubmit={this._changeDisplayName} /> onSubmit={this.changeDisplayName} />
); );
} }
} }

View file

@ -1,6 +1,5 @@
/* /*
Copyright 2016 OpenMarket Ltd Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { IMyDevice } from "matrix-js-sdk/src/client";
import * as sdk from '../../../index';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import { SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents"; import { SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents";
import { replaceableComponent } from "../../../utils/replaceableComponent"; 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") @replaceableComponent("views.settings.DevicesPanel")
export default class DevicesPanel extends React.Component { export default class DevicesPanel extends React.Component<IProps, IState> {
constructor(props) { private unmounted = false;
super(props);
this.state = { public componentDidMount(): void {
devices: undefined, this.loadDevices();
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);
} }
componentDidMount() { public componentWillUnmount(): void {
this._loadDevices(); this.unmounted = true;
} }
componentWillUnmount() { private loadDevices(): void {
this._unmounted = true;
}
_loadDevices() {
MatrixClientPeg.get().getDevices().then( MatrixClientPeg.get().getDevices().then(
(resp) => { (resp) => {
if (this._unmounted) { return; } if (this.unmounted) { return; }
this.setState({ devices: resp.devices || [] }); this.setState({ devices: resp.devices || [] });
}, },
(error) => { (error) => {
if (this._unmounted) { return; } if (this.unmounted) { return; }
let errtxt; let errtxt;
if (error.httpStatus == 404) { if (error.httpStatus == 404) {
// 404 probably means the HS doesn't yet support the API. // 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 * compare two devices, sorting from most-recently-seen to least-recently-seen
* (and then, for stability, by device id) * (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. // return < 0 if a comes before b, > 0 if a comes after b.
const lastSeenDelta = const lastSeenDelta =
(b.last_seen_ts || 0) - (a.last_seen_ts || 0); (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; return (idA < idB) ? -1 : (idA > idB) ? 1 : 0;
} }
_onDeviceSelectionToggled(device) { private onDeviceSelectionToggled = (device: IMyDevice): void => {
if (this._unmounted) { return; } if (this.unmounted) { return; }
const deviceId = device.device_id; const deviceId = device.device_id;
this.setState((state, props) => { this.setState((state, props) => {
@ -108,22 +105,21 @@ export default class DevicesPanel extends React.Component {
return { selectedDevices }; return { selectedDevices };
}); });
} };
_onDeleteClick() { private onDeleteClick = (): void => {
this.setState({ this.setState({
deleting: true, deleting: true,
}); });
this._makeDeleteRequest(null).catch((error) => { this.makeDeleteRequest(null).catch((error) => {
if (this._unmounted) { return; } if (this.unmounted) { return; }
if (error.httpStatus !== 401 || !error.data || !error.data.flows) { if (error.httpStatus !== 401 || !error.data || !error.data.flows) {
// doesn't look like an interactive-auth failure // doesn't look like an interactive-auth failure
throw error; throw error;
} }
// pop up an interactive auth dialog // pop up an interactive auth dialog
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
const numDevices = this.state.selectedDevices.length; const numDevices = this.state.selectedDevices.length;
const dialogAesthetics = { const dialogAesthetics = {
@ -148,7 +144,7 @@ export default class DevicesPanel extends React.Component {
title: _t("Authentication"), title: _t("Authentication"),
matrixClient: MatrixClientPeg.get(), matrixClient: MatrixClientPeg.get(),
authData: error.data, authData: error.data,
makeRequest: this._makeDeleteRequest.bind(this), makeRequest: this.makeDeleteRequest.bind(this),
aestheticsForStagePhases: { aestheticsForStagePhases: {
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics, [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics, [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
@ -156,15 +152,16 @@ export default class DevicesPanel extends React.Component {
}); });
}).catch((e) => { }).catch((e) => {
console.error("Error deleting sessions", e); console.error("Error deleting sessions", e);
if (this._unmounted) { return; } if (this.unmounted) { return; }
}).finally(() => { }).finally(() => {
this.setState({ this.setState({
deleting: false, deleting: false,
}); });
}); });
} };
_makeDeleteRequest(auth) { // TODO: proper typing for auth
private makeDeleteRequest(auth?: any): Promise<any> {
return MatrixClientPeg.get().deleteMultipleDevices(this.state.selectedDevices, auth).then( return MatrixClientPeg.get().deleteMultipleDevices(this.state.selectedDevices, auth).then(
() => { () => {
// Remove the deleted devices from `devices`, reset selection to [] // Remove the deleted devices from `devices`, reset selection to []
@ -178,20 +175,16 @@ export default class DevicesPanel extends React.Component {
); );
} }
_renderDevice(device) { private renderDevice = (device: IMyDevice): JSX.Element => {
const DevicesPanelEntry = sdk.getComponent('settings.DevicesPanelEntry');
return <DevicesPanelEntry return <DevicesPanelEntry
key={device.device_id} key={device.device_id}
device={device} device={device}
selected={this.state.selectedDevices.includes(device.device_id)} selected={this.state.selectedDevices.includes(device.device_id)}
onDeviceToggled={this._onDeviceSelectionToggled} onDeviceToggled={this.onDeviceSelectionToggled}
/>; />;
} };
render() {
const Spinner = sdk.getComponent("elements.Spinner");
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
public render(): JSX.Element {
if (this.state.deviceLoadError !== undefined) { if (this.state.deviceLoadError !== undefined) {
const classes = classNames(this.props.className, "error"); const classes = classNames(this.props.className, "error");
return ( return (
@ -204,15 +197,14 @@ export default class DevicesPanel extends React.Component {
const devices = this.state.devices; const devices = this.state.devices;
if (devices === undefined) { if (devices === undefined) {
// still loading // still loading
const classes = this.props.className; return <Spinner />;
return <Spinner className={classes} />;
} }
devices.sort(this._deviceCompare); devices.sort(this.deviceCompare);
const deleteButton = this.state.deleting ? const deleteButton = this.state.deleting ?
<Spinner w={22} h={22} /> : <Spinner w={22} h={22} /> :
<AccessibleButton onClick={this._onDeleteClick} kind="danger_sm"> <AccessibleButton onClick={this.onDeleteClick} kind="danger_sm">
{ _t("Delete %(count)s sessions", { count: this.state.selectedDevices.length }) } { _t("Delete %(count)s sessions", { count: this.state.selectedDevices.length }) }
</AccessibleButton>; </AccessibleButton>;
@ -227,12 +219,8 @@ export default class DevicesPanel extends React.Component {
{ this.state.selectedDevices.length > 0 ? deleteButton : null } { this.state.selectedDevices.length > 0 ? deleteButton : null }
</div> </div>
</div> </div>
{ devices.map(this._renderDevice) } { devices.map(this.renderDevice) }
</div> </div>
); );
} }
} }
DevicesPanel.propTypes = {
className: PropTypes.string,
};

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 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 { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { formatDate } from '../../../DateUtils'; import { formatDate } from '../../../DateUtils';
import StyledCheckbox from '../elements/StyledCheckbox'; import StyledCheckbox from '../elements/StyledCheckbox';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import EditableTextContainer from "../elements/EditableTextContainer";
interface IProps {
device?: IMyDevice;
onDeviceToggled?: (device: IMyDevice) => void;
selected?: boolean;
}
@replaceableComponent("views.settings.DevicesPanelEntry") @replaceableComponent("views.settings.DevicesPanelEntry")
export default class DevicesPanelEntry extends React.Component { export default class DevicesPanelEntry extends React.Component<IProps> {
constructor(props) { public static defaultProps = {
super(props); onDeviceToggled: () => {},
};
this._unmounted = false; private onDisplayNameChanged = (value: string): Promise<{}> => {
this.onDeviceToggled = this.onDeviceToggled.bind(this);
this._onDisplayNameChanged = this._onDisplayNameChanged.bind(this);
}
componentWillUnmount() {
this._unmounted = true;
}
_onDisplayNameChanged(value) {
const device = this.props.device; const device = this.props.device;
return MatrixClientPeg.get().setDeviceDetails(device.device_id, { return MatrixClientPeg.get().setDeviceDetails(device.device_id, {
display_name: value, display_name: value,
@ -46,15 +44,13 @@ export default class DevicesPanelEntry extends React.Component {
console.error("Error setting session display name", e); console.error("Error setting session display name", e);
throw new Error(_t("Failed to set display name")); throw new Error(_t("Failed to set display name"));
}); });
} };
onDeviceToggled() { private onDeviceToggled = (): void => {
this.props.onDeviceToggled(this.props.device); this.props.onDeviceToggled(this.props.device);
} };
render() {
const EditableTextContainer = sdk.getComponent('elements.EditableTextContainer');
public render(): JSX.Element {
const device = this.props.device; const device = this.props.device;
let lastSeen = ""; let lastSeen = "";
@ -76,7 +72,7 @@ export default class DevicesPanelEntry extends React.Component {
</div> </div>
<div className="mx_DevicesPanel_deviceName"> <div className="mx_DevicesPanel_deviceName">
<EditableTextContainer initialValue={device.display_name} <EditableTextContainer initialValue={device.display_name}
onSubmit={this._onDisplayNameChanged} onSubmit={this.onDisplayNameChanged}
placeholder={device.device_id} placeholder={device.device_id}
/> />
</div> </div>
@ -90,12 +86,3 @@ export default class DevicesPanelEntry extends React.Component {
); );
} }
} }
DevicesPanelEntry.propTypes = {
device: PropTypes.object.isRequired,
onDeviceToggled: PropTypes.func,
};
DevicesPanelEntry.defaultProps = {
onDeviceToggled: function() {},
};

View file

@ -1,6 +1,5 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import { Key } from "../../../Keyboard"; import { Key } from "../../../Keyboard";
import { replaceableComponent } from "../../../utils/replaceableComponent"; 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") @replaceableComponent("views.settings.IntegrationManager")
export default class IntegrationManager extends React.Component { export default class IntegrationManager extends React.Component<IProps, IState> {
static propTypes = { private dispatcherRef: string;
// false to display an error saying that we couldn't connect to the integration manager
connected: PropTypes.bool.isRequired,
// true to display a loading spinner public static defaultProps = {
loading: PropTypes.bool.isRequired,
// The source URL to load
url: PropTypes.string,
// callback when the manager is dismissed
onFinished: PropTypes.func.isRequired,
};
static defaultProps = {
connected: true, connected: true,
loading: false, loading: false,
}; };
constructor(props) { public state = {
super(props); errored: false,
};
this.state = { public componentDidMount(): void {
errored: false,
};
}
componentDidMount() {
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
document.addEventListener("keydown", this.onKeyDown); document.addEventListener("keydown", this.onKeyDown);
} }
componentWillUnmount() { public componentWillUnmount(): void {
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
document.removeEventListener("keydown", this.onKeyDown); document.removeEventListener("keydown", this.onKeyDown);
} }
onKeyDown = (ev) => { private onKeyDown = (ev: KeyboardEvent): void => {
if (ev.key === Key.ESCAPE) { if (ev.key === Key.ESCAPE) {
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); 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') { if (payload.action === 'close_scalar') {
this.props.onFinished(); this.props.onFinished();
} }
}; };
onError = () => { private onError = (): void => {
this.setState({ errored: true }); this.setState({ errored: true });
}; };
render() { public render(): JSX.Element {
if (this.props.loading) { if (this.props.loading) {
const Spinner = sdk.getComponent("elements.Spinner");
return ( return (
<div className='mx_IntegrationManager_loading'> <div className='mx_IntegrationManager_loading'>
<h3>{ _t("Connecting to integration manager...") }</h3> <h3>{ _t("Connecting to integration manager...") }</h3>

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 { MatrixClientPeg } from "../../../MatrixClientPeg";
import Field from "../elements/Field"; import Field from "../elements/Field";
import { getHostingLink } from '../../../utils/HostingLink'; import { getHostingLink } from '../../../utils/HostingLink';
import * as sdk from "../../../index";
import { OwnProfileStore } from "../../../stores/OwnProfileStore"; import { OwnProfileStore } from "../../../stores/OwnProfileStore";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import ErrorDialog from "../dialogs/ErrorDialog"; import ErrorDialog from "../dialogs/ErrorDialog";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media"; 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") @replaceableComponent("views.settings.ProfileSettings")
export default class ProfileSettings extends React.Component { export default class ProfileSettings extends React.Component<{}, IState> {
constructor() { private avatarUpload: React.RefObject<HTMLInputElement> = createRef();
super();
constructor(props: {}) {
super(props);
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
let avatarUrl = OwnProfileStore.instance.avatarMxc; let avatarUrl = OwnProfileStore.instance.avatarMxc;
@ -43,17 +56,15 @@ export default class ProfileSettings extends React.Component {
avatarFile: null, avatarFile: null,
enableProfileSave: false, enableProfileSave: false,
}; };
this._avatarUpload = createRef();
} }
_uploadAvatar = () => { private uploadAvatar = (): void => {
this._avatarUpload.current.click(); this.avatarUpload.current.click();
}; };
_removeAvatar = () => { private removeAvatar = (): void => {
// clear file upload field so same file can be selected // clear file upload field so same file can be selected
this._avatarUpload.current.value = ""; this.avatarUpload.current.value = "";
this.setState({ this.setState({
avatarUrl: null, avatarUrl: null,
avatarFile: null, avatarFile: null,
@ -61,7 +72,7 @@ export default class ProfileSettings extends React.Component {
}); });
}; };
_cancelProfileChanges = async (e) => { private cancelProfileChanges = async (e: React.MouseEvent): Promise<void> => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -74,7 +85,7 @@ export default class ProfileSettings extends React.Component {
}); });
}; };
_saveProfile = async (e) => { private saveProfile = async (e: React.FormEvent<HTMLFormElement>): Promise<void> => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -82,7 +93,7 @@ export default class ProfileSettings extends React.Component {
this.setState({ enableProfileSave: false }); this.setState({ enableProfileSave: false });
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const newState = {}; const newState: IState = {};
const displayName = this.state.displayName.trim(); const displayName = this.state.displayName.trim();
try { try {
@ -115,14 +126,14 @@ export default class ProfileSettings extends React.Component {
this.setState(newState); this.setState(newState);
}; };
_onDisplayNameChanged = (e) => { private onDisplayNameChanged = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ this.setState({
displayName: e.target.value, displayName: e.target.value,
enableProfileSave: true, enableProfileSave: true,
}); });
}; };
_onAvatarChanged = (e) => { private onAvatarChanged = (e: React.ChangeEvent<HTMLInputElement>): void => {
if (!e.target.files || !e.target.files.length) { if (!e.target.files || !e.target.files.length) {
this.setState({ this.setState({
avatarUrl: this.state.originalAvatarUrl, avatarUrl: this.state.originalAvatarUrl,
@ -144,7 +155,7 @@ export default class ProfileSettings extends React.Component {
reader.readAsDataURL(file); reader.readAsDataURL(file);
}; };
render() { public render(): JSX.Element {
const hostingSignupLink = getHostingLink('user-settings'); const hostingSignupLink = getHostingLink('user-settings');
let hostingSignup = null; let hostingSignup = null;
if (hostingSignupLink) { if (hostingSignupLink) {
@ -161,20 +172,18 @@ export default class ProfileSettings extends React.Component {
</span>; </span>;
} }
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const AvatarSetting = sdk.getComponent('settings.AvatarSetting');
return ( return (
<form <form
onSubmit={this._saveProfile} onSubmit={this.saveProfile}
autoComplete="off" autoComplete="off"
noValidate={true} noValidate={true}
className="mx_ProfileSettings_profileForm" className="mx_ProfileSettings_profileForm"
> >
<input <input
type="file" type="file"
ref={this._avatarUpload} ref={this.avatarUpload}
className="mx_ProfileSettings_avatarUpload" className="mx_ProfileSettings_avatarUpload"
onChange={this._onAvatarChanged} onChange={this.onAvatarChanged}
accept="image/*" accept="image/*"
/> />
<div className="mx_ProfileSettings_profile"> <div className="mx_ProfileSettings_profile">
@ -185,7 +194,7 @@ export default class ProfileSettings extends React.Component {
type="text" type="text"
value={this.state.displayName} value={this.state.displayName}
autoComplete="off" autoComplete="off"
onChange={this._onDisplayNameChanged} onChange={this.onDisplayNameChanged}
/> />
<p> <p>
{ this.state.userId } { this.state.userId }
@ -193,22 +202,22 @@ export default class ProfileSettings extends React.Component {
</p> </p>
</div> </div>
<AvatarSetting <AvatarSetting
avatarUrl={this.state.avatarUrl} avatarUrl={this.state.avatarUrl.toString()}
avatarName={this.state.displayName || this.state.userId} avatarName={this.state.displayName || this.state.userId}
avatarAltText={_t("Profile picture")} avatarAltText={_t("Profile picture")}
uploadAvatar={this._uploadAvatar} uploadAvatar={this.uploadAvatar}
removeAvatar={this._removeAvatar} /> removeAvatar={this.removeAvatar} />
</div> </div>
<div className="mx_ProfileSettings_buttons"> <div className="mx_ProfileSettings_buttons">
<AccessibleButton <AccessibleButton
onClick={this._cancelProfileChanges} onClick={this.cancelProfileChanges}
kind="link" kind="link"
disabled={!this.state.enableProfileSave} disabled={!this.state.enableProfileSave}
> >
{ _t("Cancel") } { _t("Cancel") }
</AccessibleButton> </AccessibleButton>
<AccessibleButton <AccessibleButton
onClick={this._saveProfile} onClick={this.saveProfile}
kind="primary" kind="primary"
disabled={!this.state.enableProfileSave} disabled={!this.state.enableProfileSave}
> >