Merge branch 'develop' into t3chguy/password_completion

This commit is contained in:
Michael Telatynski 2020-02-05 20:24:37 +00:00 committed by GitHub
commit 4d0d6cdaa4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
150 changed files with 2181 additions and 4589 deletions

View file

@ -53,6 +53,7 @@ import createRoom from "../../createRoom";
import KeyRequestHandler from '../../KeyRequestHandler';
import { _t, getCurrentLanguage } from '../../languageHandler';
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
import ThemeController from "../../settings/controllers/ThemeController";
import { startAnyRegistrationFlow } from "../../Registration.js";
import { messageForSyncError } from '../../utils/ErrorUtils';
import ResizeNotifier from "../../utils/ResizeNotifier";
@ -506,6 +507,8 @@ export default createReactClass({
view: VIEWS.LOGIN,
});
this.notifyNewScreen('login');
ThemeController.isLogin = true;
this._themeWatcher.recheck();
break;
case 'start_post_registration':
this.setState({
@ -760,6 +763,8 @@ export default createReactClass({
}
this.setStateForNewView(newState);
ThemeController.isLogin = true;
this._themeWatcher.recheck();
this.notifyNewScreen('register');
},
@ -910,6 +915,8 @@ export default createReactClass({
view: VIEWS.WELCOME,
});
this.notifyNewScreen('welcome');
ThemeController.isLogin = true;
this._themeWatcher.recheck();
},
_viewHome: function() {
@ -919,6 +926,8 @@ export default createReactClass({
});
this._setPage(PageTypes.HomePage);
this.notifyNewScreen('home');
ThemeController.isLogin = false;
this._themeWatcher.recheck();
},
_viewUser: function(userId, subAction) {
@ -1231,6 +1240,8 @@ export default createReactClass({
});
this.subTitleStatus = '';
this._setPageSubtitle();
ThemeController.isLogin = true;
this._themeWatcher.recheck();
},
/**
@ -1864,7 +1875,9 @@ export default createReactClass({
try {
masterKeyInStorage = !!await cli.getAccountDataFromServer("m.cross_signing.master");
} catch (e) {
if (e.errcode !== "M_NOT_FOUND") throw e;
if (e.errcode !== "M_NOT_FOUND") {
console.warn("Secret storage account data check failed", e);
}
}
if (masterKeyInStorage) {
@ -1983,6 +1996,7 @@ export default createReactClass({
onLoggedIn={this.onRegisterFlowComplete}
onLoginClick={this.onLoginClick}
onServerConfigChange={this.onServerConfigChange}
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
{...this.getServerProperties()}
/>
);

View file

@ -465,6 +465,12 @@ export default class MessagePanel extends React.Component {
}
return false;
};
// events that we include in the group but then eject out and place
// above the group.
const shouldEject = (ev) => {
if (ev.getType() === "m.room.encryption") return true;
return false;
};
if (mxEv.getType() === "m.room.create") {
let summaryReadMarker = null;
const ts1 = mxEv.getTs();
@ -484,6 +490,7 @@ export default class MessagePanel extends React.Component {
}
const summarisedEvents = []; // Don't add m.room.create here as we don't want it inside the summary
const ejectedEvents = [];
for (;i + 1 < this.props.events.length; i++) {
const collapsedMxEv = this.props.events[i + 1];
@ -501,7 +508,11 @@ export default class MessagePanel extends React.Component {
// If RM event is in the summary, mark it as such and the RM will be appended after the summary.
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
summarisedEvents.push(collapsedMxEv);
if (shouldEject(collapsedMxEv)) {
ejectedEvents.push(collapsedMxEv);
} else {
summarisedEvents.push(collapsedMxEv);
}
}
// At this point, i = the index of the last event in the summary sequence
@ -513,6 +524,10 @@ export default class MessagePanel extends React.Component {
return this._getTilesForEvent(e, e, e === lastShownEvent);
}).reduce((a, b) => a.concat(b), []);
for (const ejected of ejectedEvents) {
ret.push(...this._getTilesForEvent(mxEv, ejected, last));
}
// Get sender profile from the latest event in the summary as the m.room.create doesn't contain one
const ev = this.props.events[i];
ret.push(<EventListSummary

View file

@ -48,6 +48,7 @@ export default class RightPanel extends React.Component {
phase: this._getPhaseFromProps(),
isUserPrivilegedInGroup: null,
member: this._getUserForPanel(),
verificationRequest: RightPanelStore.getSharedInstance().roomPanelPhaseParams.verificationRequest,
};
this.onAction = this.onAction.bind(this);
this.onRoomStateMember = this.onRoomStateMember.bind(this);
@ -68,15 +69,34 @@ export default class RightPanel extends React.Component {
return this.props.user || lastParams['member'];
}
// gets the current phase from the props and also maybe the store
_getPhaseFromProps() {
const rps = RightPanelStore.getSharedInstance();
const userForPanel = this._getUserForPanel();
if (this.props.groupId) {
if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) {
dis.dispatch({action: "set_right_panel_phase", phase: RIGHT_PANEL_PHASES.GroupMemberList});
return RIGHT_PANEL_PHASES.GroupMemberList;
}
return rps.groupPanelPhase;
} else if (this._getUserForPanel()) {
} else if (userForPanel) {
// XXX FIXME AAAAAARGH: What is going on with this class!? It takes some of its state
// from its props and some from a store, except if the contents of the store changes
// while it's mounted in which case it replaces all of its state with that of the store,
// except it uses a dispatch instead of a normal store listener?
// Unfortunately rewriting this would almost certainly break showing the right panel
// in some of the many cases, and I don't have time to re-architect it and test all
// the flows now, so adding yet another special case so if the store thinks there is
// a verification going on for the member we're displaying, we show that, otherwise
// we race if a verification is started while the panel isn't displayed because we're
// not mounted in time to get the dispatch.
// Until then, let this code serve as a warning from history.
if (
userForPanel.userId === rps.roomPanelPhaseParams.member.userId &&
rps.roomPanelPhaseParams.verificationRequest
) {
return rps.roomPanelPhase;
}
return RIGHT_PANEL_PHASES.RoomMemberInfo;
} else {
if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.roomPanelPhase)) {

View file

@ -155,7 +155,7 @@ export default createReactClass({
this.nextBatch = data.next_batch;
this.setState((s) => {
s.publicRooms.push(...data.chunk);
s.publicRooms.push(...(data.chunk || []));
s.loading = false;
return s;
});

View file

@ -220,12 +220,12 @@ export default createReactClass({
});
if (hasUDE) {
title = _t("Message not sent due to unknown devices being present");
title = _t("Message not sent due to unknown sessions being present");
content = _t(
"<showDevicesText>Show devices</showDevicesText>, <sendAnywayText>send anyway</sendAnywayText> or <cancelText>cancel</cancelText>.",
"<showSessionsText>Show sessions</showSessionsText>, <sendAnywayText>send anyway</sendAnywayText> or <cancelText>cancel</cancelText>.",
{},
{
'showDevicesText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onShowDevicesClick}>{ sub }</a>,
'showSessionsText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onShowDevicesClick}>{ sub }</a>,
'sendAnywayText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="sendAnyway" onClick={this._onSendWithoutVerifyingClick}>{ sub }</a>,
'cancelText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
},

View file

@ -811,7 +811,9 @@ export default createReactClass({
debuglog("e2e verified", verified, "unverified", unverified);
/* Check all verified user devices. */
for (const userId of [...verified, cli.getUserId()]) {
/* Don't alarm if no other users are verified */
const targets = (verified.length > 0) ? [...verified, cli.getUserId()] : verified;
for (const userId of targets) {
const devices = await cli.getStoredDevicesForUser(userId);
const anyDeviceNotVerified = devices.some(({deviceId}) => {
return !cli.checkDeviceTrust(userId, deviceId).isVerified();
@ -820,7 +822,7 @@ export default createReactClass({
this.setState({
e2eStatus: "warning",
});
debuglog("e2e status set to warning as not all users trust all of their devices." +
debuglog("e2e status set to warning as not all users trust all of their sessions." +
" Aborted on user", userId);
return;
}

View file

@ -1,5 +1,6 @@
/*
Copyright 2017, 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.
@ -64,8 +65,8 @@ const TagPanel = createReactClass({
this.unmounted = true;
this.context.removeListener("Group.myMembership", this._onGroupMyMembership);
this.context.removeListener("sync", this._onClientSync);
if (this._filterStoreToken) {
this._filterStoreToken.remove();
if (this._tagOrderStoreToken) {
this._tagOrderStoreToken.remove();
}
},

View file

@ -1171,28 +1171,40 @@ const TimelinePanel = createReactClass({
// get the user's membership at the last event by getting the timeline
// that the event belongs to, and traversing the timeline looking for
// that event, while keeping track of the user's membership
const lastEvent = events[events.length - 1];
const timeline = room.getTimelineForEvent(lastEvent.getId());
const userMembershipEvent =
timeline.getState(EventTimeline.FORWARDS).getMember(userId);
let userMembership = userMembershipEvent
? userMembershipEvent.membership : "leave";
const timelineEvents = timeline.getEvents();
for (let i = timelineEvents.length - 1; i >= 0; i--) {
const event = timelineEvents[i];
if (event.getId() === lastEvent.getId()) {
// found the last event, so we can stop looking through the timeline
break;
} else if (event.getStateKey() === userId
&& event.getType() === "m.room.member") {
const prevContent = event.getPrevContent();
userMembership = prevContent.membership || "leave";
let i;
let userMembership = "leave";
for (i = events.length - 1; i >= 0; i--) {
const timeline = room.getTimelineForEvent(events[i].getId());
if (!timeline) {
// Somehow, it seems to be possible for live events to not have
// a timeline, even though that should not happen. :(
// https://github.com/vector-im/riot-web/issues/12120
console.warn(
`Event ${events[i].getId()} in room ${room.roomId} is live, ` +
`but it does not have a timeline`,
);
continue;
}
const userMembershipEvent =
timeline.getState(EventTimeline.FORWARDS).getMember(userId);
userMembership = userMembershipEvent ? userMembershipEvent.membership : "leave";
const timelineEvents = timeline.getEvents();
for (let j = timelineEvents.length - 1; j >= 0; j--) {
const event = timelineEvents[j];
if (event.getId() === events[i].getId()) {
break;
} else if (event.getStateKey() === userId
&& event.getType() === "m.room.member") {
const prevContent = event.getPrevContent();
userMembership = prevContent.membership || "leave";
}
}
break;
}
// now go through the events that we have and find the first undecryptable
// now go through the rest of the events and find the first undecryptable
// one that was sent when the user wasn't in the room
for (let i = events.length - 1; i >= 0; i--) {
for (; i >= 0; i--) {
const event = events[i];
if (event.getStateKey() === userId
&& event.getType() === "m.room.member") {

View file

@ -19,7 +19,7 @@ import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { accessSecretStorage } from '../../../CrossSigningManager';
import { accessSecretStorage, AccessCancelledError } from '../../../CrossSigningManager';
const PHASE_INTRO = 0;
const PHASE_BUSY = 1;
@ -73,6 +73,9 @@ export default class CompleteSecurity extends React.Component {
});
}
} catch (e) {
if (!(e instanceof AccessCancelledError)) {
console.log(e);
}
// this will throw if the user hits cancel, so ignore
this.setState({
phase: PHASE_INTRO,
@ -197,7 +200,7 @@ export default class CompleteSecurity extends React.Component {
body = (
<div>
<p>{_t(
"Without completing security on this device, it wont have " +
"Without completing security on this session, it wont have " +
"access to encrypted messages.",
)}</p>
<div className="mx_CompleteSecurity_actionRow">
@ -220,7 +223,7 @@ export default class CompleteSecurity extends React.Component {
} else if (phase === PHASE_BUSY) {
const Spinner = sdk.getComponent('views.elements.Spinner');
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
title = '';
title = _t("Complete security");
body = <Spinner />;
} else {
throw new Error(`Unknown phase ${phase}`);

View file

@ -152,8 +152,8 @@ export default createReactClass({
<div>
{ _t(
"Changing your password will reset any end-to-end encryption keys " +
"on all of your devices, making encrypted chat history unreadable. Set up " +
"Key Backup or export your room keys from another device before resetting your " +
"on all of your sessions, making encrypted chat history unreadable. Set up " +
"Key Backup or export your room keys from another session before resetting your " +
"password.",
) }
</div>,
@ -358,7 +358,7 @@ export default createReactClass({
return <div>
<p>{_t("Your password has been reset.")}</p>
<p>{_t(
"You have been logged out of all devices and will no longer receive " +
"You have been logged out of all sessions and will no longer receive " +
"push notifications. To re-enable notifications, sign in again on each " +
"device.",
)}</p>

View file

@ -2,7 +2,7 @@
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018, 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -62,6 +62,7 @@ export default createReactClass({
// registration shouldn't know or care how login is done.
onLoginClick: PropTypes.func.isRequired,
onServerConfigChange: PropTypes.func.isRequired,
defaultDeviceDisplayName: PropTypes.string,
},
getInitialState: function() {
@ -432,15 +433,14 @@ export default createReactClass({
// session).
if (!this.state.formVals.password) inhibitLogin = null;
return this.state.matrixClient.register(
this.state.formVals.username,
this.state.formVals.password,
undefined, // session id: included in the auth dict already
auth,
null,
null,
inhibitLogin,
);
const registerParams = {
username: this.state.formVals.username,
password: this.state.formVals.password,
initial_device_display_name: this.props.defaultDeviceDisplayName,
};
if (auth) registerParams.auth = auth;
if (inhibitLogin !== undefined && inhibitLogin !== null) registerParams.inhibitLogin = inhibitLogin;
return this.state.matrixClient.registerRequest(registerParams);
},
_getUIAuthInputs: function() {

View file

@ -83,7 +83,7 @@ export default class SoftLogout extends React.Component {
onFinished: (wipeData) => {
if (!wipeData) return;
console.log("Clearing data from soft-logged-out device");
console.log("Clearing data from soft-logged-out session");
Lifecycle.logout();
},
});
@ -212,8 +212,8 @@ export default class SoftLogout extends React.Component {
let introText = null; // null is translated to something area specific in this function
if (this.state.keyBackupNeeded) {
introText = _t(
"Regain access to your account and recover encryption keys stored on this device. " +
"Without them, you wont be able to read all of your secure messages on any device.");
"Regain access to your account and recover encryption keys stored in this session. " +
"Without them, you wont be able to read all of your secure messages in any session.");
}
if (this.state.loginView === LOGIN_VIEW.PASSWORD) {
@ -306,7 +306,7 @@ export default class SoftLogout extends React.Component {
<p>
{_t(
"Warning: Your personal data (including encryption keys) is still stored " +
"on this device. Clear it if you're finished using this device, or want to sign " +
"in this session. Clear it if you're finished using this session, or want to sign " +
"in to another account.",
)}
</p>

View file

@ -142,7 +142,7 @@ export const PasswordAuthEntry = createReactClass({
return (
<div>
<p>{ _t("To continue, please enter your password.") }</p>
<p>{ _t("Confirm your identity by entering your account password below.") }</p>
<form onSubmit={this._onSubmit} className="mx_InteractiveAuthEntryComponents_passwordSection">
<Field
id="mx_InteractiveAuthEntryComponents_password"

View file

@ -202,6 +202,7 @@ export default class PasswordLogin extends React.Component {
value={this.state.username}
onChange={this.onUsernameChanged}
onBlur={this.onUsernameBlur}
disabled={this.props.disableSubmit}
autoFocus
/>;
case PasswordLogin.LOGIN_FIELD_MXID:
@ -216,6 +217,7 @@ export default class PasswordLogin extends React.Component {
value={this.state.username}
onChange={this.onUsernameChanged}
onBlur={this.onUsernameBlur}
disabled={this.props.disableSubmit}
autoFocus
/>;
case PasswordLogin.LOGIN_FIELD_PHONE: {
@ -240,6 +242,7 @@ export default class PasswordLogin extends React.Component {
prefix={phoneCountry}
onChange={this.onPhoneNumberChanged}
onBlur={this.onPhoneNumberBlur}
disabled={this.props.disableSubmit}
autoFocus
/>;
}
@ -291,6 +294,7 @@ export default class PasswordLogin extends React.Component {
element="select"
value={this.state.loginType}
onChange={this.onLoginTypeChange}
disabled={this.props.disableSubmit}
>
<option
key={PasswordLogin.LOGIN_FIELD_MXID}
@ -330,6 +334,7 @@ export default class PasswordLogin extends React.Component {
label={_t('Password')}
value={this.state.password}
onChange={this.onPasswordChanged}
disabled={this.props.disableSubmit}
/>
{forgotPasswordJsx}
<input className="mx_Login_submit"

View file

@ -39,11 +39,11 @@ export default class ConfirmWipeDeviceDialog extends React.Component {
return (
<BaseDialog className='mx_ConfirmWipeDeviceDialog' hasCancel={true}
onFinished={this.props.onFinished}
title={_t("Clear all data on this device?")}>
title={_t("Clear all data in this session?")}>
<div className='mx_ConfirmWipeDeviceDialog_content'>
<p>
{_t(
"Clearing all data from this device is permanent. Encrypted messages will be lost " +
"Clearing all data from this session is permanent. Encrypted messages will be lost " +
"unless their keys have been backed up.",
)}
</p>

View file

@ -172,7 +172,7 @@ export default class DeviceVerifyDialog extends React.Component {
const BaseDialog = sdk.getComponent("dialogs.BaseDialog");
return (
<BaseDialog
title={_t("Verify device")}
title={_t("Verify session")}
onFinished={this._onCancelClick}
>
{body}
@ -194,10 +194,7 @@ export default class DeviceVerifyDialog extends React.Component {
{ _t("Verify by comparing a short text string.") }
</p>
<p>
{_t(
"For maximum security, we recommend you do this in person or " +
"use another trusted means of communication.",
)}
{_t("To be secure, do this in person or use a trusted way to communicate.")}
</p>
<DialogButtons
primaryButton={_t('Begin Verifying')}
@ -236,6 +233,7 @@ export default class DeviceVerifyDialog extends React.Component {
sas={this._showSasEvent.sas}
onCancel={this._onCancelClick}
onDone={this._onSasMatchesClick}
isSelf={MatrixClientPeg.get().getUserId() === this.props.userId}
/>;
}
@ -265,12 +263,12 @@ export default class DeviceVerifyDialog extends React.Component {
let text;
if (MatrixClientPeg.get().getUserId() === this.props.userId) {
text = _t("To verify that this device can be trusted, please check that the key you see " +
text = _t("To verify that this session can be trusted, please check that the key you see " +
"in User Settings on that device matches the key below:");
} else {
text = _t("To verify that this device can be trusted, please contact its owner using some other " +
text = _t("To verify that this session can be trusted, please contact its owner using some other " +
"means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings " +
"for this device matches the key below:");
"for this session matches the key below:");
}
const key = FormattingUtils.formatCryptoKey(this.props.device.getFingerprint());
@ -286,14 +284,14 @@ export default class DeviceVerifyDialog extends React.Component {
</p>
<div className="mx_DeviceVerifyDialog_cryptoSection">
<ul>
<li><label>{ _t("Device name") }:</label> <span>{ this.props.device.getDisplayName() }</span></li>
<li><label>{ _t("Device ID") }:</label> <span><code>{ this.props.device.deviceId }</code></span></li>
<li><label>{ _t("Device key") }:</label> <span><code><b>{ key }</b></code></span></li>
<li><label>{ _t("Session name") }:</label> <span>{ this.props.device.getDisplayName() }</span></li>
<li><label>{ _t("Session ID") }:</label> <span><code>{ this.props.device.deviceId }</code></span></li>
<li><label>{ _t("Session key") }:</label> <span><code><b>{ key }</b></code></span></li>
</ul>
</div>
<p>
{ _t("If it matches, press the verify button below. " +
"If it doesn't, then someone else is intercepting this device " +
"If it doesn't, then someone else is intercepting this session " +
"and you probably want to press the blacklist button instead.") }
</p>
</div>
@ -301,7 +299,7 @@ export default class DeviceVerifyDialog extends React.Component {
return (
<QuestionDialog
title={_t("Verify device")}
title={_t("Verify session")}
description={body}
button={_t("I verify that the keys match")}
onFinished={this._onLegacyFinished}
@ -321,7 +319,7 @@ export default class DeviceVerifyDialog extends React.Component {
}
async function ensureDMExistsAndOpen(userId) {
const roomId = ensureDMExists(MatrixClientPeg.get(), userId);
const roomId = await ensureDMExists(MatrixClientPeg.get(), userId);
// don't use andView and spinner in createRoom, together, they cause this dialog to close and reopen,
// we causes us to loose the verifier and restart, and we end up having two verification requests
dis.dispatch({

View file

@ -121,6 +121,8 @@ export default class IncomingSasDialog extends React.Component {
const Spinner = sdk.getComponent("views.elements.Spinner");
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
const isSelf = this.props.verifier.userId == MatrixClientPeg.get().getUserId();
let profile;
if (this.state.opponentProfile) {
profile = <div className="mx_IncomingSasDialog_opponentProfile">
@ -148,20 +150,36 @@ export default class IncomingSasDialog extends React.Component {
profile = <Spinner />;
}
const userDetailText = [
<p key="p1">{_t(
"Verify this user to mark them as trusted. " +
"Trusting users gives you extra peace of mind when using " +
"end-to-end encrypted messages.",
)}</p>,
<p key="p2">{_t(
// NB. Below wording adjusted to singular 'session' until we have
// cross-signing
"Verifying this user will mark their session as trusted, and " +
"also mark your session as trusted to them.",
)}</p>,
];
const selfDetailText = [
<p key="p1">{_t(
"Verify this device to mark it as trusted. " +
"Trusting this device gives you and other users extra peace of mind when using " +
"end-to-end encrypted messages.",
)}</p>,
<p key="p2">{_t(
"Verifying this device will mark it as trusted, and users who have verified with " +
"you will trust this device.",
)}</p>,
];
return (
<div>
{profile}
<p>{_t(
"Verify this user to mark them as trusted. " +
"Trusting users gives you extra peace of mind when using " +
"end-to-end encrypted messages.",
)}</p>
<p>{_t(
// NB. Below wording adjusted to singular 'device' until we have
// cross-signing
"Verifying this user will mark their device as trusted, and " +
"also mark your device as trusted to them.",
)}</p>
{isSelf ? selfDetailText : userDetailText}
<DialogButtons
primaryButton={_t('Continue')}
hasCancel={true}
@ -178,6 +196,7 @@ export default class IncomingSasDialog extends React.Component {
sas={this._showSasEvent.sas}
onCancel={this._onCancelClick}
onDone={this._onSasMatchesClick}
isSelf={this.props.verifier.userId == MatrixClientPeg.get().getUserId()}
/>;
}
@ -227,6 +246,7 @@ export default class IncomingSasDialog extends React.Component {
<BaseDialog
title={_t("Incoming Verification Request")}
onFinished={this._onFinished}
fixedWidth={false}
>
{body}
</BaseDialog>

View file

@ -301,18 +301,16 @@ export default class InviteDialog extends React.PureComponent {
throw new Error("When using KIND_INVITE a roomId is required for an InviteDialog");
}
let alreadyInvited = [];
const alreadyInvited = new Set([MatrixClientPeg.get().getUserId(), SdkConfig.get()['welcomeUserId']]);
if (props.roomId) {
const room = MatrixClientPeg.get().getRoom(props.roomId);
if (!room) throw new Error("Room ID given to InviteDialog does not look like a room");
alreadyInvited = [
...room.getMembersWithMembership('invite'),
...room.getMembersWithMembership('join'),
...room.getMembersWithMembership('ban'), // so we don't try to invite them
].map(m => m.userId);
room.getMembersWithMembership('invite').forEach(m => alreadyInvited.add(m.userId));
room.getMembersWithMembership('join').forEach(m => alreadyInvited.add(m.userId));
// add banned users, so we don't try to invite them
room.getMembersWithMembership('ban').forEach(m => alreadyInvited.add(m.userId));
}
this.state = {
targets: [], // array of Member objects (see interface above)
filterText: "",
@ -333,12 +331,12 @@ export default class InviteDialog extends React.PureComponent {
this._editorRef = createRef();
}
_buildRecents(excludedTargetIds: string[]): {userId: string, user: RoomMember, lastActive: number} {
_buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number} {
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals();
const recents = [];
for (const userId in rooms) {
// Filter out user IDs that are already in the room / should be excluded
if (excludedTargetIds.includes(userId)) {
if (excludedTargetIds.has(userId)) {
console.warn(`[Invite:Recents] Excluding ${userId} from recents`);
continue;
}
@ -381,13 +379,10 @@ export default class InviteDialog extends React.PureComponent {
return recents;
}
_buildSuggestions(excludedTargetIds: string[]): {userId: string, user: RoomMember} {
_buildSuggestions(excludedTargetIds: Set<string>): {userId: string, user: RoomMember} {
const maxConsideredMembers = 200;
const client = MatrixClientPeg.get();
const excludedUserIds = [client.getUserId(), SdkConfig.get()['welcomeUserId']];
const joinedRooms = client.getRooms()
.filter(r => r.getMyMembership() === 'join')
.filter(r => r.getJoinedMemberCount() <= maxConsideredMembers);
const joinedRooms = MatrixClientPeg.get().getRooms()
.filter(r => r.getMyMembership() === 'join' && r.getJoinedMemberCount() <= maxConsideredMembers);
// Generates { userId: {member, rooms[]} }
const memberRooms = joinedRooms.reduce((members, room) => {
@ -396,10 +391,10 @@ export default class InviteDialog extends React.PureComponent {
return members; // Do nothing
}
const joinedMembers = room.getJoinedMembers().filter(u => !excludedUserIds.includes(u.userId));
const joinedMembers = room.getJoinedMembers().filter(u => !excludedTargetIds.has(u.userId));
for (const member of joinedMembers) {
// Filter out user IDs that are already in the room / should be excluded
if (excludedTargetIds.includes(member.userId)) {
if (excludedTargetIds.has(member.userId)) {
continue;
}
@ -440,7 +435,7 @@ export default class InviteDialog extends React.PureComponent {
// room to see who has sent a message in the last few hours, and giving them a score
// which correlates to the freshness of their message. In theory, this results in suggestions
// which are closer to "continue this conversation" rather than "this person exists".
const trueJoinedRooms = client.getRooms().filter(r => r.getMyMembership() === 'join');
const trueJoinedRooms = MatrixClientPeg.get().getRooms().filter(r => r.getMyMembership() === 'join');
const now = (new Date()).getTime();
const earliestAgeConsidered = now - (60 * 60 * 1000); // 1 hour ago
const maxMessagesConsidered = 50; // so we don't iterate over a huge amount of traffic
@ -456,7 +451,7 @@ export default class InviteDialog extends React.PureComponent {
const events = room.getLiveTimeline().getEvents(); // timelines are most recent last
for (let i = events.length - 1; i >= Math.max(0, events.length - maxMessagesConsidered); i--) {
const ev = events[i];
if (excludedUserIds.includes(ev.getSender())) {
if (excludedTargetIds.has(ev.getSender())) {
continue;
}
if (ev.getTs() <= earliestAgeConsidered) {

View file

@ -60,7 +60,7 @@ export default createReactClass({
const deviceInfo = r[userId][deviceId];
if (!deviceInfo) {
console.warn(`No details found for device ${userId}:${deviceId}`);
console.warn(`No details found for session ${userId}:${deviceId}`);
this.props.onFinished(false);
return;
@ -121,10 +121,10 @@ export default createReactClass({
let text;
if (this.state.wasNewDevice) {
text = _td("You added a new device '%(displayName)s', which is"
text = _td("You added a new session '%(displayName)s', which is"
+ " requesting encryption keys.");
} else {
text = _td("Your unverified device '%(displayName)s' is requesting"
text = _td("Your unverified session '%(displayName)s' is requesting"
+ " encryption keys.");
}
text = _t(text, {displayName: displayName});
@ -159,7 +159,7 @@ export default createReactClass({
} else {
content = (
<div id='mx_Dialog_content'>
<p>{ _t('Loading device info...') }</p>
<p>{ _t('Loading session info...') }</p>
<Spinner />
</div>
);

View file

@ -138,7 +138,7 @@ export default class LogoutDialog extends React.Component {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let setupButtonCaption;
if (this.state.backupInfo) {
setupButtonCaption = _t("Connect this device to Key Backup");
setupButtonCaption = _t("Connect this session to Key Backup");
} else {
// if there's an error fetching the backup info, we'll just assume there's
// no backup for the purpose of the button caption

View file

@ -114,7 +114,7 @@ export default createReactClass({
>
<div className="mx_Dialog_content">
<p>
{ _t('This will allow you to return to your account after signing out, and sign in on other devices.') }
{ _t('This will allow you to return to your account after signing out, and sign in on other sessions.') }
</p>
<ChangePassword
className="mx_SetPasswordDialog_change_password"

View file

@ -132,8 +132,8 @@ export default createReactClass({
if (SettingsStore.getValue("blacklistUnverifiedDevices", this.props.room.roomId)) {
warning = (
<h4>
{ _t("You are currently blacklisting unverified devices; to send " +
"messages to these devices you must verify them.") }
{ _t("You are currently blacklisting unverified sessions; to send " +
"messages to these sessions you must verify them.") }
</h4>
);
} else {
@ -141,7 +141,7 @@ export default createReactClass({
<div>
<p>
{ _t("We recommend you go through the verification process " +
"for each device to confirm they belong to their legitimate owner, " +
"for each session to confirm they belong to their legitimate owner, " +
"but you can resend the message without verifying if you prefer.") }
</p>
</div>
@ -165,15 +165,15 @@ export default createReactClass({
return (
<BaseDialog className='mx_UnknownDeviceDialog'
onFinished={this.props.onFinished}
title={_t('Room contains unknown devices')}
title={_t('Room contains unknown sessions')}
contentId='mx_Dialog_content'
>
<GeminiScrollbarWrapper autoshow={false} className="mx_Dialog_content" id='mx_Dialog_content'>
<h4>
{ _t('"%(RoomName)s" contains devices that you haven\'t seen before.', {RoomName: this.props.room.name}) }
{ _t('"%(RoomName)s" contains sessions that you haven\'t seen before.', {RoomName: this.props.room.name}) }
</h4>
{ warning }
{ _t("Unknown devices") }:
{ _t("Unknown sessions") }:
<UnknownDeviceList devices={this.props.devices} />
</GeminiScrollbarWrapper>

View file

@ -237,7 +237,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
} else if (this.state.restoreError) {
if (this.state.restoreError.errcode === MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY) {
if (this.state.restoreType === RESTORE_TYPE_RECOVERYKEY) {
title = _t("Recovery Key Mismatch");
title = _t("Recovery key mismatch");
content = <div>
<p>{_t(
"Backup could not be decrypted with this key: " +
@ -245,7 +245,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
)}</p>
</div>;
} else {
title = _t("Incorrect Recovery Passphrase");
title = _t("Incorrect recovery passphrase");
content = <div>
<p>{_t(
"Backup could not be decrypted with this passphrase: " +
@ -262,7 +262,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("Backup restored");
let failedToDecrypt;
if (this.state.recoverInfo.total > this.state.recoverInfo.imported) {
failedToDecrypt = <p>{_t(
@ -282,7 +282,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
} else if (backupHasPassphrase && !this.state.forceRecoveryKey) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
title = _t("Enter Recovery Passphrase");
title = _t("Enter recovery passphrase");
content = <div>
<p>{_t(
"<b>Warning</b>: you should only set up key backup " +
@ -330,7 +330,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
})}
</div>;
} else {
title = _t("Enter Recovery Key");
title = _t("Enter recovery key");
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');

View file

@ -137,7 +137,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
)}</p>
<p>{_t(
"Access your secure message history and your cross-signing " +
"identity for verifying other devices by entering your passphrase.",
"identity for verifying other sessions by entering your passphrase.",
)}</p>
<form className="mx_AccessSecretStorageDialog_primaryContainer">
@ -212,7 +212,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
)}</p>
<p>{_t(
"Access your secure message history and your cross-signing " +
"identity for verifying other devices by entering your recovery key.",
"identity for verifying other sessions by entering your recovery key.",
)}</p>
<form className="mx_AccessSecretStorageDialog_primaryContainer">

View file

@ -0,0 +1,58 @@
/*
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 { MatrixClientPeg } from '../../../MatrixClientPeg';
export default class EncryptionEvent extends React.Component {
render() {
const {mxEvent} = this.props;
let body;
let classes = "mx_EventTile_bubble mx_cryptoEvent mx_cryptoEvent_icon";
if (
mxEvent.getContent().algorithm === 'm.megolm.v1.aes-sha2' &&
MatrixClientPeg.get().isRoomEncrypted(mxEvent.getRoomId())
) {
body = <div>
<div className="mx_cryptoEvent_title">{_t("Encryption enabled")}</div>
<div className="mx_cryptoEvent_subtitle">
{_t(
"Messages in this room are end-to-end encrypted. " +
"Learn more & verify this user in their user profile.",
)}
</div>
</div>;
} else {
body = <div>
<div className="mx_cryptoEvent_title">{_t("Encryption not enabled")}</div>
<div className="mx_cryptoEvent_subtitle">{_t("The encryption used by this room isn't supported.")}</div>
</div>;
classes += " mx_cryptoEvent_icon_warning";
}
return (<div className={classes}>
{body}
</div>);
}
}
EncryptionEvent.propTypes = {
/* the MatrixEvent to show */
mxEvent: PropTypes.object.isRequired,
};

View file

@ -1,5 +1,5 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -32,6 +32,7 @@ export default class MKeyVerificationConclusion extends React.Component {
if (request) {
request.on("change", this._onRequestChanged);
}
MatrixClientPeg.get().on("userTrustStatusChanged", this._onTrustChanged);
}
componentWillUnmount() {
@ -39,12 +40,25 @@ export default class MKeyVerificationConclusion extends React.Component {
if (request) {
request.off("change", this._onRequestChanged);
}
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("userTrustStatusChanged", this._onTrustChanged);
}
}
_onRequestChanged = () => {
this.forceUpdate();
};
_onTrustChanged = (userId, status) => {
const { mxEvent } = this.props;
const request = mxEvent.verificationRequest;
if (!request || request.otherUserId !== userId) {
return;
}
this.forceUpdate();
};
_shouldRender(mxEvent, request) {
// normally should not happen
if (!request) {
@ -63,6 +77,14 @@ export default class MKeyVerificationConclusion extends React.Component {
if (request.pending) {
return false;
}
// User isn't actually verified
if (!MatrixClientPeg.get()
.checkUserTrust(request.otherUserId)
.isCrossSigningVerified()) {
return false;
}
return true;
}
@ -94,12 +116,12 @@ export default class MKeyVerificationConclusion extends React.Component {
if (title) {
const subtitle = userLabelForEventRoom(request.otherUserId, mxEvent.getRoomId());
const classes = classNames("mx_EventTile_bubble", "mx_KeyVerification", "mx_KeyVerification_icon", {
mx_KeyVerification_icon_verified: request.done,
const classes = classNames("mx_EventTile_bubble", "mx_cryptoEvent", "mx_cryptoEvent_icon", {
mx_cryptoEvent_icon_verified: request.done,
});
return (<div className={classes}>
<div className="mx_KeyVerification_title">{title}</div>
<div className="mx_KeyVerification_subtitle">{subtitle}</div>
<div className="mx_cryptoEvent_title">{title}</div>
<div className="mx_cryptoEvent_subtitle">{subtitle}</div>
</div>);
}

View file

@ -1,5 +1,5 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -125,30 +125,30 @@ export default class MKeyVerificationRequest extends React.Component {
} else {
stateLabel = this._cancelledLabel(request.cancellingUserId);
}
stateNode = (<div className="mx_KeyVerification_state">{stateLabel}</div>);
stateNode = (<div className="mx_cryptoEvent_state">{stateLabel}</div>);
}
if (!request.initiatedByMe) {
const name = getNameForEventRoom(request.requestingUserId, mxEvent.getRoomId());
title = (<div className="mx_KeyVerification_title">{
title = (<div className="mx_cryptoEvent_title">{
_t("%(name)s wants to verify", {name})}</div>);
subtitle = (<div className="mx_KeyVerification_subtitle">{
subtitle = (<div className="mx_cryptoEvent_subtitle">{
userLabelForEventRoom(request.requestingUserId, mxEvent.getRoomId())}</div>);
if (request.requested && !request.observeOnly) {
stateNode = (<div className="mx_KeyVerification_buttons">
stateNode = (<div className="mx_cryptoEvent_buttons">
<FormButton kind="danger" onClick={this._onRejectClicked} label={_t("Decline")} />
<FormButton onClick={this._onAcceptClicked} label={_t("Accept")} />
</div>);
}
} else { // request sent by us
title = (<div className="mx_KeyVerification_title">{
title = (<div className="mx_cryptoEvent_title">{
_t("You sent a verification request")}</div>);
subtitle = (<div className="mx_KeyVerification_subtitle">{
subtitle = (<div className="mx_cryptoEvent_subtitle">{
userLabelForEventRoom(request.receivingUserId, mxEvent.getRoomId())}</div>);
}
if (title) {
return (<div className="mx_EventTile_bubble mx_KeyVerification mx_KeyVerification_icon">
return (<div className="mx_EventTile_bubble mx_cryptoEvent mx_cryptoEvent_icon">
{title}
{subtitle}
{stateNode}

View file

@ -32,6 +32,13 @@ export default class ViewSourceEvent extends React.PureComponent {
};
}
componentDidMount() {
const {mxEvent} = this.props;
if (mxEvent.isBeingDecrypted()) {
mxEvent.once("Event.decrypted", () => this.forceUpdate());
}
}
onToggle = (ev) => {
ev.preventDefault();
const { expanded } = this.state;

View file

@ -38,7 +38,7 @@ const EncryptionInfo = ({pending, member, onStartVerification}) => {
} else {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
content = (
<AccessibleButton kind="primary" className="mx_UserInfo_verify" onClick={onStartVerification}>
<AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={onStartVerification}>
{_t("Start Verification")}
</AccessibleButton>
);
@ -56,7 +56,7 @@ const EncryptionInfo = ({pending, member, onStartVerification}) => {
<h3>{_t("Verify User")}</h3>
<div>
<p>{_t("For extra security, verify this user by checking a one-time code on both of your devices.")}</p>
<p>{_t("For maximum security, do this in person.")}</p>
<p>{_t("To be secure, do this in person or use a trusted way to communicate.")}</p>
{ content }
</div>
</div>

View file

@ -36,7 +36,7 @@ const EncryptionPanel = ({verificationRequest, member, onClose}) => {
setRequest(verificationRequest);
}, [verificationRequest]);
const [phase, setPhase] = useState(undefined);
const [phase, setPhase] = useState(request && request.phase);
const changeHandler = useCallback(() => {
// handle transitions -> cancelled for mismatches which fire a modal instead of showing a card
if (request && request.cancelled && MISMATCHES.includes(request.cancellationCode)) {
@ -50,7 +50,7 @@ const EncryptionPanel = ({verificationRequest, member, onClose}) => {
<li>{_t("Your homeserver")}</li>
<li>{_t("The homeserver the user youre verifying is connected to")}</li>
<li>{_t("Yours, or the other users internet connection")}</li>
<li>{_t("Yours, or the other users device")}</li>
<li>{_t("Yours, or the other users session")}</li>
</ul>
</div>,
onFinished: onClose,

View file

@ -2,7 +2,7 @@
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017, 2018 Vector Creations Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -67,7 +67,9 @@ export const getE2EStatus = (cli, userId, devices) => {
}
const isMe = userId === cli.getUserId();
const userVerified = cli.checkUserTrust(userId).isCrossSigningVerified();
const allDevicesVerified = devices.every(device => {
if (!userVerified) return "normal";
const anyDeviceUnverified = devices.some(device => {
const { deviceId } = device;
// For your own devices, we use the stricter check of cross-signing
// verification to encourage everyone to trust their own devices via
@ -75,12 +77,9 @@ export const getE2EStatus = (cli, userId, devices) => {
// For other people's devices, the more general verified check that
// includes locally verified devices can be used.
const deviceTrust = cli.checkDeviceTrust(userId, deviceId);
return isMe ? deviceTrust.isCrossSigningVerified() : deviceTrust.isVerified();
return isMe ? !deviceTrust.isCrossSigningVerified() : !deviceTrust.isVerified();
});
if (allDevicesVerified) {
return userVerified ? "verified" : "normal";
}
return "warning";
return anyDeviceUnverified ? "warning" : "verified";
};
async function openDMForUser(matrixClient, userId) {
@ -156,6 +155,7 @@ function DeviceItem({userId, device}) {
const cli = useContext(MatrixClientContext);
const isMe = userId === cli.getUserId();
const deviceTrust = cli.checkDeviceTrust(userId, device.deviceId);
const userTrust = cli.checkUserTrust(userId);
// For your own devices, we use the stricter check of cross-signing
// verification to encourage everyone to trust their own devices via
// cross-signing so that other users can then safely trust you.
@ -170,8 +170,9 @@ function DeviceItem({userId, device}) {
mx_UserInfo_device_unverified: !isVerified,
});
const iconClasses = classNames("mx_E2EIcon", {
mx_E2EIcon_normal: !userTrust.isVerified(),
mx_E2EIcon_verified: isVerified,
mx_E2EIcon_warning: !isVerified,
mx_E2EIcon_warning: userTrust.isVerified() && !isVerified,
});
const onDeviceClick = () => {
@ -183,7 +184,8 @@ function DeviceItem({userId, device}) {
const deviceName = device.ambiguous ?
(device.getDisplayName() ? device.getDisplayName() : "") + " (" + device.deviceId + ")" :
device.getDisplayName();
const trustedLabel = isVerified ? _t("Trusted") : _t("Not trusted");
let trustedLabel = null;
if (userTrust.isVerified()) trustedLabel = isVerified ? _t("Trusted") : _t("Not trusted");
return (
<AccessibleButton
className={classes}
@ -200,6 +202,7 @@ function DeviceItem({userId, device}) {
function DevicesSection({devices, userId, loading}) {
const Spinner = sdk.getComponent("elements.Spinner");
const cli = useContext(MatrixClientContext);
const userTrust = cli.checkUserTrust(userId);
const [isExpanded, setExpanded] = useState(false);
@ -208,43 +211,61 @@ function DevicesSection({devices, userId, loading}) {
return <Spinner />;
}
if (devices === null) {
return _t("Unable to load device list");
return _t("Unable to load session list");
}
const isMe = userId === cli.getUserId();
const deviceTrusts = devices.map(d => cli.checkDeviceTrust(userId, d.deviceId));
let expandSectionDevices = [];
const unverifiedDevices = [];
const verifiedDevices = [];
for (let i = 0; i < devices.length; ++i) {
const device = devices[i];
const deviceTrust = deviceTrusts[i];
// For your own devices, we use the stricter check of cross-signing
// verification to encourage everyone to trust their own devices via
// 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")) ?
deviceTrust.isCrossSigningVerified() :
deviceTrust.isVerified();
let expandCountCaption;
let expandHideCaption;
let expandIconClasses = "mx_E2EIcon";
if (isVerified) {
verifiedDevices.push(device);
} else {
unverifiedDevices.push(device);
if (userTrust.isVerified()) {
for (let i = 0; i < devices.length; ++i) {
const device = devices[i];
const deviceTrust = deviceTrusts[i];
// For your own devices, we use the stricter check of cross-signing
// verification to encourage everyone to trust their own devices via
// 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")) ?
deviceTrust.isCrossSigningVerified() :
deviceTrust.isVerified();
if (isVerified) {
expandSectionDevices.push(device);
} else {
unverifiedDevices.push(device);
}
}
expandCountCaption = _t("%(count)s verified sessions", {count: expandSectionDevices.length});
expandHideCaption = _t("Hide verified sessions");
expandIconClasses += " mx_E2EIcon_verified";
} else {
expandSectionDevices = devices;
expandCountCaption = _t("%(count)s sessions", {count: devices.length});
expandHideCaption = _t("Hide sessions");
expandIconClasses += " mx_E2EIcon_normal";
}
let expandButton;
if (verifiedDevices.length) {
if (expandSectionDevices.length) {
if (isExpanded) {
expandButton = (<AccessibleButton className="mx_UserInfo_expand" onClick={() => setExpanded(false)}>
<div>{_t("Hide verified sessions")}</div>
expandButton = (<AccessibleButton className="mx_UserInfo_expand mx_linkButton"
onClick={() => setExpanded(false)}
>
<div>{expandHideCaption}</div>
</AccessibleButton>);
} else {
expandButton = (<AccessibleButton className="mx_UserInfo_expand" onClick={() => setExpanded(true)}>
<div className="mx_E2EIcon mx_E2EIcon_verified" />
<div>{_t("%(count)s verified sessions", {count: verifiedDevices.length})}</div>
expandButton = (<AccessibleButton className="mx_UserInfo_expand mx_linkButton"
onClick={() => setExpanded(true)}
>
<div className={expandIconClasses} />
<div>{expandCountCaption}</div>
</AccessibleButton>);
}
}
@ -254,7 +275,7 @@ function DevicesSection({devices, userId, loading}) {
});
if (isExpanded) {
const keyStart = unverifiedDevices.length;
deviceList = deviceList.concat(verifiedDevices.map((device, i) => {
deviceList = deviceList.concat(expandSectionDevices.map((device, i) => {
return (<DeviceItem key={i + keyStart} userId={userId} device={device} />);
}));
}
@ -1092,22 +1113,32 @@ export const useDevices = (userId) => {
// Listen to changes
useEffect(() => {
let cancel = false;
const onDeviceVerificationChanged = (_userId, device) => {
if (_userId === userId) {
// 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(cli.getStoredDevicesForUser(userId)).then((devices) => {
if (cancel) return;
setDevices(devices);
});
}
const updateDevices = async () => {
const newDevices = await cli.getStoredDevicesForUser(userId);
if (cancel) return;
setDevices(newDevices);
};
const onDevicesUpdated = (users) => {
if (!users.includes(userId)) return;
updateDevices();
};
const onDeviceVerificationChanged = (_userId, device) => {
if (_userId !== userId) return;
updateDevices();
};
const onUserTrustStatusChanged = (_userId, trustStatus) => {
if (_userId !== userId) return;
updateDevices();
};
cli.on("crypto.devicesUpdated", onDevicesUpdated);
cli.on("deviceVerificationChanged", onDeviceVerificationChanged);
cli.on("userTrustStatusChanged", onUserTrustStatusChanged);
// Handle being unmounted
return () => {
cancel = true;
cli.removeListener("crypto.devicesUpdated", onDevicesUpdated);
cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged);
cli.removeListener("userTrustStatusChanged", onUserTrustStatusChanged);
};
}, [cli, userId]);
@ -1255,10 +1286,11 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
userTrust.isCrossSigningVerified() :
userTrust.isVerified();
const isMe = member.userId === cli.getUserId();
let verifyButton;
if (isRoomEncrypted && !userVerified && !isMe) {
verifyButton = (
<AccessibleButton kind="primary" className="mx_UserInfo_verify" onClick={() => verifyUser(member)}>
<AccessibleButton className="mx_UserInfo_field" onClick={() => verifyUser(member)}>
{_t("Verify")}
</AccessibleButton>
);

View file

@ -51,7 +51,7 @@ export default class VerificationPanel extends React.PureComponent {
constructor(props) {
super(props);
this.state = {};
this._hasVerifier = !!props.request.verifier;
this._hasVerifier = false;
}
renderQRPhase(pending) {
@ -63,7 +63,7 @@ export default class VerificationPanel extends React.PureComponent {
button = <Spinner />;
} else {
button = (
<AccessibleButton kind="primary" className="mx_UserInfo_verify" onClick={this._startSAS}>
<AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={this._startSAS}>
{_t("Verify by emoji")}
</AccessibleButton>
);
@ -128,7 +128,7 @@ export default class VerificationPanel extends React.PureComponent {
<E2EIcon isUser={true} status="verified" size={128} />
<p>Verify all users in a room to ensure it's secure.</p>
<AccessibleButton kind="primary" className="mx_UserInfo_verify" onClick={this.props.onClose}>
<AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={this.props.onClose}>
{_t("Got it")}
</AccessibleButton>
</div>
@ -156,7 +156,7 @@ export default class VerificationPanel extends React.PureComponent {
<h3>Verification cancelled</h3>
<p>{ text }</p>
<AccessibleButton kind="primary" className="mx_UserInfo_verify" onClick={this.props.onClose}>
<AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={this.props.onClose}>
{_t("Got it")}
</AccessibleButton>
</div>
@ -218,8 +218,10 @@ export default class VerificationPanel extends React.PureComponent {
_onRequestChange = async () => {
const {request} = this.props;
if (!this._hasVerifier && !!request.verifier) {
request.verifier.on('show_sas', this._onVerifierShowSas);
const hadVerifier = this._hasVerifier;
this._hasVerifier = !!request.verifier;
if (!hadVerifier && this._hasVerifier) {
request.verifier.once('show_sas', this._onVerifierShowSas);
try {
// on the requester side, this is also awaited in _startSAS,
// but that's ok as verify should return the same promise.
@ -227,14 +229,12 @@ export default class VerificationPanel extends React.PureComponent {
} catch (err) {
console.error("error verify", err);
}
} else if (this._hasVerifier && !request.verifier) {
request.verifier.removeListener('show_sas', this._onVerifierShowSas);
}
this._hasVerifier = !!request.verifier;
};
componentDidMount() {
this.props.request.on("change", this._onRequestChange);
this._onRequestChange();
}
componentWillUnmount() {

View file

@ -94,6 +94,17 @@ export default class BasicMessageEditor extends React.Component {
this._emoticonSettingHandle = null;
}
componentDidUpdate(prevProps) {
if (this.props.placeholder !== prevProps.placeholder && this.props.placeholder) {
const {isEmpty} = this.props.model;
if (isEmpty) {
this._showPlaceholder();
} else {
this._hidePlaceholder();
}
}
}
_replaceEmoticon = (caretPosition, inputType, diff) => {
const {model} = this.props;
const range = model.startRange(caretPosition);

View file

@ -32,23 +32,23 @@ export const E2E_STATE = {
};
const crossSigningUserTitles = {
[E2E_STATE.WARNING]: _td("This user has not verified all of their devices."),
[E2E_STATE.NORMAL]: _td("You have not verified this user. This user has verified all of their devices."),
[E2E_STATE.VERIFIED]: _td("You have verified this user. This user has verified all of their devices."),
[E2E_STATE.WARNING]: _td("This user has not verified all of their sessions."),
[E2E_STATE.NORMAL]: _td("You have not verified this user."),
[E2E_STATE.VERIFIED]: _td("You have verified this user. This user has verified all of their sessions."),
};
const crossSigningRoomTitles = {
[E2E_STATE.WARNING]: _td("Someone is using an unknown device"),
[E2E_STATE.WARNING]: _td("Someone is using an unknown session"),
[E2E_STATE.NORMAL]: _td("This room is end-to-end encrypted"),
[E2E_STATE.VERIFIED]: _td("Everyone in this room is verified"),
};
const legacyUserTitles = {
[E2E_STATE.WARNING]: _td("Some devices for this user are not trusted"),
[E2E_STATE.VERIFIED]: _td("All devices for this user are trusted"),
[E2E_STATE.WARNING]: _td("Some sessions for this user are not trusted"),
[E2E_STATE.VERIFIED]: _td("All sessions for this user are trusted"),
};
const legacyRoomTitles = {
[E2E_STATE.WARNING]: _td("Some devices in this encrypted room are not trusted"),
[E2E_STATE.VERIFIED]: _td("All devices in this encrypted room are trusted"),
[E2E_STATE.WARNING]: _td("Some sessions in this encrypted room are not trusted"),
[E2E_STATE.VERIFIED]: _td("All sessions in this encrypted room are trusted"),
};
const E2EIcon = ({isUser, status, className, size, onClick}) => {

View file

@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
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.
@ -22,7 +23,7 @@ import * as sdk from '../../../index';
import AccessibleButton from '../elements/AccessibleButton';
import { _t } from '../../../languageHandler';
import classNames from "classnames";
import E2EIcon from './E2EIcon';
const PRESENCE_CLASS = {
"offline": "mx_EntityTile_offline",
@ -30,7 +31,6 @@ const PRESENCE_CLASS = {
"unavailable": "mx_EntityTile_unavailable",
};
function presenceClassForMember(presenceState, lastActiveAgo, showPresence) {
if (showPresence === false) {
return 'mx_EntityTile_online_beenactive';
@ -69,6 +69,7 @@ const EntityTile = createReactClass({
suppressOnHover: PropTypes.bool,
showPresence: PropTypes.bool,
subtextLabel: PropTypes.string,
e2eStatus: PropTypes.string,
},
getDefaultProps: function() {
@ -156,18 +157,20 @@ const EntityTile = createReactClass({
);
}
let power;
let powerLabel;
const powerStatus = this.props.powerStatus;
if (powerStatus) {
const src = {
[EntityTile.POWER_STATUS_MODERATOR]: require("../../../../res/img/mod.svg"),
[EntityTile.POWER_STATUS_ADMIN]: require("../../../../res/img/admin.svg"),
}[powerStatus];
const alt = {
[EntityTile.POWER_STATUS_MODERATOR]: _t("Moderator"),
const powerText = {
[EntityTile.POWER_STATUS_MODERATOR]: _t("Mod"),
[EntityTile.POWER_STATUS_ADMIN]: _t("Admin"),
}[powerStatus];
power = <img src={src} className="mx_EntityTile_power" width="16" height="17" alt={alt} />;
powerLabel = <div className="mx_EntityTile_power">{powerText}</div>;
}
let e2eIcon;
const { e2eStatus } = this.props;
if (e2eStatus) {
e2eIcon = <E2EIcon status={e2eStatus} isUser={true} />;
}
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
@ -181,9 +184,10 @@ const EntityTile = createReactClass({
onClick={this.props.onClick}>
<div className="mx_EntityTile_avatar">
{ av }
{ power }
{ e2eIcon }
</div>
{ nameEl }
{ powerLabel }
{ inviteButton }
</AccessibleButton>
</div>
@ -194,5 +198,4 @@ const EntityTile = createReactClass({
EntityTile.POWER_STATUS_MODERATOR = "moderator";
EntityTile.POWER_STATUS_ADMIN = "admin";
export default EntityTile;

View file

@ -40,12 +40,14 @@ const eventTileTypes = {
'm.sticker': 'messages.MessageEvent',
'm.key.verification.cancel': 'messages.MKeyVerificationConclusion',
'm.key.verification.done': 'messages.MKeyVerificationConclusion',
'm.room.encryption': 'messages.EncryptionEvent',
'm.call.invite': 'messages.TextualEvent',
'm.call.answer': 'messages.TextualEvent',
'm.call.hangup': 'messages.TextualEvent',
};
const stateEventTileTypes = {
'm.room.encryption': 'messages.EncryptionEvent',
'm.room.aliases': 'messages.TextualEvent',
// 'm.room.aliases': 'messages.RoomAliasesEvent', // too complex
'm.room.canonical_alias': 'messages.TextualEvent',
@ -55,7 +57,6 @@ const stateEventTileTypes = {
'm.room.avatar': 'messages.RoomAvatarEvent',
'm.room.third_party_invite': 'messages.TextualEvent',
'm.room.history_visibility': 'messages.TextualEvent',
'm.room.encryption': 'messages.TextualEvent',
'm.room.topic': 'messages.TextualEvent',
'm.room.power_levels': 'messages.TextualEvent',
'm.room.pinned_events': 'messages.TextualEvent',
@ -600,7 +601,8 @@ export default createReactClass({
// Info messages are basically information about commands processed on a room
const isBubbleMessage = eventType.startsWith("m.key.verification") ||
(eventType === "m.room.message" && msgtype && msgtype.startsWith("m.key.verification"));
(eventType === "m.room.message" && msgtype && msgtype.startsWith("m.key.verification")) ||
(eventType === "m.room.encryption");
let isInfoMessage = (
!isBubbleMessage && eventType !== 'm.room.message' &&
eventType !== 'm.sticker' && eventType !== 'm.room.create'
@ -733,15 +735,15 @@ export default createReactClass({
<div className="mx_EventTile_keyRequestInfo_tooltip_contents">
<p>
{ this.state.previouslyRequestedKeys ?
_t( 'Your key share request has been sent - please check your other devices ' +
_t( 'Your key share request has been sent - please check your other sessions ' +
'for key share requests.') :
_t( 'Key share requests are sent to your other devices automatically. If you ' +
'rejected or dismissed the key share request on your other devices, click ' +
_t( 'Key share requests are sent to your other sessions automatically. If you ' +
'rejected or dismissed the key share request on your other sessions, click ' +
'here to request the keys for this session again.')
}
</p>
<p>
{ _t( 'If your other devices do not have the key for this message you will not ' +
{ _t( 'If your other sessions do not have the key for this message you will not ' +
'be able to decrypt them.')
}
</p>
@ -749,7 +751,7 @@ export default createReactClass({
const keyRequestInfoContent = this.state.previouslyRequestedKeys ?
_t('Key request sent.') :
_t(
'<requestLink>Re-request encryption keys</requestLink> from your other devices.',
'<requestLink>Re-request encryption keys</requestLink> from your other sessions.',
{},
{'requestLink': (sub) => <a onClick={this.onRequestKeysClick}>{ sub }</a>},
);
@ -938,7 +940,7 @@ function E2ePadlockUndecryptable(props) {
function E2ePadlockUnverified(props) {
return (
<E2ePadlock title={_t("Encrypted by an unverified device")} icon="unverified" {...props} />
<E2ePadlock title={_t("Encrypted by an unverified session")} icon="unverified" {...props} />
);
}
@ -950,7 +952,7 @@ function E2ePadlockUnencrypted(props) {
function E2ePadlockUnknown(props) {
return (
<E2ePadlock title={_t("Encrypted by a deleted device")} icon="unknown" {...props} />
<E2ePadlock title={_t("Encrypted by a deleted session")} icon="unknown" {...props} />
);
}

View file

@ -17,6 +17,7 @@ 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() {
@ -36,6 +37,10 @@ export default class InviteOnlyIcon extends React.Component {
};
render() {
if (!SettingsStore.isFeatureEnabled("feature_invite_only_padlocks")) {
return null;
}
const Tooltip = sdk.getComponent("elements.Tooltip");
let tooltip;
if (this.state.hover) {

View file

@ -260,7 +260,7 @@ export default createReactClass({
e2eStatus: self._getE2EStatus(devices),
});
}, function(err) {
console.log("Error downloading devices", err);
console.log("Error downloading sessions", err);
self.setState({devicesLoading: false});
});
},
@ -766,9 +766,9 @@ export default createReactClass({
// still loading
devComponents = <Spinner />;
} else if (devices === null) {
devComponents = _t("Unable to load device list");
devComponents = _t("Unable to load session list");
} else if (devices.length === 0) {
devComponents = _t("No devices with registered encryption keys");
devComponents = _t("No sessions with registered encryption keys");
} else {
devComponents = [];
for (let i = 0; i < devices.length; i++) {
@ -780,7 +780,7 @@ export default createReactClass({
return (
<div>
<h3>{ _t("Devices") }</h3>
<h3>{ _t("Sessions") }</h3>
<div className="mx_MemberInfo_devices">
{ devComponents }
</div>

View file

@ -1,6 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -22,6 +22,7 @@ import createReactClass from 'create-react-class';
import * as sdk from "../../../index";
import dis from "../../../dispatcher";
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from "../../../MatrixClientPeg";
export default createReactClass({
displayName: 'MemberTile',
@ -40,29 +41,101 @@ export default createReactClass({
getInitialState: function() {
return {
statusMessage: this.getStatusMessage(),
isRoomEncrypted: false,
e2eStatus: null,
};
},
componentDidMount() {
if (!SettingsStore.isFeatureEnabled("feature_custom_status")) {
return;
const cli = MatrixClientPeg.get();
if (SettingsStore.isFeatureEnabled("feature_custom_status")) {
const { user } = this.props.member;
if (user) {
user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
}
}
const { user } = this.props.member;
if (!user) {
return;
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
const { roomId } = this.props.member;
if (roomId) {
const isRoomEncrypted = cli.isRoomEncrypted(roomId);
this.setState({
isRoomEncrypted,
});
if (isRoomEncrypted) {
cli.on("userTrustStatusChanged", this.onUserTrustStatusChanged);
this.updateE2EStatus();
} else {
// Listen for room to become encrypted
cli.on("RoomState.events", this.onRoomStateEvents);
}
}
}
user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
},
componentWillUnmount() {
const cli = MatrixClientPeg.get();
const { user } = this.props.member;
if (!user) {
if (user) {
user.removeListener(
"User._unstable_statusMessage",
this._onStatusMessageCommitted,
);
}
if (cli) {
cli.removeListener("RoomState.events", this.onRoomStateEvents);
cli.removeListener("userTrustStatusChanged", this.onUserTrustStatusChanged);
}
},
onRoomStateEvents: function(ev) {
if (ev.getType() !== "m.room.encryption") return;
const { roomId } = this.props.member;
if (ev.getRoomId() !== roomId) return;
// The room is encrypted now.
const cli = MatrixClientPeg.get();
cli.removeListener("RoomState.events", this.onRoomStateEvents);
this.setState({
isRoomEncrypted: true,
});
this.updateE2EStatus();
},
onUserTrustStatusChanged: function(userId, trustStatus) {
if (userId !== this.props.member.userId) return;
this.updateE2EStatus();
},
updateE2EStatus: async function() {
const cli = MatrixClientPeg.get();
const { userId } = this.props.member;
const isMe = userId === cli.getUserId();
const userVerified = cli.checkUserTrust(userId).isCrossSigningVerified();
if (!userVerified) {
this.setState({
e2eStatus: "normal",
});
return;
}
user.removeListener(
"User._unstable_statusMessage",
this._onStatusMessageCommitted,
);
const devices = await cli.getStoredDevicesForUser(userId);
const anyDeviceUnverified = devices.some(device => {
const { deviceId } = device;
// For your own devices, we use the stricter check of cross-signing
// verification to encourage everyone to trust their own devices via
// 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 deviceTrust = cli.checkDeviceTrust(userId, deviceId);
return isMe ? !deviceTrust.isCrossSigningVerified() : !deviceTrust.isVerified();
});
this.setState({
e2eStatus: anyDeviceUnverified ? "warning" : "verified",
});
},
getStatusMessage() {
@ -94,6 +167,12 @@ export default createReactClass({
) {
return true;
}
if (
nextState.isRoomEncrypted !== this.state.isRoomEncrypted ||
nextState.e2eStatus !== this.state.e2eStatus
) {
return true;
}
return false;
},
@ -153,14 +232,26 @@ export default createReactClass({
const powerStatus = powerStatusMap.get(powerLevel);
let e2eStatus;
if (this.state.isRoomEncrypted) {
e2eStatus = this.state.e2eStatus;
}
return (
<EntityTile {...this.props} presenceState={presenceState}
<EntityTile
{...this.props}
presenceState={presenceState}
presenceLastActiveAgo={member.user ? member.user.lastActiveAgo : 0}
presenceLastTs={member.user ? member.user.lastPresenceTs : 0}
presenceCurrentlyActive={member.user ? member.user.currentlyActive : false}
avatarJsx={av} title={this.getPowerLabel()} onClick={this.onClick}
name={name} powerStatus={powerStatus} showPresence={this.props.showPresence}
avatarJsx={av}
title={this.getPowerLabel()}
name={name}
powerStatus={powerStatus}
showPresence={this.props.showPresence}
subtextLabel={statusMessage}
e2eStatus={e2eStatus}
onClick={this.onClick}
/>
);
},

View file

@ -124,7 +124,7 @@ export default class RoomRecoveryReminder extends React.PureComponent {
let setupCaption;
if (this.state.backupInfo) {
setupCaption = _t("Connect this device to Key Backup");
setupCaption = _t("Connect this session to Key Backup");
} else {
setupCaption = _t("Start using Key Backup");
}

View file

@ -166,7 +166,9 @@ export default createReactClass({
});
/* Check all verified user devices. */
for (const userId of [...verified, cli.getUserId()]) {
/* Don't alarm if no other users are verified */
const targets = (verified.length > 0) ? [...verified, cli.getUserId()] : verified;
for (const userId of targets) {
const devices = await cli.getStoredDevicesForUser(userId);
const allDevicesVerified = devices.every(({deviceId}) => {
return cli.checkDeviceTrust(userId, deviceId).isVerified();

View file

@ -1,53 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 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 createReactClass from 'create-react-class';
import * as Avatar from '../../../Avatar';
import * as sdk from "../../../index";
export default createReactClass({
displayName: 'UserTile',
propTypes: {
user: PropTypes.any.isRequired, // User
},
render: function() {
const EntityTile = sdk.getComponent("rooms.EntityTile");
const user = this.props.user;
const name = user.displayName || user.userId;
let active = -1;
// FIXME: make presence data update whenever User.presence changes...
active = user.lastActiveAgo ?
(Date.now() - (user.lastPresenceTs - user.lastActiveAgo)) : -1;
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const avatarJsx = (
<BaseAvatar width={36} height={36} name={name} idName={user.userId}
url={Avatar.avatarUrlForUser(user, 36, 36, "crop")} />
);
return (
<EntityTile {...this.props} presenceState={user.presence} presenceActiveAgo={active}
presenceCurrentlyActive={user.currentlyActive}
name={name} title={user.userId} avatarJsx={avatarJsx} />
);
},
});

View file

@ -113,7 +113,7 @@ export default createReactClass({
description:
<div>
{ _t(
'Changing password will currently reset any end-to-end encryption keys on all devices, ' +
'Changing password will currently reset any end-to-end encryption keys on all sessions, ' +
'making encrypted chat history unreadable, unless you first export your room keys ' +
'and re-import them afterwards. ' +
'In future this will be improved.',

View file

@ -127,7 +127,7 @@ export default class CrossSigningPanel extends React.PureComponent {
} else if (crossSigningPrivateKeysInStorage) {
summarisedStatus = <p>{_t(
"Your account has a cross-signing identity in secret storage, but it " +
"is not yet trusted by this device.",
"is not yet trusted by this session.",
)}</p>;
} else {
summarisedStatus = <p>{_t(
@ -152,7 +152,7 @@ export default class CrossSigningPanel extends React.PureComponent {
<table className="mx_CrossSigningPanel_statusList"><tbody>
<tr>
<td>{_t("Cross-signing public keys:")}</td>
<td>{crossSigningPublicKeysOnDevice ? _t("on device") : _t("not found")}</td>
<td>{crossSigningPublicKeysOnDevice ? _t("in memory") : _t("not found")}</td>
</tr>
<tr>
<td>{_t("Cross-signing private keys:")}</td>

View file

@ -62,10 +62,10 @@ export default class DevicesPanel extends React.Component {
let errtxt;
if (error.httpStatus == 404) {
// 404 probably means the HS doesn't yet support the API.
errtxt = _t("Your homeserver does not support device management.");
errtxt = _t("Your homeserver does not support session management.");
} else {
console.error("Error loading devices:", error);
errtxt = _t("Unable to load device list");
console.error("Error loading sessions:", error);
errtxt = _t("Unable to load session list");
}
this.setState({deviceLoadError: errtxt});
},
@ -130,7 +130,7 @@ export default class DevicesPanel extends React.Component {
makeRequest: this._makeDeleteRequest.bind(this),
});
}).catch((e) => {
console.error("Error deleting devices", e);
console.error("Error deleting sessions", e);
if (this._unmounted) { return; }
}).finally(() => {
this.setState({
@ -188,7 +188,7 @@ export default class DevicesPanel extends React.Component {
const deleteButton = this.state.deleting ?
<Spinner w={22} h={22} /> :
<AccessibleButton onClick={this._onDeleteClick} kind="danger_sm">
{ _t("Delete %(count)s devices", {count: this.state.selectedDevices.length}) }
{ _t("Delete %(count)s sessions", {count: this.state.selectedDevices.length}) }
</AccessibleButton>;
const classes = classNames(this.props.className, "mx_DevicesPanel");

View file

@ -40,7 +40,7 @@ export default class DevicesPanelEntry extends React.Component {
return MatrixClientPeg.get().setDeviceDetails(device.device_id, {
display_name: value,
}).catch((e) => {
console.error("Error setting device display name", e);
console.error("Error setting session display name", e);
throw new Error(_t("Failed to set display name"));
});
}

View file

@ -186,23 +186,23 @@ export default class KeyBackupPanel extends React.PureComponent {
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
clientBackupStatus = <div>
<p>{encryptedMessageAreEncrypted}</p>
<p> {_t("This device is backing up your keys. ")}</p>
<p> {_t("This session is backing up your keys. ")}</p>
</div>;
} else {
clientBackupStatus = <div>
<p>{encryptedMessageAreEncrypted}</p>
<p>{_t(
"This device is <b>not backing up your keys</b>, " +
"This session is <b>not backing up your keys</b>, " +
"but you do have an existing backup you can restore from " +
"and add to going forward.", {},
{b: sub => <b>{sub}</b>},
)}</p>
<p>{_t(
"Connect this device to key backup before signing out to avoid " +
"losing any keys that may only be on this device.",
"Connect this session to key backup before signing out to avoid " +
"losing any keys that may only be on this session.",
)}</p>
</div>;
restoreButtonCaption = _t("Connect this device to Key Backup");
restoreButtonCaption = _t("Connect this session to Key Backup");
}
let keyStatus;
@ -264,42 +264,42 @@ export default class KeyBackupPanel extends React.PureComponent {
);
} else if (!sig.device) {
sigStatus = _t(
"Backup has a signature from <verify>unknown</verify> device with ID %(deviceId)s",
"Backup has a signature from <verify>unknown</verify> session with ID %(deviceId)s",
{ deviceId: sig.deviceId }, { verify },
);
} else if (sig.valid && fromThisDevice) {
sigStatus = _t(
"Backup has a <validity>valid</validity> signature from this device",
"Backup has a <validity>valid</validity> signature from this session",
{}, { validity },
);
} else if (!sig.valid && fromThisDevice) {
// it can happen...
sigStatus = _t(
"Backup has an <validity>invalid</validity> signature from this device",
"Backup has an <validity>invalid</validity> signature from this session",
{}, { validity },
);
} else if (sig.valid && sig.device.isVerified()) {
sigStatus = _t(
"Backup has a <validity>valid</validity> signature from " +
"<verify>verified</verify> device <device></device>",
"<verify>verified</verify> session <device></device>",
{}, { validity, verify, device },
);
} else if (sig.valid && !sig.device.isVerified()) {
sigStatus = _t(
"Backup has a <validity>valid</validity> signature from " +
"<verify>unverified</verify> device <device></device>",
"<verify>unverified</verify> session <device></device>",
{}, { validity, verify, device },
);
} else if (!sig.valid && sig.device.isVerified()) {
sigStatus = _t(
"Backup has an <validity>invalid</validity> signature from " +
"<verify>verified</verify> device <device></device>",
"<verify>verified</verify> session <device></device>",
{}, { validity, verify, device },
);
} else if (!sig.valid && !sig.device.isVerified()) {
sigStatus = _t(
"Backup has an <validity>invalid</validity> signature from " +
"<verify>unverified</verify> device <device></device>",
"<verify>unverified</verify> session <device></device>",
{}, { validity, verify, device },
);
}
@ -309,12 +309,12 @@ export default class KeyBackupPanel extends React.PureComponent {
</div>;
});
if (this.state.backupSigStatus.sigs.length === 0) {
backupSigStatuses = _t("Backup is not signed by any of your devices");
backupSigStatuses = _t("Backup is not signed by any of your sessions");
}
let trustedLocally;
if (this.state.backupSigStatus.trusted_locally) {
trustedLocally = _t("This backup is trusted because it has been restored on this device");
trustedLocally = _t("This backup is trusted because it has been restored on this session");
}
let buttonRow = (
@ -330,7 +330,7 @@ export default class KeyBackupPanel extends React.PureComponent {
if (this.state.backupKeyStored && !SettingsStore.isFeatureEnabled("feature_cross_signing")) {
buttonRow = <p> {_t(
"Backup key stored in secret storage, but this feature is not " +
"enabled on this device. Please enable cross-signing in Labs to " +
"enabled on this session. Please enable cross-signing in Labs to " +
"modify key backup state.",
)}</p>;
}
@ -352,7 +352,7 @@ export default class KeyBackupPanel extends React.PureComponent {
return <div>
<div>
<p>{_t(
"Your keys are <b>not being backed up from this device</b>.", {},
"Your keys are <b>not being backed up from this session</b>.", {},
{b: sub => <b>{sub}</b>},
)}</p>
<p>{encryptedMessageAreEncrypted}</p>

View file

@ -864,7 +864,7 @@ export default createReactClass({
<LabelledToggleSwitch value={SettingsStore.getValue("notificationsEnabled")}
onChange={this.onEnableDesktopNotificationsChange}
label={_t('Enable desktop notifications for this device')} />
label={_t('Enable desktop notifications for this session')} />
<LabelledToggleSwitch value={SettingsStore.getValue("notificationBodyEnabled")}
onChange={this.onEnableDesktopNotificationBodyChange}
@ -872,7 +872,7 @@ export default createReactClass({
<LabelledToggleSwitch value={SettingsStore.getValue("audioNotificationsEnabled")}
onChange={this.onEnableAudioNotificationsChange}
label={_t('Enable audible notifications for this device')} />
label={_t('Enable audible notifications for this session')} />
{ emailNotificationsRows }

View file

@ -261,7 +261,7 @@ export default class GeneralUserSettingsTab extends React.Component {
title: _t("Success"),
description: _t(
"Your password was successfully changed. You will not receive " +
"push notifications on other devices until you log back in to them",
"push notifications on other sessions until you log back in to them",
) + ".",
});
};

View file

@ -66,6 +66,7 @@ export default class LabsUserSettingsTab extends React.Component {
<SettingsFlag name={"showHiddenEventsInTimeline"} level={SettingLevel.DEVICE} />
<SettingsFlag name={"lowBandwidth"} level={SettingLevel.DEVICE} />
<SettingsFlag name={"sendReadReceipts"} level={SettingLevel.ACCOUNT} />
<SettingsFlag name={"keepSecretStoragePassphraseForSession"} level={SettingLevel.DEVICE} />
</div>
</div>
);

View file

@ -185,11 +185,11 @@ export default class SecurityUserSettingsTab extends React.Component {
<span className='mx_SettingsTab_subheading'>{_t("Cryptography")}</span>
<ul className='mx_SettingsTab_subsectionText mx_SecurityUserSettingsTab_deviceInfo'>
<li>
<label>{_t("Device ID:")}</label>
<label>{_t("Session ID:")}</label>
<span><code>{deviceId}</code></span>
</li>
<li>
<label>{_t("Device key:")}</label>
<label>{_t("Session key:")}</label>
<span><code><b>{identityKey}</b></code></span>
</li>
</ul>
@ -285,9 +285,9 @@ 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("Devices")}</span>
<span className="mx_SettingsTab_subheading">{_t("Sessions")}</span>
<div className='mx_SettingsTab_subsectionText'>
{_t("A device's public name is visible to people you communicate with")}
{_t("A session's public name is visible to people you communicate with")}
<DevicesPanel />
</div>
</div>

View file

@ -39,7 +39,7 @@ export default class SetupEncryptionToast extends React.PureComponent {
switch (this.props.kind) {
case 'set_up_encryption':
case 'upgrade_encryption':
return _t('Verify your other devices easier');
return _t('Verify yourself & others to keep your chats safe');
case 'verify_this_session':
return _t('Other users may not trust it');
}

View file

@ -24,21 +24,20 @@ import NewSessionReviewDialog from '../dialogs/NewSessionReviewDialog';
import FormButton from '../elements/FormButton';
import { replaceableComponent } from '../../../utils/replaceableComponent';
@replaceableComponent("views.toasts.VerifySessionToast")
export default class VerifySessionToast extends React.PureComponent {
@replaceableComponent("views.toasts.UnverifiedSessionToast")
export default class UnverifiedSessionToast extends React.PureComponent {
static propTypes = {
toastKey: PropTypes.string.isRequired,
deviceId: PropTypes.string,
device: PropTypes.object.isRequired,
};
_onLaterClick = () => {
DeviceListener.sharedInstance().dismissVerification(this.props.deviceId);
const { device } = this.props;
DeviceListener.sharedInstance().dismissVerification(device.deviceId);
};
_onReviewClick = async () => {
const cli = MatrixClientPeg.get();
const device = await cli.getStoredDevice(cli.getUserId(), this.props.deviceId);
const { device } = this.props;
Modal.createTrackedDialog('New Session Review', 'Starting dialog', NewSessionReviewDialog, {
userId: MatrixClientPeg.get().getUserId(),
@ -47,8 +46,16 @@ export default class VerifySessionToast extends React.PureComponent {
};
render() {
const { device } = this.props;
return (<div>
<div className="mx_Toast_description">{_t("Review & verify your new session")}</div>
<div className="mx_Toast_description">
<span className="mx_Toast_deviceName">
{device.getDisplayName()}
</span> <span className="mx_Toast_deviceID">
({device.deviceId})
</span>
</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} />

View file

@ -33,11 +33,13 @@ export default class VerificationRequestToast extends React.PureComponent {
componentDidMount() {
const {request} = this.props;
this._intervalHandle = setInterval(() => {
let {counter} = this.state;
counter = Math.max(0, counter - 1);
this.setState({counter});
}, 1000);
if (request.timeout && request.timeout > 0) {
this._intervalHandle = setInterval(() => {
let {counter} = this.state;
counter = Math.max(0, counter - 1);
this.setState({counter});
}, 1000);
}
request.on("change", this._checkRequestIsPending);
// We should probably have a separate class managing the active verification toasts,
// rather than monitoring this in the toast component itself, since we'll get problems
@ -56,7 +58,10 @@ export default class VerificationRequestToast extends React.PureComponent {
_checkRequestIsPending = () => {
const {request} = this.props;
if (request.ready || request.started || request.done || request.cancelled || request.observeOnly) {
const isPendingInRoomRequest = request.channel.roomId &&
!(request.ready || request.started || request.done || request.cancelled || request.observeOnly);
const isPendingDeviceRequest = request.channel.deviceId && request.started;
if (!isPendingInRoomRequest && !isPendingDeviceRequest) {
ToastStore.sharedInstance().dismissToast(this.props.toastKey);
}
};
@ -117,10 +122,13 @@ export default class VerificationRequestToast extends React.PureComponent {
nameLabel = _t("%(name)s (%(userId)s)", {name: user.displayName, userId});
}
}
const declineLabel = this.state.counter == 0 ?
_t("Decline") :
_t("Decline (%(counter)s)", {counter: this.state.counter});
return (<div>
<div className="mx_Toast_description">{nameLabel}</div>
<div className="mx_Toast_buttons" aria-live="off">
<FormButton label={_t("Decline (%(counter)s)", {counter: this.state.counter})} kind="danger" onClick={this.cancel} />
<FormButton label={declineLabel} kind="danger" onClick={this.cancel} />
<FormButton label={_t("Accept")} onClick={this.accept} />
</div>
</div>);

View file

@ -32,6 +32,7 @@ export default class VerificationShowSas extends React.Component {
onDone: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
sas: PropTypes.object.isRequired,
isSelf: PropTypes.bool,
};
constructor(props) {
@ -62,11 +63,17 @@ export default class VerificationShowSas extends React.Component {
</div>,
);
sasDisplay = <div className="mx_VerificationShowSas_emojiSas">
{emojiBlocks}
{emojiBlocks.slice(0, 4)}
<div className="mx_VerificationShowSas_emojiSas_break" />
{emojiBlocks.slice(4)}
</div>;
sasCaption = _t(
"Verify this user by confirming the following emoji appear on their screen.",
);
sasCaption = this.props.isSelf ?
_t(
"Confirm the emoji below are displayed on both devices, in the same order:",
):
_t(
"Verify this user by confirming the following emoji appear on their screen.",
);
} else if (this.props.sas.decimal) {
const numberBlocks = this.props.sas.decimal.map((num, i) => <span key={i}>
{num}
@ -74,13 +81,17 @@ export default class VerificationShowSas extends React.Component {
sasDisplay = <div className="mx_VerificationShowSas_decimalSas">
{numberBlocks}
</div>;
sasCaption = _t(
"Verify this user by confirming the following number appears on their screen.",
);
sasCaption = this.props.isSelf ?
_t(
"Verify this device by confirming the following number appears on its screen.",
):
_t(
"Verify this user by confirming the following number appears on their screen.",
);
} else {
return <div>
{_t("Unable to find a supported verification method.")}
<AccessibleButton kind="primary" onClick={this.props.onCancel} className="mx_UserInfo_verify">
<AccessibleButton kind="primary" onClick={this.props.onCancel} className="mx_UserInfo_wideButton">
{_t('Cancel')}
</AccessibleButton>
</div>;
@ -96,17 +107,19 @@ export default class VerificationShowSas extends React.Component {
confirm = <DialogButtons
primaryButton={_t("They match")}
onPrimaryButtonClick={this.onMatchClick}
primaryButtonClass="mx_UserInfo_verify"
primaryButtonClass="mx_UserInfo_wideButton"
cancelButton={_t("They don't match")}
onCancel={this.props.onCancel}
cancelButtonClass="mx_UserInfo_verify"
cancelButtonClass="mx_UserInfo_wideButton"
/>;
}
return <div className="mx_VerificationShowSas">
<p>{sasCaption}</p>
<p>{_t("For ultimate security, do this in person or use another way to communicate.")}</p>
{sasDisplay}
<p>{this.props.isSelf ?
"":
_t("To be secure, do this in person or use a trusted way to communicate.")}</p>
{confirm}
</div>;
}