Merge remote-tracking branch 'upstream/develop' into feature_confetti#14676
This commit is contained in:
commit
c86964cd5e
478 changed files with 21997 additions and 13673 deletions
|
@ -14,25 +14,25 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useCallback} from "react";
|
||||
import React, {useState} from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import * as sdk from "../../../index";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import Modal from "../../../Modal";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import classNames from "classnames";
|
||||
|
||||
const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, removeAvatar}) => {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
const hoveringProps = {
|
||||
onMouseEnter: () => setIsHovering(true),
|
||||
onMouseLeave: () => setIsHovering(false),
|
||||
};
|
||||
|
||||
const openImageView = useCallback(() => {
|
||||
const ImageView = sdk.getComponent("elements.ImageView");
|
||||
Modal.createDialog(ImageView, {
|
||||
src: avatarUrl,
|
||||
name: avatarName,
|
||||
}, "mx_Dialog_lightbox");
|
||||
}, [avatarUrl, avatarName]);
|
||||
|
||||
let avatarElement = <div className="mx_AvatarSetting_avatarPlaceholder" />;
|
||||
let avatarElement = <AccessibleButton
|
||||
element="div"
|
||||
onClick={uploadAvatar}
|
||||
className="mx_AvatarSetting_avatarPlaceholder"
|
||||
{...hoveringProps}
|
||||
/>;
|
||||
if (avatarUrl) {
|
||||
avatarElement = (
|
||||
<AccessibleButton
|
||||
|
@ -40,16 +40,20 @@ const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, remo
|
|||
src={avatarUrl}
|
||||
alt={avatarAltText}
|
||||
aria-label={avatarAltText}
|
||||
onClick={openImageView} />
|
||||
onClick={uploadAvatar}
|
||||
{...hoveringProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let uploadAvatarBtn;
|
||||
if (uploadAvatar) {
|
||||
// insert an empty div to be the host for a css mask containing the upload.svg
|
||||
uploadAvatarBtn = <AccessibleButton onClick={uploadAvatar} kind="primary">
|
||||
{_t("Upload")}
|
||||
</AccessibleButton>;
|
||||
uploadAvatarBtn = <AccessibleButton
|
||||
onClick={uploadAvatar}
|
||||
className='mx_AvatarSetting_uploadButton'
|
||||
{...hoveringProps}
|
||||
/>;
|
||||
}
|
||||
|
||||
let removeAvatarBtn;
|
||||
|
@ -59,10 +63,18 @@ const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, remo
|
|||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
return <div className="mx_AvatarSetting_avatar">
|
||||
{ avatarElement }
|
||||
{ uploadAvatarBtn }
|
||||
{ removeAvatarBtn }
|
||||
const avatarClasses = classNames({
|
||||
"mx_AvatarSetting_avatar": true,
|
||||
"mx_AvatarSetting_avatar_hovering": isHovering,
|
||||
});
|
||||
return <div className={avatarClasses}>
|
||||
{avatarElement}
|
||||
<div className="mx_AvatarSetting_hover">
|
||||
<div className="mx_AvatarSetting_hoverBg" />
|
||||
<span>{_t("Upload")}</span>
|
||||
</div>
|
||||
{uploadAvatarBtn}
|
||||
{removeAvatarBtn}
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import {makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
|||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
@replaceableComponent("views.settings.BridgeTile")
|
||||
export default class BridgeTile extends React.PureComponent {
|
||||
|
@ -56,7 +57,7 @@ export default class BridgeTile extends React.PureComponent {
|
|||
type={Pill.TYPE_USER_MENTION}
|
||||
room={this.props.room}
|
||||
url={makeUserPermalink(content.creator)}
|
||||
shouldShowPillAvatar={true}
|
||||
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
|
||||
/>,
|
||||
});
|
||||
}
|
||||
|
@ -66,7 +67,7 @@ export default class BridgeTile extends React.PureComponent {
|
|||
type={Pill.TYPE_USER_MENTION}
|
||||
room={this.props.room}
|
||||
url={makeUserPermalink(this.props.ev.getSender())}
|
||||
shouldShowPillAvatar={true}
|
||||
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
|
||||
/>,
|
||||
});
|
||||
|
||||
|
|
|
@ -16,14 +16,12 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'ChangeAvatar',
|
||||
propTypes: {
|
||||
export default class ChangeAvatar extends React.Component {
|
||||
static propTypes = {
|
||||
initialAvatarUrl: PropTypes.string,
|
||||
room: PropTypes.object,
|
||||
// if false, you need to call changeAvatar.onFileSelected yourself.
|
||||
|
@ -31,36 +29,36 @@ export default createReactClass({
|
|||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
className: PropTypes.string,
|
||||
},
|
||||
};
|
||||
|
||||
Phases: {
|
||||
static Phases = {
|
||||
Display: "display",
|
||||
Uploading: "uploading",
|
||||
Error: "error",
|
||||
},
|
||||
};
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
showUploadSection: true,
|
||||
className: "",
|
||||
width: 80,
|
||||
height: 80,
|
||||
};
|
||||
},
|
||||
static defaultProps = {
|
||||
showUploadSection: true,
|
||||
className: "",
|
||||
width: 80,
|
||||
height: 80,
|
||||
};
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
avatarUrl: this.props.initialAvatarUrl,
|
||||
phase: this.Phases.Display,
|
||||
phase: ChangeAvatar.Phases.Display,
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
componentDidMount: function() {
|
||||
componentDidMount() {
|
||||
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps: function(newProps) {
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
if (this.avatarSet) {
|
||||
// don't clobber what the user has just set
|
||||
return;
|
||||
|
@ -68,15 +66,15 @@ export default createReactClass({
|
|||
this.setState({
|
||||
avatarUrl: newProps.initialAvatarUrl,
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
componentWillUnmount: function() {
|
||||
componentWillUnmount() {
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
onRoomStateEvents: function(ev) {
|
||||
onRoomStateEvents = (ev) => {
|
||||
if (!this.props.room) {
|
||||
return;
|
||||
}
|
||||
|
@ -90,13 +88,13 @@ export default createReactClass({
|
|||
this.avatarSet = false;
|
||||
this.setState({}); // force update
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
setAvatarFromFile: function(file) {
|
||||
setAvatarFromFile(file) {
|
||||
let newUrl = null;
|
||||
|
||||
this.setState({
|
||||
phase: this.Phases.Uploading,
|
||||
phase: ChangeAvatar.Phases.Uploading,
|
||||
});
|
||||
const self = this;
|
||||
const httpPromise = MatrixClientPeg.get().uploadContent(file).then(function(url) {
|
||||
|
@ -115,31 +113,31 @@ export default createReactClass({
|
|||
|
||||
httpPromise.then(function() {
|
||||
self.setState({
|
||||
phase: self.Phases.Display,
|
||||
phase: ChangeAvatar.Phases.Display,
|
||||
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl),
|
||||
});
|
||||
}, function(error) {
|
||||
self.setState({
|
||||
phase: self.Phases.Error,
|
||||
phase: ChangeAvatar.Phases.Error,
|
||||
});
|
||||
self.onError(error);
|
||||
});
|
||||
|
||||
return httpPromise;
|
||||
},
|
||||
}
|
||||
|
||||
onFileSelected: function(ev) {
|
||||
onFileSelected = (ev) => {
|
||||
this.avatarSet = true;
|
||||
return this.setAvatarFromFile(ev.target.files[0]);
|
||||
},
|
||||
};
|
||||
|
||||
onError: function(error) {
|
||||
onError = (error) => {
|
||||
this.setState({
|
||||
errorText: _t("Failed to upload profile picture!"),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
render: function() {
|
||||
render() {
|
||||
let avatarImg;
|
||||
// Having just set an avatar we just display that since it will take a little
|
||||
// time to propagate through to the RoomAvatar.
|
||||
|
@ -165,8 +163,8 @@ export default createReactClass({
|
|||
}
|
||||
|
||||
switch (this.state.phase) {
|
||||
case this.Phases.Display:
|
||||
case this.Phases.Error:
|
||||
case ChangeAvatar.Phases.Display:
|
||||
case ChangeAvatar.Phases.Error:
|
||||
return (
|
||||
<div>
|
||||
<div className={this.props.className}>
|
||||
|
@ -175,11 +173,11 @@ export default createReactClass({
|
|||
{ uploadSection }
|
||||
</div>
|
||||
);
|
||||
case this.Phases.Uploading:
|
||||
case ChangeAvatar.Phases.Uploading:
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
return (
|
||||
<Loader />
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,15 +17,12 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import * as sdk from '../../../index';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'ChangeDisplayName',
|
||||
|
||||
_getDisplayName: async function() {
|
||||
export default class ChangeDisplayName extends React.Component {
|
||||
_getDisplayName = async () => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
try {
|
||||
const res = await cli.getProfileInfo(cli.getUserId());
|
||||
|
@ -33,16 +30,16 @@ export default createReactClass({
|
|||
} catch (e) {
|
||||
throw new Error("Failed to fetch display name");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
_changeDisplayName: function(newDisplayname) {
|
||||
_changeDisplayName = (newDisplayname) => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
return cli.setDisplayName(newDisplayname).catch(function(e) {
|
||||
throw new Error("Failed to set display name", e);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
render: function() {
|
||||
render() {
|
||||
const EditableTextContainer = sdk.getComponent('elements.EditableTextContainer');
|
||||
return (
|
||||
<EditableTextContainer
|
||||
|
@ -51,5 +48,5 @@ export default createReactClass({
|
|||
blurToSubmit={true}
|
||||
onSubmit={this._changeDisplayName} />
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,88 +18,58 @@ limitations under the License.
|
|||
import Field from "../elements/Field";
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import Spinner from '../elements/Spinner';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from "../../../index";
|
||||
import Modal from "../../../Modal";
|
||||
|
||||
import sessionStore from '../../../stores/SessionStore';
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'ChangePassword',
|
||||
|
||||
propTypes: {
|
||||
export default class ChangePassword extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func,
|
||||
onError: PropTypes.func,
|
||||
onCheckPassword: PropTypes.func,
|
||||
rowClassName: PropTypes.string,
|
||||
buttonClassName: PropTypes.string,
|
||||
buttonKind: PropTypes.string,
|
||||
buttonLabel: PropTypes.string,
|
||||
confirm: PropTypes.bool,
|
||||
// Whether to autoFocus the new password input
|
||||
autoFocusNewPasswordInput: PropTypes.bool,
|
||||
},
|
||||
};
|
||||
|
||||
Phases: {
|
||||
static Phases = {
|
||||
Edit: "edit",
|
||||
Uploading: "uploading",
|
||||
Error: "error",
|
||||
},
|
||||
};
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onFinished: function() {},
|
||||
onError: function() {},
|
||||
onCheckPassword: function(oldPass, newPass, confirmPass) {
|
||||
if (newPass !== confirmPass) {
|
||||
return {
|
||||
error: _t("New passwords don't match"),
|
||||
};
|
||||
} else if (!newPass || newPass.length === 0) {
|
||||
return {
|
||||
error: _t("Passwords can't be empty"),
|
||||
};
|
||||
}
|
||||
},
|
||||
confirm: true,
|
||||
};
|
||||
},
|
||||
static defaultProps = {
|
||||
onFinished() {},
|
||||
onError() {},
|
||||
onCheckPassword(oldPass, newPass, confirmPass) {
|
||||
if (newPass !== confirmPass) {
|
||||
return {
|
||||
error: _t("New passwords don't match"),
|
||||
};
|
||||
} else if (!newPass || newPass.length === 0) {
|
||||
return {
|
||||
error: _t("Passwords can't be empty"),
|
||||
};
|
||||
}
|
||||
},
|
||||
confirm: true,
|
||||
}
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
phase: this.Phases.Edit,
|
||||
cachedPassword: null,
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
newPasswordConfirm: "",
|
||||
};
|
||||
},
|
||||
state = {
|
||||
phase: ChangePassword.Phases.Edit,
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
newPasswordConfirm: "",
|
||||
};
|
||||
|
||||
componentDidMount: function() {
|
||||
this._sessionStore = sessionStore;
|
||||
this._sessionStoreToken = this._sessionStore.addListener(
|
||||
this._setStateFromSessionStore,
|
||||
);
|
||||
|
||||
this._setStateFromSessionStore();
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
if (this._sessionStoreToken) {
|
||||
this._sessionStoreToken.remove();
|
||||
}
|
||||
},
|
||||
|
||||
_setStateFromSessionStore: function() {
|
||||
this.setState({
|
||||
cachedPassword: this._sessionStore.getCachedPassword(),
|
||||
});
|
||||
},
|
||||
|
||||
changePassword: function(oldPassword, newPassword) {
|
||||
changePassword(oldPassword, newPassword) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
if (!this.props.confirm) {
|
||||
|
@ -125,8 +95,11 @@ export default createReactClass({
|
|||
</div>,
|
||||
button: _t("Continue"),
|
||||
extraButtons: [
|
||||
<button className="mx_Dialog_primary"
|
||||
onClick={this._onExportE2eKeysClicked}>
|
||||
<button
|
||||
key="exportRoomKeys"
|
||||
className="mx_Dialog_primary"
|
||||
onClick={this._onExportE2eKeysClicked}
|
||||
>
|
||||
{ _t('Export E2E room keys') }
|
||||
</button>,
|
||||
],
|
||||
|
@ -136,9 +109,9 @@ export default createReactClass({
|
|||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
_changePassword: function(cli, oldPassword, newPassword) {
|
||||
_changePassword(cli, oldPassword, newPassword) {
|
||||
const authDict = {
|
||||
type: 'm.login.password',
|
||||
identifier: {
|
||||
|
@ -152,13 +125,10 @@ export default createReactClass({
|
|||
};
|
||||
|
||||
this.setState({
|
||||
phase: this.Phases.Uploading,
|
||||
phase: ChangePassword.Phases.Uploading,
|
||||
});
|
||||
|
||||
cli.setPassword(authDict, newPassword).then(() => {
|
||||
// Notify SessionStore that the user's password was changed
|
||||
dis.dispatch({action: 'password_changed'});
|
||||
|
||||
if (this.props.shouldAskForEmail) {
|
||||
return this._optionallySetEmail().then((confirmed) => {
|
||||
this.props.onFinished({
|
||||
|
@ -172,53 +142,53 @@ export default createReactClass({
|
|||
this.props.onError(err);
|
||||
}).finally(() => {
|
||||
this.setState({
|
||||
phase: this.Phases.Edit,
|
||||
phase: ChangePassword.Phases.Edit,
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
newPasswordConfirm: "",
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
_optionallySetEmail: function() {
|
||||
_optionallySetEmail() {
|
||||
// Ask for an email otherwise the user has no way to reset their password
|
||||
const SetEmailDialog = sdk.getComponent("dialogs.SetEmailDialog");
|
||||
const modal = Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, {
|
||||
title: _t('Do you want to set an email address?'),
|
||||
});
|
||||
return modal.finished.then(([confirmed]) => confirmed);
|
||||
},
|
||||
}
|
||||
|
||||
_onExportE2eKeysClicked: function() {
|
||||
_onExportE2eKeysClicked = () => {
|
||||
Modal.createTrackedDialogAsync('Export E2E Keys', 'Change Password',
|
||||
import('../../../async-components/views/dialogs/ExportE2eKeysDialog'),
|
||||
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
|
||||
{
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
onChangeOldPassword(ev) {
|
||||
onChangeOldPassword = (ev) => {
|
||||
this.setState({
|
||||
oldPassword: ev.target.value,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
onChangeNewPassword(ev) {
|
||||
onChangeNewPassword = (ev) => {
|
||||
this.setState({
|
||||
newPassword: ev.target.value,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
onChangeNewPasswordConfirm(ev) {
|
||||
onChangeNewPasswordConfirm = (ev) => {
|
||||
this.setState({
|
||||
newPasswordConfirm: ev.target.value,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
onClickChange: function(ev) {
|
||||
onClickChange = (ev) => {
|
||||
ev.preventDefault();
|
||||
const oldPassword = this.state.cachedPassword || this.state.oldPassword;
|
||||
const oldPassword = this.state.oldPassword;
|
||||
const newPassword = this.state.newPassword;
|
||||
const confirmPassword = this.state.newPasswordConfirm;
|
||||
const err = this.props.onCheckPassword(
|
||||
|
@ -229,39 +199,30 @@ export default createReactClass({
|
|||
} else {
|
||||
this.changePassword(oldPassword, newPassword);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
render: function() {
|
||||
render() {
|
||||
// TODO: Live validation on `new pw == confirm pw`
|
||||
|
||||
const rowClassName = this.props.rowClassName;
|
||||
const buttonClassName = this.props.buttonClassName;
|
||||
|
||||
let currentPassword = null;
|
||||
if (!this.state.cachedPassword) {
|
||||
currentPassword = (
|
||||
<div className={rowClassName}>
|
||||
<Field
|
||||
type="password"
|
||||
label={_t('Current password')}
|
||||
value={this.state.oldPassword}
|
||||
onChange={this.onChangeOldPassword}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
switch (this.state.phase) {
|
||||
case this.Phases.Edit:
|
||||
const passwordLabel = this.state.cachedPassword ?
|
||||
_t('Password') : _t('New Password');
|
||||
case ChangePassword.Phases.Edit:
|
||||
return (
|
||||
<form className={this.props.className} onSubmit={this.onClickChange}>
|
||||
{ currentPassword }
|
||||
<div className={rowClassName}>
|
||||
<Field
|
||||
type="password"
|
||||
label={passwordLabel}
|
||||
label={_t('Current password')}
|
||||
value={this.state.oldPassword}
|
||||
onChange={this.onChangeOldPassword}
|
||||
/>
|
||||
</div>
|
||||
<div className={rowClassName}>
|
||||
<Field
|
||||
type="password"
|
||||
label={_t('New Password')}
|
||||
value={this.state.newPassword}
|
||||
autoFocus={this.props.autoFocusNewPasswordInput}
|
||||
onChange={this.onChangeNewPassword}
|
||||
|
@ -278,17 +239,16 @@ export default createReactClass({
|
|||
/>
|
||||
</div>
|
||||
<AccessibleButton className={buttonClassName} kind={this.props.buttonKind} onClick={this.onClickChange}>
|
||||
{ _t('Change Password') }
|
||||
{ this.props.buttonLabel || _t('Change Password') }
|
||||
</AccessibleButton>
|
||||
</form>
|
||||
);
|
||||
case this.Phases.Uploading:
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
case ChangePassword.Phases.Uploading:
|
||||
return (
|
||||
<div className="mx_Dialog_content">
|
||||
<Loader />
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,8 +19,10 @@ import React from 'react';
|
|||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import { accessSecretStorage } from '../../../CrossSigningManager';
|
||||
import Modal from '../../../Modal';
|
||||
import Spinner from '../elements/Spinner';
|
||||
import InteractiveAuthDialog from '../dialogs/InteractiveAuthDialog';
|
||||
import ConfirmDestroyCrossSigningDialog from '../dialogs/security/ConfirmDestroyCrossSigningDialog';
|
||||
|
||||
export default class CrossSigningPanel extends React.PureComponent {
|
||||
constructor(props) {
|
||||
|
@ -30,13 +32,13 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
|
||||
this.state = {
|
||||
error: null,
|
||||
crossSigningPublicKeysOnDevice: false,
|
||||
crossSigningPrivateKeysInStorage: false,
|
||||
masterPrivateKeyCached: false,
|
||||
selfSigningPrivateKeyCached: false,
|
||||
userSigningPrivateKeyCached: false,
|
||||
sessionBackupKeyCached: false,
|
||||
secretStorageKeyInAccount: false,
|
||||
crossSigningPublicKeysOnDevice: null,
|
||||
crossSigningPrivateKeysInStorage: null,
|
||||
masterPrivateKeyCached: null,
|
||||
selfSigningPrivateKeyCached: null,
|
||||
userSigningPrivateKeyCached: null,
|
||||
homeserverSupportsCrossSigning: null,
|
||||
crossSigningReady: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -65,7 +67,7 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
};
|
||||
|
||||
_onBootstrapClick = () => {
|
||||
this._bootstrapSecureSecretStorage(false);
|
||||
this._bootstrapCrossSigning({ forceReset: false });
|
||||
};
|
||||
|
||||
onStatusChanged = () => {
|
||||
|
@ -82,10 +84,6 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master"));
|
||||
const selfSigningPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("self_signing"));
|
||||
const userSigningPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("user_signing"));
|
||||
const sessionBackupKeyFromCache = await cli._crypto.getSessionBackupPrivateKey();
|
||||
const sessionBackupKeyCached = !!(sessionBackupKeyFromCache);
|
||||
const sessionBackupKeyWellFormed = sessionBackupKeyFromCache instanceof Uint8Array;
|
||||
const secretStorageKeyInAccount = await secretStorage.hasKey();
|
||||
const homeserverSupportsCrossSigning =
|
||||
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
|
||||
const crossSigningReady = await cli.isCrossSigningReady();
|
||||
|
@ -96,44 +94,55 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
masterPrivateKeyCached,
|
||||
selfSigningPrivateKeyCached,
|
||||
userSigningPrivateKeyCached,
|
||||
sessionBackupKeyCached,
|
||||
sessionBackupKeyWellFormed,
|
||||
secretStorageKeyInAccount,
|
||||
homeserverSupportsCrossSigning,
|
||||
crossSigningReady,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrapping secret storage may take one of these paths:
|
||||
* 1. Create secret storage from a passphrase and store cross-signing keys
|
||||
* in secret storage.
|
||||
* Bootstrapping cross-signing take one of these paths:
|
||||
* 1. Create cross-signing keys locally and store in secret storage (if it
|
||||
* already exists on the account).
|
||||
* 2. Access existing secret storage by requesting passphrase and accessing
|
||||
* cross-signing keys as needed.
|
||||
* 3. All keys are loaded and there's nothing to do.
|
||||
* @param {bool} [forceReset] Bootstrap again even if keys already present
|
||||
*/
|
||||
_bootstrapSecureSecretStorage = async (forceReset=false) => {
|
||||
_bootstrapCrossSigning = async ({ forceReset = false }) => {
|
||||
this.setState({ error: null });
|
||||
try {
|
||||
await accessSecretStorage(() => undefined, forceReset);
|
||||
const cli = MatrixClientPeg.get();
|
||||
await cli.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async (makeRequest) => {
|
||||
const { finished } = Modal.createTrackedDialog(
|
||||
'Cross-signing keys dialog', '', InteractiveAuthDialog,
|
||||
{
|
||||
title: _t("Setting up keys"),
|
||||
matrixClient: cli,
|
||||
makeRequest,
|
||||
},
|
||||
);
|
||||
const [confirmed] = await finished;
|
||||
if (!confirmed) {
|
||||
throw new Error("Cross-signing key upload auth canceled");
|
||||
}
|
||||
},
|
||||
setupNewCrossSigning: forceReset,
|
||||
});
|
||||
} catch (e) {
|
||||
this.setState({ error: e });
|
||||
console.error("Error bootstrapping secret storage", e);
|
||||
console.error("Error bootstrapping cross-signing", e);
|
||||
}
|
||||
if (this._unmounted) return;
|
||||
this._getUpdatedStatus();
|
||||
}
|
||||
|
||||
onDestroyStorage = (act) => {
|
||||
if (!act) return;
|
||||
this._bootstrapSecureSecretStorage(true);
|
||||
}
|
||||
|
||||
_destroySecureSecretStorage = () => {
|
||||
const ConfirmDestroyCrossSigningDialog = sdk.getComponent("dialogs.ConfirmDestroyCrossSigningDialog");
|
||||
_resetCrossSigning = () => {
|
||||
Modal.createDialog(ConfirmDestroyCrossSigningDialog, {
|
||||
onFinished: this.onDestroyStorage,
|
||||
onFinished: (act) => {
|
||||
if (!act) return;
|
||||
this._bootstrapCrossSigning({ forceReset: true });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -146,9 +155,6 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
masterPrivateKeyCached,
|
||||
selfSigningPrivateKeyCached,
|
||||
userSigningPrivateKeyCached,
|
||||
sessionBackupKeyCached,
|
||||
sessionBackupKeyWellFormed,
|
||||
secretStorageKeyInAccount,
|
||||
homeserverSupportsCrossSigning,
|
||||
crossSigningReady,
|
||||
} = this.state;
|
||||
|
@ -160,69 +166,65 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
|
||||
let summarisedStatus;
|
||||
if (homeserverSupportsCrossSigning === undefined) {
|
||||
const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner');
|
||||
summarisedStatus = <p><InlineSpinner /></p>;
|
||||
summarisedStatus = <Spinner />;
|
||||
} else if (!homeserverSupportsCrossSigning) {
|
||||
summarisedStatus = <p>{_t(
|
||||
"Your homeserver does not support cross-signing.",
|
||||
)}</p>;
|
||||
} else if (crossSigningReady) {
|
||||
summarisedStatus = <p>✅ {_t(
|
||||
"Cross-signing and secret storage are enabled.",
|
||||
"Cross-signing is ready for use.",
|
||||
)}</p>;
|
||||
} else if (crossSigningPrivateKeysInStorage) {
|
||||
summarisedStatus = <p>{_t(
|
||||
"Your account has a cross-signing identity in secret storage, but it " +
|
||||
"is not yet trusted by this session.",
|
||||
"Your account has a cross-signing identity in secret storage, " +
|
||||
"but it is not yet trusted by this session.",
|
||||
)}</p>;
|
||||
} else {
|
||||
summarisedStatus = <p>{_t(
|
||||
"Cross-signing and secret storage are not yet set up.",
|
||||
"Cross-signing is not set up.",
|
||||
)}</p>;
|
||||
}
|
||||
|
||||
const keysExistAnywhere = (
|
||||
secretStorageKeyInAccount ||
|
||||
crossSigningPublicKeysOnDevice ||
|
||||
crossSigningPrivateKeysInStorage ||
|
||||
crossSigningPublicKeysOnDevice
|
||||
masterPrivateKeyCached ||
|
||||
selfSigningPrivateKeyCached ||
|
||||
userSigningPrivateKeyCached
|
||||
);
|
||||
const keysExistEverywhere = (
|
||||
secretStorageKeyInAccount &&
|
||||
crossSigningPublicKeysOnDevice &&
|
||||
crossSigningPrivateKeysInStorage &&
|
||||
crossSigningPublicKeysOnDevice
|
||||
masterPrivateKeyCached &&
|
||||
selfSigningPrivateKeyCached &&
|
||||
userSigningPrivateKeyCached
|
||||
);
|
||||
|
||||
let resetButton;
|
||||
if (keysExistAnywhere) {
|
||||
resetButton = (
|
||||
<div className="mx_CrossSigningPanel_buttonRow">
|
||||
<AccessibleButton kind="danger" onClick={this._destroySecureSecretStorage}>
|
||||
{_t("Reset cross-signing and secret storage")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const actions = [];
|
||||
|
||||
// TODO: determine how better to expose this to users in addition to prompts at login/toast
|
||||
let bootstrapButton;
|
||||
if (!keysExistEverywhere && homeserverSupportsCrossSigning) {
|
||||
bootstrapButton = (
|
||||
<div className="mx_CrossSigningPanel_buttonRow">
|
||||
<AccessibleButton kind="primary" onClick={this._onBootstrapClick}>
|
||||
{_t("Bootstrap cross-signing and secret storage")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
actions.push(
|
||||
<AccessibleButton key="setup" kind="primary" onClick={this._onBootstrapClick}>
|
||||
{_t("Set up")}
|
||||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
|
||||
let sessionBackupKeyWellFormedText = "";
|
||||
if (sessionBackupKeyCached) {
|
||||
sessionBackupKeyWellFormedText = ", ";
|
||||
if (sessionBackupKeyWellFormed) {
|
||||
sessionBackupKeyWellFormedText += _t("well formed");
|
||||
} else {
|
||||
sessionBackupKeyWellFormedText += _t("unexpected type");
|
||||
}
|
||||
if (keysExistAnywhere) {
|
||||
actions.push(
|
||||
<AccessibleButton key="reset" kind="danger" onClick={this._resetCrossSigning}>
|
||||
{_t("Reset")}
|
||||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
|
||||
let actionRow;
|
||||
if (actions.length) {
|
||||
actionRow = <div className="mx_CrossSigningPanel_buttonRow">
|
||||
{actions}
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -237,7 +239,7 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
</tr>
|
||||
<tr>
|
||||
<td>{_t("Cross-signing private keys:")}</td>
|
||||
<td>{crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found")}</td>
|
||||
<td>{crossSigningPrivateKeysInStorage ? _t("in secret storage") : _t("not found in storage")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_t("Master private key:")}</td>
|
||||
|
@ -251,17 +253,6 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
<td>{_t("User signing private key:")}</td>
|
||||
<td>{userSigningPrivateKeyCached ? _t("cached locally") : _t("not found locally")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_t("Session backup key:")}</td>
|
||||
<td>
|
||||
{sessionBackupKeyCached ? _t("cached locally") : _t("not found locally")}
|
||||
{sessionBackupKeyWellFormedText}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_t("Secret storage public key:")}</td>
|
||||
<td>{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_t("Homeserver feature support:")}</td>
|
||||
<td>{homeserverSupportsCrossSigning ? _t("exists") : _t("not found")}</td>
|
||||
|
@ -269,8 +260,7 @@ export default class CrossSigningPanel extends React.PureComponent {
|
|||
</tbody></table>
|
||||
</details>
|
||||
{errorSection}
|
||||
{bootstrapButton}
|
||||
{resetButton}
|
||||
{actionRow}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -19,13 +19,14 @@ import React from 'react';
|
|||
import * as sdk from '../../../index';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {SettingLevel} from "../../../settings/SettingLevel";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
const SETTING_MANUALLY_VERIFY_ALL_SESSIONS = "e2ee.manuallyVerifyAllSessions";
|
||||
|
||||
const E2eAdvancedPanel = props => {
|
||||
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
|
||||
return <div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Advanced")}</span>
|
||||
<span className="mx_SettingsTab_subheading">{_t("Encryption")}</span>
|
||||
|
||||
<SettingsFlag name={SETTING_MANUALLY_VERIFY_ALL_SESSIONS}
|
||||
level={SettingLevel.DEVICE}
|
||||
|
@ -37,3 +38,7 @@ const E2eAdvancedPanel = props => {
|
|||
};
|
||||
|
||||
export default E2eAdvancedPanel;
|
||||
|
||||
export function isE2eAdvancedPanelPossible(): boolean {
|
||||
return SettingsStore.isEnabled(SETTING_MANUALLY_VERIFY_ALL_SESSIONS);
|
||||
}
|
||||
|
|
|
@ -42,6 +42,14 @@ export default class IntegrationManager extends React.Component {
|
|||
loading: false,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
errored: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
document.addEventListener("keydown", this.onKeyDown);
|
||||
|
@ -66,6 +74,10 @@ export default class IntegrationManager extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
onError = () => {
|
||||
this.setState({ errored: true });
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.props.loading) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
|
@ -77,7 +89,7 @@ export default class IntegrationManager extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
if (!this.props.connected) {
|
||||
if (!this.props.connected || this.state.errored) {
|
||||
return (
|
||||
<div className='mx_IntegrationManager_error'>
|
||||
<h3>{_t("Cannot connect to integration manager")}</h3>
|
||||
|
@ -86,6 +98,6 @@ export default class IntegrationManager extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
return <iframe src={this.props.url}></iframe>;
|
||||
return <iframe src={this.props.url} onError={this.onError} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
|
@ -65,46 +64,42 @@ function portLegacyActions(actions) {
|
|||
}
|
||||
}
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'Notifications',
|
||||
|
||||
phases: {
|
||||
export default class Notifications extends React.Component {
|
||||
static phases = {
|
||||
LOADING: "LOADING", // The component is loading or sending data to the hs
|
||||
DISPLAY: "DISPLAY", // The component is ready and display data
|
||||
ERROR: "ERROR", // There was an error
|
||||
},
|
||||
};
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
phase: this.phases.LOADING,
|
||||
masterPushRule: undefined, // The master rule ('.m.rule.master')
|
||||
vectorPushRules: [], // HS default push rules displayed in Vector UI
|
||||
vectorContentRules: { // Keyword push rules displayed in Vector UI
|
||||
vectorState: PushRuleVectorState.ON,
|
||||
rules: [],
|
||||
},
|
||||
externalPushRules: [], // Push rules (except content rule) that have been defined outside Vector UI
|
||||
externalContentRules: [], // Keyword push rules that have been defined outside Vector UI
|
||||
threepids: [], // used for email notifications
|
||||
};
|
||||
},
|
||||
state = {
|
||||
phase: Notifications.phases.LOADING,
|
||||
masterPushRule: undefined, // The master rule ('.m.rule.master')
|
||||
vectorPushRules: [], // HS default push rules displayed in Vector UI
|
||||
vectorContentRules: { // Keyword push rules displayed in Vector UI
|
||||
vectorState: PushRuleVectorState.ON,
|
||||
rules: [],
|
||||
},
|
||||
externalPushRules: [], // Push rules (except content rule) that have been defined outside Vector UI
|
||||
externalContentRules: [], // Keyword push rules that have been defined outside Vector UI
|
||||
threepids: [], // used for email notifications
|
||||
};
|
||||
|
||||
componentDidMount: function() {
|
||||
componentDidMount() {
|
||||
this._refreshFromServer();
|
||||
},
|
||||
}
|
||||
|
||||
onEnableNotificationsChange: function(checked) {
|
||||
onEnableNotificationsChange = (checked) => {
|
||||
const self = this;
|
||||
this.setState({
|
||||
phase: this.phases.LOADING,
|
||||
phase: Notifications.phases.LOADING,
|
||||
});
|
||||
|
||||
MatrixClientPeg.get().setPushRuleEnabled('global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked).then(function() {
|
||||
self._refreshFromServer();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
onEnableDesktopNotificationsChange: function(checked) {
|
||||
onEnableDesktopNotificationsChange = (checked) => {
|
||||
SettingsStore.setValue(
|
||||
"notificationsEnabled", null,
|
||||
SettingLevel.DEVICE,
|
||||
|
@ -112,9 +107,9 @@ export default createReactClass({
|
|||
).finally(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
onEnableDesktopNotificationBodyChange: function(checked) {
|
||||
onEnableDesktopNotificationBodyChange = (checked) => {
|
||||
SettingsStore.setValue(
|
||||
"notificationBodyEnabled", null,
|
||||
SettingLevel.DEVICE,
|
||||
|
@ -122,9 +117,9 @@ export default createReactClass({
|
|||
).finally(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
onEnableAudioNotificationsChange: function(checked) {
|
||||
onEnableAudioNotificationsChange = (checked) => {
|
||||
SettingsStore.setValue(
|
||||
"audioNotificationsEnabled", null,
|
||||
SettingLevel.DEVICE,
|
||||
|
@ -132,7 +127,7 @@ export default createReactClass({
|
|||
).finally(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
* Returns the email pusher (pusher of type 'email') for a given
|
||||
|
@ -140,7 +135,7 @@ export default createReactClass({
|
|||
* pushers are unique over (app ID, pushkey), there will be at most
|
||||
* one such pusher.
|
||||
*/
|
||||
getEmailPusher: function(pushers, address) {
|
||||
getEmailPusher(pushers, address) {
|
||||
if (pushers === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -150,9 +145,9 @@ export default createReactClass({
|
|||
}
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
}
|
||||
|
||||
onEnableEmailNotificationsChange: function(address, checked) {
|
||||
onEnableEmailNotificationsChange = (address, checked) => {
|
||||
let emailPusherPromise;
|
||||
if (checked) {
|
||||
const data = {};
|
||||
|
@ -181,9 +176,9 @@ export default createReactClass({
|
|||
description: _t('An error occurred whilst saving your email notification preferences.'),
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
onNotifStateButtonClicked: function(event) {
|
||||
onNotifStateButtonClicked = (event) => {
|
||||
// FIXME: use .bind() rather than className metadata here surely
|
||||
const vectorRuleId = event.target.className.split("-")[0];
|
||||
const newPushRuleVectorState = event.target.className.split("-")[1];
|
||||
|
@ -196,11 +191,9 @@ export default createReactClass({
|
|||
this._setPushRuleVectorState(rule, newPushRuleVectorState);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onKeywordsClicked: function(event) {
|
||||
const self = this;
|
||||
};
|
||||
|
||||
onKeywordsClicked = (event) => {
|
||||
// Compute the keywords list to display
|
||||
let keywords = [];
|
||||
for (const i in this.state.vectorContentRules.rules) {
|
||||
|
@ -223,7 +216,7 @@ export default createReactClass({
|
|||
description: _t('Enter keywords separated by a comma:'),
|
||||
button: _t('OK'),
|
||||
value: keywords,
|
||||
onFinished: function onFinished(should_leave, newValue) {
|
||||
onFinished: (should_leave, newValue) => {
|
||||
if (should_leave && newValue !== keywords) {
|
||||
let newKeywords = newValue.split(',');
|
||||
for (const i in newKeywords) {
|
||||
|
@ -238,25 +231,25 @@ export default createReactClass({
|
|||
return array;
|
||||
}, []);
|
||||
|
||||
self._setKeywords(newKeywords);
|
||||
this._setKeywords(newKeywords);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
getRule: function(vectorRuleId) {
|
||||
getRule(vectorRuleId) {
|
||||
for (const i in this.state.vectorPushRules) {
|
||||
const rule = this.state.vectorPushRules[i];
|
||||
if (rule.vectorRuleId === vectorRuleId) {
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_setPushRuleVectorState: function(rule, newPushRuleVectorState) {
|
||||
_setPushRuleVectorState(rule, newPushRuleVectorState) {
|
||||
if (rule && rule.vectorState !== newPushRuleVectorState) {
|
||||
this.setState({
|
||||
phase: this.phases.LOADING,
|
||||
phase: Notifications.phases.LOADING,
|
||||
});
|
||||
|
||||
const self = this;
|
||||
|
@ -288,9 +281,9 @@ export default createReactClass({
|
|||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_setKeywordsPushRuleVectorState: function(newPushRuleVectorState) {
|
||||
_setKeywordsPushRuleVectorState(newPushRuleVectorState) {
|
||||
// Is there really a change?
|
||||
if (this.state.vectorContentRules.vectorState === newPushRuleVectorState
|
||||
|| this.state.vectorContentRules.rules.length === 0) {
|
||||
|
@ -301,7 +294,7 @@ export default createReactClass({
|
|||
const cli = MatrixClientPeg.get();
|
||||
|
||||
this.setState({
|
||||
phase: this.phases.LOADING,
|
||||
phase: Notifications.phases.LOADING,
|
||||
});
|
||||
|
||||
// Update all rules in self.state.vectorContentRules
|
||||
|
@ -356,11 +349,11 @@ export default createReactClass({
|
|||
onFinished: self._refreshFromServer,
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
_setKeywords: function(newKeywords) {
|
||||
_setKeywords(newKeywords) {
|
||||
this.setState({
|
||||
phase: this.phases.LOADING,
|
||||
phase: Notifications.phases.LOADING,
|
||||
});
|
||||
|
||||
const self = this;
|
||||
|
@ -440,19 +433,19 @@ export default createReactClass({
|
|||
self._refreshFromServer();
|
||||
}, onError);
|
||||
}, onError);
|
||||
},
|
||||
}
|
||||
|
||||
// Create a push rule but disabled
|
||||
_addDisabledPushRule: function(scope, kind, ruleId, body) {
|
||||
_addDisabledPushRule(scope, kind, ruleId, body) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
return cli.addPushRule(scope, kind, ruleId, body).then(() =>
|
||||
cli.setPushRuleEnabled(scope, kind, ruleId, false),
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
// Check if any legacy im.vector rules need to be ported to the new API
|
||||
// for overriding the actions of default rules.
|
||||
_portRulesToNewAPI: function(rulesets) {
|
||||
_portRulesToNewAPI(rulesets) {
|
||||
const needsUpdate = [];
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
|
@ -485,9 +478,9 @@ export default createReactClass({
|
|||
// Otherwise return the rules that we already have.
|
||||
return rulesets;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_refreshFromServer: function() {
|
||||
_refreshFromServer = () => {
|
||||
const self = this;
|
||||
const pushRulesPromise = MatrixClientPeg.get().getPushRules().then(self._portRulesToNewAPI).then(function(rulesets) {
|
||||
/// XXX seriously? wtf is this?
|
||||
|
@ -636,12 +629,12 @@ export default createReactClass({
|
|||
|
||||
Promise.all([pushRulesPromise, pushersPromise]).then(function() {
|
||||
self.setState({
|
||||
phase: self.phases.DISPLAY,
|
||||
phase: Notifications.phases.DISPLAY,
|
||||
});
|
||||
}, function(error) {
|
||||
console.error(error);
|
||||
self.setState({
|
||||
phase: self.phases.ERROR,
|
||||
phase: Notifications.phases.ERROR,
|
||||
});
|
||||
}).finally(() => {
|
||||
// actually explicitly update our state having been deep-manipulating it
|
||||
|
@ -655,9 +648,9 @@ export default createReactClass({
|
|||
});
|
||||
|
||||
MatrixClientPeg.get().getThreePids().then((r) => this.setState({threepids: r.threepids}));
|
||||
},
|
||||
};
|
||||
|
||||
_onClearNotifications: function() {
|
||||
_onClearNotifications = () => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
cli.getRooms().forEach(r => {
|
||||
|
@ -666,9 +659,9 @@ export default createReactClass({
|
|||
if (events.length) cli.sendReadReceipt(events.pop());
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
_updatePushRuleActions: function(rule, actions, enabled) {
|
||||
_updatePushRuleActions(rule, actions, enabled) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
return cli.setPushRuleActions(
|
||||
|
@ -681,9 +674,9 @@ export default createReactClass({
|
|||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
renderNotifRulesTableRow: function(title, className, pushRuleVectorState) {
|
||||
renderNotifRulesTableRow(title, className, pushRuleVectorState) {
|
||||
return (
|
||||
<tr key={ className }>
|
||||
<th>
|
||||
|
@ -712,9 +705,9 @@ export default createReactClass({
|
|||
</th>
|
||||
</tr>
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
renderNotifRulesTableRows: function() {
|
||||
renderNotifRulesTableRows() {
|
||||
const rows = [];
|
||||
for (const i in this.state.vectorPushRules) {
|
||||
const rule = this.state.vectorPushRules[i];
|
||||
|
@ -726,9 +719,9 @@ export default createReactClass({
|
|||
rows.push(this.renderNotifRulesTableRow(rule.description, rule.vectorRuleId, rule.vectorState));
|
||||
}
|
||||
return rows;
|
||||
},
|
||||
}
|
||||
|
||||
hasEmailPusher: function(pushers, address) {
|
||||
hasEmailPusher(pushers, address) {
|
||||
if (pushers === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
@ -738,17 +731,17 @@ export default createReactClass({
|
|||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
}
|
||||
|
||||
emailNotificationsRow: function(address, label) {
|
||||
emailNotificationsRow(address, label) {
|
||||
return <LabelledToggleSwitch value={this.hasEmailPusher(this.state.pushers, address)}
|
||||
onChange={this.onEnableEmailNotificationsChange.bind(this, address)}
|
||||
label={label} key={`emailNotif_${label}`} />;
|
||||
},
|
||||
}
|
||||
|
||||
render: function() {
|
||||
render() {
|
||||
let spinner;
|
||||
if (this.state.phase === this.phases.LOADING) {
|
||||
if (this.state.phase === Notifications.phases.LOADING) {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
spinner = <Loader />;
|
||||
}
|
||||
|
@ -910,5 +903,5 @@ export default createReactClass({
|
|||
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,30 +18,23 @@ import React, {createRef} from 'react';
|
|||
import {_t} from "../../../languageHandler";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import Field from "../elements/Field";
|
||||
import {User} from "matrix-js-sdk";
|
||||
import { getHostingLink } from '../../../utils/HostingLink';
|
||||
import * as sdk from "../../../index";
|
||||
import {OwnProfileStore} from "../../../stores/OwnProfileStore";
|
||||
import Modal from "../../../Modal";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
|
||||
export default class ProfileSettings extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
let user = client.getUser(client.getUserId());
|
||||
if (!user) {
|
||||
// XXX: We shouldn't have to do this.
|
||||
// There seems to be a condition where the User object won't exist until a room
|
||||
// exists on the account. To work around this, we'll just create a temporary User
|
||||
// and use that.
|
||||
console.warn("User object not found - creating one for ProfileSettings");
|
||||
user = new User(client.getUserId());
|
||||
}
|
||||
let avatarUrl = user.avatarUrl;
|
||||
let avatarUrl = OwnProfileStore.instance.avatarMxc;
|
||||
if (avatarUrl) avatarUrl = client.mxcUrlToHttp(avatarUrl, 96, 96, 'crop', false);
|
||||
this.state = {
|
||||
userId: user.userId,
|
||||
originalDisplayName: user.rawDisplayName,
|
||||
displayName: user.rawDisplayName,
|
||||
userId: client.getUserId(),
|
||||
originalDisplayName: OwnProfileStore.instance.displayName,
|
||||
displayName: OwnProfileStore.instance.displayName,
|
||||
originalAvatarUrl: avatarUrl,
|
||||
avatarUrl: avatarUrl,
|
||||
avatarFile: null,
|
||||
|
@ -65,6 +58,15 @@ export default class ProfileSettings extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_clearProfile = async (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.state.enableProfileSave) return;
|
||||
this._removeAvatar();
|
||||
this.setState({enableProfileSave: false, displayName: this.state.originalDisplayName});
|
||||
};
|
||||
|
||||
_saveProfile = async (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
@ -75,21 +77,26 @@ export default class ProfileSettings extends React.Component {
|
|||
const client = MatrixClientPeg.get();
|
||||
const newState = {};
|
||||
|
||||
// TODO: What do we do about errors?
|
||||
try {
|
||||
if (this.state.originalDisplayName !== this.state.displayName) {
|
||||
await client.setDisplayName(this.state.displayName);
|
||||
newState.originalDisplayName = this.state.displayName;
|
||||
}
|
||||
|
||||
if (this.state.originalDisplayName !== this.state.displayName) {
|
||||
await client.setDisplayName(this.state.displayName);
|
||||
newState.originalDisplayName = this.state.displayName;
|
||||
}
|
||||
|
||||
if (this.state.avatarFile) {
|
||||
const uri = await client.uploadContent(this.state.avatarFile);
|
||||
await client.setAvatarUrl(uri);
|
||||
newState.avatarUrl = client.mxcUrlToHttp(uri, 96, 96, 'crop', false);
|
||||
newState.originalAvatarUrl = newState.avatarUrl;
|
||||
newState.avatarFile = null;
|
||||
} else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {
|
||||
await client.setAvatarUrl(""); // use empty string as Synapse 500s on undefined
|
||||
if (this.state.avatarFile) {
|
||||
const uri = await client.uploadContent(this.state.avatarFile);
|
||||
await client.setAvatarUrl(uri);
|
||||
newState.avatarUrl = client.mxcUrlToHttp(uri, 96, 96, 'crop', false);
|
||||
newState.originalAvatarUrl = newState.avatarUrl;
|
||||
newState.avatarFile = null;
|
||||
} else if (this.state.originalAvatarUrl !== this.state.avatarUrl) {
|
||||
await client.setAvatarUrl(""); // use empty string as Synapse 500s on undefined
|
||||
}
|
||||
} catch (err) {
|
||||
Modal.createTrackedDialog('Failed to save profile', '', ErrorDialog, {
|
||||
title: _t("Failed to save your profile"),
|
||||
description: ((err && err.message) ? err.message : _t("The operation could not be completed")),
|
||||
});
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
|
@ -144,18 +151,27 @@ export default class ProfileSettings extends React.Component {
|
|||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const AvatarSetting = sdk.getComponent('settings.AvatarSetting');
|
||||
return (
|
||||
<form onSubmit={this._saveProfile} autoComplete="off" noValidate={true}>
|
||||
<form
|
||||
onSubmit={this._saveProfile}
|
||||
autoComplete="off"
|
||||
noValidate={true}
|
||||
className="mx_ProfileSettings_profileForm"
|
||||
>
|
||||
<input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
|
||||
onChange={this._onAvatarChanged} accept="image/*" />
|
||||
<div className="mx_ProfileSettings_profile">
|
||||
<div className="mx_ProfileSettings_controls">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Profile")}</span>
|
||||
<Field
|
||||
label={_t("Display Name")}
|
||||
type="text" value={this.state.displayName}
|
||||
autoComplete="off"
|
||||
onChange={this._onDisplayNameChanged}
|
||||
/>
|
||||
<p>
|
||||
{this.state.userId}
|
||||
{hostingSignup}
|
||||
</p>
|
||||
<Field label={_t("Display Name")}
|
||||
type="text" value={this.state.displayName} autoComplete="off"
|
||||
onChange={this._onDisplayNameChanged} />
|
||||
</div>
|
||||
<AvatarSetting
|
||||
avatarUrl={this.state.avatarUrl}
|
||||
|
@ -164,10 +180,22 @@ export default class ProfileSettings extends React.Component {
|
|||
uploadAvatar={this._uploadAvatar}
|
||||
removeAvatar={this._removeAvatar} />
|
||||
</div>
|
||||
<AccessibleButton onClick={this._saveProfile} kind="primary"
|
||||
disabled={!this.state.enableProfileSave}>
|
||||
{_t("Save")}
|
||||
</AccessibleButton>
|
||||
<div className="mx_ProfileSettings_buttons">
|
||||
<AccessibleButton
|
||||
onClick={this._clearProfile}
|
||||
kind="link"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
>
|
||||
{_t("Cancel")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
onClick={this._saveProfile}
|
||||
kind="primary"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
>
|
||||
{_t("Save")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,12 +17,17 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
import { isSecureBackupRequired } from '../../../utils/WellKnownUtils';
|
||||
import Spinner from '../elements/Spinner';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import QuestionDialog from '../dialogs/QuestionDialog';
|
||||
import RestoreKeyBackupDialog from '../dialogs/security/RestoreKeyBackupDialog';
|
||||
import { accessSecretStorage } from '../../../SecurityManager';
|
||||
|
||||
export default class KeyBackupPanel extends React.PureComponent {
|
||||
export default class SecureBackupPanel extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
@ -30,9 +35,13 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
this.state = {
|
||||
loading: true,
|
||||
error: null,
|
||||
backupKeyStored: null,
|
||||
backupKeyCached: null,
|
||||
backupKeyWellFormed: null,
|
||||
secretStorageKeyInAccount: null,
|
||||
secretStorageReady: null,
|
||||
backupInfo: null,
|
||||
backupSigStatus: null,
|
||||
backupKeyStored: null,
|
||||
sessionsRemaining: 0,
|
||||
};
|
||||
}
|
||||
|
@ -72,59 +81,76 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
}
|
||||
|
||||
async _checkKeyBackupStatus() {
|
||||
this._getUpdatedDiagnostics();
|
||||
try {
|
||||
const {backupInfo, trustInfo} = await MatrixClientPeg.get().checkKeyBackup();
|
||||
const backupKeyStored = Boolean(await MatrixClientPeg.get().isKeyBackupKeyStored());
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: null,
|
||||
backupInfo,
|
||||
backupSigStatus: trustInfo,
|
||||
backupKeyStored,
|
||||
error: null,
|
||||
loading: false,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Unable to fetch check backup status", e);
|
||||
if (this._unmounted) return;
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: e,
|
||||
backupInfo: null,
|
||||
backupSigStatus: null,
|
||||
backupKeyStored: null,
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async _loadBackupStatus() {
|
||||
this.setState({loading: true});
|
||||
this.setState({ loading: true });
|
||||
this._getUpdatedDiagnostics();
|
||||
try {
|
||||
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||
const backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo);
|
||||
const backupKeyStored = await MatrixClientPeg.get().isKeyBackupKeyStored();
|
||||
if (this._unmounted) return;
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: null,
|
||||
backupInfo,
|
||||
backupSigStatus,
|
||||
backupKeyStored,
|
||||
loading: false,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Unable to fetch key backup status", e);
|
||||
if (this._unmounted) return;
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: e,
|
||||
backupInfo: null,
|
||||
backupSigStatus: null,
|
||||
backupKeyStored: null,
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async _getUpdatedDiagnostics() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const secretStorage = cli._crypto._secretStorage;
|
||||
|
||||
const backupKeyStored = !!(await cli.isKeyBackupKeyStored());
|
||||
const backupKeyFromCache = await cli._crypto.getSessionBackupPrivateKey();
|
||||
const backupKeyCached = !!(backupKeyFromCache);
|
||||
const backupKeyWellFormed = backupKeyFromCache instanceof Uint8Array;
|
||||
const secretStorageKeyInAccount = await secretStorage.hasKey();
|
||||
const secretStorageReady = await cli.isSecretStorageReady();
|
||||
|
||||
if (this._unmounted) return;
|
||||
this.setState({
|
||||
backupKeyStored,
|
||||
backupKeyCached,
|
||||
backupKeyWellFormed,
|
||||
secretStorageKeyInAccount,
|
||||
secretStorageReady,
|
||||
});
|
||||
}
|
||||
|
||||
_startNewBackup = () => {
|
||||
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
|
||||
import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
|
||||
import('../../../async-components/views/dialogs/security/CreateKeyBackupDialog'),
|
||||
{
|
||||
onFinished: () => {
|
||||
this._loadBackupStatus();
|
||||
|
@ -134,7 +160,6 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
}
|
||||
|
||||
_deleteBackup = () => {
|
||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||
Modal.createTrackedDialog('Delete Backup', '', QuestionDialog, {
|
||||
title: _t('Delete Backup'),
|
||||
description: _t(
|
||||
|
@ -154,41 +179,58 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
}
|
||||
|
||||
_restoreBackup = async () => {
|
||||
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
||||
Modal.createTrackedDialog(
|
||||
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
|
||||
/* priority = */ false, /* static = */ true,
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
const encryptedMessageAreEncrypted = _t(
|
||||
"Encrypted messages are secured with end-to-end encryption. " +
|
||||
"Only you and the recipient(s) have the keys to read these messages.",
|
||||
);
|
||||
_resetSecretStorage = async () => {
|
||||
this.setState({ error: null });
|
||||
try {
|
||||
await accessSecretStorage(() => { }, /* forceReset = */ true);
|
||||
} catch (e) {
|
||||
console.error("Error resetting secret storage", e);
|
||||
if (this._unmounted) return;
|
||||
this.setState({ error: e });
|
||||
}
|
||||
if (this._unmounted) return;
|
||||
this._loadBackupStatus();
|
||||
}
|
||||
|
||||
if (this.state.error) {
|
||||
return (
|
||||
render() {
|
||||
const {
|
||||
loading,
|
||||
error,
|
||||
backupKeyStored,
|
||||
backupKeyCached,
|
||||
backupKeyWellFormed,
|
||||
secretStorageKeyInAccount,
|
||||
secretStorageReady,
|
||||
backupInfo,
|
||||
backupSigStatus,
|
||||
sessionsRemaining,
|
||||
} = this.state;
|
||||
|
||||
let statusDescription;
|
||||
let extraDetailsTableRows;
|
||||
let extraDetails;
|
||||
const actions = [];
|
||||
if (error) {
|
||||
statusDescription = (
|
||||
<div className="error">
|
||||
{_t("Unable to load key backup status")}
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.loading) {
|
||||
return <Spinner />;
|
||||
} else if (this.state.backupInfo) {
|
||||
let clientBackupStatus;
|
||||
} else if (loading) {
|
||||
statusDescription = <Spinner />;
|
||||
} else if (backupInfo) {
|
||||
let restoreButtonCaption = _t("Restore from Backup");
|
||||
|
||||
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
|
||||
clientBackupStatus = <div>
|
||||
<p>{encryptedMessageAreEncrypted}</p>
|
||||
<p>✅ {_t("This session is backing up your keys. ")}</p>
|
||||
</div>;
|
||||
statusDescription = <p>✅ {_t("This session is backing up your keys. ")}</p>;
|
||||
} else {
|
||||
clientBackupStatus = <div>
|
||||
<p>{encryptedMessageAreEncrypted}</p>
|
||||
statusDescription = <>
|
||||
<p>{_t(
|
||||
"This session is <b>not backing up your keys</b>, " +
|
||||
"but you do have an existing backup you can restore from " +
|
||||
|
@ -199,19 +241,11 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
"Connect this session to key backup before signing out to avoid " +
|
||||
"losing any keys that may only be on this session.",
|
||||
)}</p>
|
||||
</div>;
|
||||
</>;
|
||||
restoreButtonCaption = _t("Connect this session to Key Backup");
|
||||
}
|
||||
|
||||
let keyStatus;
|
||||
if (this.state.backupKeyStored === true) {
|
||||
keyStatus = _t("in secret storage");
|
||||
} else {
|
||||
keyStatus = _t("not stored");
|
||||
}
|
||||
|
||||
let uploadStatus;
|
||||
const { sessionsRemaining } = this.state;
|
||||
if (!MatrixClientPeg.get().getKeyBackupEnabled()) {
|
||||
// No upload status to show when backup disabled.
|
||||
uploadStatus = "";
|
||||
|
@ -225,17 +259,17 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
</div>;
|
||||
}
|
||||
|
||||
let backupSigStatuses = this.state.backupSigStatus.sigs.map((sig, i) => {
|
||||
let backupSigStatuses = backupSigStatus.sigs.map((sig, i) => {
|
||||
const deviceName = sig.device ? (sig.device.getDisplayName() || sig.device.deviceId) : null;
|
||||
const validity = sub =>
|
||||
<span className={sig.valid ? 'mx_KeyBackupPanel_sigValid' : 'mx_KeyBackupPanel_sigInvalid'}>
|
||||
<span className={sig.valid ? 'mx_SecureBackupPanel_sigValid' : 'mx_SecureBackupPanel_sigInvalid'}>
|
||||
{sub}
|
||||
</span>;
|
||||
const verify = sub =>
|
||||
<span className={sig.device && sig.deviceTrust.isVerified() ? 'mx_KeyBackupPanel_deviceVerified' : 'mx_KeyBackupPanel_deviceNotVerified'}>
|
||||
<span className={sig.device && sig.deviceTrust.isVerified() ? 'mx_SecureBackupPanel_deviceVerified' : 'mx_SecureBackupPanel_deviceNotVerified'}>
|
||||
{sub}
|
||||
</span>;
|
||||
const device = sub => <span className="mx_KeyBackupPanel_deviceName">{deviceName}</span>;
|
||||
const device = sub => <span className="mx_SecureBackupPanel_deviceName">{deviceName}</span>;
|
||||
const fromThisDevice = (
|
||||
sig.device &&
|
||||
sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key()
|
||||
|
@ -306,55 +340,123 @@ export default class KeyBackupPanel extends React.PureComponent {
|
|||
{sigStatus}
|
||||
</div>;
|
||||
});
|
||||
if (this.state.backupSigStatus.sigs.length === 0) {
|
||||
if (backupSigStatus.sigs.length === 0) {
|
||||
backupSigStatuses = _t("Backup is not signed by any of your sessions");
|
||||
}
|
||||
|
||||
let trustedLocally;
|
||||
if (this.state.backupSigStatus.trusted_locally) {
|
||||
if (backupSigStatus.trusted_locally) {
|
||||
trustedLocally = _t("This backup is trusted because it has been restored on this session");
|
||||
}
|
||||
|
||||
const buttonRow = (
|
||||
<div className="mx_KeyBackupPanel_buttonRow">
|
||||
<AccessibleButton kind="primary" onClick={this._restoreBackup}>
|
||||
{restoreButtonCaption}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="danger" onClick={this._deleteBackup}>
|
||||
{_t("Delete Backup")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
extraDetailsTableRows = <>
|
||||
<tr>
|
||||
<td>{_t("Backup version:")}</td>
|
||||
<td>{backupInfo.version}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_t("Algorithm:")}</td>
|
||||
<td>{backupInfo.algorithm}</td>
|
||||
</tr>
|
||||
</>;
|
||||
|
||||
extraDetails = <>
|
||||
{uploadStatus}
|
||||
<div>{backupSigStatuses}</div>
|
||||
<div>{trustedLocally}</div>
|
||||
</>;
|
||||
|
||||
actions.push(
|
||||
<AccessibleButton key="restore" kind="primary" onClick={this._restoreBackup}>
|
||||
{restoreButtonCaption}
|
||||
</AccessibleButton>,
|
||||
);
|
||||
|
||||
return <div>
|
||||
<div>{clientBackupStatus}</div>
|
||||
<details>
|
||||
<summary>{_t("Advanced")}</summary>
|
||||
<div>{_t("Backup version: ")}{this.state.backupInfo.version}</div>
|
||||
<div>{_t("Algorithm: ")}{this.state.backupInfo.algorithm}</div>
|
||||
<div>{_t("Backup key stored: ")}{keyStatus}</div>
|
||||
{uploadStatus}
|
||||
<div>{backupSigStatuses}</div>
|
||||
<div>{trustedLocally}</div>
|
||||
</details>
|
||||
{buttonRow}
|
||||
</div>;
|
||||
if (!isSecureBackupRequired()) {
|
||||
actions.push(
|
||||
<AccessibleButton key="delete" kind="danger" onClick={this._deleteBackup}>
|
||||
{_t("Delete Backup")}
|
||||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return <div>
|
||||
<div>
|
||||
<p>{_t(
|
||||
"Your keys are <b>not being backed up from this session</b>.", {},
|
||||
{b: sub => <b>{sub}</b>},
|
||||
)}</p>
|
||||
<p>{encryptedMessageAreEncrypted}</p>
|
||||
<p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
|
||||
</div>
|
||||
<div className="mx_KeyBackupPanel_buttonRow">
|
||||
<AccessibleButton kind="primary" onClick={this._startNewBackup}>
|
||||
{_t("Start using Key Backup")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
statusDescription = <>
|
||||
<p>{_t(
|
||||
"Your keys are <b>not being backed up from this session</b>.", {},
|
||||
{b: sub => <b>{sub}</b>},
|
||||
)}</p>
|
||||
<p>{_t("Back up your keys before signing out to avoid losing them.")}</p>
|
||||
</>;
|
||||
actions.push(
|
||||
<AccessibleButton key="setup" kind="primary" onClick={this._startNewBackup}>
|
||||
{_t("Set up")}
|
||||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
|
||||
if (secretStorageKeyInAccount) {
|
||||
actions.push(
|
||||
<AccessibleButton key="reset" kind="danger" onClick={this._resetSecretStorage}>
|
||||
{_t("Reset")}
|
||||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
|
||||
let backupKeyWellFormedText = "";
|
||||
if (backupKeyCached) {
|
||||
backupKeyWellFormedText = ", ";
|
||||
if (backupKeyWellFormed) {
|
||||
backupKeyWellFormedText += _t("well formed");
|
||||
} else {
|
||||
backupKeyWellFormedText += _t("unexpected type");
|
||||
}
|
||||
}
|
||||
|
||||
let actionRow;
|
||||
if (actions.length) {
|
||||
actionRow = <div className="mx_SecureBackupPanel_buttonRow">
|
||||
{actions}
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{_t(
|
||||
"Back up your encryption keys with your account data in case you " +
|
||||
"lose access to your sessions. Your keys will be secured with a " +
|
||||
"unique Recovery Key.",
|
||||
)}</p>
|
||||
{statusDescription}
|
||||
<details>
|
||||
<summary>{_t("Advanced")}</summary>
|
||||
<table className="mx_SecureBackupPanel_statusList"><tbody>
|
||||
<tr>
|
||||
<td>{_t("Backup key stored:")}</td>
|
||||
<td>{
|
||||
backupKeyStored === true ? _t("in secret storage") : _t("not stored")
|
||||
}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_t("Backup key cached:")}</td>
|
||||
<td>
|
||||
{backupKeyCached ? _t("cached locally") : _t("not found locally")}
|
||||
{backupKeyWellFormedText}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_t("Secret storage public key:")}</td>
|
||||
<td>{secretStorageKeyInAccount ? _t("in account data") : _t("not found")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{_t("Secret storage:")}</td>
|
||||
<td>{secretStorageReady ? _t("ready") : _t("not ready")}</td>
|
||||
</tr>
|
||||
{extraDetailsTableRows}
|
||||
</tbody></table>
|
||||
{extraDetails}
|
||||
</details>
|
||||
{actionRow}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -42,7 +42,7 @@ function getStatusText(status: UpdateCheckStatus, errorDetail?: string) {
|
|||
return _t('Downloading update...');
|
||||
case UpdateCheckStatus.Ready:
|
||||
return _t("New version available. <a>Update now.</a>", {}, {
|
||||
a: sub => <AccessibleButton kind="link" onClick={installUpdate}>{sub}</AccessibleButton>
|
||||
a: sub => <AccessibleButton kind="link" onClick={installUpdate}>{sub}</AccessibleButton>,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,9 +46,10 @@ export default class BridgeSettingsTab extends React.Component<IProps> {
|
|||
const client = MatrixClientPeg.get();
|
||||
const roomState = client.getRoom(roomId).currentState;
|
||||
|
||||
return [].concat(...BRIDGE_EVENT_TYPES.map((typeName) =>
|
||||
Array.from(roomState.events.get(typeName).values()),
|
||||
));
|
||||
return BRIDGE_EVENT_TYPES.map(typeName => {
|
||||
const events = roomState.events.get(typeName);
|
||||
return events ? Array.from(events.values()) : [];
|
||||
}).flat(1);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -22,6 +22,8 @@ import * as sdk from "../../../../..";
|
|||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import dis from "../../../../../dispatcher/dispatcher";
|
||||
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import {UIFeature} from "../../../../../settings/UIFeature";
|
||||
|
||||
export default class GeneralRoomSettingsTab extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -61,6 +63,28 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
|||
const canChangeGroups = room.currentState.mayClientSendStateEvent("m.room.related_groups", client);
|
||||
const groupsEvent = room.currentState.getStateEvents("m.room.related_groups", "");
|
||||
|
||||
let urlPreviewSettings = <>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("URL Previews")}</span>
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<UrlPreviewSettings room={room} />
|
||||
</div>
|
||||
</>;
|
||||
if (!SettingsStore.getValue(UIFeature.URLPreviews)) {
|
||||
urlPreviewSettings = null;
|
||||
}
|
||||
|
||||
let flairSection;
|
||||
if (SettingsStore.getValue(UIFeature.Flair)) {
|
||||
flairSection = <>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Flair")}</span>
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
<RelatedGroupSettings roomId={room.roomId}
|
||||
canSetRelatedGroups={canChangeGroups}
|
||||
relatedGroupsEvent={groupsEvent} />
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_GeneralRoomSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("General")}</div>
|
||||
|
@ -75,17 +99,8 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
|||
canonicalAliasEvent={canonicalAliasEv} aliasEvents={aliasEvents} />
|
||||
</div>
|
||||
<div className="mx_SettingsTab_heading">{_t("Other")}</div>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Flair")}</span>
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
<RelatedGroupSettings roomId={room.roomId}
|
||||
canSetRelatedGroups={canChangeGroups}
|
||||
relatedGroupsEvent={groupsEvent} />
|
||||
</div>
|
||||
|
||||
<span className='mx_SettingsTab_subheading'>{_t("URL Previews")}</span>
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<UrlPreviewSettings room={room} />
|
||||
</div>
|
||||
{ flairSection }
|
||||
{ urlPreviewSettings }
|
||||
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Leave room")}</span>
|
||||
<div className='mx_SettingsTab_section'>
|
||||
|
|
|
@ -239,7 +239,7 @@ export default class RolesRoomSettingsTab extends React.Component {
|
|||
defaultValue: 50,
|
||||
},
|
||||
"redact": {
|
||||
desc: _t('Remove messages'),
|
||||
desc: _t('Remove messages sent by others'),
|
||||
defaultValue: 50,
|
||||
},
|
||||
"notifications.room": {
|
||||
|
|
|
@ -24,6 +24,7 @@ import Modal from "../../../../../Modal";
|
|||
import QuestionDialog from "../../../dialogs/QuestionDialog";
|
||||
import StyledRadioGroup from '../../../elements/StyledRadioGroup';
|
||||
import {SettingLevel} from "../../../../../settings/SettingLevel";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
|
||||
export default class SecurityRoomSettingsTab extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -340,10 +341,13 @@ export default class SecurityRoomSettingsTab extends React.Component {
|
|||
const canEnableEncryption = !isEncrypted && hasEncryptionPermission;
|
||||
|
||||
let encryptionSettings = null;
|
||||
if (isEncrypted) {
|
||||
encryptionSettings = <SettingsFlag name="blacklistUnverifiedDevices" level={SettingLevel.ROOM_DEVICE}
|
||||
onChange={this._updateBlacklistDevicesFlag}
|
||||
roomId={this.props.roomId} />;
|
||||
if (isEncrypted && SettingsStore.isEnabled("blacklistUnverifiedDevices")) {
|
||||
encryptionSettings = <SettingsFlag
|
||||
name="blacklistUnverifiedDevices"
|
||||
level={SettingLevel.ROOM_DEVICE}
|
||||
onChange={this._updateBlacklistDevicesFlag}
|
||||
roomId={this.props.roomId}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -36,6 +36,7 @@ import EventTilePreview from '../../../elements/EventTilePreview';
|
|||
import StyledRadioGroup from "../../../elements/StyledRadioGroup";
|
||||
import classNames from 'classnames';
|
||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||
import {UIFeature} from "../../../../../settings/UIFeature";
|
||||
|
||||
interface IProps {
|
||||
}
|
||||
|
@ -170,7 +171,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
"baseFontSize",
|
||||
null,
|
||||
SettingLevel.DEVICE,
|
||||
parseInt(value, 10) - FontWatcher.SIZE_DIFF
|
||||
parseInt(value, 10) - FontWatcher.SIZE_DIFF,
|
||||
);
|
||||
|
||||
return {valid: true, feedback: _t('Use between %(min)s pt and %(max)s pt', {min, max})};
|
||||
|
@ -294,7 +295,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
/>
|
||||
</div>
|
||||
{customThemeForm}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -386,6 +387,8 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
|||
};
|
||||
|
||||
private renderAdvancedSection() {
|
||||
if (!SettingsStore.getValue(UIFeature.AdvancedSettings)) return null;
|
||||
|
||||
const brand = SdkConfig.get().brand;
|
||||
const toggle = <div
|
||||
className="mx_AppearanceUserSettingsTab_AdvancedToggle"
|
||||
|
|
|
@ -37,6 +37,7 @@ import {abbreviateUrl} from "../../../../../utils/UrlUtils";
|
|||
import { getThreepidsWithBindStatus } from '../../../../../boundThreepids';
|
||||
import Spinner from "../../../elements/Spinner";
|
||||
import {SettingLevel} from "../../../../../settings/SettingLevel";
|
||||
import {UIFeature} from "../../../../../settings/UIFeature";
|
||||
|
||||
export default class GeneralUserSettingsTab extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -220,7 +221,6 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
_renderProfileSection() {
|
||||
return (
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Profile")}</span>
|
||||
<ProfileSettings />
|
||||
</div>
|
||||
);
|
||||
|
@ -247,7 +247,9 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
// validate 3PID ownership even if we're just adding to the homeserver only.
|
||||
// For newer homeservers with separate 3PID add and bind methods (MSC2290),
|
||||
// there is no such concern, so we can always show the HS account 3PIDs.
|
||||
if (this.state.haveIdServer || this.state.serverSupportsSeparateAddAndBind === true) {
|
||||
if (SettingsStore.getValue(UIFeature.ThirdPartyID) &&
|
||||
(this.state.haveIdServer || this.state.serverSupportsSeparateAddAndBind === true)
|
||||
) {
|
||||
const emails = this.state.loading3pids
|
||||
? <Spinner />
|
||||
: <EmailAddresses
|
||||
|
@ -366,6 +368,8 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
}
|
||||
|
||||
_renderIntegrationManagerSection() {
|
||||
if (!SettingsStore.getValue(UIFeature.Widgets)) return null;
|
||||
|
||||
const SetIntegrationManager = sdk.getComponent("views.settings.SetIntegrationManager");
|
||||
|
||||
return (
|
||||
|
@ -383,17 +387,31 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
width="18" height="18" alt={_t("Warning")} />
|
||||
: null;
|
||||
|
||||
let accountManagementSection;
|
||||
if (SettingsStore.getValue(UIFeature.Deactivate)) {
|
||||
accountManagementSection = <>
|
||||
<div className="mx_SettingsTab_heading">{_t("Deactivate account")}</div>
|
||||
{this._renderManagementSection()}
|
||||
</>;
|
||||
}
|
||||
|
||||
let discoverySection;
|
||||
if (SettingsStore.getValue(UIFeature.IdentityServer)) {
|
||||
discoverySection = <>
|
||||
<div className="mx_SettingsTab_heading">{discoWarning} {_t("Discovery")}</div>
|
||||
{this._renderDiscoverySection()}
|
||||
</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("General")}</div>
|
||||
{this._renderProfileSection()}
|
||||
{this._renderAccountSection()}
|
||||
{this._renderLanguageSection()}
|
||||
<div className="mx_SettingsTab_heading">{discoWarning} {_t("Discovery")}</div>
|
||||
{this._renderDiscoverySection()}
|
||||
{ discoverySection }
|
||||
{this._renderIntegrationManagerSection() /* Has its own title */}
|
||||
<div className="mx_SettingsTab_heading">{_t("Deactivate account")}</div>
|
||||
{this._renderManagementSection()}
|
||||
{ accountManagementSection }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -204,9 +204,9 @@ export default class HelpUserSettingsTab extends React.Component {
|
|||
updateButton = <UpdateCheckButton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_HelpUserSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Help & About")}</div>
|
||||
let bugReportingSection;
|
||||
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||
bugReportingSection = (
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className='mx_SettingsTab_subheading'>{_t('Bug reporting')}</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
|
@ -223,22 +223,24 @@ export default class HelpUserSettingsTab extends React.Component {
|
|||
{_t("Submit debug logs")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div className='mx_HelpUserSettingsTab_debugButton'>
|
||||
<AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'>
|
||||
{_t("Clear cache and reload")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
{
|
||||
_t( "To report a Matrix-related security issue, please read the Matrix.org " +
|
||||
"<a>Security Disclosure Policy</a>.", {},
|
||||
{
|
||||
'a': (sub) =>
|
||||
<a href="https://matrix.org/security-disclosure-policy/"
|
||||
rel="noreferrer noopener" target="_blank">{sub}</a>,
|
||||
rel="noreferrer noopener" target="_blank">{sub}</a>,
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_HelpUserSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Help & About")}</div>
|
||||
{ bugReportingSection }
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("FAQ")}</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
|
@ -268,6 +270,11 @@ export default class HelpUserSettingsTab extends React.Component {
|
|||
data-spoiler={MatrixClientPeg.get().getAccessToken()}>
|
||||
<{ _t("click to reveal") }>
|
||||
</AccessibleButton>
|
||||
<div className='mx_HelpUserSettingsTab_debugButton'>
|
||||
<AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'>
|
||||
{_t("Clear cache and reload")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -50,11 +50,10 @@ export default class PreferencesUserSettingsTab extends React.Component {
|
|||
'showDisplaynameChanges',
|
||||
'showImages',
|
||||
'dontShowChatEffects',
|
||||
'Pill.shouldShowPillAvatar',
|
||||
];
|
||||
|
||||
static ADVANCED_SETTINGS = [
|
||||
'alwaysShowEncryptionIcons',
|
||||
'Pill.shouldShowPillAvatar',
|
||||
static GENERAL_SETTINGS = [
|
||||
'TagPanel.enableTagPanel',
|
||||
'promptBeforeInviteUnknownUsers',
|
||||
// Start automatically after startup (electron-only)
|
||||
|
@ -140,7 +139,9 @@ export default class PreferencesUserSettingsTab extends React.Component {
|
|||
|
||||
_renderGroup(settingIds) {
|
||||
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
|
||||
return settingIds.map(i => <SettingsFlag key={i} name={i} level={SettingLevel.ACCOUNT} />);
|
||||
return settingIds.filter(SettingsStore.isEnabled).map(i => {
|
||||
return <SettingsFlag key={i} name={i} level={SettingLevel.ACCOUNT} />;
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -188,8 +189,8 @@ export default class PreferencesUserSettingsTab extends React.Component {
|
|||
</div>
|
||||
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Advanced")}</span>
|
||||
{this._renderGroup(PreferencesUserSettingsTab.ADVANCED_SETTINGS)}
|
||||
<span className="mx_SettingsTab_subheading">{_t("General")}</span>
|
||||
{this._renderGroup(PreferencesUserSettingsTab.GENERAL_SETTINGS)}
|
||||
{minimizeToTrayOption}
|
||||
{autoHideMenuOption}
|
||||
{autoLaunchOption}
|
||||
|
|
|
@ -29,6 +29,10 @@ import {sleep} from "../../../../../utils/promise";
|
|||
import dis from "../../../../../dispatcher/dispatcher";
|
||||
import {privateShouldBeEncrypted} from "../../../../../createRoom";
|
||||
import {SettingLevel} from "../../../../../settings/SettingLevel";
|
||||
import SecureBackupPanel from "../../SecureBackupPanel";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import {UIFeature} from "../../../../../settings/UIFeature";
|
||||
import {isE2eAdvancedPanelPossible} from "../../E2eAdvancedPanel";
|
||||
|
||||
export class IgnoredUser extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -102,14 +106,14 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
|
||||
_onExportE2eKeysClicked = () => {
|
||||
Modal.createTrackedDialogAsync('Export E2E Keys', '',
|
||||
import('../../../../../async-components/views/dialogs/ExportE2eKeysDialog'),
|
||||
import('../../../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
|
||||
{matrixClient: MatrixClientPeg.get()},
|
||||
);
|
||||
};
|
||||
|
||||
_onImportE2eKeysClicked = () => {
|
||||
Modal.createTrackedDialogAsync('Import E2E Keys', '',
|
||||
import('../../../../../async-components/views/dialogs/ImportE2eKeysDialog'),
|
||||
import('../../../../../async-components/views/dialogs/security/ImportE2eKeysDialog'),
|
||||
{matrixClient: MatrixClientPeg.get()},
|
||||
);
|
||||
};
|
||||
|
@ -216,6 +220,15 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
let noSendUnverifiedSetting;
|
||||
if (SettingsStore.isEnabled("blacklistUnverifiedDevices")) {
|
||||
noSendUnverifiedSetting = <SettingsFlag
|
||||
name='blacklistUnverifiedDevices'
|
||||
level={SettingLevel.DEVICE}
|
||||
onChange={this._updateBlacklistDevicesFlag}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Cryptography")}</span>
|
||||
|
@ -230,8 +243,7 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
</li>
|
||||
</ul>
|
||||
{importExportButtons}
|
||||
<SettingsFlag name='blacklistUnverifiedDevices' level={SettingLevel.DEVICE}
|
||||
onChange={this._updateBlacklistDevicesFlag} />
|
||||
{noSendUnverifiedSetting}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -288,12 +300,11 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
const SettingsFlag = sdk.getComponent('views.elements.SettingsFlag');
|
||||
const EventIndexPanel = sdk.getComponent('views.settings.EventIndexPanel');
|
||||
|
||||
const KeyBackupPanel = sdk.getComponent('views.settings.KeyBackupPanel');
|
||||
const keyBackup = (
|
||||
const secureBackup = (
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<span className="mx_SettingsTab_subheading">{_t("Key backup")}</span>
|
||||
<span className="mx_SettingsTab_subheading">{_t("Secure Backup")}</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
<KeyBackupPanel />
|
||||
<SecureBackupPanel />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -311,15 +322,13 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
// can remove this.
|
||||
const CrossSigningPanel = sdk.getComponent('views.settings.CrossSigningPanel');
|
||||
const crossSigning = (
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<span className="mx_SettingsTab_subheading">{_t("Cross-signing")}</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
<CrossSigningPanel />
|
||||
</div>
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<span className="mx_SettingsTab_subheading">{_t("Cross-signing")}</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
<CrossSigningPanel />
|
||||
</div>
|
||||
);
|
||||
|
||||
const E2eAdvancedPanel = sdk.getComponent('views.settings.E2eAdvancedPanel');
|
||||
</div>
|
||||
);
|
||||
|
||||
let warning;
|
||||
if (!privateShouldBeEncrypted()) {
|
||||
|
@ -329,12 +338,53 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
</div>;
|
||||
}
|
||||
|
||||
let privacySection;
|
||||
if (Analytics.canEnable()) {
|
||||
privacySection = <React.Fragment>
|
||||
<div className="mx_SettingsTab_heading">{_t("Privacy")}</div>
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Analytics")}</span>
|
||||
<div className="mx_SettingsTab_subsectionText">
|
||||
{_t(
|
||||
"%(brand)s collects anonymous analytics to allow us to improve the application.",
|
||||
{ brand },
|
||||
)}
|
||||
|
||||
{_t("Privacy is important to us, so we don't collect any personal or " +
|
||||
"identifiable data for our analytics.")}
|
||||
<AccessibleButton className="mx_SettingsTab_linkBtn" onClick={Analytics.showDetailsModal}>
|
||||
{_t("Learn more about how we use analytics.")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<SettingsFlag name="analyticsOptIn" level={SettingLevel.DEVICE} onChange={this._updateAnalytics} />
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
const E2eAdvancedPanel = sdk.getComponent('views.settings.E2eAdvancedPanel');
|
||||
let advancedSection;
|
||||
if (SettingsStore.getValue(UIFeature.AdvancedSettings)) {
|
||||
const ignoreUsersPanel = this._renderIgnoredUsers();
|
||||
const invitesPanel = this._renderManageInvites();
|
||||
const e2ePanel = isE2eAdvancedPanelPossible() ? <E2eAdvancedPanel /> : null;
|
||||
// only show the section if there's something to show
|
||||
if (ignoreUsersPanel || invitesPanel || e2ePanel) {
|
||||
advancedSection = <>
|
||||
<div className="mx_SettingsTab_heading">{_t("Advanced")}</div>
|
||||
<div className="mx_SettingsTab_section">
|
||||
{ignoreUsersPanel}
|
||||
{invitesPanel}
|
||||
{e2ePanel}
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab mx_SecurityUserSettingsTab">
|
||||
{warning}
|
||||
<div className="mx_SettingsTab_heading">{_t("Security & Privacy")}</div>
|
||||
<div className="mx_SettingsTab_heading">{_t("Where you’re logged in")}</div>
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Where you’re logged in")}</span>
|
||||
<span>
|
||||
{_t(
|
||||
"Manage the names of and sign out of your sessions below or " +
|
||||
|
@ -351,30 +401,15 @@ export default class SecurityUserSettingsTab extends React.Component {
|
|||
<DevicesPanel />
|
||||
</div>
|
||||
</div>
|
||||
{keyBackup}
|
||||
{eventIndex}
|
||||
{crossSigning}
|
||||
{this._renderCurrentDeviceInfo()}
|
||||
<div className='mx_SettingsTab_section'>
|
||||
<span className="mx_SettingsTab_subheading">{_t("Analytics")}</span>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
{_t(
|
||||
"%(brand)s collects anonymous analytics to allow us to improve the application.",
|
||||
{ brand },
|
||||
)}
|
||||
|
||||
{_t("Privacy is important to us, so we don't collect any personal or " +
|
||||
"identifiable data for our analytics.")}
|
||||
<AccessibleButton className="mx_SettingsTab_linkBtn" onClick={Analytics.showDetailsModal}>
|
||||
{_t("Learn more about how we use analytics.")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<SettingsFlag name='analyticsOptIn' level={SettingLevel.DEVICE}
|
||||
onChange={this._updateAnalytics} />
|
||||
<div className="mx_SettingsTab_heading">{_t("Encryption")}</div>
|
||||
<div className="mx_SettingsTab_section">
|
||||
{secureBackup}
|
||||
{eventIndex}
|
||||
{crossSigning}
|
||||
{this._renderCurrentDeviceInfo()}
|
||||
</div>
|
||||
{this._renderIgnoredUsers()}
|
||||
{this._renderManageInvites()}
|
||||
<E2eAdvancedPanel />
|
||||
{ privacySection }
|
||||
{ advancedSection }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue