mirror of
https://github.com/element-hq/element-web
synced 2024-11-24 18:25:49 +03:00
Merge pull request #6619 from matrix-org/gsouquet/ts-components-migration
This commit is contained in:
commit
e16921e1f1
20 changed files with 347 additions and 346 deletions
|
@ -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();
|
||||||
|
|
|
@ -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}
|
|
||||||
/>);
|
/>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
|
@ -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>
|
||||||
);
|
);
|
|
@ -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) {
|
|
@ -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")} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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">
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
|
@ -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 />
|
||||||
);
|
);
|
|
@ -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} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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,
|
|
||||||
};
|
|
|
@ -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() {},
|
|
||||||
};
|
|
|
@ -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>
|
|
@ -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}
|
||||||
>
|
>
|
Loading…
Reference in a new issue