Merge branch 'develop' into gsouquet-seshat-reset
This commit is contained in:
commit
a4345811b0
79 changed files with 2453 additions and 878 deletions
|
@ -43,6 +43,7 @@ import {Room} from "matrix-js-sdk/src/models/room";
|
|||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import {getAddressType} from "../../../UserAddress";
|
||||
|
||||
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||
/* eslint-disable camelcase */
|
||||
|
@ -678,14 +679,15 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
});
|
||||
};
|
||||
|
||||
_inviteUsers = () => {
|
||||
_inviteUsers = async () => {
|
||||
const startTime = CountlyAnalytics.getTimestamp();
|
||||
this.setState({busy: true});
|
||||
this._convertFilter();
|
||||
const targets = this._convertFilter();
|
||||
const targetIds = targets.map(t => t.userId);
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(this.props.roomId);
|
||||
if (!room) {
|
||||
console.error("Failed to find the room to invite users to");
|
||||
this.setState({
|
||||
|
@ -695,12 +697,34 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
return;
|
||||
}
|
||||
|
||||
inviteMultipleToRoom(this.props.roomId, targetIds).then(result => {
|
||||
try {
|
||||
const result = await inviteMultipleToRoom(this.props.roomId, targetIds)
|
||||
CountlyAnalytics.instance.trackSendInvite(startTime, this.props.roomId, targetIds.length);
|
||||
if (!this._shouldAbortAfterInviteError(result)) { // handles setting error message too
|
||||
this.props.onFinished();
|
||||
}
|
||||
}).catch(err => {
|
||||
|
||||
if (cli.isRoomEncrypted(this.props.roomId) &&
|
||||
SettingsStore.getValue("feature_room_history_key_sharing")) {
|
||||
const visibilityEvent = room.currentState.getStateEvents(
|
||||
"m.room.history_visibility", "",
|
||||
);
|
||||
const visibility = visibilityEvent && visibilityEvent.getContent() &&
|
||||
visibilityEvent.getContent().history_visibility;
|
||||
if (visibility == "world_readable" || visibility == "shared") {
|
||||
const invitedUsers = [];
|
||||
for (const [addr, state] of Object.entries(result.states)) {
|
||||
if (state === "invited" && getAddressType(addr) === "mx-user-id") {
|
||||
invitedUsers.push(addr);
|
||||
}
|
||||
}
|
||||
console.log("Sharing history with", invitedUsers);
|
||||
cli.sendSharedHistoryKeys(
|
||||
this.props.roomId, invitedUsers,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.setState({
|
||||
busy: false,
|
||||
|
@ -708,7 +732,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
"We couldn't invite those users. Please check the users you want to invite and try again.",
|
||||
),
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_transferCall = async () => {
|
||||
|
@ -1191,10 +1215,12 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
let helpText;
|
||||
let buttonText;
|
||||
let goButtonFn;
|
||||
let keySharingWarning = <span />;
|
||||
|
||||
const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
|
||||
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
const cli = MatrixClientPeg.get();
|
||||
const userId = cli.getUserId();
|
||||
if (this.props.kind === KIND_DM) {
|
||||
title = _t("Direct Messages");
|
||||
|
||||
|
@ -1290,6 +1316,25 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
|
||||
buttonText = _t("Invite");
|
||||
goButtonFn = this._inviteUsers;
|
||||
|
||||
if (SettingsStore.getValue("feature_room_history_key_sharing") &&
|
||||
cli.isRoomEncrypted(this.props.roomId)) {
|
||||
const room = cli.getRoom(this.props.roomId);
|
||||
const visibilityEvent = room.currentState.getStateEvents(
|
||||
"m.room.history_visibility", "",
|
||||
);
|
||||
const visibility = visibilityEvent && visibilityEvent.getContent() &&
|
||||
visibilityEvent.getContent().history_visibility;
|
||||
if (visibility === "world_readable" || visibility === "shared") {
|
||||
keySharingWarning =
|
||||
<p className='mx_InviteDialog_helpText'>
|
||||
<img
|
||||
src={require("../../../../res/img/element-icons/info.svg")}
|
||||
width={14} height={14} />
|
||||
{" " + _t("Invited people will be able to read old messages.")}
|
||||
</p>;
|
||||
}
|
||||
}
|
||||
} else if (this.props.kind === KIND_CALL_TRANSFER) {
|
||||
title = _t("Transfer");
|
||||
buttonText = _t("Transfer");
|
||||
|
@ -1323,6 +1368,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
{spinner}
|
||||
</div>
|
||||
</div>
|
||||
{keySharingWarning}
|
||||
{this._renderIdentityServerWarning()}
|
||||
<div className='error'>{this.state.errorText}</div>
|
||||
<div className='mx_InviteDialog_userSections'>
|
||||
|
|
|
@ -126,8 +126,8 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
|
|||
<div>
|
||||
{ _t("Make this space private") }
|
||||
<ToggleSwitch
|
||||
checked={joinRule === "private"}
|
||||
onChange={checked => setJoinRule(checked ? "private" : "invite")}
|
||||
checked={joinRule !== "public"}
|
||||
onChange={checked => setJoinRule(checked ? "invite" : "public")}
|
||||
disabled={!canSetJoinRule}
|
||||
aria-label={_t("Make this space private")}
|
||||
/>
|
||||
|
|
|
@ -50,7 +50,7 @@ export default class VerificationRequestDialog extends React.Component {
|
|||
const member = this.props.member ||
|
||||
otherUserId && MatrixClientPeg.get().getUser(otherUserId);
|
||||
const title = request && request.isSelfVerification ?
|
||||
_t("Verify other session") : _t("Verification Request");
|
||||
_t("Verify other login") : _t("Verification Request");
|
||||
|
||||
return <BaseDialog className="mx_InfoDialog" onFinished={this.props.onFinished}
|
||||
contentId="mx_Dialog_content"
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2018-2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,14 +16,15 @@ limitations under the License.
|
|||
|
||||
import {debounce} from "lodash";
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import PropTypes from "prop-types";
|
||||
import React, {ChangeEvent, FormEvent} from 'react';
|
||||
import {ISecretStorageKeyInfo} from "matrix-js-sdk/src";
|
||||
|
||||
import * as sdk from '../../../../index';
|
||||
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
||||
import Field from '../../elements/Field';
|
||||
import AccessibleButton from '../../elements/AccessibleButton';
|
||||
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import {_t} from '../../../../languageHandler';
|
||||
import {IDialogProps} from "../IDialogProps";
|
||||
|
||||
// Maximum acceptable size of a key file. It's 59 characters including the spaces we encode,
|
||||
// so this should be plenty and allow for people putting extra whitespace in the file because
|
||||
|
@ -34,22 +34,30 @@ const KEY_FILE_MAX_SIZE = 128;
|
|||
// Don't shout at the user that their key is invalid every time they type a key: wait a short time
|
||||
const VALIDATION_THROTTLE_MS = 200;
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
keyInfo: ISecretStorageKeyInfo;
|
||||
checkPrivateKey: (k: {passphrase?: string, recoveryKey?: string}) => boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
recoveryKey: string;
|
||||
recoveryKeyValid: boolean | null;
|
||||
recoveryKeyCorrect: boolean | null;
|
||||
recoveryKeyFileError: boolean | null;
|
||||
forceRecoveryKey: boolean;
|
||||
passPhrase: string;
|
||||
keyMatches: boolean | null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Access Secure Secret Storage by requesting the user's passphrase.
|
||||
*/
|
||||
export default class AccessSecretStorageDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// { passphrase, pubkey }
|
||||
keyInfo: PropTypes.object.isRequired,
|
||||
// Function from one of { passphrase, recoveryKey } -> boolean
|
||||
checkPrivateKey: PropTypes.func.isRequired,
|
||||
}
|
||||
export default class AccessSecretStorageDialog extends React.PureComponent<IProps, IState> {
|
||||
private fileUpload = React.createRef<HTMLInputElement>();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._fileUpload = React.createRef();
|
||||
|
||||
this.state = {
|
||||
recoveryKey: "",
|
||||
recoveryKeyValid: null,
|
||||
|
@ -61,21 +69,21 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
_onCancel = () => {
|
||||
private onCancel = () => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
};
|
||||
|
||||
_onUseRecoveryKeyClick = () => {
|
||||
private onUseRecoveryKeyClick = () => {
|
||||
this.setState({
|
||||
forceRecoveryKey: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_validateRecoveryKeyOnChange = debounce(() => {
|
||||
this._validateRecoveryKey();
|
||||
private validateRecoveryKeyOnChange = debounce(async () => {
|
||||
await this.validateRecoveryKey();
|
||||
}, VALIDATION_THROTTLE_MS);
|
||||
|
||||
async _validateRecoveryKey() {
|
||||
private async validateRecoveryKey() {
|
||||
if (this.state.recoveryKey === '') {
|
||||
this.setState({
|
||||
recoveryKeyValid: null,
|
||||
|
@ -102,27 +110,27 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
_onRecoveryKeyChange = (e) => {
|
||||
private onRecoveryKeyChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
recoveryKey: e.target.value,
|
||||
recoveryKey: ev.target.value,
|
||||
recoveryKeyFileError: null,
|
||||
});
|
||||
|
||||
// also clear the file upload control so that the user can upload the same file
|
||||
// the did before (otherwise the onchange wouldn't fire)
|
||||
if (this._fileUpload.current) this._fileUpload.current.value = null;
|
||||
if (this.fileUpload.current) this.fileUpload.current.value = null;
|
||||
|
||||
// We don't use Field's validation here because a) we want it in a separate place rather
|
||||
// than in a tooltip and b) we want it to display feedback based on the uploaded file
|
||||
// as well as the text box. Ideally we would refactor Field's validation logic so we could
|
||||
// re-use some of it.
|
||||
this._validateRecoveryKeyOnChange();
|
||||
}
|
||||
this.validateRecoveryKeyOnChange();
|
||||
};
|
||||
|
||||
_onRecoveryKeyFileChange = async e => {
|
||||
if (e.target.files.length === 0) return;
|
||||
private onRecoveryKeyFileChange = async (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
if (ev.target.files.length === 0) return;
|
||||
|
||||
const f = e.target.files[0];
|
||||
const f = ev.target.files[0];
|
||||
|
||||
if (f.size > KEY_FILE_MAX_SIZE) {
|
||||
this.setState({
|
||||
|
@ -140,7 +148,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
recoveryKeyFileError: null,
|
||||
recoveryKey: contents.trim(),
|
||||
});
|
||||
this._validateRecoveryKey();
|
||||
await this.validateRecoveryKey();
|
||||
} else {
|
||||
this.setState({
|
||||
recoveryKeyFileError: true,
|
||||
|
@ -150,14 +158,14 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private onRecoveryKeyFileUploadClick = () => {
|
||||
this.fileUpload.current.click();
|
||||
}
|
||||
|
||||
_onRecoveryKeyFileUploadClick = () => {
|
||||
this._fileUpload.current.click();
|
||||
}
|
||||
|
||||
_onPassPhraseNext = async (e) => {
|
||||
e.preventDefault();
|
||||
private onPassPhraseNext = async (ev: FormEvent<HTMLFormElement>) => {
|
||||
ev.preventDefault();
|
||||
|
||||
if (this.state.passPhrase.length <= 0) return;
|
||||
|
||||
|
@ -169,10 +177,10 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
} else {
|
||||
this.setState({ keyMatches });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_onRecoveryKeyNext = async (e) => {
|
||||
e.preventDefault();
|
||||
private onRecoveryKeyNext = async (ev: FormEvent<HTMLFormElement>) => {
|
||||
ev.preventDefault();
|
||||
|
||||
if (!this.state.recoveryKeyValid) return;
|
||||
|
||||
|
@ -184,16 +192,16 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
} else {
|
||||
this.setState({ keyMatches });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_onPassPhraseChange = (e) => {
|
||||
private onPassPhraseChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
passPhrase: e.target.value,
|
||||
passPhrase: ev.target.value,
|
||||
keyMatches: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
getKeyValidationText() {
|
||||
private getKeyValidationText(): string {
|
||||
if (this.state.recoveryKeyFileError) {
|
||||
return _t("Wrong file type");
|
||||
} else if (this.state.recoveryKeyCorrect) {
|
||||
|
@ -208,7 +216,8 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
// Caution: Making this an import will break tests.
|
||||
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
|
||||
|
||||
const hasPassphrase = (
|
||||
this.props.keyInfo &&
|
||||
|
@ -244,18 +253,18 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
{
|
||||
button: s => <AccessibleButton className="mx_linkButton"
|
||||
element="span"
|
||||
onClick={this._onUseRecoveryKeyClick}
|
||||
onClick={this.onUseRecoveryKeyClick}
|
||||
>
|
||||
{s}
|
||||
</AccessibleButton>,
|
||||
},
|
||||
)}</p>
|
||||
|
||||
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onPassPhraseNext}>
|
||||
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this.onPassPhraseNext}>
|
||||
<input
|
||||
type="password"
|
||||
className="mx_AccessSecretStorageDialog_passPhraseInput"
|
||||
onChange={this._onPassPhraseChange}
|
||||
onChange={this.onPassPhraseChange}
|
||||
value={this.state.passPhrase}
|
||||
autoFocus={true}
|
||||
autoComplete="new-password"
|
||||
|
@ -264,9 +273,9 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
{keyStatus}
|
||||
<DialogButtons
|
||||
primaryButton={_t('Continue')}
|
||||
onPrimaryButtonClick={this._onPassPhraseNext}
|
||||
onPrimaryButtonClick={this.onPassPhraseNext}
|
||||
hasCancel={true}
|
||||
onCancel={this._onCancel}
|
||||
onCancel={this.onCancel}
|
||||
focus={false}
|
||||
primaryDisabled={this.state.passPhrase.length === 0}
|
||||
/>
|
||||
|
@ -291,7 +300,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
|
||||
<form
|
||||
className="mx_AccessSecretStorageDialog_primaryContainer"
|
||||
onSubmit={this._onRecoveryKeyNext}
|
||||
onSubmit={this.onRecoveryKeyNext}
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
>
|
||||
|
@ -301,7 +310,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
type="password"
|
||||
label={_t('Security Key')}
|
||||
value={this.state.recoveryKey}
|
||||
onChange={this._onRecoveryKeyChange}
|
||||
onChange={this.onRecoveryKeyChange}
|
||||
forceValidity={this.state.recoveryKeyCorrect}
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
@ -312,10 +321,10 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
<div>
|
||||
<input type="file"
|
||||
className="mx_AccessSecretStorageDialog_recoveryKeyEntry_fileInput"
|
||||
ref={this._fileUpload}
|
||||
onChange={this._onRecoveryKeyFileChange}
|
||||
ref={this.fileUpload}
|
||||
onChange={this.onRecoveryKeyFileChange}
|
||||
/>
|
||||
<AccessibleButton kind="primary" onClick={this._onRecoveryKeyFileUploadClick}>
|
||||
<AccessibleButton kind="primary" onClick={this.onRecoveryKeyFileUploadClick}>
|
||||
{_t("Upload")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
|
@ -323,11 +332,11 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
{recoveryKeyFeedback}
|
||||
<DialogButtons
|
||||
primaryButton={_t('Continue')}
|
||||
onPrimaryButtonClick={this._onRecoveryKeyNext}
|
||||
onPrimaryButtonClick={this.onRecoveryKeyNext}
|
||||
hasCancel={true}
|
||||
cancelButton={_t("Go Back")}
|
||||
cancelButtonClass='danger'
|
||||
onCancel={this._onCancel}
|
||||
onCancel={this.onCancel}
|
||||
focus={false}
|
||||
primaryDisabled={!this.state.recoveryKeyValid}
|
||||
/>
|
||||
|
@ -341,9 +350,9 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
|||
title={title}
|
||||
titleClass={titleClass}
|
||||
>
|
||||
<div>
|
||||
{content}
|
||||
</div>
|
||||
<div>
|
||||
{content}
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue