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

 Conflicts:
	package.json
This commit is contained in:
Michael Telatynski
2020-05-14 10:50:42 +01:00
476 changed files with 8624 additions and 2356 deletions
+6 -1
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>
);
},
+2 -2
View File
@@ -19,7 +19,7 @@ import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { COUNTRIES } from '../../../phonenumber';
import {COUNTRIES, getEmojiFlag} from '../../../phonenumber';
import SdkConfig from "../../../SdkConfig";
import { _t } from "../../../languageHandler";
@@ -80,7 +80,7 @@ export default class CountryDropdown extends React.Component {
}
_flagImgForIso2(iso2) {
return <img src={require(`../../../../res/img/flags/${iso2}.png`)} />;
return <div className="mx_Dropdown_option_emoji">{ getEmojiFlag(iso2) }</div>;
}
_getShortOption(iso2) {
@@ -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}
/>;
}
+23 -13
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 = {
@@ -183,7 +185,7 @@ export default class PasswordLogin extends React.Component {
this.props.onPasswordChanged(ev.target.value);
}
renderLoginField(loginType) {
renderLoginField(loginType, autoFocus) {
const Field = sdk.getComponent('elements.Field');
const classes = {};
@@ -202,7 +204,7 @@ export default class PasswordLogin extends React.Component {
onChange={this.onUsernameChanged}
onBlur={this.onUsernameBlur}
disabled={this.props.disableSubmit}
autoFocus
autoFocus={autoFocus}
/>;
case PasswordLogin.LOGIN_FIELD_MXID:
classes.error = this.props.loginIncorrect && !this.state.username;
@@ -216,7 +218,7 @@ export default class PasswordLogin extends React.Component {
onChange={this.onUsernameChanged}
onBlur={this.onUsernameBlur}
disabled={this.props.disableSubmit}
autoFocus
autoFocus={autoFocus}
/>;
case PasswordLogin.LOGIN_FIELD_PHONE: {
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
@@ -240,7 +242,7 @@ export default class PasswordLogin extends React.Component {
onChange={this.onPhoneNumberChanged}
onBlur={this.onPhoneNumberBlur}
disabled={this.props.disableSubmit}
autoFocus
autoFocus={autoFocus}
/>;
}
}
@@ -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>;
}
@@ -279,7 +285,10 @@ export default class PasswordLogin extends React.Component {
error: this.props.loginIncorrect && !this.isLoginEmpty(), // only error password if error isn't top field
});
const loginField = this.renderLoginField(this.state.loginType);
// If login is empty, autoFocus login, otherwise autoFocus password.
// this is for when auto server discovery remounts us when the user tries to tab from username to password
const autoFocusPassword = !this.isLoginEmpty();
const loginField = this.renderLoginField(this.state.loginType, !autoFocusPassword);
let loginType;
if (!SdkConfig.get().disable_3pid_login) {
@@ -330,13 +339,14 @@ export default class PasswordLogin extends React.Component {
value={this.state.password}
onChange={this.onPasswordChanged}
disabled={this.props.disableSubmit}
autoFocus={autoFocusPassword}
/>
{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>
);
@@ -76,7 +76,7 @@ export default createReactClass({
email: this.props.defaultEmail || "",
phoneNumber: this.props.defaultPhoneNumber || "",
password: this.props.defaultPassword || "",
passwordConfirm: "",
passwordConfirm: this.props.defaultPassword || "",
passwordComplexity: null,
passwordSafe: false,
};
@@ -102,11 +102,15 @@ export default createReactClass({
"No identity server is configured so you cannot add an email address in order to " +
"reset your password in the future.",
);
} else {
} else if (this._showEmail()) {
desc = _t(
"If you don't specify an email address, you won't be able to reset your password. " +
"Are you sure?",
);
} else {
// user can't set an e-mail so don't prompt them to
self._doSubmit(ev);
return;
}
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
+19 -6
View File
@@ -24,6 +24,7 @@ import * as AvatarLogic from '../../../Avatar';
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from '../elements/AccessibleButton';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import toRem from "../../../utils/rem";
export default createReactClass({
displayName: 'BaseAvatar',
@@ -164,9 +165,11 @@ export default createReactClass({
const initialLetter = AvatarLogic.getInitialLetter(name);
const textNode = (
<span className="mx_BaseAvatar_initial" aria-hidden="true"
style={{ fontSize: (width * 0.65) + "px",
width: width + "px",
lineHeight: height + "px" }}
style={{
fontSize: toRem(width * 0.65),
width: toRem(width),
lineHeight: toRem(height),
}}
>
{ initialLetter }
</span>
@@ -174,7 +177,11 @@ export default createReactClass({
const imgNode = (
<img className="mx_BaseAvatar_image" src={imageUrl}
alt="" title={title} onError={this.onError}
width={width} height={height} aria-hidden="true" />
aria-hidden="true"
style={{
width: toRem(width),
height: toRem(height)
}} />
);
if (onClick != null) {
return (
@@ -202,7 +209,10 @@ export default createReactClass({
src={imageUrl}
onClick={onClick}
onError={this.onError}
width={width} height={height}
style={{
width: toRem(width),
height: toRem(height),
}}
title={title} alt=""
inputRef={inputRef}
{...otherProps} />
@@ -213,7 +223,10 @@ export default createReactClass({
className="mx_BaseAvatar mx_BaseAvatar_image"
src={imageUrl}
onError={this.onError}
width={width} height={height}
style={{
width: toRem(width),
height: toRem(height),
}}
title={title} alt=""
ref={inputRef}
{...otherProps} />
@@ -130,22 +130,24 @@ export default createReactClass({
},
onViewSourceClick: function() {
const ev = this.props.mxEvent.replacingEvent() || this.props.mxEvent;
const ViewSource = sdk.getComponent('structures.ViewSource');
Modal.createTrackedDialog('View Event Source', '', ViewSource, {
roomId: this.props.mxEvent.getRoomId(),
eventId: this.props.mxEvent.getId(),
content: this.props.mxEvent.event,
roomId: ev.getRoomId(),
eventId: ev.getId(),
content: ev.event,
}, 'mx_Dialog_viewsource');
this.closeMenu();
},
onViewClearSourceClick: function() {
const ev = this.props.mxEvent.replacingEvent() || this.props.mxEvent;
const ViewSource = sdk.getComponent('structures.ViewSource');
Modal.createTrackedDialog('View Clear Event Source', '', ViewSource, {
roomId: this.props.mxEvent.getRoomId(),
eventId: this.props.mxEvent.getId(),
roomId: ev.getRoomId(),
eventId: ev.getId(),
// FIXME: _clearEvent is private
content: this.props.mxEvent._clearEvent,
content: ev._clearEvent,
}, 'mx_Dialog_viewsource');
this.closeMenu();
},
@@ -137,12 +137,20 @@ export default class BugReportDialog extends React.Component {
);
}
let warning;
if (window.Modernizr && Object.values(window.Modernizr).some(support => support === false)) {
warning = <p><b>
{ _t("Reminder: Your browser is unsupported, so your experience may be unpredictable.") }
</b></p>;
}
return (
<BaseDialog className="mx_BugReportDialog" onFinished={this._onCancel}
title={_t('Submit debug logs')}
contentId='mx_Dialog_content'
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
{ warning }
<p>
{ _t(
"Debug logs contain application usage data including your " +
@@ -30,12 +30,13 @@ export default createReactClass({
displayName: 'CreateRoomDialog',
propTypes: {
onFinished: PropTypes.func.isRequired,
defaultPublic: PropTypes.bool,
},
getInitialState() {
const config = SdkConfig.get();
return {
isPublic: false,
isPublic: this.props.defaultPublic || false,
isEncrypted: true,
name: "",
topic: "",
@@ -65,8 +66,8 @@ export default createReactClass({
createOpts.creation_content = {'m.federate': false};
}
if (!this.state.isPublic && SettingsStore.isFeatureEnabled("feature_cross_signing")) {
createOpts.encryption = this.state.isEncrypted;
if (!this.state.isPublic && SettingsStore.getValue("feature_cross_signing")) {
opts.encryption = this.state.isEncrypted;
}
return opts;
@@ -192,9 +193,14 @@ export default createReactClass({
}
let e2eeSection;
if (!this.state.isPublic && SettingsStore.isFeatureEnabled("feature_cross_signing")) {
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} />
<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>;
}
@@ -26,30 +26,6 @@ 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);
@@ -84,6 +60,30 @@ export default class DeactivateAccountDialog extends React.Component {
}
_onStagePhaseChange = (stage, phase) => {
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:"),
},
},
};
const aesthetics = DEACTIVATE_AESTHETICS[stage];
let bodyText = null;
let continueText = null;
@@ -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,
@@ -696,6 +696,9 @@ class VerificationExplorer extends React.Component {
<VerificationRequest txnId={txnId} request={request} key={txnId} />,
)}
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.props.onBack}>{_t("Back")}</button>
</div>
</div>);
}
}
@@ -25,6 +25,7 @@ import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
import {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth";
import {SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents";
export default createReactClass({
displayName: 'InteractiveAuthDialog',
@@ -66,6 +67,8 @@ export default createReactClass({
// }
// }
// }
//
// Default is defined in _getDefaultDialogAesthetics()
aestheticsForStagePhases: PropTypes.object,
},
@@ -79,6 +82,28 @@ export default createReactClass({
};
},
_getDefaultDialogAesthetics: function() {
const ssoAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("Use Single Sign On to continue"),
body: _t("To continue, use Single Sign On to prove your identity."),
continueText: _t("Single Sign On"),
continueKind: "primary",
},
[SSOAuthEntry.PHASE_POSTAUTH]: {
title: _t("Confirm to continue"),
body: _t("Click the button below to confirm your identity."),
continueText: _t("Confirm"),
continueKind: "primary",
},
};
return {
[SSOAuthEntry.LOGIN_TYPE]: ssoAesthetics,
[SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: ssoAesthetics,
};
},
_onAuthFinished: function(success, result) {
if (success) {
this.props.onFinished(true, result);
@@ -113,9 +138,10 @@ export default createReactClass({
let body = this.state.authError ? null : this.props.body;
let continueText = null;
let continueKind = null;
if (!this.state.authError && this.props.aestheticsForStagePhases) {
if (this.props.aestheticsForStagePhases[this.state.uiaStage]) {
const aesthetics = this.props.aestheticsForStagePhases[this.state.uiaStage][this.state.uiaStagePhase];
const dialogAesthetics = this.props.aestheticsForStagePhases || this._getDefaultDialogAesthetics();
if (!this.state.authError && dialogAesthetics) {
if (dialogAesthetics[this.state.uiaStage]) {
const aesthetics = dialogAesthetics[this.state.uiaStage][this.state.uiaStagePhase];
if (aesthetics && aesthetics.title) title = aesthetics.title;
if (aesthetics && aesthetics.body) body = aesthetics.body;
if (aesthetics && aesthetics.continueText) continueText = aesthetics.continueText;
+13 -9
View File
@@ -574,13 +574,16 @@ 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();
const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds);
if (allHaveDeviceKeys) {
createRoomOptions.encryption = true;
const has3PidMembers = targets.some(t => t instanceof ThreepidMember);
if (!has3PidMembers) {
const client = MatrixClientPeg.get();
const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds);
if (allHaveDeviceKeys) {
createRoomOptions.encryption = true;
}
}
}
@@ -1067,9 +1070,8 @@ export default class InviteDialog extends React.PureComponent {
let buttonText;
let goButtonFn;
const userId = MatrixClientPeg.get().getUserId();
if (this.props.kind === KIND_DM) {
const userId = MatrixClientPeg.get().getUserId();
title = _t("Direct Messages");
helpText = _t(
"Start a conversation with someone using their name, username (like <userId/>) or email address.",
@@ -1083,9 +1085,11 @@ export default class InviteDialog extends React.PureComponent {
} else { // KIND_INVITE
title = _t("Invite to this room");
helpText = _t(
"If you can't find someone, ask them for their username (e.g. @user:server.com) or " +
"<a>share this room</a>.", {},
"Invite someone using their name, username (like <userId/>), email address or <a>share this room</a>.",
{},
{
userId: () =>
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
a: (sub) =>
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{sub}</a>,
},
@@ -85,7 +85,7 @@ export default function KeySignatureUploadFailedDialog({
<span>{_t("Upload completed")}</span> :
cancelled ?
<span>{_t("Cancelled signature upload")}</span> :
<span>{_t("Unabled to upload")}</span>}
<span>{_t("Unable to upload")}</span>}
<DialogButtons
primaryButton={_t("OK")}
hasCancel={false}
@@ -14,16 +14,52 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import SetupEncryptionBody from '../../structures/auth/SetupEncryptionBody';
import BaseDialog from './BaseDialog';
import { _t } from '../../../languageHandler';
import { SetupEncryptionStore, PHASE_DONE } from '../../../stores/SetupEncryptionStore';
export default function SetupEncryptionDialog({onFinished}) {
return <BaseDialog
headerImage={require("../../../../res/img/e2e/warning.svg")}
onFinished={onFinished}
title={_t("Verify this session")}
>
<SetupEncryptionBody onFinished={onFinished} />
</BaseDialog>;
function iconFromPhase(phase) {
if (phase === PHASE_DONE) {
return require("../../../../res/img/e2e/verified.svg");
} else {
return require("../../../../res/img/e2e/warning.svg");
}
}
export default class SetupEncryptionDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
};
constructor() {
super();
this.store = SetupEncryptionStore.sharedInstance();
this.state = {icon: iconFromPhase(this.store.phase)};
}
componentDidMount() {
this.store.on("update", this._onStoreUpdate);
}
componentWillUnmount() {
this.store.removeListener("update", this._onStoreUpdate);
}
_onStoreUpdate = () => {
this.setState({icon: iconFromPhase(this.store.phase)});
};
render() {
return <BaseDialog
headerImage={this.state.icon}
onFinished={this.props.onFinished}
title={_t("Verify this session")}
>
<SetupEncryptionBody onFinished={this.props.onFinished} />
</BaseDialog>;
}
}
@@ -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 "../elements/QRCode";
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,6 +81,8 @@ export default class ShareDialog extends React.Component {
]).isRequired,
};
protected closeCopiedTooltip: () => void;
constructor(props) {
super(props);
@@ -81,45 +100,26 @@ export default class ShareDialog extends React.Component {
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() {
@@ -134,10 +134,32 @@ export default class ShareDialog extends React.Component {
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) {
@@ -155,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>
@@ -178,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');
@@ -196,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"
>
@@ -216,17 +224,18 @@ export default class ShareDialog extends React.Component {
<QRCode data={matrixToUrl} />
</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>
@@ -89,7 +89,7 @@ export default class UserSettingsDialog extends React.Component {
tabs.push(new Tab(
_td("Security & Privacy"),
"mx_UserSettingsDialog_securityIcon",
<SecurityUserSettingsTab />,
<SecurityUserSettingsTab closeSettingsFn={this.props.onFinished} />,
));
if (SdkConfig.get()['showLabsSettings'] || SettingsStore.getLabsFeatures().length > 0) {
tabs.push(new Tab(
@@ -48,7 +48,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 this session") : _t("Verification Request");
_t("Verify other session") : _t("Verification Request");
return <BaseDialog className="mx_InfoDialog" onFinished={this.onFinished}
contentId="mx_Dialog_content"
@@ -23,6 +23,7 @@ import { MatrixClient } from 'matrix-js-sdk';
import Modal from '../../../../Modal';
import { _t } from '../../../../languageHandler';
import { accessSecretStorage } from '../../../../CrossSigningManager';
import SettingsStore from "../../../../settings/SettingsStore";
const RESTORE_TYPE_PASSPHRASE = 0;
const RESTORE_TYPE_RECOVERYKEY = 1;
@@ -59,6 +60,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
forceRecoveryKey: false,
passPhrase: '',
restoreType: null,
progress: { stage: "prefetch" },
};
}
@@ -80,16 +82,29 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
});
}
_progressCallback = (data) => {
this.setState({
progress: data,
});
}
_onResetRecoveryClick = () => {
this.props.onFinished(false);
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
import('../../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
{
onFinished: () => {
this._loadBackupStatus();
},
}, null, /* priority = */ false, /* static = */ true,
);
if (SettingsStore.getValue("feature_cross_signing")) {
// If cross-signing is enabled, we reset the SSSS recovery passphrase (and cross-signing keys)
this.props.onFinished(false);
accessSecretStorage(() => {}, /* forceReset = */ true);
} else {
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
import('../../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
{
onFinished: () => {
this._loadBackupStatus();
},
}, null, /* priority = */ false, /* static = */ true,
);
}
}
_onRecoveryKeyChange = (e) => {
@@ -110,6 +125,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
// is the right one and restoring it is currently the only way we can do this.
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithPassword(
this.state.passPhrase, undefined, undefined, this.state.backupInfo,
{ progressCallback: this._progressCallback },
);
if (this.props.keyCallback) {
const key = await MatrixClientPeg.get().keyBackupKeyFromPassword(
@@ -146,6 +162,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
try {
const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithRecoveryKey(
this.state.recoveryKey, undefined, undefined, this.state.backupInfo,
{ progressCallback: this._progressCallback },
);
if (this.props.keyCallback) {
const key = MatrixClientPeg.get().keyBackupKeyFromRecoveryKey(this.state.recoveryKey);
@@ -185,6 +202,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
const recoverInfo = await accessSecretStorage(async () => {
return MatrixClientPeg.get().restoreKeyBackupWithSecretStorage(
this.state.backupInfo,
{ progressCallback: this._progressCallback },
);
});
this.setState({
@@ -207,6 +225,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
undefined, /* targetRoomId */
undefined, /* targetSessionId */
backupInfo,
{ progressCallback: this._progressCallback },
);
this.setState({
recoverInfo,
@@ -272,8 +291,20 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
let content;
let title;
if (this.state.loading) {
title = _t("Loading...");
content = <Spinner />;
title = _t("Restoring keys from backup");
let details;
if (this.state.progress.stage === "fetch") {
details = _t("Fetching keys from server...");
} else if (this.state.progress.stage === "load_keys") {
const { total, successes, failures } = this.state.progress;
details = _t("%(completed)s of %(total)s keys restored", { total, completed: successes + failures });
} else if (this.state.progress.stage === "prefetch") {
details = _t("Fetching keys from server...");
}
content = <div>
<div>{details}</div>
<Spinner />
</div>;
} else if (this.state.loadError) {
title = _t("Error");
content = _t("Unable to load backup status");
@@ -283,7 +314,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 +322,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>;
@@ -305,7 +336,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
content = _t("No backup found!");
} else if (this.state.recoverInfo) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
title = _t("Backup restored");
title = _t("Keys restored");
let failedToDecrypt;
if (this.state.recoverInfo.total > this.state.recoverInfo.imported) {
failedToDecrypt = <p>{_t(
@@ -314,7 +345,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
)}</p>;
}
content = <div>
<p>{_t("Restored %(sessionCount)s session keys", {sessionCount: this.state.recoverInfo.imported})}</p>
<p>{_t("Successfully restored %(sessionCount)s keys", {sessionCount: this.state.recoverInfo.imported})}</p>
{failedToDecrypt}
<DialogButtons primaryButton={_t('OK')}
onPrimaryButtonClick={this._onDone}
@@ -435,7 +466,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
onFinished={this.props.onFinished}
title={title}
>
<div>
<div className='mx_RestoreKeyBackupDialog_content'>
{content}
</div>
</BaseDialog>
@@ -21,6 +21,7 @@ import * as sdk from '../../../../index';
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
import { _t } from '../../../../languageHandler';
import { accessSecretStorage } from '../../../../CrossSigningManager';
/*
* Access Secure Secret Storage by requesting the user's passphrase.
@@ -55,8 +56,9 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
}
_onResetRecoveryClick = () => {
// Re-enter the access flow, but resetting storage this time around.
this.props.onFinished(false);
throw new Error("Resetting secret storage unimplemented");
accessSecretStorage(() => {}, /* forceReset = */ true);
}
_onRecoveryKeyChange = (e) => {
@@ -119,14 +121,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 +137,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 +165,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 +184,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 +194,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 +210,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(
+27 -13
View File
@@ -38,6 +38,7 @@ import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
import PersistedElement from "./PersistedElement";
import {WidgetType} from "../../../widgets/WidgetType";
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
const ENABLE_REACT_PERF = false;
@@ -187,13 +188,14 @@ export default class AppTile extends React.Component {
}
}
// TODO: Generify the name of this function. It's not just scalar tokens.
/**
* Adds a scalar token to the widget URL, if required
* Component initialisation is only complete when this function has resolved
*/
setScalarToken() {
if (!WidgetUtils.isScalarUrl(this.props.app.url)) {
console.warn('Non-scalar widget, not setting scalar token!', url);
console.warn('Widget does not match integration manager, refusing to set auth token', url);
this.setState({
error: null,
widgetUrl: this._addWurlParams(this.props.app.url),
@@ -217,7 +219,7 @@ export default class AppTile extends React.Component {
const defaultManager = managers.getPrimaryManager();
if (!WidgetUtils.isScalarUrl(defaultManager.apiUrl)) {
console.warn('Non-scalar manager, not setting scalar token!', url);
console.warn('Unknown integration manager, refusing to set auth token', url);
this.setState({
error: null,
widgetUrl: this._addWurlParams(this.props.app.url),
@@ -423,7 +425,12 @@ export default class AppTile extends React.Component {
// FIXME: There's probably no reason to do this here: it should probably be done entirely
// in ActiveWidgetStore.
const widgetMessaging = new WidgetMessaging(
this.props.app.id, this._getRenderedUrl(), this.props.userWidget, this._appFrame.current.contentWindow);
this.props.app.id,
this.props.app.url,
this._getRenderedUrl(),
this.props.userWidget,
this._appFrame.current.contentWindow,
);
ActiveWidgetStore.setWidgetMessaging(this.props.app.id, widgetMessaging);
widgetMessaging.getCapabilities().then((requestedCapabilities) => {
console.log(`Widget ${this.props.app.id} requested capabilities: ` + requestedCapabilities);
@@ -454,7 +461,7 @@ export default class AppTile extends React.Component {
// We only tell Jitsi widgets that we're ready because they're realistically the only ones
// using this custom extension to the widget API.
if (this.props.app.type === 'jitsi') {
if (WidgetType.JITSI.matches(this.props.app.type)) {
widgetMessaging.flagReadyToContinue();
}
}).catch((err) => {
@@ -559,15 +566,18 @@ export default class AppTile extends React.Component {
* Replace the widget template variables in a url with their values
*
* @param {string} u The URL with template variables
* @param {string} widgetType The widget's type
*
* @returns {string} url with temlate variables replaced
*/
_templatedUrl(u) {
_templatedUrl(u, widgetType: string) {
const targetData = {};
if (WidgetType.JITSI.matches(widgetType)) {
targetData['domain'] = 'jitsi.riot.im'; // v1 jitsi widgets have this hardcoded
}
const myUserId = MatrixClientPeg.get().credentials.userId;
const myUser = MatrixClientPeg.get().getUser(myUserId);
const vars = Object.assign({
domain: "jitsi.riot.im", // v1 widgets have this hardcoded
}, this.props.app.data, {
const vars = Object.assign(targetData, this.props.app.data, {
'matrix_user_id': myUserId,
'matrix_room_id': this.props.room.roomId,
'matrix_display_name': myUser ? myUser.displayName : myUserId,
@@ -597,25 +607,26 @@ export default class AppTile extends React.Component {
_getRenderedUrl() {
let url;
if (this.props.app.type === 'jitsi') {
if (WidgetType.JITSI.matches(this.props.app.type)) {
console.log("Replacing Jitsi widget URL with local wrapper");
url = WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: true});
url = this._addWurlParams(url);
} else {
url = this._getSafeUrl(this.state.widgetUrl);
}
return this._templatedUrl(url);
return this._templatedUrl(url, this.props.app.type);
}
_getPopoutUrl() {
if (this.props.app.type === 'jitsi') {
if (WidgetType.JITSI.matches(this.props.app.type)) {
return this._templatedUrl(
WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: false}),
this.props.app.type,
);
} else {
// use app.url, not state.widgetUrl, because we want the one without
// the wURL params for the popped-out version.
return this._templatedUrl(this._getSafeUrl(this.props.app.url));
return this._templatedUrl(this._getSafeUrl(this.props.app.url), this.props.app.type);
}
}
@@ -629,7 +640,10 @@ export default class AppTile extends React.Component {
if (ALLOWED_APP_URL_SCHEMES.includes(parsedWidgetUrl.protocol)) {
safeWidgetUrl = url.format(parsedWidgetUrl);
}
return safeWidgetUrl;
// Replace all the dollar signs back to dollar signs as they don't affect HTTP at all.
// We also need the dollar signs in-tact for variable substitution.
return safeWidgetUrl.replace(/%24/g, '$');
}
_getTileTitle() {
+28 -11
View File
@@ -19,8 +19,22 @@ import TagTile from './TagTile';
import React from 'react';
import { Draggable } from 'react-beautiful-dnd';
import { ContextMenu, toRightOf, useContextMenu } from "../../structures/ContextMenu";
import * as sdk from '../../../index';
export default function DNDTagTile(props) {
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
let contextMenu = null;
if (menuDisplayed && handle.current) {
const elementRect = handle.current.getBoundingClientRect();
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
contextMenu = (
<ContextMenu {...toRightOf(elementRect)} onFinished={closeMenu}>
<TagTileContextMenu tag={props.tag} onFinished={closeMenu} />
</ContextMenu>
);
}
return <div>
<Draggable
key={props.tag}
@@ -28,18 +42,21 @@ export default function DNDTagTile(props) {
index={props.index}
type="draggable-TagTile"
>
{ (provided, snapshot) => (
<div>
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<TagTile {...props} />
</div>
{ provided.placeholder }
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<TagTile
{...props}
contextMenuButtonRef={handle}
menuDisplayed={menuDisplayed}
openMenu={openMenu}
/>
</div>
) }
)}
</Draggable>
{contextMenu}
</div>;
}
@@ -66,12 +66,12 @@ const EventListSummary = ({events, children, threshold=3, onToggle, startExpande
}
return (
<div className="mx_EventListSummary" data-scroll-tokens={eventIds}>
<li className="mx_EventListSummary" data-scroll-tokens={eventIds}>
<AccessibleButton className="mx_EventListSummary_toggle" onClick={toggleExpanded} aria-expanded={expanded}>
{ expanded ? _t('collapse') : _t('expand') }
</AccessibleButton>
{ body }
</div>
</li>
);
};
@@ -90,7 +90,7 @@ EventListSummary.propTypes = {
// The list of room members for which to show avatars next to the summary
summaryMembers: PropTypes.arrayOf(PropTypes.instanceOf(RoomMember)),
// The text to show as the summary of this event list
summaryText: PropTypes.string.isRequired,
summaryText: PropTypes.string,
};
export default EventListSummary;
@@ -73,6 +73,7 @@ export default class ImageView extends React.Component {
Modal.createTrackedDialog('Confirm Redact Dialog', 'Image View', ConfirmRedactDialog, {
onFinished: (proceed) => {
if (!proceed) return;
this.props.onFinished();
MatrixClientPeg.get().redactEvent(
this.props.mxEvent.getRoomId(), this.props.mxEvent.getId(),
).catch(function(e) {
@@ -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>
@@ -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>;
+36 -52
View File
@@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {createRef} from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import classNames from 'classnames';
@@ -28,7 +28,6 @@ import * as FormattingUtils from '../../../utils/FormattingUtils';
import FlairStore from '../../../stores/FlairStore';
import GroupStore from '../../../stores/GroupStore';
import TagOrderStore from '../../../stores/TagOrderStore';
import {ContextMenu, toRightOf} from "../../structures/ContextMenu";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
// A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents
@@ -43,6 +42,9 @@ export default createReactClass({
// A string tag such as "m.favourite" or a group ID such as "+groupid:domain.bla"
// For now, only group IDs are handled.
tag: PropTypes.string,
contextMenuButtonRef: PropTypes.object,
openMenu: PropTypes.func,
menuDisplayed: PropTypes.bool,
},
statics: {
@@ -55,14 +57,10 @@ export default createReactClass({
hover: false,
// The profile data of the group if this.props.tag is a group ID
profile: null,
// Whether or not the context menu is open
menuDisplayed: false,
};
},
componentDidMount() {
this._contextMenuButton = createRef();
this.unmounted = false;
if (this.props.tag[0] === '+') {
FlairStore.addListener('updateGroupProfile', this._onFlairStoreUpdated);
@@ -86,7 +84,7 @@ export default createReactClass({
this.props.tag,
).then((profile) => {
if (this.unmounted) return;
this.setState({profile});
this.setState({ profile });
}).catch((err) => {
console.warn('Could not fetch group profile for ' + this.props.tag, err);
});
@@ -113,28 +111,19 @@ export default createReactClass({
},
onMouseOver: function() {
this.setState({hover: true});
this.setState({ hover: true });
},
onMouseOut: function() {
this.setState({hover: false});
this.setState({ hover: false });
},
openMenu: function(e) {
// Prevent the TagTile onClick event firing as well
e.stopPropagation();
e.preventDefault();
this.setState({
menuDisplayed: true,
hover: false,
});
},
closeMenu: function() {
this.setState({
menuDisplayed: false,
});
this.setState({ hover: false });
this.props.openMenu();
},
render: function() {
@@ -154,7 +143,7 @@ export default createReactClass({
const badge = TagOrderStore.getGroupBadge(this.props.tag);
let badgeElement;
if (badge && !this.state.hover) {
if (badge && !this.state.hover && !this.props.menuDisplayed) {
const badgeClasses = classNames({
"mx_TagTile_badge": true,
"mx_TagTile_badgeHighlight": badge.highlight,
@@ -163,39 +152,34 @@ export default createReactClass({
}
// FIXME: this ought to use AccessibleButton for a11y but that causes onMouseOut/onMouseOver to fire too much
const contextButton = this.state.hover || this.state.menuDisplayed ?
<div className="mx_TagTile_context_button" onClick={this.openMenu} ref={this._contextMenuButton}>
{ "\u00B7\u00B7\u00B7" }
</div> : <div ref={this._contextMenuButton} />;
let contextMenu;
if (this.state.menuDisplayed) {
const elementRect = this._contextMenuButton.current.getBoundingClientRect();
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
contextMenu = (
<ContextMenu {...toRightOf(elementRect)} onFinished={this.closeMenu}>
<TagTileContextMenu tag={this.props.tag} onFinished={this.closeMenu} />
</ContextMenu>
);
}
const contextButton = this.state.hover || this.props.menuDisplayed ?
<div className="mx_TagTile_context_button" onClick={this.openMenu} ref={this.props.contextMenuButtonRef}>
{"\u00B7\u00B7\u00B7"}
</div> : <div ref={this.props.contextMenuButtonRef} />;
const AccessibleTooltipButton = sdk.getComponent("elements.AccessibleTooltipButton");
return <React.Fragment>
<AccessibleTooltipButton className={className} onClick={this.onClick} onContextMenu={this.openMenu} title={name}>
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
<BaseAvatar
name={name}
idName={this.props.tag}
url={httpUrl}
width={avatarHeight}
height={avatarHeight}
/>
{ contextButton }
{ badgeElement }
</div>
</AccessibleTooltipButton>
{ contextMenu }
</React.Fragment>;
return <AccessibleTooltipButton
className={className}
onClick={this.onClick}
onContextMenu={this.openMenu}
title={name}
>
<div
className="mx_TagTile_avatar"
onMouseOver={this.onMouseOver}
onMouseOut={this.onMouseOut}
>
<BaseAvatar
name={name}
idName={this.props.tag}
url={httpUrl}
width={avatarHeight}
height={avatarHeight}
/>
{contextButton}
{badgeElement}
</div>
</AccessibleTooltipButton>;
},
});
@@ -26,6 +26,7 @@ import * as sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import classNames from 'classnames';
import RedactedBody from "./RedactedBody";
function getReplacedContent(event) {
const originalContent = event.getOriginalContent();
@@ -132,8 +133,7 @@ export default class EditHistoryMessage extends React.PureComponent {
const content = getReplacedContent(mxEvent);
let contentContainer;
if (mxEvent.isRedacted()) {
const UnknownBody = sdk.getComponent('messages.UnknownBody');
contentContainer = <UnknownBody mxEvent={this.props.mxEvent} />;
contentContainer = <RedactedBody mxEvent={this.props.mxEvent} />;
} else {
let contentElements;
if (this.props.previousEdit) {
@@ -158,7 +158,6 @@ export default class EditHistoryMessage extends React.PureComponent {
const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.state.sendStatus) !== -1);
const classes = classNames({
"mx_EventTile": true,
"mx_EventTile_redacted": mxEvent.isRedacted(),
"mx_EventTile_sending": isSending,
"mx_EventTile_notSent": this.state.sendStatus === 'not_sent',
});
@@ -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;
}
@@ -20,6 +20,8 @@ import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
import SettingsStore from "../../../settings/SettingsStore";
import {Mjolnir} from "../../../mjolnir/Mjolnir";
import RedactedBody from "./RedactedBody";
import UnknownBody from "./UnknownBody";
export default createReactClass({
displayName: 'MessageEvent',
@@ -61,8 +63,6 @@ export default createReactClass({
},
render: function() {
const UnknownBody = sdk.getComponent('messages.UnknownBody');
const bodyTypes = {
'm.text': sdk.getComponent('messages.TextualBody'),
'm.notice': sdk.getComponent('messages.TextualBody'),
@@ -79,7 +79,7 @@ export default createReactClass({
const content = this.props.mxEvent.getContent();
const type = this.props.mxEvent.getType();
const msgtype = content.msgtype;
let BodyType = UnknownBody;
let BodyType = RedactedBody;
if (!this.props.mxEvent.isRedacted()) {
// only resolve BodyType if event is not redacted
if (type && evTypes[type]) {
@@ -89,6 +89,9 @@ export default createReactClass({
} else if (content.url) {
// Fallback to MFileBody if there's a content URL
BodyType = bodyTypes['m.file'];
} else {
// Fallback to UnknownBody otherwise if not redacted
BodyType = UnknownBody;
}
}
@@ -0,0 +1,46 @@
/*
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.
*/
import React, {useContext} from "react";
import {MatrixClient} from "matrix-js-sdk/src/client";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import { _t } from "../../../languageHandler";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
interface IProps {
mxEvent: MatrixEvent;
}
const RedactedBody = React.forwardRef<any, IProps>(({mxEvent}, ref) => {
const cli: MatrixClient = useContext(MatrixClientContext);
let text = _t("Message deleted");
const unsigned = mxEvent.getUnsigned();
const redactedBecauseUserId = unsigned && unsigned.redacted_because && unsigned.redacted_because.sender;
if (redactedBecauseUserId && redactedBecauseUserId !== mxEvent.getSender()) {
const room = cli.getRoom(mxEvent.getRoomId());
const sender = room && room.getMember(redactedBecauseUserId);
text = _t("Message deleted by %(name)s", { name: sender ? sender.name : redactedBecauseUserId });
}
return (
<span className="mx_RedactedBody" ref={ref}>
{ text }
</span>
);
});
export default RedactedBody;
+5 -21
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) => {
button.onclick = async () => {
const copyCode = button.parentNode.getElementsByTagName("pre")[0];
const successful = this.copyToClipboard(copyCode.textContent);
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
@@ -0,0 +1,72 @@
/*
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.
*/
import React from 'react';
import classNames from 'classnames';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import Modal from '../../../Modal';
export default class TileErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
};
}
static getDerivedStateFromError(error) {
// Side effects are not permitted here, so we only update the state so
// that the next render shows an error message.
return { error };
}
_onBugReport = () => {
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
if (!BugReportDialog) {
return;
}
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {
label: 'react-soft-crash-tile',
});
};
render() {
if (this.state.error) {
const { mxEvent } = this.props;
const classes = {
mx_EventTile: true,
mx_EventTile_info: true,
mx_EventTile_content: true,
mx_EventTile_tileError: true,
};
return (<div className={classNames(classes)}>
<div className="mx_EventTile_line">
<span>
{_t("Can't load this message")}
{ mxEvent && ` (${mxEvent.getType()})` }
<a onClick={this._onBugReport} href="#">
{_t("Submit logs")}
</a>
</span>
</div>
</div>);
}
return this.props.children;
}
}
+10 -23
View File
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket 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,27 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
import React from "react";
export default createReactClass({
displayName: 'UnknownBody',
render: function() {
let tooltip = _t("Removed or unknown message type");
if (this.props.mxEvent.isRedacted()) {
const redactedBecauseUserId = this.props.mxEvent.getUnsigned().redacted_because.sender;
tooltip = redactedBecauseUserId ?
_t("Message removed by %(userId)s", { userId: redactedBecauseUserId }) :
_t("Message removed");
}
const text = this.props.mxEvent.getContent().body;
return (
<span className="mx_UnknownBody" title={tooltip}>
{ text }
</span>
);
},
});
export default ({mxEvent}) => {
const text = mxEvent.getContent().body;
return (
<span className="mx_UnknownBody">
{ text }
</span>
);
};
@@ -22,7 +22,6 @@ 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";
@@ -46,12 +45,6 @@ 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);
@@ -147,7 +140,7 @@ const EncryptionPanel = (props) => {
key={request.channel.transactionId}
inDialog={layout === "dialog"}
phase={phase}
device={device} />
/>
</React.Fragment>);
}
};
+56 -36
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]);
@@ -142,7 +142,7 @@ function useIsEncrypted(cli, room) {
function useHasCrossSigningKeys(cli, member, canVerify, setUpdating) {
return useAsyncMemo(async () => {
if (!canVerify) {
return false;
return undefined;
}
setUpdating(true);
try {
@@ -153,7 +153,7 @@ function useHasCrossSigningKeys(cli, member, canVerify, setUpdating) {
} finally {
setUpdating(false);
}
}, [cli, member, canVerify], false);
}, [cli, member, canVerify], undefined);
}
function DeviceItem({userId, device}) {
@@ -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();
@@ -181,9 +181,7 @@ function DeviceItem({userId, device}) {
});
const onDeviceClick = () => {
if (!isVerified) {
verifyDevice(cli.getUser(userId), device);
}
verifyDevice(cli.getUser(userId), device);
};
const deviceName = device.ambiguous ?
@@ -191,17 +189,29 @@ function DeviceItem({userId, device}) {
device.getDisplayName();
let trustedLabel = null;
if (userTrust.isVerified()) trustedLabel = isVerified ? _t("Trusted") : _t("Not trusted");
return (
<AccessibleButton
className={classes}
title={device.deviceId}
onClick={onDeviceClick}
>
<div className={iconClasses} />
<div className="mx_UserInfo_device_name">{deviceName}</div>
<div className="mx_UserInfo_device_trusted">{trustedLabel}</div>
</AccessibleButton>
);
if (isVerified) {
return (
<div className={classes} title={device.deviceId} >
<div className={iconClasses} />
<div className="mx_UserInfo_device_name">{deviceName}</div>
<div className="mx_UserInfo_device_trusted">{trustedLabel}</div>
</div>
);
} else {
return (
<AccessibleButton
className={classes}
title={device.deviceId}
onClick={onDeviceClick}
>
<div className={iconClasses} />
<div className="mx_UserInfo_device_name">{deviceName}</div>
<div className="mx_UserInfo_device_trusted">{trustedLabel}</div>
</AccessibleButton>
);
}
}
function DevicesSection({devices, userId, loading}) {
@@ -237,7 +247,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();
@@ -547,7 +557,7 @@ const RedactMessagesButton = ({member}) => {
let eventsToRedact = [];
while (timeline) {
eventsToRedact = timeline.getEvents().reduce((events, event) => {
if (event.getSender() === userId && !event.isRedacted()) {
if (event.getSender() === userId && !event.isRedacted() && !event.isRedaction()) {
return events.concat(event);
} else {
return events;
@@ -1100,7 +1110,7 @@ export const useDevices = (userId) => {
async function _downloadDeviceList() {
try {
await cli.downloadKeys([userId], true);
const devices = await cli.getStoredDevicesForUser(userId);
const devices = cli.getStoredDevicesForUser(userId);
if (cancelled) {
// we got cancelled - presumably a different user now
@@ -1125,7 +1135,7 @@ export const useDevices = (userId) => {
useEffect(() => {
let cancel = false;
const updateDevices = async () => {
const newDevices = await cli.getStoredDevicesForUser(userId);
const newDevices = cli.getStoredDevicesForUser(userId);
if (cancel) return;
setDevices(newDevices);
};
@@ -1298,7 +1308,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) => {
@@ -1307,18 +1317,28 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
const hasCrossSigningKeys =
useHasCrossSigningKeys(cli, member, canVerify, setUpdating );
const showDeviceListSpinner = devices === undefined;
if (canVerify) {
verifyButton = (
<AccessibleButton className="mx_UserInfo_field" onClick={() => {
if (hasCrossSigningKeys) {
verifyUser(member);
} else {
legacyVerifyUser(member);
}
}}>
{_t("Verify")}
</AccessibleButton>
);
if (hasCrossSigningKeys !== undefined) {
// Note: mx_UserInfo_verifyButton is for the end-to-end tests
verifyButton = (
<AccessibleButton className="mx_UserInfo_field mx_UserInfo_verifyButton" onClick={() => {
if (hasCrossSigningKeys) {
verifyUser(member);
} else {
legacyVerifyUser(member);
}
}}>
{_t("Verify")}
</AccessibleButton>
);
} else if (!showDeviceListSpinner) {
// HACK: only show a spinner if the device section spinner is not shown,
// to avoid showing a double spinner
// We should ask for a design that includes all the different loading states here
const Spinner = sdk.getComponent('elements.Spinner');
verifyButton = <Spinner />;
}
}
const securitySection = (
@@ -1327,7 +1347,7 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
<p>{ text }</p>
{ verifyButton }
<DevicesSection
loading={devices === undefined}
loading={showDeviceListSpinner}
devices={devices}
userId={member.userId} />
</div>
@@ -17,6 +17,7 @@ limitations under the License.
import React from "react";
import PropTypes from "prop-types";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import * as sdk from '../../../index';
import {verificationMethods} from 'matrix-js-sdk/src/crypto';
import {SCAN_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
@@ -123,10 +124,17 @@ export default class VerificationPanel extends React.PureComponent {
const sasLabel = showQR ?
_t("If you can't scan the code above, verify by comparing unique emoji.") :
_t("Verify by comparing unique emoji.");
// Note: mx_VerificationPanel_verifyByEmojiButton is for the end-to-end tests
sasBlock = <div className="mx_UserInfo_container">
<h3>{_t("Verify by emoji")}</h3>
<p>{sasLabel}</p>
<AccessibleButton disabled={disabled} kind="primary" className="mx_UserInfo_wideButton" onClick={this._startSAS}>
<AccessibleButton
disabled={disabled}
kind="primary"
className="mx_UserInfo_wideButton mx_VerificationPanel_verifyByEmojiButton"
onClick={this._startSAS}
>
{_t("Verify by emoji")}
</AccessibleButton>
</div>;
@@ -154,6 +162,11 @@ export default class VerificationPanel extends React.PureComponent {
this.state.reciprocateQREvent.cancel();
};
_getDevice() {
const deviceId = this.props.request && this.props.request.channel.deviceId;
return MatrixClientPeg.get().getStoredDevice(MatrixClientPeg.get().getUserId(), deviceId);
}
renderQRReciprocatePhase() {
const {member, request} = this.props;
let Button;
@@ -210,16 +223,27 @@ export default class VerificationPanel extends React.PureComponent {
}
}
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!", {
let description;
if (request.isSelfVerification) {
const device = this._getDevice();
if (!device) {
// This can happen if the device is logged out while we're still showing verification
// UI for it.
console.warn("Verified device we don't know about: " + this.props.request.channel.deviceId);
description = _t("You've successfully verified your device!");
} else {
description = _t("You've successfully verified %(deviceName)s (%(deviceId)s)!", {
deviceName: device ? device.getDisplayName() : '',
deviceId: this.props.request.channel.deviceId,
});
}
} else {
description = _t("You've successfully verified %(displayName)s!", {
displayName: member.displayName || member.name || member.userId,
});
}
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (
<div className="mx_UserInfo_container mx_VerificationPanel_verified_section">
<h3>{_t("Verified")}</h3>
@@ -290,12 +314,12 @@ export default class VerificationPanel extends React.PureComponent {
const emojis = this.state.sasEvent ?
<VerificationShowSas
displayName={displayName}
device={this._getDevice()}
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>
@@ -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);
+4 -2
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;
@@ -17,28 +17,49 @@ limitations under the License.
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import flatMap from 'lodash/flatMap';
import type {Completion} from '../../../autocomplete/Autocompleter';
import { Room } from 'matrix-js-sdk';
import {ICompletion, ISelectionRange, IProviderCompletions} from '../../../autocomplete/Autocompleter';
import {Room} from 'matrix-js-sdk/src/models/room';
import SettingsStore from "../../../settings/SettingsStore";
import Autocompleter from '../../../autocomplete/Autocompleter';
import {sleep} from "../../../utils/promise";
const COMPOSER_SELECTED = 0;
export const generateCompletionDomId = (number) => `mx_Autocomplete_Completion_${number}`;
export default class Autocomplete extends React.Component {
interface IProps {
// the query string for which to show autocomplete suggestions
query: string;
// method invoked with range and text content when completion is confirmed
onConfirm: (ICompletion) => void;
// method invoked when selected (if any) completion changes
onSelectionChange?: (ICompletion, number) => void;
selection: ISelectionRange;
// The room in which we're autocompleting
room: Room;
}
interface IState {
completions: IProviderCompletions[];
completionList: ICompletion[];
selectionOffset: number;
shouldShowCompletions: boolean;
hide: boolean;
forceComplete: boolean;
}
export default class Autocomplete extends React.PureComponent<IProps, IState> {
autocompleter: Autocompleter;
queryRequested: string;
debounceCompletionsRequest: NodeJS.Timeout;
containerRef: React.RefObject<HTMLDivElement>;
constructor(props) {
super(props);
this.autocompleter = new Autocompleter(props.room);
this.completionPromise = null;
this.hide = this.hide.bind(this);
this.onCompletionClicked = this.onCompletionClicked.bind(this);
this.state = {
// list of completionResults, each containing completions
@@ -57,13 +78,15 @@ export default class Autocomplete extends React.Component {
forceComplete: false,
};
this.containerRef = React.createRef();
}
componentDidMount() {
this._applyNewProps();
this.applyNewProps();
}
_applyNewProps(oldQuery, oldRoom) {
private applyNewProps(oldQuery?: string, oldRoom?: Room) {
if (oldRoom && this.props.room.roomId !== oldRoom.roomId) {
this.autocompleter.destroy();
this.autocompleter = new Autocompleter(this.props.room);
@@ -81,7 +104,7 @@ export default class Autocomplete extends React.Component {
this.autocompleter.destroy();
}
complete(query, selection) {
complete(query: string, selection: ISelectionRange) {
this.queryRequested = query;
if (this.debounceCompletionsRequest) {
clearTimeout(this.debounceCompletionsRequest);
@@ -112,7 +135,7 @@ export default class Autocomplete extends React.Component {
});
}
processQuery(query, selection) {
processQuery(query: string, selection: ISelectionRange) {
return this.autocompleter.getCompletions(
query, selection, this.state.forceComplete,
).then((completions) => {
@@ -124,7 +147,7 @@ export default class Autocomplete extends React.Component {
});
}
processCompletions(completions) {
processCompletions(completions: IProviderCompletions[]) {
const completionList = flatMap(completions, (provider) => provider.completions);
// Reset selection when completion list becomes empty.
@@ -159,7 +182,7 @@ export default class Autocomplete extends React.Component {
});
}
hasSelection(): bool {
hasSelection(): boolean {
return this.countCompletions() > 0 && this.state.selectionOffset !== 0;
}
@@ -168,7 +191,7 @@ export default class Autocomplete extends React.Component {
}
// called from MessageComposerInput
moveSelection(delta): ?Completion {
moveSelection(delta: number) {
const completionCount = this.countCompletions();
if (completionCount === 0) return; // there are no items to move the selection through
@@ -177,7 +200,7 @@ export default class Autocomplete extends React.Component {
this.setSelection(index);
}
onEscape(e): boolean {
onEscape(e: KeyboardEvent): boolean {
const completionCount = this.countCompletions();
if (completionCount === 0) {
// autocomplete is already empty, so don't preventDefault
@@ -190,9 +213,14 @@ export default class Autocomplete extends React.Component {
this.hide();
}
hide() {
this.setState({hide: true, selectionOffset: 0, completions: [], completionList: []});
}
hide = () => {
this.setState({
hide: true,
selectionOffset: 0,
completions: [],
completionList: [],
});
};
forceComplete() {
return new Promise((resolve) => {
@@ -207,7 +235,7 @@ export default class Autocomplete extends React.Component {
});
}
onCompletionClicked(selectionOffset: number): boolean {
onCompletionClicked = (selectionOffset: number): boolean => {
if (this.countCompletions() === 0 || selectionOffset === COMPOSER_SELECTED) {
return false;
}
@@ -216,7 +244,7 @@ export default class Autocomplete extends React.Component {
this.hide();
return true;
}
};
setSelection(selectionOffset: number) {
this.setState({selectionOffset, hide: false});
@@ -225,28 +253,24 @@ export default class Autocomplete extends React.Component {
}
}
componentDidUpdate(prevProps) {
this._applyNewProps(prevProps.query, prevProps.room);
componentDidUpdate(prevProps: IProps) {
this.applyNewProps(prevProps.query, prevProps.room);
// this is the selected completion, so scroll it into view if needed
const selectedCompletion = this.refs[`completion${this.state.selectionOffset}`];
if (selectedCompletion && this.container) {
if (selectedCompletion && this.containerRef.current) {
const domNode = ReactDOM.findDOMNode(selectedCompletion);
const offsetTop = domNode && domNode.offsetTop;
if (offsetTop > this.container.scrollTop + this.container.offsetHeight ||
offsetTop < this.container.scrollTop) {
this.container.scrollTop = offsetTop - this.container.offsetTop;
const offsetTop = domNode && (domNode as HTMLElement).offsetTop;
if (offsetTop > this.containerRef.current.scrollTop + this.containerRef.current.offsetHeight ||
offsetTop < this.containerRef.current.scrollTop) {
this.containerRef.current.scrollTop = offsetTop - this.containerRef.current.offsetTop;
}
}
}
setState(state, func) {
super.setState(state, func);
}
render() {
let position = 1;
const renderedCompletions = this.state.completions.map((completionResult, i) => {
const completions = completionResult.completions.map((completion, i) => {
const completions = completionResult.completions.map((completion, j) => {
const selected = position === this.state.selectionOffset;
const className = classNames('mx_Autocomplete_Completion', {selected});
const componentPosition = position;
@@ -257,7 +281,7 @@ export default class Autocomplete extends React.Component {
};
return React.cloneElement(completion.component, {
"key": i,
"key": j,
"ref": `completion${componentPosition}`,
"id": generateCompletionDomId(componentPosition - 1), // 0 index the completion IDs
className,
@@ -276,23 +300,9 @@ export default class Autocomplete extends React.Component {
}).filter((completion) => !!completion);
return !this.state.hide && renderedCompletions.length > 0 ? (
<div className="mx_Autocomplete" ref={(e) => this.container = e}>
<div className="mx_Autocomplete" ref={this.containerRef}>
{ renderedCompletions }
</div>
) : null;
}
}
Autocomplete.propTypes = {
// the query string for which to show autocomplete suggestions
query: PropTypes.string.isRequired,
// method invoked with range and text content when completion is confirmed
onConfirm: PropTypes.func.isRequired,
// method invoked when selected (if any) completion changes
onSelectionChange: PropTypes.func,
// The room in which we're autocompleting
room: PropTypes.instanceOf(Room),
};
@@ -170,7 +170,7 @@ export default class BasicMessageEditor extends React.Component {
// 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.get(cmd).category !== CommandCategories.messages) {
if (!CommandMap.has(cmd) || CommandMap.get(cmd).category !== CommandCategories.messages) {
isTyping = false;
}
}
+2 -2
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) {
+4 -3
View File
@@ -34,6 +34,7 @@ import {ALL_RULE_TYPES} from "../../../mjolnir/BanList";
import * as ObjectUtils from "../../../ObjectUtils";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {E2E_STATE} from "./E2EIcon";
import toRem from "../../../utils/rem";
const eventTileTypes = {
'm.room.message': 'messages.MessageEvent',
@@ -59,6 +60,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 +324,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);
@@ -472,7 +474,7 @@ export default createReactClass({
if (remainder > 0) {
remText = <span className="mx_EventTile_readAvatarRemainder"
onClick={this.toggleAllReadAvatars}
style={{ right: -(left - receiptOffset) }}>{ remainder }+
style={{ right: "calc(" + toRem(-left) + " + " + receiptOffset + "px)" }}>{ remainder }+
</span>;
}
}
@@ -668,7 +670,6 @@ export default createReactClass({
mx_EventTile_unknown: !isBubbleMessage && this.state.verified === E2E_STATE.UNKNOWN,
mx_EventTile_bad: isEncryptionFailure,
mx_EventTile_emote: msgtype === 'm.emote',
mx_EventTile_redacted: isRedacted,
});
let permalink = "#";
@@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import SettingsStore from '../../../settings/SettingsStore';
export default class InviteOnlyIcon extends React.Component {
constructor() {
@@ -39,10 +38,6 @@ export default class InviteOnlyIcon extends React.Component {
render() {
const classes = this.props.collapsedPanel ? "mx_InviteOnlyIcon_small": "mx_InviteOnlyIcon_large";
if (!SettingsStore.isFeatureEnabled("feature_invite_only_padlocks")) {
return null;
}
const Tooltip = sdk.getComponent("elements.Tooltip");
let tooltip;
if (this.state.hover) {
+5 -8
View File
@@ -160,13 +160,10 @@ export default createReactClass({
// no need to re-download the whole thing; just update our copy of
// the list.
// Promise.resolve to handle transition from static result to promise; can be removed
// in future
Promise.resolve(this.context.getStoredDevicesForUser(userId)).then((devices) => {
this.setState({
devices: devices,
e2eStatus: this._getE2EStatus(devices),
});
const devices = this.context.getStoredDevicesForUser(userId);
this.setState({
devices: devices,
e2eStatus: this._getE2EStatus(devices),
});
}
},
@@ -367,7 +364,7 @@ export default createReactClass({
let eventsToRedact = [];
for (const timeline of timelineSet.getTimelines()) {
eventsToRedact = timeline.getEvents().reduce((events, event) => {
if (event.getSender() === userId && !event.isRedacted()) {
if (event.getSender() === userId && !event.isRedacted() && !event.isRedaction()) {
return events.concat(event);
} else {
return events;
+2 -2
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);
@@ -129,7 +129,7 @@ export default createReactClass({
return;
}
const devices = await cli.getStoredDevicesForUser(userId);
const devices = cli.getStoredDevicesForUser(userId);
const anyDeviceUnverified = devices.some(device => {
const { deviceId } = device;
// For your own devices, we use the stricter check of cross-signing
@@ -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…');
@@ -23,6 +23,7 @@ import { _t } from '../../../languageHandler';
import {formatDate} from '../../../DateUtils';
import Velociraptor from "../../../Velociraptor";
import * as sdk from "../../../index";
import toRem from "../../../utils/rem";
let bounce = false;
try {
@@ -148,7 +149,7 @@ export default createReactClass({
// start at the old height and in the old h pos
startStyles.push({ top: startTopOffset+"px",
left: oldInfo.left+"px" });
left: toRem(oldInfo.left) });
const reorderTransitionOpts = {
duration: 100,
@@ -181,7 +182,7 @@ export default createReactClass({
}
const style = {
left: this.props.leftOffset+'px',
left: toRem(this.props.leftOffset),
top: '0px',
visibility: this.props.hidden ? 'hidden' : 'visible',
};
@@ -363,17 +363,6 @@ export default class RoomBreadcrumbs extends React.Component {
badge = <div className={badgeClasses}>{r.formattedCount}</div>;
}
let dmIndicator;
if (this._isDmRoom(r.room) && !SettingsStore.isFeatureEnabled("feature_cross_signing")) {
dmIndicator = <img
src={require("../../../../res/img/icon_person.svg")}
className="mx_RoomBreadcrumbs_dmIndicator"
width="13"
height="15"
alt={_t("Direct Chat")}
/>;
}
return (
<AccessibleButton
className={classes}
@@ -385,7 +374,6 @@ export default class RoomBreadcrumbs extends React.Component {
>
<RoomAvatar room={r.room} width={32} height={32} />
{badge}
{dmIndicator}
{tooltip}
</AccessibleButton>
);
+1 -1
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 />;
}
+3 -3
View File
@@ -266,9 +266,9 @@ export default createReactClass({
params: {
email: this.props.invitedEmail,
signurl: this.props.signUrl,
room_name: this.props.oobData.room_name,
room_avatar_url: this.props.oobData.avatarUrl,
inviter_name: this.props.oobData.inviterName,
room_name: this.props.oobData ? this.props.oobData.room_name : null,
room_avatar_url: this.props.oobData ? this.props.oobData.avatarUrl : null,
inviter_name: this.props.oobData ? this.props.oobData.inviterName : null,
}
};
},
@@ -150,7 +150,7 @@ export default class RoomRecoveryReminder extends React.PureComponent {
)}</p>
</div>
<div className="mx_RoomRecoveryReminder_buttons">
<AccessibleButton className="mx_RoomRecoveryReminder_button"
<AccessibleButton kind="primary"
onClick={this.onSetupClick}>
{setupCaption}
</AccessibleButton>
+4 -22
View File
@@ -155,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;
}
@@ -432,10 +432,9 @@ export default createReactClass({
});
let name = this.state.roomName;
if (name == undefined || name == null) name = '';
if (typeof name !== 'string') name = '';
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
let badge;
if (badges) {
const limitedCount = FormattingUtils.formatCount(notificationCount);
@@ -484,26 +483,10 @@ export default createReactClass({
let ariaLabel = name;
let dmIndicator;
let dmOnline;
/* Post-cross-signing we don't show DM indicators at all, instead relying on user
context to let them know when that is. */
if (dmUserId && !SettingsStore.isFeatureEnabled("feature_cross_signing")) {
dmIndicator = <img
src={require("../../../../res/img/icon_person.svg")}
className="mx_RoomTile_dm"
width="11"
height="13"
alt="dm"
/>;
}
const { room } = this.props;
const member = room.getMember(dmUserId);
if (
member && member.membership === "join" && room.getJoinedMemberCount() === 2 &&
SettingsStore.isFeatureEnabled("feature_presence_in_room_list")
) {
if (member && member.membership === "join" && room.getJoinedMemberCount() === 2) {
const UserOnlineDot = sdk.getComponent('rooms.UserOnlineDot');
dmOnline = <UserOnlineDot userId={dmUserId} />;
}
@@ -532,7 +515,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} />;
}
@@ -562,7 +545,6 @@ export default createReactClass({
<div className={avatarClasses}>
<div className="mx_RoomTile_avatar_container">
<RoomAvatar room={this.props.room} width={24} height={24} />
{ dmIndicator }
{ e2eIcon }
</div>
</div>
+4 -5
View File
@@ -26,8 +26,7 @@ import PersistedElement from "../elements/PersistedElement";
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
import {ContextMenu} from "../../structures/ContextMenu";
const widgetType = 'm.stickerpicker';
import {WidgetType} from "../../../widgets/WidgetType";
// This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
// We sit in a context menu, so this should be given to the context menu.
@@ -87,7 +86,7 @@ export default class Stickerpicker extends React.Component {
console.log('Removing Stickerpicker widgets');
if (this.state.widgetId) {
if (scalarClient) {
scalarClient.disableWidgetAssets(widgetType, this.state.widgetId).then(() => {
scalarClient.disableWidgetAssets(WidgetType.STICKERPICKER, this.state.widgetId).then(() => {
console.log('Assets disabled');
}).catch((err) => {
console.error('Failed to disable assets');
@@ -364,13 +363,13 @@ export default class Stickerpicker extends React.Component {
if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) {
IntegrationManagers.sharedInstance().openAll(
this.props.room,
`type_${widgetType}`,
`type_${WidgetType.STICKERPICKER.preferred}`,
this.state.widgetId,
);
} else {
IntegrationManagers.sharedInstance().getPrimaryManager().open(
this.props.room,
`type_${widgetType}`,
`type_${WidgetType.STICKERPICKER.preferred}`,
this.state.widgetId,
);
}
@@ -36,7 +36,6 @@ export default class CrossSigningPanel extends React.PureComponent {
userSigningPrivateKeyCached: false,
sessionBackupKeyCached: false,
secretStorageKeyInAccount: false,
secretStorageKeyNeedsUpgrade: null,
};
}
@@ -88,7 +87,6 @@ export default class CrossSigningPanel extends React.PureComponent {
const homeserverSupportsCrossSigning =
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
const crossSigningReady = await cli.isCrossSigningReady();
const secretStorageKeyNeedsUpgrade = await cli.secretStorageKeyNeedsUpgrade();
this.setState({
crossSigningPublicKeysOnDevice,
@@ -100,7 +98,6 @@ export default class CrossSigningPanel extends React.PureComponent {
secretStorageKeyInAccount,
homeserverSupportsCrossSigning,
crossSigningReady,
secretStorageKeyNeedsUpgrade,
});
}
@@ -131,8 +128,8 @@ export default class CrossSigningPanel extends React.PureComponent {
}
_destroySecureSecretStorage = () => {
const ConfirmDestoryCrossSigningDialog = sdk.getComponent("dialogs.ConfirmDestroyCrossSigningDialog");
Modal.createDialog(ConfirmDestoryCrossSigningDialog, {
const ConfirmDestroyCrossSigningDialog = sdk.getComponent("dialogs.ConfirmDestroyCrossSigningDialog");
Modal.createDialog(ConfirmDestroyCrossSigningDialog, {
onFinished: this.onDestroyStorage,
});
}
@@ -150,7 +147,6 @@ export default class CrossSigningPanel extends React.PureComponent {
secretStorageKeyInAccount,
homeserverSupportsCrossSigning,
crossSigningReady,
secretStorageKeyNeedsUpgrade,
} = this.state;
let errorSection;
@@ -259,10 +255,6 @@ export default class CrossSigningPanel extends React.PureComponent {
<td>{_t("Homeserver feature support:")}</td>
<td>{homeserverSupportsCrossSigning ? _t("exists") : _t("not found")}</td>
</tr>
<tr>
<td>{_t("Secret Storage key format:")}</td>
<td>{secretStorageKeyNeedsUpgrade ? _t("outdated") : _t("up to date")}</td>
</tr>
</tbody></table>
</details>
{errorSection}
@@ -163,7 +163,7 @@ export default class EventIndexPanel extends React.Component {
);
eventIndexingSettings = (
<div>
<div className='mx_SettingsTab_subsectionText'>
{
_t( "Riot is missing some components required for securely " +
"caching encrypted messages locally. If you'd like to " +
@@ -180,7 +180,7 @@ export default class EventIndexPanel extends React.Component {
);
} else {
eventIndexingSettings = (
<div>
<div className='mx_SettingsTab_subsectionText'>
{
_t( "Riot can't securely cache encrypted messages locally " +
"while running in a web browser. Use <riotLink>Riot Desktop</riotLink> " +
@@ -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 " +
@@ -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},
};
@@ -38,6 +38,7 @@ import {SERVICE_TYPES} from "matrix-js-sdk";
import IdentityAuthClient from "../../../../../IdentityAuthClient";
import {abbreviateUrl} from "../../../../../utils/UrlUtils";
import { getThreepidsWithBindStatus } from '../../../../../boundThreepids';
import Spinner from "../../../elements/Spinner";
export default class GeneralUserSettingsTab extends React.Component {
static propTypes = {
@@ -60,6 +61,7 @@ export default class GeneralUserSettingsTab extends React.Component {
},
emails: [],
msisdns: [],
loading3pids: true, // whether or not the emails and msisdns have been loaded
...this._calculateThemeState(),
customThemeUrl: "",
customThemeMessage: {isError: false, text: ""},
@@ -158,8 +160,11 @@ export default class GeneralUserSettingsTab extends React.Component {
);
console.warn(e);
}
this.setState({ emails: threepids.filter((a) => a.medium === 'email') });
this.setState({ msisdns: threepids.filter((a) => a.medium === 'msisdn') });
this.setState({
emails: threepids.filter((a) => a.medium === 'email'),
msisdns: threepids.filter((a) => a.medium === 'msisdn'),
loading3pids: false,
});
}
async _checkTerms() {
@@ -325,7 +330,6 @@ export default class GeneralUserSettingsTab extends React.Component {
const ChangePassword = sdk.getComponent("views.settings.ChangePassword");
const EmailAddresses = sdk.getComponent("views.settings.account.EmailAddresses");
const PhoneNumbers = sdk.getComponent("views.settings.account.PhoneNumbers");
const Spinner = sdk.getComponent("views.elements.Spinner");
let passwordChangeForm = (
<ChangePassword
@@ -344,18 +348,24 @@ export default class GeneralUserSettingsTab extends React.Component {
// For newer homeservers with separate 3PID add and bind methods (MSC2290),
// there is no such concern, so we can always show the HS account 3PIDs.
if (this.state.haveIdServer || this.state.serverSupportsSeparateAddAndBind === true) {
threepidSection = <div>
<span className="mx_SettingsTab_subheading">{_t("Email addresses")}</span>
<EmailAddresses
const emails = this.state.loading3pids || true
? <Spinner />
: <EmailAddresses
emails={this.state.emails}
onEmailsChange={this._onEmailsChange}
/>
<span className="mx_SettingsTab_subheading">{_t("Phone numbers")}</span>
<PhoneNumbers
/>;
const msisdns = this.state.loading3pids
? <Spinner />
: <PhoneNumbers
msisdns={this.state.msisdns}
onMsisdnsChange={this._onMsisdnsChange}
/>
/>;
threepidSection = <div>
<span className="mx_SettingsTab_subheading">{_t("Email addresses")}</span>
{emails}
<span className="mx_SettingsTab_subheading">{_t("Phone numbers")}</span>
{msisdns}
</div>;
} else if (this.state.serverSupportsSeparateAddAndBind === null) {
threepidSection = <Spinner />;
@@ -491,12 +501,15 @@ export default class GeneralUserSettingsTab extends React.Component {
const EmailAddresses = sdk.getComponent("views.settings.discovery.EmailAddresses");
const PhoneNumbers = sdk.getComponent("views.settings.discovery.PhoneNumbers");
const emails = this.state.loading3pids ? <Spinner /> : <EmailAddresses emails={this.state.emails} />;
const msisdns = this.state.loading3pids ? <Spinner /> : <PhoneNumbers msisdns={this.state.msisdns} />;
const threepidSection = this.state.haveIdServer ? <div className='mx_GeneralUserSettingsTab_discovery'>
<span className="mx_SettingsTab_subheading">{_t("Email addresses")}</span>
<EmailAddresses emails={this.state.emails} />
{emails}
<span className="mx_SettingsTab_subheading">{_t("Phone numbers")}</span>
<PhoneNumbers msisdns={this.state.msisdns} />
{msisdns}
</div> : null;
return (
@@ -25,11 +25,13 @@ import Analytics from "../../../../../Analytics";
import Modal from "../../../../../Modal";
import * as sdk from "../../../../..";
import {sleep} from "../../../../../utils/promise";
import dis from "../../../../../dispatcher";
export class IgnoredUser extends React.Component {
static propTypes = {
userId: PropTypes.string.isRequired,
onUnignored: PropTypes.func.isRequired,
inProgress: PropTypes.bool.isRequired,
};
_onUnignoreClicked = (e) => {
@@ -40,7 +42,7 @@ export class IgnoredUser extends React.Component {
const id = `mx_SecurityUserSettingsTab_ignoredUser_${this.props.userId}`;
return (
<div className='mx_SecurityUserSettingsTab_ignoredUser'>
<AccessibleButton onClick={this._onUnignoreClicked} kind='primary_sm' aria-describedby={id}>
<AccessibleButton onClick={this._onUnignoreClicked} kind='primary_sm' aria-describedby={id} disabled={this.props.inProgress}>
{ _t('Unignore') }
</AccessibleButton>
<span id={id}>{ this.props.userId }</span>
@@ -50,6 +52,10 @@ export class IgnoredUser extends React.Component {
}
export default class SecurityUserSettingsTab extends React.Component {
static propTypes = {
closeSettingsFn: PropTypes.func.isRequired,
};
constructor() {
super();
@@ -58,9 +64,29 @@ export default class SecurityUserSettingsTab extends React.Component {
this.state = {
ignoredUserIds: MatrixClientPeg.get().getIgnoredUsers(),
waitingUnignored: [],
managingInvites: false,
invitedRoomAmt: invitedRooms.length,
};
this._onAction = this._onAction.bind(this);
}
_onAction({action}) {
if (action === "ignore_state_changed") {
const ignoredUserIds = MatrixClientPeg.get().getIgnoredUsers();
const newWaitingUnignored = this.state.waitingUnignored.filter(e=> ignoredUserIds.includes(e));
this.setState({ignoredUserIds, waitingUnignored: newWaitingUnignored});
}
}
componentDidMount() {
this.dispatcherRef = dis.register(this._onAction);
}
componentWillUnmount() {
dis.unregister(this.dispatcherRef);
}
_updateBlacklistDevicesFlag = (checked) => {
@@ -85,17 +111,24 @@ export default class SecurityUserSettingsTab extends React.Component {
);
};
_onGoToUserProfileClick = () => {
dis.dispatch({
action: 'view_user_info',
userId: MatrixClientPeg.get().getUserId(),
});
this.props.closeSettingsFn();
}
_onUserUnignored = async (userId) => {
// Don't use this.state to get the ignored user list as it might be
// ever so slightly outdated. Instead, prefer to get a fresh list and
// update that.
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
const index = ignoredUsers.indexOf(userId);
const {ignoredUserIds, waitingUnignored} = this.state;
const currentlyIgnoredUserIds = ignoredUserIds.filter(e => !waitingUnignored.includes(e));
const index = currentlyIgnoredUserIds.indexOf(userId);
if (index !== -1) {
ignoredUsers.splice(index, 1);
MatrixClientPeg.get().setIgnoredUsers(ignoredUsers);
currentlyIgnoredUserIds.splice(index, 1);
this.setState(({waitingUnignored}) => ({waitingUnignored: [...waitingUnignored, userId]}));
MatrixClientPeg.get().setIgnoredUsers(currentlyIgnoredUserIds);
}
this.setState({ignoredUsers});
};
_getInvitedRooms = () => {
@@ -201,10 +234,17 @@ export default class SecurityUserSettingsTab extends React.Component {
}
_renderIgnoredUsers() {
if (!this.state.ignoredUserIds || this.state.ignoredUserIds.length === 0) return null;
const {waitingUnignored, ignoredUserIds} = this.state;
const userIds = this.state.ignoredUserIds
.map((u) => <IgnoredUser userId={u} onUnignored={this._onUserUnignored} key={u} />);
if (!ignoredUserIds || ignoredUserIds.length === 0) return null;
const userIds = ignoredUserIds
.map((u) => <IgnoredUser
userId={u}
onUnignored={this._onUserUnignored}
key={u}
inProgress={waitingUnignored.includes(u)}
/>);
return (
<div className='mx_SettingsTab_section'>
@@ -254,15 +294,12 @@ export default class SecurityUserSettingsTab extends React.Component {
</div>
);
let eventIndex;
if (SettingsStore.isFeatureEnabled("feature_event_indexing")) {
eventIndex = (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Message search")}</span>
<EventIndexPanel />
</div>
);
}
const eventIndex = (
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Message search")}</span>
<EventIndexPanel />
</div>
);
// XXX: There's no such panel in the current cross-signing designs, but
// it's useful to have for testing the feature. If there's no interest
@@ -270,7 +307,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>
@@ -287,7 +324,18 @@ export default class SecurityUserSettingsTab extends React.Component {
<div className="mx_SettingsTab mx_SecurityUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Security & Privacy")}</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Sessions")}</span>
<span className="mx_SettingsTab_subheading">{_t("Where youre logged in")}</span>
<span>
{_t(
"Manage the names of and sign out of your sessions below or " +
"<a>verify them in your User Profile</a>.", {},
{
a: sub => <AccessibleButton kind="link" onClick={this._onGoToUserProfileClick}>
{sub}
</AccessibleButton>,
},
)}
</span>
<div className='mx_SettingsTab_subsectionText'>
{_t("A session's public name is visible to people you communicate with")}
<DevicesPanel />
@@ -0,0 +1,56 @@
/*
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.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import dis from "../../../dispatcher";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import DeviceListener from '../../../DeviceListener';
import FormButton from '../elements/FormButton';
import { replaceableComponent } from '../../../utils/replaceableComponent';
@replaceableComponent("views.toasts.BulkUnverifiedSessionsToast")
export default class BulkUnverifiedSessionsToast extends React.PureComponent {
static propTypes = {
deviceIds: PropTypes.array,
}
_onLaterClick = () => {
DeviceListener.sharedInstance().dismissUnverifiedSessions(this.props.deviceIds);
};
_onReviewClick = async () => {
DeviceListener.sharedInstance().dismissUnverifiedSessions(this.props.deviceIds);
dis.dispatch({
action: 'view_user_info',
userId: MatrixClientPeg.get().getUserId(),
});
};
render() {
return (<div>
<div className="mx_Toast_description">
{_t("Verify all your sessions to ensure your account & messages are safe")}
</div>
<div className="mx_Toast_buttons" aria-live="off">
<FormButton label={_t("Later")} kind="danger" onClick={this._onLaterClick} />
<FormButton label={_t("Review")} onClick={this._onReviewClick} />
</div>
</div>);
}
}
@@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import Modal from '../../../Modal';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import * as sdk from "../../../index";
import { _t } from '../../../languageHandler';
import DeviceListener from '../../../DeviceListener';
@@ -31,7 +30,6 @@ export default class SetupEncryptionToast extends React.PureComponent {
'set_up_encryption',
'verify_this_session',
'upgrade_encryption',
'upgrade_ssss',
]).isRequired,
};
@@ -39,24 +37,6 @@ export default class SetupEncryptionToast extends React.PureComponent {
DeviceListener.sharedInstance().dismissEncryptionSetup();
};
async _waitForCompletion() {
if (this.props.kind === 'upgrade_ssss') {
return new Promise(resolve => {
const recheck = async () => {
const needsUpgrade = await MatrixClientPeg.get().secretStorageKeyNeedsUpgrade();
if (!needsUpgrade) {
MatrixClientPeg.get().removeListener('accountData', recheck);
resolve();
}
};
MatrixClientPeg.get().on('accountData', recheck);
recheck();
});
} else {
return;
}
}
_onSetupClick = async () => {
if (this.props.kind === "verify_this_session") {
Modal.createTrackedDialog('Verify session', 'Verify session', SetupEncryptionDialog,
@@ -68,7 +48,6 @@ export default class SetupEncryptionToast extends React.PureComponent {
);
try {
await accessSecretStorage();
await this._waitForCompletion();
} finally {
modal.close();
}
@@ -82,16 +61,14 @@ export default class SetupEncryptionToast extends React.PureComponent {
return _t('Verify yourself & others to keep your chats safe');
case 'verify_this_session':
return _t('Other users may not trust it');
case 'upgrade_ssss':
return _t('Update your secure storage');
}
}
getSetupCaption() {
switch (this.props.kind) {
case 'set_up_encryption':
return _t('Set up');
case 'upgrade_encryption':
case 'upgrade_ssss':
return _t('Upgrade');
case 'verify_this_session':
return _t('Verify');
@@ -17,8 +17,8 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import Modal from "../../../Modal";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import DeviceListener from '../../../DeviceListener';
import NewSessionReviewDialog from '../dialogs/NewSessionReviewDialog';
import FormButton from '../elements/FormButton';
@@ -27,44 +27,39 @@ import { replaceableComponent } from '../../../utils/replaceableComponent';
@replaceableComponent("views.toasts.UnverifiedSessionToast")
export default class UnverifiedSessionToast extends React.PureComponent {
static propTypes = {
toastKey: PropTypes.string.isRequired,
device: PropTypes.object.isRequired,
};
deviceId: PropTypes.string,
}
_onLaterClick = () => {
const { device } = this.props;
DeviceListener.sharedInstance().dismissVerification(device.deviceId);
DeviceListener.sharedInstance().dismissUnverifiedSessions([this.props.deviceId]);
};
_onReviewClick = async () => {
const { device } = this.props;
const cli = MatrixClientPeg.get();
Modal.createTrackedDialog('New Session Review', 'Starting dialog', NewSessionReviewDialog, {
userId: MatrixClientPeg.get().getUserId(),
device,
userId: cli.getUserId(),
device: cli.getStoredDevice(cli.getUserId(), this.props.deviceId),
onFinished: (r) => {
if (!r) {
/* This'll come back false if the user clicks "this wasn't me" and saw a warning dialog */
this._onLaterClick();
DeviceListener.sharedInstance().dismissUnverifiedSessions([this.props.deviceId]);
}
},
}, null, /* priority = */ false, /* static = */ true);
};
render() {
const { device } = this.props;
const cli = MatrixClientPeg.get();
const device = cli.getStoredDevice(cli.getUserId(), this.props.deviceId);
return (<div>
<div className="mx_Toast_description">
<span className="mx_Toast_deviceName">
{device.getDisplayName()}
</span> <span className="mx_Toast_deviceID">
({device.deviceId})
</span>
{_t(
"Verify the new login accessing your account: %(name)s", { name: device.getDisplayName()})}
</div>
<div className="mx_Toast_buttons" aria-live="off">
<FormButton label={_t("Later")} kind="danger" onClick={this._onLaterClick} />
<FormButton label={_t("Review")} onClick={this._onReviewClick} />
<FormButton label={_t("Verify")} onClick={this._onReviewClick} />
</div>
</div>);
}
@@ -51,7 +51,7 @@ export default class VerificationRequestToast extends React.PureComponent {
if (request.isSelfVerification) {
const cli = MatrixClientPeg.get();
this.setState({device: await cli.getStoredDevice(cli.getUserId(), request.channel.deviceId)});
this.setState({device: cli.getStoredDevice(cli.getUserId(), request.channel.deviceId)});
}
}
@@ -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:")}
@@ -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);
@@ -29,6 +30,7 @@ export default class VerificationShowSas extends React.Component {
static propTypes = {
pending: PropTypes.bool,
displayName: PropTypes.string, // required if pending is true
device: PropTypes.object,
onDone: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
sas: PropTypes.object.isRequired,
@@ -44,6 +46,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();
@@ -108,10 +117,16 @@ export default class VerificationShowSas extends React.Component {
let text;
if (this.state.pending) {
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,
});
// device shouldn't be null in this situation but it can be, eg. if the device is
// logged out during verification
if (this.props.device) {
text = _t("Waiting for your other session, %(deviceName)s (%(deviceId)s), to verify…", {
deviceName: this.props.device ? this.props.device.getDisplayName() : '',
deviceId: this.props.device ? this.props.device.deviceId : '',
});
} else {
text = _t("Waiting for your other session to verify…");
}
} else {
const {displayName} = this.props;
text = _t("Waiting for %(displayName)s to verify…", {displayName});