Merge branches 'develop' and 't3chguy/user_online_dot' of github.com:matrix-org/matrix-react-sdk into t3chguy/user_online_dot

 Conflicts:
	src/components/views/rooms/RoomBreadcrumbs.js
	src/components/views/rooms/RoomTile.js
This commit is contained in:
Michael Telatynski 2020-04-17 12:58:54 +01:00
commit cd65bdc799
222 changed files with 4834 additions and 2280 deletions

View file

@ -16,12 +16,17 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
export default createReactClass({
displayName: 'AuthHeader',
propTypes: {
disableLanguageSelector: PropTypes.bool,
},
render: function() {
const AuthHeaderLogo = sdk.getComponent('auth.AuthHeaderLogo');
const LanguageSelector = sdk.getComponent('views.auth.LanguageSelector');
@ -29,7 +34,7 @@ export default createReactClass({
return (
<div className="mx_AuthHeader">
<AuthHeaderLogo />
<LanguageSelector />
<LanguageSelector disabled={this.props.disableLanguageSelector} />
</div>
);
},

View file

@ -28,12 +28,14 @@ function onChange(newLang) {
}
}
export default function LanguageSelector() {
export default function LanguageSelector({disabled}) {
if (SdkConfig.get()['disable_login_language_selector']) return <div />;
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
return <LanguageDropdown className="mx_AuthBody_language"
return <LanguageDropdown
className="mx_AuthBody_language"
onOptionChange={onChange}
value={getCurrentLanguage()}
disabled={disabled}
/>;
}

View file

@ -23,6 +23,7 @@ import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import AccessibleButton from "../elements/AccessibleButton";
/**
* A pure UI component which displays a username/password form.
@ -44,6 +45,7 @@ export default class PasswordLogin extends React.Component {
loginIncorrect: PropTypes.bool,
disableSubmit: PropTypes.bool,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
busy: PropTypes.bool,
};
static defaultProps = {
@ -265,12 +267,16 @@ export default class PasswordLogin extends React.Component {
if (this.props.onForgotPasswordClick) {
forgotPasswordJsx = <span>
{_t('Not sure of your password? <a>Set a new one</a>', {}, {
a: sub => <a className="mx_Login_forgot"
onClick={this.onForgotPasswordClick}
href="#"
>
{sub}
</a>,
a: sub => (
<AccessibleButton
className="mx_Login_forgot"
disabled={this.props.busy}
kind="link"
onClick={this.onForgotPasswordClick}
>
{sub}
</AccessibleButton>
),
})}
</span>;
}
@ -332,11 +338,11 @@ export default class PasswordLogin extends React.Component {
disabled={this.props.disableSubmit}
/>
{forgotPasswordJsx}
<input className="mx_Login_submit"
{ !this.props.busy && <input className="mx_Login_submit"
type="submit"
value={_t('Sign in')}
disabled={this.props.disableSubmit}
/>
/> }
</form>
</div>
);

View file

@ -26,6 +26,7 @@ import { getHostingLink } from '../../../utils/HostingLink';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {MenuItem} from "../../structures/ContextMenu";
import * as sdk from "../../../index";
import {getHomePageUrl} from "../../../utils/pages";
export default class TopLeftMenu extends React.Component {
static propTypes = {
@ -47,15 +48,7 @@ export default class TopLeftMenu extends React.Component {
}
hasHomePage() {
const config = SdkConfig.get();
const pagesConfig = config.embeddedPages;
if (pagesConfig && pagesConfig.homeUrl) {
return true;
}
// This is a deprecated config option for the home page
// (despite the name, given we also now have a welcome
// page, which is not the same).
return !!config.welcomePageUrl;
return !!getHomePageUrl(SdkConfig.get());
}
render() {

View file

@ -24,6 +24,7 @@ import withValidation from '../elements/Validation';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {Key} from "../../../Keyboard";
import SettingsStore from "../../../settings/SettingsStore";
export default createReactClass({
displayName: 'CreateRoomDialog',
@ -35,6 +36,7 @@ export default createReactClass({
const config = SdkConfig.get();
return {
isPublic: false,
isEncrypted: true,
name: "",
topic: "",
alias: "",
@ -62,6 +64,11 @@ export default createReactClass({
if (this.state.noFederate) {
createOpts.creation_content = {'m.federate': false};
}
if (!this.state.isPublic && SettingsStore.getValue("feature_cross_signing")) {
opts.encryption = this.state.isEncrypted;
}
return opts;
},
@ -127,6 +134,10 @@ export default createReactClass({
this.setState({isPublic});
},
onEncryptedChange(isEncrypted) {
this.setState({isEncrypted});
},
onAliasChange(alias) {
this.setState({alias});
},
@ -166,11 +177,10 @@ export default createReactClass({
const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch');
const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField');
let privateLabel;
let publicLabel;
let publicPrivateLabel;
let aliasField;
if (this.state.isPublic) {
publicLabel = (<p>{_t("Set a room alias to easily share your room with other people.")}</p>);
publicPrivateLabel = (<p>{_t("Set a room alias to easily share your room with other people.")}</p>);
const domain = MatrixClientPeg.get().getDomain();
aliasField = (
<div className="mx_CreateRoomDialog_aliasContainer">
@ -178,7 +188,20 @@ export default createReactClass({
</div>
);
} else {
privateLabel = (<p>{_t("This room is private, and can only be joined by invitation.")}</p>);
publicPrivateLabel = (<p>{_t("This room is private, and can only be joined by invitation.")}</p>);
}
let e2eeSection;
if (!this.state.isPublic && SettingsStore.getValue("feature_cross_signing")) {
e2eeSection = <React.Fragment>
<LabelledToggleSwitch
label={ _t("Enable end-to-end encryption")}
onChange={this.onEncryptedChange}
value={this.state.isEncrypted}
className='mx_CreateRoomDialog_e2eSwitch' // for end-to-end tests
/>
<p>{ _t("You cant disable this later. Bridges & most bots wont work yet.") }</p>
</React.Fragment>;
}
const title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room');
@ -189,10 +212,10 @@ export default createReactClass({
<form onSubmit={this.onOk} onKeyDown={this._onKeyDown}>
<div className="mx_Dialog_content">
<Field ref={ref => this._nameFieldRef = ref} label={ _t('Name') } onChange={this.onNameChange} onValidate={this.onNameValidate} value={this.state.name} className="mx_CreateRoomDialog_name" />
<Field label={ _t('Topic (optional)') } onChange={this.onTopicChange} value={this.state.topic} />
<Field label={ _t('Topic (optional)') } onChange={this.onTopicChange} value={this.state.topic} className="mx_CreateRoomDialog_topic" />
<LabelledToggleSwitch label={ _t("Make this room public")} onChange={this.onPublicChange} value={this.state.isPublic} />
{ privateLabel }
{ publicLabel }
{ publicPrivateLabel }
{ e2eeSection }
{ aliasField }
<details ref={this.collectDetailsRef} className="mx_CreateRoomDialog_details">
<summary className="mx_CreateRoomDialog_details_summary">{ this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') }</summary>

View file

@ -1,6 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
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.
@ -23,71 +23,109 @@ import Analytics from '../../../Analytics';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as Lifecycle from '../../../Lifecycle';
import { _t } from '../../../languageHandler';
import InteractiveAuth, {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth";
import {DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents";
const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
body: _t("Confirm your account deactivation by using Single Sign On to prove your identity."),
continueText: _t("Single Sign On"),
continueKind: "danger",
},
[SSOAuthEntry.PHASE_POSTAUTH]: {
body: _t("Are you sure you want to deactivate your account? This is irreversible."),
continueText: _t("Confirm account deactivation"),
continueKind: "danger",
},
};
// This is the same as aestheticsForStagePhases in InteractiveAuthDialog minus the `title`
const DEACTIVATE_AESTHETICS = {
[SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics,
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics,
[PasswordAuthEntry.LOGIN_TYPE]: {
[DEFAULT_PHASE]: {
body: _t("To continue, please enter your password:"),
},
},
};
export default class DeactivateAccountDialog extends React.Component {
constructor(props) {
super(props);
this._onOk = this._onOk.bind(this);
this._onCancel = this._onCancel.bind(this);
this._onPasswordFieldChange = this._onPasswordFieldChange.bind(this);
this._onEraseFieldChange = this._onEraseFieldChange.bind(this);
this.state = {
password: "",
busy: false,
shouldErase: false,
errStr: null,
authData: null, // for UIA
// A few strings that are passed to InteractiveAuth for design or are displayed
// next to the InteractiveAuth component.
bodyText: null,
continueText: null,
continueKind: null,
};
}
_onPasswordFieldChange(ev) {
this.setState({
password: ev.target.value,
});
}
_onEraseFieldChange(ev) {
this.setState({
shouldErase: ev.target.checked,
});
}
async _onOk() {
this.setState({busy: true});
try {
// This assumes that the HS requires password UI auth
// for this endpoint. In reality it could be any UI auth.
const auth = {
type: 'm.login.password',
// TODO: Remove `user` once servers support proper UIA
// See https://github.com/vector-im/riot-web/issues/10312
user: MatrixClientPeg.get().credentials.userId,
identifier: {
type: "m.id.user",
user: MatrixClientPeg.get().credentials.userId,
},
password: this.state.password,
};
await MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase);
} catch (err) {
let errStr = _t('Unknown error');
// https://matrix.org/jira/browse/SYN-744
if (err.httpStatus === 401 || err.httpStatus === 403) {
errStr = _t('Incorrect password');
MatrixClientPeg.get().deactivateAccount(null, false).then(r => {
// If we got here, oops. The server didn't require any auth.
// Our application lifecycle will catch the error and do the logout bits.
// We'll try to log something in an vain attempt to record what happened (storage
// is also obliterated on logout).
console.warn("User's account got deactivated without confirmation: Server had no auth");
this.setState({errStr: _t("Server did not require any authentication")});
}).catch(e => {
if (e && e.httpStatus === 401 && e.data) {
// Valid UIA response
this.setState({authData: e.data});
} else {
this.setState({errStr: _t("Server did not return valid authentication information.")});
}
this.setState({
busy: false,
errStr: errStr,
});
});
}
_onStagePhaseChange = (stage, phase) => {
const aesthetics = DEACTIVATE_AESTHETICS[stage];
let bodyText = null;
let continueText = null;
let continueKind = null;
if (aesthetics) {
const phaseAesthetics = aesthetics[phase];
if (phaseAesthetics && phaseAesthetics.body) bodyText = phaseAesthetics.body;
if (phaseAesthetics && phaseAesthetics.continueText) continueText = phaseAesthetics.continueText;
if (phaseAesthetics && phaseAesthetics.continueKind) continueKind = phaseAesthetics.continueKind;
}
this.setState({bodyText, continueText, continueKind});
};
_onUIAuthFinished = (success, result, extra) => {
if (success) return; // great! makeRequest() will be called too.
if (result === ERROR_USER_CANCELLED) {
this._onCancel();
return;
}
Analytics.trackEvent('Account', 'Deactivate Account');
Lifecycle.onLoggedOut();
this.props.onFinished(true);
}
console.error("Error during UI Auth:", {result, extra});
this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")});
};
_onUIAuthComplete = (auth) => {
MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase).then(r => {
// Deactivation worked - logout & close this dialog
Analytics.trackEvent('Account', 'Deactivate Account');
Lifecycle.onLoggedOut();
this.props.onFinished(true);
}).catch(e => {
console.error(e);
this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")});
});
};
_onEraseFieldChange = (ev) => {
this.setState({
shouldErase: ev.target.checked,
});
};
_onCancel() {
this.props.onFinished(false);
@ -95,34 +133,36 @@ export default class DeactivateAccountDialog extends React.Component {
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Loader = sdk.getComponent("elements.Spinner");
let passwordBoxClass = '';
let error = null;
if (this.state.errStr) {
error = <div className="error">
{ this.state.errStr }
</div>;
passwordBoxClass = 'error';
}
const okLabel = this.state.busy ? <Loader /> : _t('Deactivate Account');
const okEnabled = this.state.password && !this.state.busy;
let cancelButton = null;
if (!this.state.busy) {
cancelButton = <button onClick={this._onCancel} autoFocus={true}>
{ _t("Cancel") }
</button>;
let auth = <div>{_t("Loading...")}</div>;
if (this.state.authData) {
auth = (
<div>
{this.state.bodyText}
<InteractiveAuth
matrixClient={MatrixClientPeg.get()}
authData={this.state.authData}
makeRequest={this._onUIAuthComplete}
onAuthFinished={this._onUIAuthFinished}
onStagePhaseChange={this._onStagePhaseChange}
continueText={this.state.continueText}
continueKind={this.state.continueKind}
/>
</div>
);
}
const Field = sdk.getComponent('elements.Field');
// this is on purpose not a <form /> to prevent Enter triggering submission, to further prevent accidents
return (
<BaseDialog className="mx_DeactivateAccountDialog"
onFinished={this.props.onFinished}
onEnterPressed={this.onOk}
titleClass="danger"
title={_t("Deactivate Account")}
>
@ -172,28 +212,10 @@ export default class DeactivateAccountDialog extends React.Component {
</label>
</p>
<p>{ _t("To continue, please enter your password:") }</p>
<Field
type="password"
label={_t('Password')}
onChange={this._onPasswordFieldChange}
value={this.state.password}
className={passwordBoxClass}
/>
{error}
{auth}
</div>
{ error }
</div>
<div className="mx_Dialog_buttons">
<button
className="mx_Dialog_primary danger"
onClick={this._onOk}
disabled={!okEnabled}
>
{ okLabel }
</button>
{ cancelButton }
</div>
</BaseDialog>
);

View file

@ -131,7 +131,7 @@ export default class DeviceVerifyDialog extends React.Component {
} else {
this._verifier = request.verifier;
}
} else if (verifyingOwnDevice && SettingsStore.isFeatureEnabled("feature_cross_signing")) {
} else if (verifyingOwnDevice && SettingsStore.getValue("feature_cross_signing")) {
this._request = await client.requestVerification(this.props.userId, [
verificationMethods.SAS,
SHOW_QR_CODE_METHOD,

View file

@ -574,7 +574,7 @@ export default class InviteDialog extends React.PureComponent {
const createRoomOptions = {inlineErrors: true};
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (SettingsStore.getValue("feature_cross_signing")) {
// Check whether all users have uploaded device keys before.
// If so, enable encryption in the new room.
const client = MatrixClientPeg.get();

View file

@ -1,5 +1,6 @@
/*
Copyright 2018 New Vector Ltd
Copyright 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.
@ -14,15 +15,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import {Room, User, Group, RoomMember, MatrixEvent} from 'matrix-js-sdk';
import * as React from 'react';
import * as PropTypes from 'prop-types';
import {Room} from "matrix-js-sdk/src/models/room";
import {User} from "matrix-js-sdk/src/models/user";
import {Group} from "matrix-js-sdk/src/models/group";
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import QRCode from 'qrcode-react';
import {RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
import * as ContextMenu from "../../structures/ContextMenu";
import {toRightOf} from "../../structures/ContextMenu";
import {copyPlaintext, selectText} from "../../../utils/strings";
const socials = [
{
@ -52,7 +58,18 @@ const socials = [
},
];
export default class ShareDialog extends React.Component {
interface IProps {
onFinished: () => void;
target: Room | User | Group | RoomMember | MatrixEvent;
permalinkCreator: RoomPermalinkCreator;
}
interface IState {
linkSpecificEvent: boolean;
permalinkCreator: RoomPermalinkCreator;
}
export default class ShareDialog extends React.PureComponent<IProps, IState> {
static propTypes = {
onFinished: PropTypes.func.isRequired,
target: PropTypes.oneOfType([
@ -64,55 +81,45 @@ export default class ShareDialog extends React.Component {
]).isRequired,
};
protected closeCopiedTooltip: () => void;
constructor(props) {
super(props);
this.onCopyClick = this.onCopyClick.bind(this);
this.onLinkSpecificEventCheckboxClick = this.onLinkSpecificEventCheckboxClick.bind(this);
let permalinkCreator: RoomPermalinkCreator = null;
if (props.target instanceof Room) {
permalinkCreator = new RoomPermalinkCreator(props.target);
permalinkCreator.load();
}
this.state = {
// MatrixEvent defaults to share linkSpecificEvent
linkSpecificEvent: this.props.target instanceof MatrixEvent,
permalinkCreator,
};
this._link = createRef();
}
static _selectText(target) {
const range = document.createRange();
range.selectNodeContents(target);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
static onLinkClick(e) {
e.preventDefault();
const {target} = e;
ShareDialog._selectText(target);
selectText(e.target);
}
onCopyClick(e) {
async onCopyClick(e) {
e.preventDefault();
const target = e.target; // copy target before we go async and React throws it away
ShareDialog._selectText(this._link.current);
let successful;
try {
successful = document.execCommand('copy');
} catch (err) {
console.error('Failed to copy: ', err);
}
const buttonRect = e.target.getBoundingClientRect();
const successful = await copyPlaintext(this.getUrl());
const buttonRect = target.getBoundingClientRect();
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
...toRightOf(buttonRect, 2),
message: successful ? _t('Copied!') : _t('Failed to copy'),
});
// Drop a reference to this close handler for componentWillUnmount
this.closeCopiedTooltip = e.target.onmouseleave = close;
this.closeCopiedTooltip = target.onmouseleave = close;
}
onLinkSpecificEventCheckboxClick() {
@ -121,24 +128,38 @@ export default class ShareDialog extends React.Component {
});
}
componentDidMount() {
if (this.props.target instanceof Room) {
const permalinkCreator = new RoomPermalinkCreator(this.props.target);
permalinkCreator.load();
this.setState({permalinkCreator});
}
}
componentWillUnmount() {
// if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close
// the tooltip otherwise, such as pressing Escape or clicking X really quickly
if (this.closeCopiedTooltip) this.closeCopiedTooltip();
}
render() {
let title;
getUrl() {
let matrixToUrl;
if (this.props.target instanceof Room) {
if (this.state.linkSpecificEvent) {
const events = this.props.target.getLiveTimeline().getEvents();
matrixToUrl = this.state.permalinkCreator.forEvent(events[events.length - 1].getId());
} else {
matrixToUrl = this.state.permalinkCreator.forRoom();
}
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
matrixToUrl = makeUserPermalink(this.props.target.userId);
} else if (this.props.target instanceof Group) {
matrixToUrl = makeGroupPermalink(this.props.target.groupId);
} else if (this.props.target instanceof MatrixEvent) {
if (this.state.linkSpecificEvent) {
matrixToUrl = this.props.permalinkCreator.forEvent(this.props.target.getId());
} else {
matrixToUrl = this.props.permalinkCreator.forRoom();
}
}
return matrixToUrl;
}
render() {
let title;
let checkbox;
if (this.props.target instanceof Room) {
@ -156,18 +177,10 @@ export default class ShareDialog extends React.Component {
</label>
</div>;
}
if (this.state.linkSpecificEvent) {
matrixToUrl = this.state.permalinkCreator.forEvent(events[events.length - 1].getId());
} else {
matrixToUrl = this.state.permalinkCreator.forRoom();
}
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
title = _t('Share User');
matrixToUrl = makeUserPermalink(this.props.target.userId);
} else if (this.props.target instanceof Group) {
title = _t('Share Community');
matrixToUrl = makeGroupPermalink(this.props.target.groupId);
} else if (this.props.target instanceof MatrixEvent) {
title = _t('Share Room Message');
checkbox = <div>
@ -179,14 +192,9 @@ export default class ShareDialog extends React.Component {
{ _t('Link to selected message') }
</label>
</div>;
if (this.state.linkSpecificEvent) {
matrixToUrl = this.props.permalinkCreator.forEvent(this.props.target.getId());
} else {
matrixToUrl = this.props.permalinkCreator.forRoom();
}
}
const matrixToUrl = this.getUrl();
const encodedUrl = encodeURIComponent(matrixToUrl);
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
@ -197,8 +205,7 @@ export default class ShareDialog extends React.Component {
>
<div className="mx_ShareDialog_content">
<div className="mx_ShareDialog_matrixto">
<a ref={this._link}
href={matrixToUrl}
<a href={matrixToUrl}
onClick={ShareDialog.onLinkClick}
className="mx_ShareDialog_matrixto_link"
>
@ -217,17 +224,18 @@ export default class ShareDialog extends React.Component {
<QRCode value={matrixToUrl} size={256} logoWidth={48} logo={require("../../../../res/img/matrix-m.svg")} />
</div>
<div className="mx_ShareDialog_social_container">
{
socials.map((social) => <a rel="noreferrer noopener"
target="_blank"
key={social.name}
name={social.name}
href={social.url(encodedUrl)}
className="mx_ShareDialog_social_icon"
{ 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>)
}
</a>
)) }
</div>
</div>
</div>

View file

@ -30,16 +30,29 @@ export default class VerificationRequestDialog extends React.Component {
constructor(...args) {
super(...args);
this.onFinished = this.onFinished.bind(this);
this.state = {};
if (this.props.verificationRequest) {
this.state.verificationRequest = this.props.verificationRequest;
} else if (this.props.verificationRequestPromise) {
this.props.verificationRequestPromise.then(r => {
this.setState({verificationRequest: r});
});
}
}
render() {
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel");
const request = this.state.verificationRequest;
const otherUserId = request && request.otherUserId;
const member = this.props.member ||
MatrixClientPeg.get().getUser(this.props.verificationRequest.otherUserId);
otherUserId && MatrixClientPeg.get().getUser(otherUserId);
const title = request && request.isSelfVerification ?
_t("Verify other session") : _t("Verification Request");
return <BaseDialog className="mx_InfoDialog" onFinished={this.onFinished}
contentId="mx_Dialog_content"
title={_t("Verification Request")}
title={title}
hasCancel={true}
>
<EncryptionPanel
@ -48,7 +61,6 @@ export default class VerificationRequestDialog extends React.Component {
verificationRequestPromise={this.props.verificationRequestPromise}
onClose={this.props.onFinished}
member={member}
inDialog={true}
/>
</BaseDialog>;
}

View file

@ -283,7 +283,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
title = _t("Recovery key mismatch");
content = <div>
<p>{_t(
"Backup could not be decrypted with this key: " +
"Backup could not be decrypted with this recovery key: " +
"please verify that you entered the correct recovery key.",
)}</p>
</div>;
@ -291,7 +291,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
title = _t("Incorrect recovery passphrase");
content = <div>
<p>{_t(
"Backup could not be decrypted with this passphrase: " +
"Backup could not be decrypted with this recovery passphrase: " +
"please verify that you entered the correct recovery passphrase.",
)}</p>
</div>;

View file

@ -119,14 +119,14 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
if (hasPassphrase && !this.state.forceRecoveryKey) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
title = _t("Enter secret storage passphrase");
title = _t("Enter recovery passphrase");
let keyStatus;
if (this.state.keyMatches === false) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4E "}{_t(
"Unable to access secret storage. Please verify that you " +
"entered the correct passphrase.",
"Unable to access secret storage. " +
"Please verify that you entered the correct recovery passphrase.",
)}
</div>;
} else {
@ -135,13 +135,12 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
content = <div>
<p>{_t(
"<b>Warning</b>: You should only access secret storage " +
"from a trusted computer.", {},
"<b>Warning</b>: You should only do this on a trusted computer.", {},
{ b: sub => <b>{sub}</b> },
)}</p>
<p>{_t(
"Access your secure message history and your cross-signing " +
"identity for verifying other sessions by entering your passphrase.",
"identity for verifying other sessions by entering your recovery passphrase.",
)}</p>
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onPassPhraseNext}>
@ -164,7 +163,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
/>
</form>
{_t(
"If you've forgotten your passphrase you can "+
"If you've forgotten your recovery passphrase you can "+
"<button1>use your recovery key</button1> or " +
"<button2>set up new recovery options</button2>."
, {}, {
@ -183,7 +182,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
})}
</div>;
} else {
title = _t("Enter secret storage recovery key");
title = _t("Enter recovery key");
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
@ -193,8 +192,8 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
} else if (this.state.keyMatches === false) {
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
{"\uD83D\uDC4E "}{_t(
"Unable to access secret storage. Please verify that you " +
"entered the correct recovery key.",
"Unable to access secret storage. " +
"Please verify that you entered the correct recovery key.",
)}
</div>;
} else if (this.state.recoveryKeyValid) {
@ -209,8 +208,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
content = <div>
<p>{_t(
"<b>Warning</b>: You should only access secret storage " +
"from a trusted computer.", {},
"<b>Warning</b>: You should only do this on a trusted computer.", {},
{ b: sub => <b>{sub}</b> },
)}</p>
<p>{_t(

View file

@ -136,22 +136,21 @@ export default class AppTile extends React.Component {
* If url can not be parsed, it is returned unmodified.
*/
_addWurlParams(urlString) {
const u = url.parse(urlString);
if (!u) {
console.error("_addWurlParams", "Invalid URL", urlString);
return url;
try {
const parsed = new URL(urlString);
// TODO: Replace these with proper widget params
// See https://github.com/matrix-org/matrix-doc/pull/1958/files#r405714833
parsed.searchParams.set('widgetId', this.props.app.id);
parsed.searchParams.set('parentUrl', window.location.href.split('#', 2)[0]);
// Replace the encoded dollar signs back to dollar signs. They have no special meaning
// in HTTP, but URL parsers encode them anyways.
return parsed.toString().replace(/%24/g, '$');
} catch (e) {
console.error("Failed to add widget URL params:", e);
return urlString;
}
const params = qs.parse(u.query);
// Append widget ID to query parameters
params.widgetId = this.props.app.id;
// Append current / parent URL, minus the hash because that will change when
// we view a different room (ie. may change for persistent widgets)
params.parentUrl = window.location.href.split('#', 2)[0];
u.search = undefined;
u.query = params;
return u.format();
}
isMixedContent() {
@ -270,7 +269,9 @@ export default class AppTile extends React.Component {
if (this.props.show && this.state.hasPermissionToLoad) {
this.setScalarToken();
}
} else if (nextProps.show && !this.props.show) {
}
if (nextProps.show && !this.props.show) {
// We assume that persisted widgets are loaded and don't need a spinner.
if (this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey)) {
this.setState({
@ -281,7 +282,9 @@ export default class AppTile extends React.Component {
if (this.state.hasPermissionToLoad) {
this.setScalarToken();
}
} else if (nextProps.widgetPageTitle !== this.props.widgetPageTitle) {
}
if (nextProps.widgetPageTitle !== this.props.widgetPageTitle) {
this.setState({
widgetPageTitle: nextProps.widgetPageTitle,
});
@ -333,6 +336,28 @@ export default class AppTile extends React.Component {
});
}
/**
* Ends all widget interaction, such as cancelling calls and disabling webcams.
* @private
*/
_endWidgetActions() {
// HACK: This is a really dirty way to ensure that Jitsi cleans up
// its hold on the webcam. Without this, the widget holds a media
// stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
if (this._appFrame.current) {
// In practice we could just do `+= ''` to trick the browser
// into thinking the URL changed, however I can foresee this
// being optimized out by a browser. Instead, we'll just point
// the iframe at a page that is reasonably safe to use in the
// event the iframe doesn't wink away.
// This is relative to where the Riot instance is located.
this._appFrame.current.src = 'about:blank';
}
// Delete the widget from the persisted store for good measure.
PersistedElement.destroyElement(this._persistKey);
}
/* If user has permission to modify widgets, delete the widget,
* otherwise revoke access for the widget to load in the user's browser
*/
@ -354,18 +379,7 @@ export default class AppTile extends React.Component {
}
this.setState({deleting: true});
// HACK: This is a really dirty way to ensure that Jitsi cleans up
// its hold on the webcam. Without this, the widget holds a media
// stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351
if (this._appFrame.current) {
// In practice we could just do `+= ''` to trick the browser
// into thinking the URL changed, however I can foresee this
// being optimized out by a browser. Instead, we'll just point
// the iframe at a page that is reasonably safe to use in the
// event the iframe doesn't wink away.
// This is relative to where the Riot instance is located.
this._appFrame.current.src = 'about:blank';
}
this._endWidgetActions();
WidgetUtils.setRoomWidget(
this.props.room.roomId,
@ -530,6 +544,10 @@ export default class AppTile extends React.Component {
if (this.props.userWidget) {
this._onMinimiseClick();
} else {
if (this.props.show) {
// if we were being shown, end the widget as we're about to be minimized.
this._endWidgetActions();
}
dis.dispatch({
action: 'appsDrawer',
show: !this.props.show,

View file

@ -0,0 +1,19 @@
/*
Copyright 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.
*/
export default function ButtonPlaceholder(props) {
return <div class="mx_ButtonPlaceholder">{props.children}</div>;
}

View file

@ -35,6 +35,9 @@ export default class LabelledToggleSwitch extends React.Component {
// True to put the toggle in front of the label
// Default false.
toggleInFront: PropTypes.bool,
// Additional class names to append to the switch. Optional.
className: PropTypes.string,
};
render() {
@ -50,8 +53,9 @@ export default class LabelledToggleSwitch extends React.Component {
secondPart = temp;
}
const classes = `mx_SettingsFlag ${this.props.className || ""}`;
return (
<div className="mx_SettingsFlag">
<div className={classes}>
{firstPart}
{secondPart}
</div>

View file

@ -114,6 +114,7 @@ export default class LanguageDropdown extends React.Component {
searchEnabled={true}
value={value}
label={_t("Language Dropdown")}
disabled={this.props.disabled}
>
{ options }
</Dropdown>;

View file

@ -17,95 +17,17 @@ limitations under the License.
import React from "react";
import PropTypes from "prop-types";
import {replaceableComponent} from "../../../../utils/replaceableComponent";
import {MatrixClientPeg} from "../../../../MatrixClientPeg";
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import {ToDeviceChannel} from "matrix-js-sdk/src/crypto/verification/request/ToDeviceChannel";
import {decodeBase64} from "matrix-js-sdk/src/crypto/olmlib";
import Spinner from "../Spinner";
import * as QRCode from "qrcode";
const CODE_VERSION = 0x02; // the version of binary QR codes we support
const BINARY_PREFIX = "MATRIX"; // ASCII, used to prefix the binary format
const MODE_VERIFY_OTHER_USER = 0x00; // Verifying someone who isn't us
const MODE_VERIFY_SELF_TRUSTED = 0x01; // We trust the master key
const MODE_VERIFY_SELF_UNTRUSTED = 0x02; // We do not trust the master key
@replaceableComponent("views.elements.crypto.VerificationQRCode")
export default class VerificationQRCode extends React.PureComponent {
static propTypes = {
prefix: PropTypes.string.isRequired,
version: PropTypes.number.isRequired,
mode: PropTypes.number.isRequired,
transactionId: PropTypes.string.isRequired, // or requestEventId
firstKeyB64: PropTypes.string.isRequired,
secondKeyB64: PropTypes.string.isRequired,
secretB64: PropTypes.string.isRequired,
qrCodeData: PropTypes.object.isRequired,
};
static async getPropsForRequest(verificationRequest: VerificationRequest) {
const cli = MatrixClientPeg.get();
const myUserId = cli.getUserId();
const otherUserId = verificationRequest.otherUserId;
let mode = MODE_VERIFY_OTHER_USER;
if (myUserId === otherUserId) {
// Mode changes depending on whether or not we trust the master cross signing key
const myTrust = cli.checkUserTrust(myUserId);
if (myTrust.isCrossSigningVerified()) {
mode = MODE_VERIFY_SELF_TRUSTED;
} else {
mode = MODE_VERIFY_SELF_UNTRUSTED;
}
}
const requestEvent = verificationRequest.requestEvent;
const transactionId = requestEvent.getId()
? requestEvent.getId()
: ToDeviceChannel.getTransactionId(requestEvent);
const qrProps = {
prefix: BINARY_PREFIX,
version: CODE_VERSION,
mode,
transactionId,
firstKeyB64: '', // worked out shortly
secondKeyB64: '', // worked out shortly
secretB64: verificationRequest.encodedSharedSecret,
};
const myCrossSigningInfo = cli.getStoredCrossSigningForUser(myUserId);
const myDevices = (await cli.getStoredDevicesForUser(myUserId)) || [];
if (mode === MODE_VERIFY_OTHER_USER) {
// First key is our master cross signing key
qrProps.firstKeyB64 = myCrossSigningInfo.getId("master");
// Second key is the other user's master cross signing key
const otherUserCrossSigningInfo = cli.getStoredCrossSigningForUser(otherUserId);
qrProps.secondKeyB64 = otherUserCrossSigningInfo.getId("master");
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
// First key is our master cross signing key
qrProps.firstKeyB64 = myCrossSigningInfo.getId("master");
// Second key is the other device's device key
const otherDevice = verificationRequest.targetDevice;
const otherDeviceId = otherDevice ? otherDevice.deviceId : null;
const device = myDevices.find(d => d.deviceId === otherDeviceId);
qrProps.secondKeyB64 = device.getFingerprint();
} else if (mode === MODE_VERIFY_SELF_UNTRUSTED) {
// First key is our device's key
qrProps.firstKeyB64 = cli.getDeviceEd25519Key();
// Second key is what we think our master cross signing key is
qrProps.secondKeyB64 = myCrossSigningInfo.getId("master");
}
return qrProps;
}
constructor(props) {
super(props);
this.state = {
dataUri: null,
};
@ -119,39 +41,8 @@ export default class VerificationQRCode extends React.PureComponent {
}
async generateQrCode() {
let buf = Buffer.alloc(0); // we'll concat our way through life
const appendByte = (b: number) => {
const tmpBuf = Buffer.from([b]);
buf = Buffer.concat([buf, tmpBuf]);
};
const appendInt = (i: number) => {
const tmpBuf = Buffer.alloc(2);
tmpBuf.writeInt16BE(i, 0);
buf = Buffer.concat([buf, tmpBuf]);
};
const appendStr = (s: string, enc: string, withLengthPrefix = true) => {
const tmpBuf = Buffer.from(s, enc);
if (withLengthPrefix) appendInt(tmpBuf.byteLength);
buf = Buffer.concat([buf, tmpBuf]);
};
const appendEncBase64 = (b64: string) => {
const b = decodeBase64(b64);
const tmpBuf = Buffer.from(b);
buf = Buffer.concat([buf, tmpBuf]);
};
// Actually build the buffer for the QR code
appendStr(this.props.prefix, "ascii", false);
appendByte(this.props.version);
appendByte(this.props.mode);
appendStr(this.props.transactionId, "utf-8");
appendEncBase64(this.props.firstKeyB64);
appendEncBase64(this.props.secondKeyB64);
appendEncBase64(this.props.secretB64);
// Now actually assemble the QR code's data URI
const uri = await QRCode.toDataURL([{data: buf, mode: 'byte'}], {
const uri = await QRCode.toDataURL([{data: this.props.qrCodeData.buffer, mode: 'byte'}], {
errorCorrectionLevel: 'L', // we want it as trivial-looking as possible
});
this.setState({dataUri: uri});

View file

@ -26,6 +26,7 @@ import Modal from '../../../Modal';
import {aboveLeftOf, ContextMenu, ContextMenuButton, useContextMenu} from '../../structures/ContextMenu';
import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
import RoomContext from "../../../contexts/RoomContext";
import SettingsStore from '../../../settings/SettingsStore';
const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
@ -48,7 +49,7 @@ const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFo
};
let e2eInfoCallback = null;
if (mxEvent.isEncrypted()) {
if (mxEvent.isEncrypted() && !SettingsStore.getValue("feature_cross_signing")) {
e2eInfoCallback = onCryptoClick;
}

View file

@ -34,6 +34,7 @@ import {pillifyLinks, unmountPills} from '../../../utils/pillify';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import {isPermalinkHost} from "../../../utils/permalinks/Permalinks";
import {toRightOf} from "../../structures/ContextMenu";
import {copyPlaintext} from "../../../utils/strings";
export default createReactClass({
displayName: 'TextualBody',
@ -69,23 +70,6 @@ export default createReactClass({
};
},
copyToClipboard: function(text) {
const textArea = document.createElement("textarea");
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
let successful = false;
try {
successful = document.execCommand('copy');
} catch (err) {
console.log('Unable to copy');
}
document.body.removeChild(textArea);
return successful;
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._content = createRef();
@ -277,17 +261,17 @@ export default createReactClass({
Array.from(ReactDOM.findDOMNode(this).querySelectorAll('.mx_EventTile_body pre')).forEach((p) => {
const button = document.createElement("span");
button.className = "mx_EventTile_copyButton";
button.onclick = (e) => {
const copyCode = button.parentNode.getElementsByTagName("code")[0];
const successful = this.copyToClipboard(copyCode.textContent);
button.onclick = async () => {
const copyCode = button.parentNode.getElementsByTagName("pre")[0];
const successful = await copyPlaintext(copyCode.textContent);
const buttonRect = e.target.getBoundingClientRect();
const buttonRect = button.getBoundingClientRect();
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
...toRightOf(buttonRect, 2),
message: successful ? _t('Copied!') : _t('Failed to copy'),
});
e.target.onmouseleave = close;
button.onmouseleave = close;
};
// Wrap a div around <pre> so that the copy button can be correctly positioned

View file

@ -28,14 +28,26 @@ export const PendingActionSpinner = ({text}) => {
</div>;
};
const EncryptionInfo = ({waitingForOtherParty, waitingForNetwork, member, onStartVerification, isRoomEncrypted}) => {
const EncryptionInfo = ({
waitingForOtherParty,
waitingForNetwork,
member,
onStartVerification,
isRoomEncrypted,
inDialog,
isSelfVerification,
}) => {
let content;
if (waitingForOtherParty || waitingForNetwork) {
let text;
if (waitingForOtherParty) {
text = _t("Waiting for %(displayName)s to accept…", {
displayName: member.displayName || member.name || member.userId,
});
if (isSelfVerification) {
text = _t("Waiting for you to accept on your other session…");
} else {
text = _t("Waiting for %(displayName)s to accept…", {
displayName: member.displayName || member.name || member.userId,
});
}
} else {
text = _t("Accepting…");
}
@ -66,6 +78,10 @@ const EncryptionInfo = ({waitingForOtherParty, waitingForNetwork, member, onStar
);
}
if (inDialog) {
return content;
}
return <React.Fragment>
<div className="mx_UserInfo_container">
<h3>{_t("Encryption")}</h3>

View file

@ -22,6 +22,7 @@ import VerificationPanel from "./VerificationPanel";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {ensureDMExists} from "../../../createRoom";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
import {useAsyncMemo} from "../../../hooks/useAsyncMemo";
import Modal from "../../../Modal";
import {PHASE_REQUESTED, PHASE_UNSENT} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import * as sdk from "../../../index";
@ -31,7 +32,7 @@ import {_t} from "../../../languageHandler";
const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"];
const EncryptionPanel = (props) => {
const {verificationRequest, verificationRequestPromise, member, onClose, layout, isRoomEncrypted, inDialog} = props;
const {verificationRequest, verificationRequestPromise, member, onClose, layout, isRoomEncrypted} = props;
const [request, setRequest] = useState(verificationRequest);
// state to show a spinner immediately after clicking "start verification",
// before we have a request
@ -45,6 +46,12 @@ const EncryptionPanel = (props) => {
}
}, [verificationRequest]);
const deviceId = request && request.channel.deviceId;
const device = useAsyncMemo(() => {
const cli = MatrixClientPeg.get();
return cli.getStoredDevice(cli.getUserId(), deviceId);
}, [deviceId]);
useEffect(() => {
async function awaitPromise() {
setRequesting(true);
@ -112,6 +119,9 @@ const EncryptionPanel = (props) => {
const requested =
(!request && isRequesting) ||
(request && (phase === PHASE_REQUESTED || phase === PHASE_UNSENT || phase === undefined));
const isSelfVerification = request ?
request.isSelfVerification :
member.userId === MatrixClientPeg.get().getUserId();
if (!request || requested) {
const initiatedByMe = (!request && isRequesting) || (request && request.initiatedByMe);
return (<React.Fragment>
@ -120,8 +130,10 @@ const EncryptionPanel = (props) => {
isRoomEncrypted={isRoomEncrypted}
onStartVerification={onStartVerification}
member={member}
isSelfVerification={isSelfVerification}
waitingForOtherParty={requested && initiatedByMe}
waitingForNetwork={requested && !initiatedByMe} />
waitingForNetwork={requested && !initiatedByMe}
inDialog={layout === "dialog"} />
</React.Fragment>);
} else {
return (<React.Fragment>
@ -133,8 +145,9 @@ const EncryptionPanel = (props) => {
member={member}
request={request}
key={request.channel.transactionId}
inDialog={inDialog}
phase={phase} />
inDialog={layout === "dialog"}
phase={phase}
device={device} />
</React.Fragment>);
}
};

View file

@ -63,7 +63,7 @@ const _disambiguateDevices = (devices) => {
};
export const getE2EStatus = (cli, userId, devices) => {
if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (!SettingsStore.getValue("feature_cross_signing")) {
const hasUnverifiedDevice = devices.some((device) => device.isUnverified());
return hasUnverifiedDevice ? "warning" : "verified";
}
@ -111,7 +111,7 @@ async function openDMForUser(matrixClient, userId) {
dmUserId: userId,
};
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (SettingsStore.getValue("feature_cross_signing")) {
// Check whether all users have uploaded device keys before.
// If so, enable encryption in the new room.
const usersToDevicesMap = await matrixClient.downloadKeys([userId]);
@ -166,7 +166,7 @@ function DeviceItem({userId, device}) {
// cross-signing so that other users can then safely trust you.
// For other people's devices, the more general verified check that
// includes locally verified devices can be used.
const isVerified = (isMe && SettingsStore.isFeatureEnabled("feature_cross_signing")) ?
const isVerified = (isMe && SettingsStore.getValue("feature_cross_signing")) ?
deviceTrust.isCrossSigningVerified() :
deviceTrust.isVerified();
@ -237,7 +237,7 @@ function DevicesSection({devices, userId, loading}) {
// cross-signing so that other users can then safely trust you.
// For other people's devices, the more general verified check that
// includes locally verified devices can be used.
const isVerified = (isMe && SettingsStore.isFeatureEnabled("feature_cross_signing")) ?
const isVerified = (isMe && SettingsStore.getValue("feature_cross_signing")) ?
deviceTrust.isCrossSigningVerified() :
deviceTrust.isVerified();
@ -1298,7 +1298,7 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
const userTrust = cli.checkUserTrust(member.userId);
const userVerified = userTrust.isCrossSigningVerified();
const isMe = member.userId === cli.getUserId();
const canVerify = SettingsStore.isFeatureEnabled("feature_cross_signing") &&
const canVerify = SettingsStore.getValue("feature_cross_signing") &&
homeserverSupportsCrossSigning && !userVerified && !isMe;
const setUpdating = (updating) => {
@ -1444,9 +1444,11 @@ const UserInfoHeader = ({onClose, member, e2eStatus}) => {
<div className="mx_UserInfo_container mx_UserInfo_separator">
<div className="mx_UserInfo_profile">
<div>
<h2 aria-label={displayName}>
<h2>
{ e2eIcon }
{ displayName }
<span title={displayName} aria-label={displayName}>
{ displayName }
</span>
</h2>
</div>
<div>{ member.userId }</div>

View file

@ -30,7 +30,7 @@ import {
PHASE_READY,
PHASE_DONE,
PHASE_STARTED,
PHASE_CANCELLED, VerificationRequest,
PHASE_CANCELLED,
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import Spinner from "../elements/Spinner";
@ -53,25 +53,11 @@ export default class VerificationPanel extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
qrCodeProps: null, // generated by the VerificationQRCode component itself
};
this.state = {};
this._hasVerifier = false;
if (this.props.request.otherPartySupportsMethod(SCAN_QR_CODE_METHOD)) {
this._generateQRCodeProps(props.request);
}
}
async _generateQRCodeProps(verificationRequest: VerificationRequest) {
try {
this.setState({qrCodeProps: await VerificationQRCode.getPropsForRequest(verificationRequest)});
} catch (e) {
console.error(e);
// Do nothing - we won't render a QR code.
}
}
renderQRPhase(pending) {
renderQRPhase() {
const {member, request} = this.props;
const showSAS = request.otherPartySupportsMethod(verificationMethods.SAS);
const showQR = request.otherPartySupportsMethod(SCAN_QR_CODE_METHOD);
@ -86,16 +72,10 @@ export default class VerificationPanel extends React.PureComponent {
let qrBlock;
let sasBlock;
if (showQR) {
let qrCode;
if (this.state.qrCodeProps) {
qrCode = <VerificationQRCode {...this.state.qrCodeProps} />;
} else {
qrCode = <div className='mx_VerificationPanel_QRPhase_noQR'><Spinner /></div>;
}
qrBlock =
<div className='mx_VerificationPanel_QRPhase_startOption'>
<p>{_t("Scan this unique code")}</p>
{qrCode}
<VerificationQRCode qrCodeData={request.qrCodeData} />
</div>;
}
if (showSAS) {
@ -124,7 +104,7 @@ export default class VerificationPanel extends React.PureComponent {
}
let qrBlock;
if (this.state.qrCodeProps) {
if (showQR) {
qrBlock = <div className="mx_UserInfo_container">
<h3>{_t("Verify by scanning")}</h3>
<p>{_t("Ask %(displayName)s to scan your code:", {
@ -132,31 +112,23 @@ export default class VerificationPanel extends React.PureComponent {
})}</p>
<div className="mx_VerificationPanel_qrCode">
<VerificationQRCode {...this.state.qrCodeProps} />
<VerificationQRCode qrCodeData={request.qrCodeData} />
</div>
</div>;
}
let sasBlock;
if (showSAS) {
let button;
if (pending) {
button = <Spinner />;
} else {
const disabled = this.state.emojiButtonClicked;
button = (
<AccessibleButton disabled={disabled} kind="primary" className="mx_UserInfo_wideButton" onClick={this._startSAS}>
{_t("Verify by emoji")}
</AccessibleButton>
);
}
const sasLabel = this.state.qrCodeProps ?
const disabled = this.state.emojiButtonClicked;
const sasLabel = showQR ?
_t("If you can't scan the code above, verify by comparing unique emoji.") :
_t("Verify by comparing unique emoji.");
sasBlock = <div className="mx_UserInfo_container">
<h3>{_t("Verify by emoji")}</h3>
<p>{sasLabel}</p>
{ button }
<AccessibleButton disabled={disabled} kind="primary" className="mx_UserInfo_wideButton" onClick={this._startSAS}>
{_t("Verify by emoji")}
</AccessibleButton>
</div>;
}
@ -172,26 +144,88 @@ export default class VerificationPanel extends React.PureComponent {
</React.Fragment>;
}
_onReciprocateYesClick = () => {
this.setState({reciprocateButtonClicked: true});
this.state.reciprocateQREvent.confirm();
};
_onReciprocateNoClick = () => {
this.setState({reciprocateButtonClicked: true});
this.state.reciprocateQREvent.cancel();
};
renderQRReciprocatePhase() {
const {member, request} = this.props;
let Button;
// a bit of a hack, but the FormButton should only be used in the right panel
// they should probably just be the same component with a css class applied to it?
if (this.props.inDialog) {
Button = sdk.getComponent("elements.AccessibleButton");
} else {
Button = sdk.getComponent("elements.FormButton");
}
const description = request.isSelfVerification ?
_t("Almost there! Is your other session showing the same shield?") :
_t("Almost there! Is %(displayName)s showing the same shield?", {
displayName: member.displayName || member.name || member.userId,
});
let body;
if (this.state.reciprocateQREvent) {
// riot web doesn't support scanning yet, so assume here we're the client being scanned.
//
// we're passing both a label and a child string to Button as
// FormButton and AccessibleButton expect this differently
body = <React.Fragment>
<p>{description}</p>
<E2EIcon isUser={true} status="verified" size={128} hideTooltip={true} />
<div className="mx_VerificationPanel_reciprocateButtons">
<Button
label={_t("No")} kind="danger"
disabled={this.state.reciprocateButtonClicked}
onClick={this._onReciprocateNoClick}>{_t("No")}</Button>
<Button
label={_t("Yes")} kind="primary"
disabled={this.state.reciprocateButtonClicked}
onClick={this._onReciprocateYesClick}>{_t("Yes")}</Button>
</div>
</React.Fragment>;
} else {
body = <p><Spinner /></p>;
}
return <div className="mx_UserInfo_container mx_VerificationPanel_reciprocate_section">
<h3>{_t("Verify by scanning")}</h3>
{ body }
</div>;
}
renderVerifiedPhase() {
const {member} = this.props;
const {member, request} = this.props;
let text;
if (this.props.isRoomEncrypted) {
text = _t("Verify all users in a room to ensure it's secure.");
} else {
text = _t("In encrypted rooms, verify all users to ensure its secure.");
if (!request.isSelfVerification) {
if (this.props.isRoomEncrypted) {
text = _t("Verify all users in a room to ensure it's secure.");
} else {
text = _t("In encrypted rooms, verify all users to ensure its secure.");
}
}
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const description = request.isSelfVerification ?
_t("You've successfully verified %(deviceName)s (%(deviceId)s)!", {
deviceName: this.props.device.getDisplayName(),
deviceId: this.props.device.deviceId,
}):
_t("You've successfully verified %(displayName)s!", {
displayName: member.displayName || member.name || member.userId,
});
return (
<div className="mx_UserInfo_container mx_VerificationPanel_verified_section">
<h3>{_t("Verified")}</h3>
<p>{_t("You've successfully verified %(displayName)s!", {
displayName: member.displayName || member.name || member.userId,
})}</p>
<p>{description}</p>
<E2EIcon isUser={true} status="verified" size={128} hideTooltip={true} />
<p>{ text }</p>
{ text ? <p>{ text }</p> : null }
<AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={this.props.onClose}>
{_t("Got it")}
</AccessibleButton>
@ -204,15 +238,27 @@ export default class VerificationPanel extends React.PureComponent {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let startAgainInstruction;
if (request.isSelfVerification) {
startAgainInstruction = _t("Start verification again from the notification.");
} else {
startAgainInstruction = _t("Start verification again from their profile.");
}
let text;
if (request.cancellationCode === "m.timeout") {
text = _t("Verification timed out. Start verification again from their profile.");
text = _t("Verification timed out.") + ` ${startAgainInstruction}`;
} else if (request.cancellingUserId === request.otherUserId) {
text = _t("%(displayName)s cancelled verification. Start verification again from their profile.", {
displayName: member.displayName || member.name || member.userId,
});
if (request.isSelfVerification) {
text = _t("You cancelled verification on your other session.");
} else {
text = _t("%(displayName)s cancelled verification.", {
displayName: member.displayName || member.name || member.userId,
});
}
text = `${text} ${startAgainInstruction}`;
} else {
text = _t("You cancelled verification. Start verification again from their profile.");
text = _t("You cancelled verification.") + ` ${startAgainInstruction}`;
}
return (
@ -228,7 +274,7 @@ export default class VerificationPanel extends React.PureComponent {
}
render() {
const {member, phase} = this.props;
const {member, phase, request} = this.props;
const displayName = member.displayName || member.name || member.userId;
@ -236,20 +282,28 @@ export default class VerificationPanel extends React.PureComponent {
case PHASE_READY:
return this.renderQRPhase();
case PHASE_STARTED:
if (this.state.sasEvent) {
const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas');
return <div className="mx_UserInfo_container">
<h3>{_t("Compare emoji")}</h3>
<VerificationShowSas
displayName={displayName}
sas={this.state.sasEvent.sas}
onCancel={this._onSasMismatchesClick}
onDone={this._onSasMatchesClick}
inDialog={this.props.inDialog}
/>
</div>;
} else {
return this.renderQRPhase(true); // keep showing same phase but with a spinner
switch (request.chosenMethod) {
case verificationMethods.RECIPROCATE_QR_CODE:
return this.renderQRReciprocatePhase();
case verificationMethods.SAS: {
const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas');
const emojis = this.state.sasEvent ?
<VerificationShowSas
displayName={displayName}
sas={this.state.sasEvent.sas}
onCancel={this._onSasMismatchesClick}
onDone={this._onSasMatchesClick}
inDialog={this.props.inDialog}
isSelf={request.isSelfVerification}
device={this.props.device}
/> : <Spinner />;
return <div className="mx_UserInfo_container">
<h3>{_t("Compare emoji")}</h3>
{ emojis }
</div>;
}
default:
return null;
}
case PHASE_DONE:
return this.renderVerifiedPhase();
@ -278,10 +332,12 @@ export default class VerificationPanel extends React.PureComponent {
this.state.sasEvent.mismatch();
};
_onVerifierShowSas = (sasEvent) => {
_updateVerifierState = () => {
const {request} = this.props;
request.verifier.off('show_sas', this._onVerifierShowSas);
this.setState({sasEvent});
const {sasEvent, reciprocateQREvent} = request.verifier;
request.verifier.off('show_sas', this._updateVerifierState);
request.verifier.off('show_reciprocate_qr', this._updateVerifierState);
this.setState({sasEvent, reciprocateQREvent});
};
_onRequestChange = async () => {
@ -289,7 +345,8 @@ export default class VerificationPanel extends React.PureComponent {
const hadVerifier = this._hasVerifier;
this._hasVerifier = !!request.verifier;
if (!hadVerifier && this._hasVerifier) {
request.verifier.on('show_sas', this._onVerifierShowSas);
request.verifier.on('show_sas', this._updateVerifierState);
request.verifier.on('show_reciprocate_qr', this._updateVerifierState);
try {
// on the requester side, this is also awaited in _startSAS,
// but that's ok as verify should return the same promise.
@ -304,7 +361,9 @@ export default class VerificationPanel extends React.PureComponent {
const {request} = this.props;
request.on("change", this._onRequestChange);
if (request.verifier) {
this.setState({sasEvent: request.verifier.sasEvent});
const {request} = this.props;
const {sasEvent, reciprocateQREvent} = request.verifier;
this.setState({sasEvent, reciprocateQREvent});
}
this._onRequestChange();
}
@ -312,7 +371,8 @@ export default class VerificationPanel extends React.PureComponent {
componentWillUnmount() {
const {request} = this.props;
if (request.verifier) {
request.verifier.off('show_sas', this._onVerifierShowSas);
request.verifier.off('show_sas', this._updateVerifierState);
request.verifier.off('show_reciprocate_qr', this._updateVerifierState);
}
request.off("change", this._onRequestChange);
}

View file

@ -18,7 +18,9 @@ import React from 'react';
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import {_t} from "../../../languageHandler";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.room_settings.RoomPublishSetting")
export default class RoomPublishSetting extends React.PureComponent {
constructor(props) {
super(props);

View file

@ -81,12 +81,14 @@ export default createReactClass({
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
switch (action.action) {
case 'appsDrawer':
// Note: these booleans are awkward because localstorage is fundamentally
// string-based. We also do exact equality on the strings later on.
if (action.show) {
localStorage.removeItem(hideWidgetKey);
localStorage.setItem(hideWidgetKey, "false");
} else {
// Store hidden state of widget
// Don't show if previously hidden
localStorage.setItem(hideWidgetKey, true);
localStorage.setItem(hideWidgetKey, "true");
}
break;

View file

@ -39,6 +39,7 @@ import EMOTICON_REGEX from 'emojibase-regex/emoticon';
import * as sdk from '../../../index';
import {Key} from "../../../Keyboard";
import {EMOTICON_TO_EMOJI} from "../../../emoji";
import {CommandCategories, CommandMap, parseCommandString} from "../../../SlashCommands";
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
@ -84,6 +85,7 @@ export default class BasicMessageEditor extends React.Component {
super(props);
this.state = {
autoComplete: null,
showPillAvatar: SettingsStore.getValue("Pill.shouldShowPillAvatar"),
};
this._editorRef = null;
this._autocompleteRef = null;
@ -92,10 +94,10 @@ export default class BasicMessageEditor extends React.Component {
this._isIMEComposing = false;
this._hasTextSelected = false;
this._emoticonSettingHandle = null;
this._shouldShowPillAvatarSettingHandle = null;
}
// TODO: [REACT-WARNING] Move into better lifecycle position
UNSAFE_componentWillUpdate(prevProps) { // eslint-disable-line camelcase
componentDidUpdate(prevProps) {
if (this.props.placeholder !== prevProps.placeholder && this.props.placeholder) {
const {isEmpty} = this.props.model;
if (isEmpty) {
@ -163,7 +165,16 @@ export default class BasicMessageEditor extends React.Component {
}
this.setState({autoComplete: this.props.model.autoComplete});
this.historyManager.tryPush(this.props.model, selection, inputType, diff);
TypingStore.sharedInstance().setSelfTyping(this.props.room.roomId, !this.props.model.isEmpty);
let isTyping = !this.props.model.isEmpty;
// If the user is entering a command, only consider them typing if it is one which sends a message into the room
if (isTyping && this.props.model.parts[0].type === "command") {
const {cmd} = parseCommandString(this.props.model.parts[0].text);
if (!CommandMap.has(cmd) || CommandMap.get(cmd).category !== CommandCategories.messages) {
isTyping = false;
}
}
TypingStore.sharedInstance().setSelfTyping(this.props.room.roomId, isTyping);
if (this.props.onChange) {
this.props.onChange();
@ -203,9 +214,9 @@ export default class BasicMessageEditor extends React.Component {
if (isSafari) {
this._onInput({inputType: "insertCompositionText"});
} else {
setTimeout(() => {
Promise.resolve().then(() => {
this._onInput({inputType: "insertCompositionText"});
}, 0);
});
}
}
@ -509,10 +520,15 @@ export default class BasicMessageEditor extends React.Component {
this.setState({completionIndex});
}
_configureEmoticonAutoReplace() {
_configureEmoticonAutoReplace = () => {
const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji');
this.props.model.setTransformCallback(shouldReplace ? this._replaceEmoticon : null);
}
};
_configureShouldShowPillAvatar = () => {
const showPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar");
this.setState({ showPillAvatar });
};
componentWillUnmount() {
document.removeEventListener("selectionchange", this._onSelectionChange);
@ -520,15 +536,17 @@ export default class BasicMessageEditor extends React.Component {
this._editorRef.removeEventListener("compositionstart", this._onCompositionStart, true);
this._editorRef.removeEventListener("compositionend", this._onCompositionEnd, true);
SettingsStore.unwatchSetting(this._emoticonSettingHandle);
SettingsStore.unwatchSetting(this._shouldShowPillAvatarSettingHandle);
}
componentDidMount() {
const model = this.props.model;
model.setUpdateCallback(this._updateEditorState);
this._emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null, () => {
this._configureEmoticonAutoReplace();
});
this._emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null,
this._configureEmoticonAutoReplace);
this._configureEmoticonAutoReplace();
this._shouldShowPillAvatarSettingHandle = SettingsStore.watchSetting("Pill.shouldShowPillAvatar", null,
this._configureShouldShowPillAvatar);
const partCreator = model.partCreator;
// TODO: does this allow us to get rid of EditorStateTransfer?
// not really, but we could not serialize the parts, and just change the autoCompleter
@ -606,9 +624,12 @@ export default class BasicMessageEditor extends React.Component {
/>
</div>);
}
const classes = classNames("mx_BasicMessageComposer", {
const wrapperClasses = classNames("mx_BasicMessageComposer", {
"mx_BasicMessageComposer_input_error": this.state.showVisualBell,
});
const classes = classNames("mx_BasicMessageComposer_input", {
"mx_BasicMessageComposer_input_shouldShowPillAvatar": this.state.showPillAvatar,
});
const MessageComposerFormatBar = sdk.getComponent('rooms.MessageComposerFormatBar');
const shortcuts = {
@ -619,11 +640,11 @@ export default class BasicMessageEditor extends React.Component {
const {completionIndex} = this.state;
return (<div className={classes}>
return (<div className={wrapperClasses}>
{ autoComplete }
<MessageComposerFormatBar ref={ref => this._formatBarRef = ref} onAction={this._onFormatAction} shortcuts={shortcuts} />
<div
className="mx_BasicMessageComposer_input"
className={classes}
contentEditable="true"
tabIndex="0"
onBlur={this._onBlur}

View file

@ -20,7 +20,7 @@ import PropTypes from "prop-types";
import classNames from 'classnames';
import {_t, _td} from '../../../languageHandler';
import {useFeatureEnabled} from "../../../hooks/useSettings";
import {useSettingValue} from "../../../hooks/useSettings";
import AccessibleButton from "../elements/AccessibleButton";
import Tooltip from "../elements/Tooltip";
@ -62,7 +62,7 @@ const E2EIcon = ({isUser, status, className, size, onClick, hideTooltip}) => {
}, className);
let e2eTitle;
const crossSigning = useFeatureEnabled("feature_cross_signing");
const crossSigning = useSettingValue("feature_cross_signing");
if (crossSigning && isUser) {
e2eTitle = crossSigningUserTitles[status];
} else if (crossSigning && !isUser) {

View file

@ -59,6 +59,7 @@ const stateEventTileTypes = {
'm.room.power_levels': 'messages.TextualEvent',
'm.room.pinned_events': 'messages.TextualEvent',
'm.room.server_acl': 'messages.TextualEvent',
// TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111)
'im.vector.modular.widgets': 'messages.TextualEvent',
'm.room.tombstone': 'messages.TextualEvent',
'm.room.join_rules': 'messages.TextualEvent',
@ -322,7 +323,7 @@ export default createReactClass({
// If cross-signing is off, the old behaviour is to scream at the user
// as if they've done something wrong, which they haven't
if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (!SettingsStore.getValue("feature_cross_signing")) {
this.setState({
verified: E2E_STATE.WARNING,
}, this.props.onHeightChanged);

View file

@ -56,7 +56,7 @@ export default createReactClass({
}
}
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (SettingsStore.getValue("feature_cross_signing")) {
const { roomId } = this.props.member;
if (roomId) {
const isRoomEncrypted = cli.isRoomEncrypted(roomId);

View file

@ -270,7 +270,7 @@ export default class MessageComposer extends React.Component {
}
renderPlaceholderText() {
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (SettingsStore.getValue("feature_cross_signing")) {
if (this.state.isQuoting) {
if (this.props.e2eStatus) {
return _t('Send an encrypted reply…');

View file

@ -18,7 +18,7 @@ limitations under the License.
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import('../../../VelocityBounce');
import '../../../VelocityBounce';
import { _t } from '../../../languageHandler';
import {formatDate} from '../../../DateUtils';
import Velociraptor from "../../../Velociraptor";

View file

@ -168,7 +168,7 @@ export default createReactClass({
const joinRule = joinRules && joinRules.getContent().join_rule;
let privateIcon;
// Don't show an invite-only icon for DMs. Users know they're invite-only.
if (!dmUserId && SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (!dmUserId && SettingsStore.getValue("feature_cross_signing")) {
if (joinRule == "invite") {
privateIcon = <InviteOnlyIcon />;
}

View file

@ -130,6 +130,10 @@ export default createReactClass({
this._updateE2eStatus();
},
onCrossSigningKeysChanged: function() {
this._updateE2eStatus();
},
onRoomTimeline: function(ev, room) {
if (!room) return;
if (room.roomId != this.props.room.roomId) return;
@ -142,7 +146,7 @@ export default createReactClass({
const cli = MatrixClientPeg.get();
cli.on("RoomState.members", this.onRoomStateMember);
cli.on("userTrustStatusChanged", this.onUserVerificationChanged);
cli.on("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
this._updateE2eStatus();
},
@ -151,7 +155,7 @@ export default createReactClass({
if (!cli.isRoomEncrypted(this.props.room.roomId)) {
return;
}
if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (!SettingsStore.getValue("feature_cross_signing")) {
return;
}
@ -267,6 +271,7 @@ export default createReactClass({
cli.removeListener("RoomState.events", this.onJoinRule);
cli.removeListener("RoomState.members", this.onRoomStateMember);
cli.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
cli.removeListener("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
cli.removeListener("Room.timeline", this.onRoomTimeline);
}
ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange);
@ -511,7 +516,7 @@ export default createReactClass({
}
let privateIcon = null;
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (SettingsStore.getValue("feature_cross_signing")) {
if (this.state.joinRule == "invite" && !dmUserId) {
privateIcon = <InviteOnlyIcon collapsedPanel={this.props.collapsed} />;
}

View file

@ -246,6 +246,7 @@ export default class Stickerpicker extends React.Component {
url: stickerpickerWidget.content.url,
name: stickerpickerWidget.content.name,
type: stickerpickerWidget.content.type,
data: stickerpickerWidget.content.data,
};
stickersContent = (

View file

@ -81,7 +81,9 @@ export default class CrossSigningPanel extends React.PureComponent {
const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage);
const selfSigningPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("self_signing"));
const userSigningPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("user_signing"));
const sessionBackupKeyCached = !!(await cli._crypto.getSessionBackupPrivateKey());
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");
@ -94,6 +96,7 @@ export default class CrossSigningPanel extends React.PureComponent {
selfSigningPrivateKeyCached,
userSigningPrivateKeyCached,
sessionBackupKeyCached,
sessionBackupKeyWellFormed,
secretStorageKeyInAccount,
homeserverSupportsCrossSigning,
crossSigningReady,
@ -143,6 +146,7 @@ export default class CrossSigningPanel extends React.PureComponent {
selfSigningPrivateKeyCached,
userSigningPrivateKeyCached,
sessionBackupKeyCached,
sessionBackupKeyWellFormed,
secretStorageKeyInAccount,
homeserverSupportsCrossSigning,
crossSigningReady,
@ -208,6 +212,16 @@ export default class CrossSigningPanel extends React.PureComponent {
);
}
let sessionBackupKeyWellFormedText = "";
if (sessionBackupKeyCached) {
sessionBackupKeyWellFormedText = ", ";
if (sessionBackupKeyWellFormed) {
sessionBackupKeyWellFormedText += _t("well formed");
} else {
sessionBackupKeyWellFormedText += _t("unexpected type");
}
}
return (
<div>
{summarisedStatus}
@ -232,7 +246,10 @@ export default class CrossSigningPanel extends React.PureComponent {
</tr>
<tr>
<td>{_t("Session backup key:")}</td>
<td>{sessionBackupKeyCached ? _t("cached locally") : _t("not found locally")}</td>
<td>
{sessionBackupKeyCached ? _t("cached locally") : _t("not found locally")}
{sessionBackupKeyWellFormedText}
</td>
</tr>
<tr>
<td>{_t("Secret storage public key:")}</td>

View file

@ -124,17 +124,22 @@ export default class DevicesPanel extends React.Component {
// pop up an interactive auth dialog
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
const numDevices = this.state.selectedDevices.length;
const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("Use Single Sign On to continue"),
body: _t("Confirm deleting these sessions by using Single Sign On to prove your identity."),
body: _t("Confirm deleting these sessions by using Single Sign On to prove your identity.", {
count: numDevices,
}),
continueText: _t("Single Sign On"),
continueKind: "primary",
},
[SSOAuthEntry.PHASE_POSTAUTH]: {
title: _t("Confirm deleting these sessions"),
body: _t("Click the button below to confirm deleting these sessions."),
continueText: _t("Delete sessions"),
body: _t("Click the button below to confirm deleting these sessions.", {
count: numDevices,
}),
continueText: _t("Delete sessions", {count: numDevices}),
continueKind: "danger",
},
};

View file

@ -75,7 +75,7 @@ export default class KeyBackupPanel extends React.PureComponent {
async _checkKeyBackupStatus() {
try {
const {backupInfo, trustInfo} = await MatrixClientPeg.get().checkKeyBackup();
const backupKeyStored = await MatrixClientPeg.get().isKeyBackupKeyStored();
const backupKeyStored = Boolean(await MatrixClientPeg.get().isKeyBackupKeyStored());
this.setState({
backupInfo,
backupSigStatus: trustInfo,
@ -326,7 +326,7 @@ export default class KeyBackupPanel extends React.PureComponent {
</AccessibleButton>
</div>
);
if (this.state.backupKeyStored && !SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (this.state.backupKeyStored && !SettingsStore.getValue("feature_cross_signing")) {
buttonRow = <p> {_t(
"Backup key stored in secret storage, but this feature is not " +
"enabled on this session. Please enable cross-signing in Labs to " +

View file

@ -33,6 +33,7 @@ const plEventsToLabels = {
"m.room.tombstone": _td("Upgrade the room"),
"m.room.encryption": _td("Enable room encryption"),
// TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111)
"im.vector.modular.widgets": _td("Modify widgets"),
};
@ -47,6 +48,7 @@ const plEventsToShow = {
"m.room.tombstone": {isState: true},
"m.room.encryption": {isState: true},
// TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111)
"im.vector.modular.widgets": {isState: true},
};

View file

@ -270,7 +270,7 @@ export default class SecurityUserSettingsTab extends React.Component {
// can remove this.
const CrossSigningPanel = sdk.getComponent('views.settings.CrossSigningPanel');
let crossSigning;
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
if (SettingsStore.getValue("feature_cross_signing")) {
crossSigning = (
<div className='mx_SettingsTab_section'>
<span className="mx_SettingsTab_subheading">{_t("Cross-signing")}</span>

View file

@ -63,7 +63,9 @@ export default class SetupEncryptionToast extends React.PureComponent {
{}, null, /* priority = */ false, /* static = */ true);
} else {
const Spinner = sdk.getComponent("elements.Spinner");
const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
const modal = Modal.createDialog(
Spinner, null, 'mx_Dialog_spinner', /* priority */ false, /* static */ true,
);
try {
await accessSecretStorage();
await this._waitForCompletion();

View file

@ -31,7 +31,7 @@ export default class VerificationRequestToast extends React.PureComponent {
this.state = {counter: Math.ceil(props.request.timeout / 1000)};
}
componentDidMount() {
async componentDidMount() {
const {request} = this.props;
if (request.timeout && request.timeout > 0) {
this._intervalHandle = setInterval(() => {
@ -48,6 +48,11 @@ export default class VerificationRequestToast extends React.PureComponent {
// As a quick & dirty fix, check the toast is still relevant when it mounts (this prevents
// a toast hanging around after logging in if you did a verification as part of login).
this._checkRequestIsPending();
if (request.isSelfVerification) {
const cli = MatrixClientPeg.get();
this.setState({device: await cli.getStoredDevice(cli.getUserId(), request.channel.deviceId)});
}
}
componentWillUnmount() {
@ -107,15 +112,25 @@ export default class VerificationRequestToast extends React.PureComponent {
render() {
const FormButton = sdk.getComponent("elements.FormButton");
const {request} = this.props;
const userId = request.otherUserId;
const roomId = request.channel.roomId;
let nameLabel = roomId ? userLabelForEventRoom(userId, roomId) : userId;
// for legacy to_device verification requests
if (nameLabel === userId) {
const client = MatrixClientPeg.get();
const user = client.getUser(userId);
if (user && user.displayName) {
nameLabel = _t("%(name)s (%(userId)s)", {name: user.displayName, userId});
let nameLabel;
if (request.isSelfVerification) {
if (this.state.device) {
nameLabel = _t("From %(deviceName)s (%(deviceId)s)", {
deviceName: this.state.device.getDisplayName(),
deviceId: this.state.device.deviceId,
});
}
} else {
const userId = request.otherUserId;
const roomId = request.channel.roomId;
nameLabel = roomId ? userLabelForEventRoom(userId, roomId) : userId;
// for legacy to_device verification requests
if (nameLabel === userId) {
const client = MatrixClientPeg.get();
const user = client.getUser(userId);
if (user && user.displayName) {
nameLabel = _t("%(name)s (%(userId)s)", {name: user.displayName, userId});
}
}
}
const declineLabel = this.state.counter == 0 ?

View file

@ -20,8 +20,8 @@ import { _t } from '../../../languageHandler';
import AccessibleButton from "../elements/AccessibleButton";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import VerificationQRCode from "../elements/crypto/VerificationQRCode";
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import Spinner from "../elements/Spinner";
import {SCAN_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
@replaceableComponent("views.verification.VerificationQREmojiOptions")
export default class VerificationQREmojiOptions extends React.Component {
@ -31,31 +31,17 @@ export default class VerificationQREmojiOptions extends React.Component {
onStartEmoji: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
this.state = {
qrProps: null,
};
this._prepareQrCode(props.request);
}
async _prepareQrCode(request: VerificationRequest) {
try {
const props = await VerificationQRCode.getPropsForRequest(request);
this.setState({qrProps: props});
} catch (e) {
console.error(e);
// We just won't show a QR code
}
}
render() {
let qrCode = <div className='mx_VerificationQREmojiOptions_noQR'><Spinner /></div>;
if (this.state.qrProps) {
qrCode = <VerificationQRCode {...this.state.qrProps} />;
const {request} = this.props;
const showQR = request.otherPartySupportsMethod(SCAN_QR_CODE_METHOD);
let qrCode;
if (showQR) {
qrCode = <VerificationQRCode qrCodeData={request.qrCodeData} />;
} else {
qrCode = <div className='mx_VerificationQREmojiOptions_noQR'><Spinner /></div>;
}
return (
<div>
{_t("Verify this session by completing one of the following:")}

View file

@ -20,6 +20,7 @@ import { _t, _td } from '../../../languageHandler';
import {PendingActionSpinner} from "../right_panel/EncryptionInfo";
import AccessibleButton from "../elements/AccessibleButton";
import DialogButtons from "../elements/DialogButtons";
import { fixupColorFonts } from '../../../utils/FontManager';
function capFirst(s) {
return s.charAt(0).toUpperCase() + s.slice(1);
@ -44,6 +45,13 @@ export default class VerificationShowSas extends React.Component {
};
}
componentWillMount() {
// As this component is also used before login (during complete security),
// also make sure we have a working emoji font to display the SAS emojis here.
// This is also done from LoggedInView.
fixupColorFonts();
}
onMatchClick = () => {
this.setState({ pending: true });
this.props.onDone();
@ -75,7 +83,7 @@ export default class VerificationShowSas extends React.Component {
</div>;
sasCaption = this.props.isSelf ?
_t(
"Confirm the emoji below are displayed on both devices, in the same order:",
"Confirm the emoji below are displayed on both sessions, in the same order:",
):
_t(
"Verify this user by confirming the following emoji appear on their screen.",
@ -89,7 +97,7 @@ export default class VerificationShowSas extends React.Component {
</div>;
sasCaption = this.props.isSelf ?
_t(
"Verify this device by confirming the following number appears on its screen.",
"Verify this session by confirming the following number appears on its screen.",
):
_t(
"Verify this user by confirming the following number appears on their screen.",
@ -107,8 +115,15 @@ export default class VerificationShowSas extends React.Component {
if (this.state.pending || this.state.cancelling) {
let text;
if (this.state.pending) {
const {displayName} = this.props;
text = _t("Waiting for %(displayName)s to verify…", {displayName});
if (this.props.isSelf) {
text = _t("Waiting for your other session, %(deviceName)s (%(deviceId)s), to verify…", {
deviceName: this.props.device.getDisplayName(),
deviceId: this.props.device.deviceId,
});
} else {
const {displayName} = this.props;
text = _t("Waiting for %(displayName)s to verify…", {displayName});
}
} else {
text = _t("Cancelling…");
}
@ -118,19 +133,19 @@ export default class VerificationShowSas extends React.Component {
confirm = <DialogButtons
primaryButton={_t("They match")}
onPrimaryButtonClick={this.onMatchClick}
primaryButtonClass="mx_UserInfo_wideButton"
primaryButtonClass="mx_UserInfo_wideButton mx_VerificationShowSas_matchButton"
cancelButton={_t("They don't match")}
onCancel={this.onDontMatchClick}
cancelButtonClass="mx_UserInfo_wideButton"
cancelButtonClass="mx_UserInfo_wideButton mx_VerificationShowSas_noMatchButton"
/>;
} else {
confirm = <React.Fragment>
<AccessibleButton onClick={this.onMatchClick} kind="primary">
{ _t("They match") }
</AccessibleButton>
<AccessibleButton onClick={this.onDontMatchClick} kind="danger">
{ _t("They don't match") }
</AccessibleButton>
<AccessibleButton onClick={this.onMatchClick} kind="primary">
{ _t("They match") }
</AccessibleButton>
</React.Fragment>;
}