Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/15179

 Conflicts:
	src/components/views/settings/tabs/user/SecurityUserSettingsTab.js
	src/settings/Settings.ts
	src/settings/UIFeature.ts
This commit is contained in:
Michael Telatynski 2020-09-18 11:29:38 +01:00
commit 687a2e56b2
68 changed files with 880 additions and 732 deletions

View file

@ -38,6 +38,8 @@ import {Action} from "../../../dispatcher/actions";
import {DefaultTagID} from "../../../stores/room-list/models";
import RoomListStore from "../../../stores/room-list/RoomListStore";
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
import SettingsStore from "../../../settings/SettingsStore";
import {UIFeature} from "../../../settings/UIFeature";
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
/* eslint-disable camelcase */
@ -549,7 +551,7 @@ export default class InviteDialog extends React.PureComponent {
if (this.state.filterText.startsWith('@')) {
// Assume mxid
newMember = new DirectoryMember({user_id: this.state.filterText, display_name: null, avatar_url: null});
} else {
} else if (SettingsStore.getValue(UIFeature.IdentityServer)) {
// Assume email
newMember = new ThreepidMember(this.state.filterText);
}
@ -734,7 +736,7 @@ export default class InviteDialog extends React.PureComponent {
this.setState({tryingIdentityServer: true});
return;
}
if (term.indexOf('@') > 0 && Email.looksValid(term)) {
if (term.indexOf('@') > 0 && Email.looksValid(term) && SettingsStore.getValue(UIFeature.IdentityServer)) {
// Start off by suggesting the plain email while we try and resolve it
// to a real account.
this.setState({
@ -1037,7 +1039,9 @@ export default class InviteDialog extends React.PureComponent {
}
_renderIdentityServerWarning() {
if (!this.state.tryingIdentityServer || this.state.canUseIdentityServer) {
if (!this.state.tryingIdentityServer || this.state.canUseIdentityServer ||
!SettingsStore.getValue(UIFeature.IdentityServer)
) {
return null;
}
@ -1086,22 +1090,38 @@ export default class InviteDialog extends React.PureComponent {
let buttonText;
let goButtonFn;
const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
const userId = MatrixClientPeg.get().getUserId();
if (this.props.kind === KIND_DM) {
title = _t("Direct Messages");
helpText = _t(
"Start a conversation with someone using their name, username (like <userId/>) or email address.",
{},
{userId: () => {
return <a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>;
}},
);
if (identityServersEnabled) {
helpText = _t(
"Start a conversation with someone using their name, username (like <userId/>) or email address.",
{},
{userId: () => {
return (
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>
);
}},
);
} else {
helpText = _t(
"Start a conversation with someone using their name or username (like <userId/>).",
{},
{userId: () => {
return (
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>
);
}},
);
}
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
const communityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
helpText = _t(
"Start a conversation with someone using their name, username (like <userId/>) or email address. " +
"This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click " +
"<a>here</a>.",
const inviteText = _t("This won't invite them to %(communityName)s. " +
"To invite someone to %(communityName)s, click <a>here</a>",
{communityName}, {
userId: () => {
return (
@ -1122,21 +1142,40 @@ export default class InviteDialog extends React.PureComponent {
},
},
);
helpText = <React.Fragment>
{ helpText } {inviteText}
</React.Fragment>;
}
buttonText = _t("Go");
goButtonFn = this._startDm;
} else { // KIND_INVITE
title = _t("Invite to this room");
helpText = _t(
"Invite someone using their name, username (like <userId/>), email address or <a>share this room</a>.",
{},
{
userId: () =>
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
a: (sub) =>
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{sub}</a>,
},
);
if (identityServersEnabled) {
helpText = _t(
"Invite someone using their name, username (like <userId/>), email address or " +
"<a>share this room</a>.",
{},
{
userId: () =>
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
a: (sub) =>
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{sub}</a>,
},
);
} else {
helpText = _t(
"Invite someone using their name, username (like <userId/>) or <a>share this room</a>.",
{},
{
userId: () =>
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
a: (sub) =>
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{sub}</a>,
},
);
}
buttonText = _t("Invite");
goButtonFn = this._inviteUsers;
}

View file

@ -20,7 +20,8 @@ import Modal from '../../../Modal';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import RestoreKeyBackupDialog from './security/RestoreKeyBackupDialog';
export default class LogoutDialog extends React.Component {
defaultProps = {
@ -73,7 +74,7 @@ export default class LogoutDialog 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(),
},
@ -93,14 +94,13 @@ export default class LogoutDialog extends React.Component {
// A key backup exists for this account, but the creating device is not
// verified, so restore the backup which will give us the keys from it and
// allow us to trust it (ie. upload keys to it)
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
Modal.createTrackedDialog(
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
/* priority = */ false, /* static = */ true,
);
} else {
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"),
import("../../../async-components/views/dialogs/security/CreateKeyBackupDialog"),
null, null, /* priority = */ false, /* static = */ true,
);
}

View file

@ -32,6 +32,8 @@ import {copyPlaintext, selectText} from "../../../utils/strings";
import StyledCheckbox from '../elements/StyledCheckbox';
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
import { IDialogProps } from "./IDialogProps";
import SettingsStore from "../../../settings/SettingsStore";
import {UIFeature} from "../../../settings/UIFeature";
const socials = [
{
@ -197,6 +199,35 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
const matrixToUrl = this.getUrl();
const encodedUrl = encodeURIComponent(matrixToUrl);
const showQrCode = SettingsStore.getValue(UIFeature.ShareQRCode);
const showSocials = SettingsStore.getValue(UIFeature.ShareSocial);
let qrSocialSection;
if (showQrCode || showSocials) {
qrSocialSection = <>
<hr />
<div className="mx_ShareDialog_split">
{ showQrCode && <div className="mx_ShareDialog_qrcode_container">
<QRCode data={matrixToUrl} width={256} />
</div> }
{ showSocials && <div className="mx_ShareDialog_social_container">
{ socials.map((social) => (
<a
rel="noreferrer noopener"
target="_blank"
key={social.name}
title={social.name}
href={social.url(encodedUrl)}
className="mx_ShareDialog_social_icon"
>
<img src={social.img} alt={social.name} height={64} width={64} />
</a>
)) }
</div> }
</div>
</>;
}
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return <BaseDialog
title={title}
@ -220,27 +251,7 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
/>
</div>
{ checkbox }
<hr />
<div className="mx_ShareDialog_split">
<div className="mx_ShareDialog_qrcode_container">
<QRCode data={matrixToUrl} width={256} />
</div>
<div className="mx_ShareDialog_social_container">
{ socials.map((social) => (
<a
rel="noreferrer noopener"
target="_blank"
key={social.name}
title={social.name}
href={social.url(encodedUrl)}
className="mx_ShareDialog_social_icon"
>
<img src={social.img} alt={social.name} height={64} width={64} />
</a>
)) }
</div>
</div>
{ qrSocialSection }
</div>
</BaseDialog>;
}

View file

@ -32,6 +32,7 @@ import FlairUserSettingsTab from "../settings/tabs/user/FlairUserSettingsTab";
import * as sdk from "../../../index";
import SdkConfig from "../../../SdkConfig";
import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab";
import {UIFeature} from "../../../settings/UIFeature";
export const USER_GENERAL_TAB = "USER_GENERAL_TAB";
export const USER_APPEARANCE_TAB = "USER_APPEARANCE_TAB";
@ -86,12 +87,14 @@ export default class UserSettingsDialog extends React.Component {
"mx_UserSettingsDialog_appearanceIcon",
<AppearanceUserSettingsTab />,
));
tabs.push(new Tab(
USER_FLAIR_TAB,
_td("Flair"),
"mx_UserSettingsDialog_flairIcon",
<FlairUserSettingsTab />,
));
if (SettingsStore.getValue(UIFeature.Flair)) {
tabs.push(new Tab(
USER_FLAIR_TAB,
_td("Flair"),
"mx_UserSettingsDialog_flairIcon",
<FlairUserSettingsTab />,
));
}
tabs.push(new Tab(
USER_NOTIFICATIONS_TAB,
_td("Notifications"),
@ -104,12 +107,16 @@ export default class UserSettingsDialog extends React.Component {
"mx_UserSettingsDialog_preferencesIcon",
<PreferencesUserSettingsTab />,
));
tabs.push(new Tab(
USER_VOICE_TAB,
_td("Voice & Video"),
"mx_UserSettingsDialog_voiceIcon",
<VoiceUserSettingsTab />,
));
if (SettingsStore.getValue(UIFeature.Voip)) {
tabs.push(new Tab(
USER_VOICE_TAB,
_td("Voice & Video"),
"mx_UserSettingsDialog_voiceIcon",
<VoiceUserSettingsTab />,
));
}
tabs.push(new Tab(
USER_SECURITY_TAB,
_td("Security & Privacy"),

View file

@ -16,8 +16,8 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import * as sdk from "../../../index";
import {_t} from "../../../../languageHandler";
import * as sdk from "../../../../index";
export default class ConfirmDestroyCrossSigningDialog extends React.Component {
static propTypes = {

View file

@ -0,0 +1,187 @@
/*
Copyright 2018, 2019 New Vector Ltd
Copyright 2019, 2020 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.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { MatrixClientPeg } from '../../../../MatrixClientPeg';
import { _t } from '../../../../languageHandler';
import Modal from '../../../../Modal';
import { SSOAuthEntry } from '../../auth/InteractiveAuthEntryComponents';
import DialogButtons from '../../elements/DialogButtons';
import BaseDialog from '../BaseDialog';
import Spinner from '../../elements/Spinner';
import InteractiveAuthDialog from '../InteractiveAuthDialog';
/*
* Walks the user through the process of creating a cross-signing keys. In most
* cases, only a spinner is shown, but for more complex auth like SSO, the user
* may need to complete some steps to proceed.
*/
export default class CreateCrossSigningDialog extends React.PureComponent {
static propTypes = {
accountPassword: PropTypes.string,
};
constructor(props) {
super(props);
this.state = {
error: null,
// Does the server offer a UI auth flow with just m.login.password
// for /keys/device_signing/upload?
canUploadKeysWithPasswordOnly: null,
accountPassword: props.accountPassword || "",
};
if (this.state.accountPassword) {
// If we have an account password in memory, let's simplify and
// assume it means password auth is also supported for device
// signing key upload as well. This avoids hitting the server to
// test auth flows, which may be slow under high load.
this.state.canUploadKeysWithPasswordOnly = true;
} else {
this._queryKeyUploadAuth();
}
}
componentDidMount() {
this._bootstrapCrossSigning();
}
async _queryKeyUploadAuth() {
try {
await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {});
// We should never get here: the server should always require
// UI auth to upload device signing keys. If we do, we upload
// no keys which would be a no-op.
console.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!");
} catch (error) {
if (!error.data || !error.data.flows) {
console.log("uploadDeviceSigningKeys advertised no flows!");
return;
}
const canUploadKeysWithPasswordOnly = error.data.flows.some(f => {
return f.stages.length === 1 && f.stages[0] === 'm.login.password';
});
this.setState({
canUploadKeysWithPasswordOnly,
});
}
}
_doBootstrapUIAuth = async (makeRequest) => {
if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
await makeRequest({
type: 'm.login.password',
identifier: {
type: 'm.id.user',
user: MatrixClientPeg.get().getUserId(),
},
// TODO: Remove `user` once servers support proper UIA
// See https://github.com/matrix-org/synapse/issues/5665
user: MatrixClientPeg.get().getUserId(),
password: this.state.accountPassword,
});
} else {
const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("Use Single Sign On to continue"),
body: _t("To continue, use Single Sign On to prove your identity."),
continueText: _t("Single Sign On"),
continueKind: "primary",
},
[SSOAuthEntry.PHASE_POSTAUTH]: {
title: _t("Confirm encryption setup"),
body: _t("Click the button below to confirm setting up encryption."),
continueText: _t("Confirm"),
continueKind: "primary",
},
};
const { finished } = Modal.createTrackedDialog(
'Cross-signing keys dialog', '', InteractiveAuthDialog,
{
title: _t("Setting up keys"),
matrixClient: MatrixClientPeg.get(),
makeRequest,
aestheticsForStagePhases: {
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
},
},
);
const [confirmed] = await finished;
if (!confirmed) {
throw new Error("Cross-signing key upload auth canceled");
}
}
}
_bootstrapCrossSigning = async () => {
this.setState({
error: null,
});
const cli = MatrixClientPeg.get();
try {
await cli.bootstrapCrossSigning({
authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
});
this.props.onFinished(true);
} catch (e) {
this.setState({ error: e });
console.error("Error bootstrapping cross-signing", e);
}
}
_onCancel = () => {
this.props.onFinished(false);
}
render() {
let content;
if (this.state.error) {
content = <div>
<p>{_t("Unable to set up keys")}</p>
<div className="mx_Dialog_buttons">
<DialogButtons primaryButton={_t('Retry')}
onPrimaryButtonClick={this._bootstrapCrossSigning}
onCancel={this._onCancel}
/>
</div>
</div>;
} else {
content = <div>
<Spinner />
</div>;
}
return (
<BaseDialog className="mx_CreateCrossSigningDialog"
onFinished={this.props.onFinished}
title={_t("Setting up keys")}
hasCancel={false}
fixedWidth={false}
>
<div>
{content}
</div>
</BaseDialog>
);
}
}

View file

@ -16,16 +16,16 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import SetupEncryptionBody from '../../structures/auth/SetupEncryptionBody';
import BaseDialog from './BaseDialog';
import { _t } from '../../../languageHandler';
import { SetupEncryptionStore, PHASE_DONE } from '../../../stores/SetupEncryptionStore';
import SetupEncryptionBody from '../../../structures/auth/SetupEncryptionBody';
import BaseDialog from '../BaseDialog';
import { _t } from '../../../../languageHandler';
import { SetupEncryptionStore, PHASE_DONE } from '../../../../stores/SetupEncryptionStore';
function iconFromPhase(phase) {
if (phase === PHASE_DONE) {
return require("../../../../res/img/e2e/verified.svg");
return require("../../../../../res/img/e2e/verified.svg");
} else {
return require("../../../../res/img/e2e/warning.svg");
return require("../../../../../res/img/e2e/warning.svg");
}
}