Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/18798
Conflicts: src/i18n/strings/en_EN.json
This commit is contained in:
commit
600375cafe
105 changed files with 2539 additions and 967 deletions
|
@ -15,12 +15,19 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
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 hoveringProps = {
|
||||
onMouseEnter: () => setIsHovering(true),
|
||||
|
@ -78,12 +85,4 @@ const AvatarSetting = ({ avatarUrl, avatarAltText, avatarName, uploadAvatar, rem
|
|||
</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;
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,54 +15,65 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Spinner from '../elements/Spinner';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import RoomAvatar from '../avatars/RoomAvatar';
|
||||
import BaseAvatar from '../avatars/BaseAvatar';
|
||||
|
||||
interface IProps {
|
||||
initialAvatarUrl?: string;
|
||||
room?: Room;
|
||||
// if false, you need to call changeAvatar.onFileSelected yourself.
|
||||
showUploadSection?: boolean;
|
||||
width?: number;
|
||||
height?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
avatarUrl?: string;
|
||||
errorText?: string;
|
||||
phase?: Phases;
|
||||
}
|
||||
|
||||
enum Phases {
|
||||
Display = "display",
|
||||
Uploading = "uploading",
|
||||
Error = "error",
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.ChangeAvatar")
|
||||
export default class ChangeAvatar extends React.Component {
|
||||
static propTypes = {
|
||||
initialAvatarUrl: PropTypes.string,
|
||||
room: PropTypes.object,
|
||||
// if false, you need to call changeAvatar.onFileSelected yourself.
|
||||
showUploadSection: PropTypes.bool,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
static Phases = {
|
||||
Display: "display",
|
||||
Uploading: "uploading",
|
||||
Error: "error",
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
export default class ChangeAvatar extends React.Component<IProps, IState> {
|
||||
public static defaultProps = {
|
||||
showUploadSection: true,
|
||||
className: "",
|
||||
width: 80,
|
||||
height: 80,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
private avatarSet = false;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
avatarUrl: this.props.initialAvatarUrl,
|
||||
phase: ChangeAvatar.Phases.Display,
|
||||
phase: Phases.Display,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
public componentDidMount(): void {
|
||||
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
|
||||
// eslint-disable-next-line
|
||||
public UNSAFE_componentWillReceiveProps(newProps: IProps): void {
|
||||
if (this.avatarSet) {
|
||||
// don't clobber what the user has just set
|
||||
return;
|
||||
|
@ -72,13 +83,13 @@ export default class ChangeAvatar extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
public componentWillUnmount(): void {
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
}
|
||||
}
|
||||
|
||||
onRoomStateEvents = (ev) => {
|
||||
private onRoomStateEvents = (ev: MatrixEvent) => {
|
||||
if (!this.props.room) {
|
||||
return;
|
||||
}
|
||||
|
@ -94,18 +105,17 @@ export default class ChangeAvatar extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
setAvatarFromFile(file) {
|
||||
private setAvatarFromFile(file: File): Promise<{}> {
|
||||
let newUrl = null;
|
||||
|
||||
this.setState({
|
||||
phase: ChangeAvatar.Phases.Uploading,
|
||||
phase: Phases.Uploading,
|
||||
});
|
||||
const self = this;
|
||||
const httpPromise = MatrixClientPeg.get().uploadContent(file).then(function(url) {
|
||||
const httpPromise = MatrixClientPeg.get().uploadContent(file).then((url) => {
|
||||
newUrl = url;
|
||||
if (self.props.room) {
|
||||
if (this.props.room) {
|
||||
return MatrixClientPeg.get().sendStateEvent(
|
||||
self.props.room.roomId,
|
||||
this.props.room.roomId,
|
||||
'm.room.avatar',
|
||||
{ url: url },
|
||||
'',
|
||||
|
@ -115,38 +125,37 @@ export default class ChangeAvatar extends React.Component {
|
|||
}
|
||||
});
|
||||
|
||||
httpPromise.then(function() {
|
||||
self.setState({
|
||||
phase: ChangeAvatar.Phases.Display,
|
||||
httpPromise.then(() => {
|
||||
this.setState({
|
||||
phase: Phases.Display,
|
||||
avatarUrl: mediaFromMxc(newUrl).srcHttp,
|
||||
});
|
||||
}, function(error) {
|
||||
self.setState({
|
||||
phase: ChangeAvatar.Phases.Error,
|
||||
}, () => {
|
||||
this.setState({
|
||||
phase: Phases.Error,
|
||||
});
|
||||
self.onError(error);
|
||||
this.onError();
|
||||
});
|
||||
|
||||
return httpPromise;
|
||||
}
|
||||
|
||||
onFileSelected = (ev) => {
|
||||
private onFileSelected = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.avatarSet = true;
|
||||
return this.setAvatarFromFile(ev.target.files[0]);
|
||||
};
|
||||
|
||||
onError = (error) => {
|
||||
private onError = (): void => {
|
||||
this.setState({
|
||||
errorText: _t("Failed to upload profile picture!"),
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
let avatarImg;
|
||||
// Having just set an avatar we just display that since it will take a little
|
||||
// time to propagate through to the RoomAvatar.
|
||||
if (this.props.room && !this.avatarSet) {
|
||||
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
|
||||
avatarImg = <RoomAvatar
|
||||
room={this.props.room}
|
||||
width={this.props.width}
|
||||
|
@ -154,7 +163,6 @@ export default class ChangeAvatar extends React.Component {
|
|||
resizeMethod='crop'
|
||||
/>;
|
||||
} else {
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
// XXX: FIXME: once we track in the JS what our own displayname is(!) then use it here rather than ?
|
||||
avatarImg = <BaseAvatar
|
||||
width={this.props.width}
|
||||
|
@ -178,8 +186,8 @@ export default class ChangeAvatar extends React.Component {
|
|||
}
|
||||
|
||||
switch (this.state.phase) {
|
||||
case ChangeAvatar.Phases.Display:
|
||||
case ChangeAvatar.Phases.Error:
|
||||
case Phases.Display:
|
||||
case Phases.Error:
|
||||
return (
|
||||
<div>
|
||||
<div className={this.props.className}>
|
||||
|
@ -188,7 +196,7 @@ export default class ChangeAvatar extends React.Component {
|
|||
{ uploadSection }
|
||||
</div>
|
||||
);
|
||||
case ChangeAvatar.Phases.Uploading:
|
||||
case Phases.Uploading:
|
||||
return (
|
||||
<Spinner />
|
||||
);
|
|
@ -1,7 +1,5 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,14 +15,14 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import EditableTextContainer from "../elements/EditableTextContainer";
|
||||
|
||||
@replaceableComponent("views.settings.ChangeDisplayName")
|
||||
export default class ChangeDisplayName extends React.Component {
|
||||
_getDisplayName = async () => {
|
||||
private getDisplayName = async (): Promise<string> => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
try {
|
||||
const res = await cli.getProfileInfo(cli.getUserId());
|
||||
|
@ -34,21 +32,20 @@ export default class ChangeDisplayName extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
_changeDisplayName = (newDisplayname) => {
|
||||
private changeDisplayName = (newDisplayname: string): Promise<{}> => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
return cli.setDisplayName(newDisplayname).catch(function(e) {
|
||||
throw new Error("Failed to set display name", e);
|
||||
return cli.setDisplayName(newDisplayname).catch(function() {
|
||||
throw new Error("Failed to set display name");
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const EditableTextContainer = sdk.getComponent('elements.EditableTextContainer');
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<EditableTextContainer
|
||||
getInitialValue={this._getDisplayName}
|
||||
getInitialValue={this.getDisplayName}
|
||||
placeholder={_t("No display name")}
|
||||
blurToSubmit={true}
|
||||
onSubmit={this._changeDisplayName} />
|
||||
onSubmit={this.changeDisplayName} />
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -16,52 +15,58 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { IMyDevice } from "matrix-js-sdk/src/client";
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
import { SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import InteractiveAuthDialog from "../dialogs/InteractiveAuthDialog";
|
||||
import DevicesPanelEntry from "./DevicesPanelEntry";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
|
||||
interface IProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
devices: IMyDevice[];
|
||||
deviceLoadError?: string;
|
||||
selectedDevices: string[];
|
||||
deleting?: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.DevicesPanel")
|
||||
export default class DevicesPanel extends React.Component {
|
||||
constructor(props) {
|
||||
export default class DevicesPanel extends React.Component<IProps, IState> {
|
||||
private unmounted = false;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
devices: undefined,
|
||||
deviceLoadError: undefined,
|
||||
|
||||
devices: [],
|
||||
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() {
|
||||
this._loadDevices();
|
||||
public componentDidMount(): void {
|
||||
this.loadDevices();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
public componentWillUnmount(): void {
|
||||
this.unmounted = true;
|
||||
}
|
||||
|
||||
_loadDevices() {
|
||||
private loadDevices(): void {
|
||||
MatrixClientPeg.get().getDevices().then(
|
||||
(resp) => {
|
||||
if (this._unmounted) { return; }
|
||||
if (this.unmounted) { return; }
|
||||
this.setState({ devices: resp.devices || [] });
|
||||
},
|
||||
(error) => {
|
||||
if (this._unmounted) { return; }
|
||||
if (this.unmounted) { return; }
|
||||
let errtxt;
|
||||
if (error.httpStatus == 404) {
|
||||
// 404 probably means the HS doesn't yet support the API.
|
||||
|
@ -79,7 +84,7 @@ export default class DevicesPanel extends React.Component {
|
|||
* compare two devices, sorting from most-recently-seen to least-recently-seen
|
||||
* (and then, for stability, by device id)
|
||||
*/
|
||||
_deviceCompare(a, b) {
|
||||
private deviceCompare(a: IMyDevice, b: IMyDevice): number {
|
||||
// return < 0 if a comes before b, > 0 if a comes after b.
|
||||
const lastSeenDelta =
|
||||
(b.last_seen_ts || 0) - (a.last_seen_ts || 0);
|
||||
|
@ -91,8 +96,8 @@ export default class DevicesPanel extends React.Component {
|
|||
return (idA < idB) ? -1 : (idA > idB) ? 1 : 0;
|
||||
}
|
||||
|
||||
_onDeviceSelectionToggled(device) {
|
||||
if (this._unmounted) { return; }
|
||||
private onDeviceSelectionToggled = (device: IMyDevice): void => {
|
||||
if (this.unmounted) { return; }
|
||||
|
||||
const deviceId = device.device_id;
|
||||
this.setState((state, props) => {
|
||||
|
@ -108,22 +113,21 @@ export default class DevicesPanel extends React.Component {
|
|||
|
||||
return { selectedDevices };
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_onDeleteClick() {
|
||||
private onDeleteClick = (): void => {
|
||||
this.setState({
|
||||
deleting: true,
|
||||
});
|
||||
|
||||
this._makeDeleteRequest(null).catch((error) => {
|
||||
if (this._unmounted) { return; }
|
||||
this.makeDeleteRequest(null).catch((error) => {
|
||||
if (this.unmounted) { return; }
|
||||
if (error.httpStatus !== 401 || !error.data || !error.data.flows) {
|
||||
// doesn't look like an interactive-auth failure
|
||||
throw error;
|
||||
}
|
||||
|
||||
// pop up an interactive auth dialog
|
||||
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
||||
|
||||
const numDevices = this.state.selectedDevices.length;
|
||||
const dialogAesthetics = {
|
||||
|
@ -148,7 +152,7 @@ export default class DevicesPanel extends React.Component {
|
|||
title: _t("Authentication"),
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
authData: error.data,
|
||||
makeRequest: this._makeDeleteRequest.bind(this),
|
||||
makeRequest: this.makeDeleteRequest.bind(this),
|
||||
aestheticsForStagePhases: {
|
||||
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
|
||||
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
|
||||
|
@ -156,15 +160,16 @@ export default class DevicesPanel extends React.Component {
|
|||
});
|
||||
}).catch((e) => {
|
||||
console.error("Error deleting sessions", e);
|
||||
if (this._unmounted) { return; }
|
||||
if (this.unmounted) { return; }
|
||||
}).finally(() => {
|
||||
this.setState({
|
||||
deleting: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_makeDeleteRequest(auth) {
|
||||
// TODO: proper typing for auth
|
||||
private makeDeleteRequest(auth?: any): Promise<any> {
|
||||
return MatrixClientPeg.get().deleteMultipleDevices(this.state.selectedDevices, auth).then(
|
||||
() => {
|
||||
// Remove the deleted devices from `devices`, reset selection to []
|
||||
|
@ -178,20 +183,16 @@ export default class DevicesPanel extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
_renderDevice(device) {
|
||||
const DevicesPanelEntry = sdk.getComponent('settings.DevicesPanelEntry');
|
||||
private renderDevice = (device: IMyDevice): JSX.Element => {
|
||||
return <DevicesPanelEntry
|
||||
key={device.device_id}
|
||||
device={device}
|
||||
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) {
|
||||
const classes = classNames(this.props.className, "error");
|
||||
return (
|
||||
|
@ -204,15 +205,14 @@ export default class DevicesPanel extends React.Component {
|
|||
const devices = this.state.devices;
|
||||
if (devices === undefined) {
|
||||
// still loading
|
||||
const classes = this.props.className;
|
||||
return <Spinner className={classes} />;
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
devices.sort(this._deviceCompare);
|
||||
devices.sort(this.deviceCompare);
|
||||
|
||||
const deleteButton = this.state.deleting ?
|
||||
<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 }) }
|
||||
</AccessibleButton>;
|
||||
|
||||
|
@ -227,12 +227,8 @@ export default class DevicesPanel extends React.Component {
|
|||
{ this.state.selectedDevices.length > 0 ? deleteButton : null }
|
||||
</div>
|
||||
</div>
|
||||
{ devices.map(this._renderDevice) }
|
||||
{ devices.map(this.renderDevice) }
|
||||
</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");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,30 +15,28 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { IMyDevice } from 'matrix-js-sdk/src/client';
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { formatDate } from '../../../DateUtils';
|
||||
import StyledCheckbox from '../elements/StyledCheckbox';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import EditableTextContainer from "../elements/EditableTextContainer";
|
||||
|
||||
interface IProps {
|
||||
device?: IMyDevice;
|
||||
onDeviceToggled?: (device: IMyDevice) => void;
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.DevicesPanelEntry")
|
||||
export default class DevicesPanelEntry extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
export default class DevicesPanelEntry extends React.Component<IProps> {
|
||||
public static defaultProps = {
|
||||
onDeviceToggled: () => {},
|
||||
};
|
||||
|
||||
this._unmounted = false;
|
||||
this.onDeviceToggled = this.onDeviceToggled.bind(this);
|
||||
this._onDisplayNameChanged = this._onDisplayNameChanged.bind(this);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
}
|
||||
|
||||
_onDisplayNameChanged(value) {
|
||||
private onDisplayNameChanged = (value: string): Promise<{}> => {
|
||||
const device = this.props.device;
|
||||
return MatrixClientPeg.get().setDeviceDetails(device.device_id, {
|
||||
display_name: value,
|
||||
|
@ -46,15 +44,13 @@ export default class DevicesPanelEntry extends React.Component {
|
|||
console.error("Error setting session display name", e);
|
||||
throw new Error(_t("Failed to set display name"));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onDeviceToggled() {
|
||||
private onDeviceToggled = (): void => {
|
||||
this.props.onDeviceToggled(this.props.device);
|
||||
}
|
||||
|
||||
render() {
|
||||
const EditableTextContainer = sdk.getComponent('elements.EditableTextContainer');
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const device = this.props.device;
|
||||
|
||||
let lastSeen = "";
|
||||
|
@ -76,7 +72,7 @@ export default class DevicesPanelEntry extends React.Component {
|
|||
</div>
|
||||
<div className="mx_DevicesPanel_deviceName">
|
||||
<EditableTextContainer initialValue={device.display_name}
|
||||
onSubmit={this._onDisplayNameChanged}
|
||||
onSubmit={this.onDisplayNameChanged}
|
||||
placeholder={device.device_id}
|
||||
/>
|
||||
</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 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -16,53 +15,55 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { Key } from "../../../Keyboard";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { ActionPayload } from '../../../dispatcher/payloads';
|
||||
import Spinner from "../elements/Spinner";
|
||||
|
||||
interface IProps {
|
||||
// false to display an error saying that we couldn't connect to the integration manager
|
||||
connected: boolean;
|
||||
|
||||
// true to display a loading spinner
|
||||
loading: boolean;
|
||||
|
||||
// The source URL to load
|
||||
url?: string;
|
||||
|
||||
// callback when the manager is dismissed
|
||||
onFinished: () => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
errored: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.IntegrationManager")
|
||||
export default class IntegrationManager extends React.Component {
|
||||
static propTypes = {
|
||||
// false to display an error saying that we couldn't connect to the integration manager
|
||||
connected: PropTypes.bool.isRequired,
|
||||
export default class IntegrationManager extends React.Component<IProps, IState> {
|
||||
private dispatcherRef: string;
|
||||
|
||||
// true to display a loading spinner
|
||||
loading: PropTypes.bool.isRequired,
|
||||
|
||||
// The source URL to load
|
||||
url: PropTypes.string,
|
||||
|
||||
// callback when the manager is dismissed
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
public static defaultProps = {
|
||||
connected: true,
|
||||
loading: false,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
public state = {
|
||||
errored: false,
|
||||
};
|
||||
|
||||
this.state = {
|
||||
errored: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
public componentDidMount(): void {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
document.addEventListener("keydown", this.onKeyDown);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
public componentWillUnmount(): void {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
document.removeEventListener("keydown", this.onKeyDown);
|
||||
}
|
||||
|
||||
onKeyDown = (ev) => {
|
||||
private onKeyDown = (ev: KeyboardEvent): void => {
|
||||
if (ev.key === Key.ESCAPE) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
@ -70,19 +71,18 @@ export default class IntegrationManager extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
onAction = (payload) => {
|
||||
private onAction = (payload: ActionPayload): void => {
|
||||
if (payload.action === 'close_scalar') {
|
||||
this.props.onFinished();
|
||||
}
|
||||
};
|
||||
|
||||
onError = () => {
|
||||
private onError = (): void => {
|
||||
this.setState({ errored: true });
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
if (this.props.loading) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return (
|
||||
<div className='mx_IntegrationManager_loading'>
|
||||
<h3>{ _t("Connecting to integration manager...") }</h3>
|
|
@ -26,7 +26,7 @@ import { Layout } from "../../../settings/Layout";
|
|||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
|
||||
interface IProps {
|
||||
userId: string;
|
||||
userId?: string;
|
||||
displayName: string;
|
||||
avatarUrl: string;
|
||||
messagePreviewText: string;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -19,17 +19,30 @@ import { _t } from "../../../languageHandler";
|
|||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import Field from "../elements/Field";
|
||||
import { getHostingLink } from '../../../utils/HostingLink';
|
||||
import * as sdk from "../../../index";
|
||||
import { OwnProfileStore } from "../../../stores/OwnProfileStore";
|
||||
import Modal from "../../../Modal";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import AvatarSetting from './AvatarSetting';
|
||||
|
||||
interface IState {
|
||||
userId?: string;
|
||||
originalDisplayName?: string;
|
||||
displayName?: string;
|
||||
originalAvatarUrl?: string;
|
||||
avatarUrl?: string | ArrayBuffer;
|
||||
avatarFile?: File;
|
||||
enableProfileSave?: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.settings.ProfileSettings")
|
||||
export default class ProfileSettings extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
export default class ProfileSettings extends React.Component<{}, IState> {
|
||||
private avatarUpload: React.RefObject<HTMLInputElement> = createRef();
|
||||
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
let avatarUrl = OwnProfileStore.instance.avatarMxc;
|
||||
|
@ -43,17 +56,15 @@ export default class ProfileSettings extends React.Component {
|
|||
avatarFile: null,
|
||||
enableProfileSave: false,
|
||||
};
|
||||
|
||||
this._avatarUpload = createRef();
|
||||
}
|
||||
|
||||
_uploadAvatar = () => {
|
||||
this._avatarUpload.current.click();
|
||||
private uploadAvatar = (): void => {
|
||||
this.avatarUpload.current.click();
|
||||
};
|
||||
|
||||
_removeAvatar = () => {
|
||||
private removeAvatar = (): void => {
|
||||
// clear file upload field so same file can be selected
|
||||
this._avatarUpload.current.value = "";
|
||||
this.avatarUpload.current.value = "";
|
||||
this.setState({
|
||||
avatarUrl: null,
|
||||
avatarFile: null,
|
||||
|
@ -61,7 +72,7 @@ export default class ProfileSettings extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_cancelProfileChanges = async (e) => {
|
||||
private cancelProfileChanges = async (e: React.MouseEvent): Promise<void> => {
|
||||
e.stopPropagation();
|
||||
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.preventDefault();
|
||||
|
||||
|
@ -82,7 +93,7 @@ export default class ProfileSettings extends React.Component {
|
|||
this.setState({ enableProfileSave: false });
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
const newState = {};
|
||||
const newState: IState = {};
|
||||
|
||||
const displayName = this.state.displayName.trim();
|
||||
try {
|
||||
|
@ -115,14 +126,14 @@ export default class ProfileSettings extends React.Component {
|
|||
this.setState(newState);
|
||||
};
|
||||
|
||||
_onDisplayNameChanged = (e) => {
|
||||
private onDisplayNameChanged = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
displayName: e.target.value,
|
||||
enableProfileSave: true,
|
||||
});
|
||||
};
|
||||
|
||||
_onAvatarChanged = (e) => {
|
||||
private onAvatarChanged = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
if (!e.target.files || !e.target.files.length) {
|
||||
this.setState({
|
||||
avatarUrl: this.state.originalAvatarUrl,
|
||||
|
@ -144,7 +155,7 @@ export default class ProfileSettings extends React.Component {
|
|||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
render() {
|
||||
public render(): JSX.Element {
|
||||
const hostingSignupLink = getHostingLink('user-settings');
|
||||
let hostingSignup = null;
|
||||
if (hostingSignupLink) {
|
||||
|
@ -161,20 +172,18 @@ export default class ProfileSettings extends React.Component {
|
|||
</span>;
|
||||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const AvatarSetting = sdk.getComponent('settings.AvatarSetting');
|
||||
return (
|
||||
<form
|
||||
onSubmit={this._saveProfile}
|
||||
onSubmit={this.saveProfile}
|
||||
autoComplete="off"
|
||||
noValidate={true}
|
||||
className="mx_ProfileSettings_profileForm"
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
ref={this._avatarUpload}
|
||||
ref={this.avatarUpload}
|
||||
className="mx_ProfileSettings_avatarUpload"
|
||||
onChange={this._onAvatarChanged}
|
||||
onChange={this.onAvatarChanged}
|
||||
accept="image/*"
|
||||
/>
|
||||
<div className="mx_ProfileSettings_profile">
|
||||
|
@ -185,7 +194,7 @@ export default class ProfileSettings extends React.Component {
|
|||
type="text"
|
||||
value={this.state.displayName}
|
||||
autoComplete="off"
|
||||
onChange={this._onDisplayNameChanged}
|
||||
onChange={this.onDisplayNameChanged}
|
||||
/>
|
||||
<p>
|
||||
{ this.state.userId }
|
||||
|
@ -193,22 +202,22 @@ export default class ProfileSettings extends React.Component {
|
|||
</p>
|
||||
</div>
|
||||
<AvatarSetting
|
||||
avatarUrl={this.state.avatarUrl}
|
||||
avatarUrl={this.state.avatarUrl?.toString()}
|
||||
avatarName={this.state.displayName || this.state.userId}
|
||||
avatarAltText={_t("Profile picture")}
|
||||
uploadAvatar={this._uploadAvatar}
|
||||
removeAvatar={this._removeAvatar} />
|
||||
uploadAvatar={this.uploadAvatar}
|
||||
removeAvatar={this.removeAvatar} />
|
||||
</div>
|
||||
<div className="mx_ProfileSettings_buttons">
|
||||
<AccessibleButton
|
||||
onClick={this._cancelProfileChanges}
|
||||
onClick={this.cancelProfileChanges}
|
||||
kind="link"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
>
|
||||
{ _t("Cancel") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
onClick={this._saveProfile}
|
||||
onClick={this.saveProfile}
|
||||
kind="primary"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
>
|
|
@ -67,7 +67,7 @@ interface IState extends IThemeState {
|
|||
showAdvanced: boolean;
|
||||
layout: Layout;
|
||||
// User profile data for the message preview
|
||||
userId: string;
|
||||
userId?: string;
|
||||
displayName: string;
|
||||
avatarUrl: string;
|
||||
}
|
||||
|
@ -92,8 +92,8 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
systemFont: SettingsStore.getValue("systemFont"),
|
||||
showAdvanced: false,
|
||||
layout: SettingsStore.getValue("layout"),
|
||||
userId: "@erim:fink.fink",
|
||||
displayName: "Erimayas Fink",
|
||||
userId: null,
|
||||
displayName: null,
|
||||
avatarUrl: null,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -72,8 +72,10 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
private getVersionInfo(): { appVersion: string, olmVersion: string } {
|
||||
const brand = SdkConfig.get().brand;
|
||||
const appVersion = this.state.appVersion || 'unknown';
|
||||
let olmVersion = MatrixClientPeg.get().olmVersion;
|
||||
olmVersion = olmVersion ? `${olmVersion[0]}.${olmVersion[1]}.${olmVersion[2]}` : '<not-enabled>';
|
||||
const olmVersionTuple = MatrixClientPeg.get().olmVersion;
|
||||
const olmVersion = olmVersionTuple
|
||||
? `${olmVersionTuple[0]}.${olmVersionTuple[1]}.${olmVersionTuple[2]}`
|
||||
: '<not-enabled>';
|
||||
|
||||
return {
|
||||
appVersion: `${_t("%(brand)s version:", { brand })} ${appVersion}`,
|
||||
|
|
|
@ -172,7 +172,8 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
|||
];
|
||||
static IMAGES_AND_VIDEOS_SETTINGS = [
|
||||
'urlPreviewsEnabled',
|
||||
'autoplayGifsAndVideos',
|
||||
'autoplayGifs',
|
||||
'autoplayVideo',
|
||||
'showImages',
|
||||
];
|
||||
static TIMELINE_SETTINGS = [
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue