Merge branch 'develop' into travis/media-customization

This commit is contained in:
Travis Ralston 2021-03-11 08:37:49 -07:00
commit a9a4bd50ca
19 changed files with 357 additions and 194 deletions

View file

@ -489,54 +489,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
margin-top: 69px; margin-top: 69px;
} }
.mx_Beta {
color: red;
margin-right: 10px;
position: relative;
top: -3px;
background-color: white;
padding: 0 4px;
border-radius: 3px;
border: 1px solid darkred;
cursor: help;
transition-duration: 200ms;
font-size: smaller;
filter: opacity(0.5);
}
.mx_Beta:hover {
color: white;
border: 1px solid gray;
background-color: darkred;
}
.mx_TintableSvgButton {
position: relative;
display: flex;
flex-direction: row;
justify-content: center;
align-content: center;
}
.mx_TintableSvgButton object {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
}
.mx_TintableSvgButton span {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
opacity: 0;
cursor: pointer;
}
// username colors // username colors
// used by SenderProfile & RoomPreviewBar // used by SenderProfile & RoomPreviewBar
.mx_Username_color1 { .mx_Username_color1 {

View file

@ -20,6 +20,8 @@ $SpaceRoomViewInnerWidth: 428px;
.mx_MainSplit > div:first-child { .mx_MainSplit > div:first-child {
padding: 80px 60px; padding: 80px 60px;
flex-grow: 1; flex-grow: 1;
max-height: 100%;
overflow-y: auto;
h1 { h1 {
margin: 0; margin: 0;
@ -69,9 +71,116 @@ $SpaceRoomViewInnerWidth: 428px;
} }
} }
.mx_SpaceRoomView_landing { .mx_SpaceRoomView_preview {
overflow-y: auto; padding: 32px 24px !important; // override default padding from above
margin: auto;
max-width: 480px;
box-sizing: border-box;
box-shadow: 2px 15px 30px $dialog-shadow-color;
border: 1px solid $input-border-color;
border-radius: 8px;
.mx_SpaceRoomView_preview_inviter {
display: flex;
align-items: center;
margin-bottom: 20px;
font-size: $font-15px;
> div {
margin-left: 8px;
.mx_SpaceRoomView_preview_inviter_name {
line-height: $font-18px;
}
.mx_SpaceRoomView_preview_inviter_mxid {
line-height: $font-24px;
color: $secondary-fg-color;
}
}
}
> .mx_BaseAvatar_image,
> .mx_BaseAvatar > .mx_BaseAvatar_image {
border-radius: 12px;
}
h1.mx_SpaceRoomView_preview_name {
margin: 20px 0 !important; // override default margin from above
}
.mx_SpaceRoomView_preview_info {
color: $tertiary-fg-color;
font-size: $font-15px;
line-height: $font-24px;
margin: 20px 0;
.mx_SpaceRoomView_preview_info_public,
.mx_SpaceRoomView_preview_info_private {
padding-left: 20px;
position: relative;
&::before {
position: absolute;
content: "";
width: 20px;
height: 20px;
top: 0;
left: -2px;
mask-position: center;
mask-repeat: no-repeat;
background-color: $tertiary-fg-color;
}
}
.mx_SpaceRoomView_preview_info_public::before {
mask-size: 12px;
mask-image: url("$(res)/img/globe.svg");
}
.mx_SpaceRoomView_preview_info_private::before {
mask-size: 14px;
mask-image: url("$(res)/img/element-icons/lock.svg");
}
.mx_AccessibleButton_kind_link {
color: inherit;
position: relative;
padding-left: 16px;
&::before {
content: "·"; // visual separator
position: absolute;
left: 6px;
}
}
}
.mx_SpaceRoomView_preview_topic {
font-size: $font-14px;
line-height: $font-22px;
color: $secondary-fg-color;
margin: 20px 0;
max-height: 160px;
overflow-y: auto;
}
.mx_SpaceRoomView_preview_joinButtons {
margin-top: 20px;
.mx_AccessibleButton {
width: 200px;
box-sizing: border-box;
padding: 14px 0;
& + .mx_AccessibleButton {
margin-left: 20px;
}
}
}
}
.mx_SpaceRoomView_landing {
> .mx_BaseAvatar_image, > .mx_BaseAvatar_image,
> .mx_BaseAvatar > .mx_BaseAvatar_image { > .mx_BaseAvatar > .mx_BaseAvatar_image {
border-radius: 12px; border-radius: 12px;
@ -128,14 +237,6 @@ $SpaceRoomViewInnerWidth: 428px;
font-size: $font-15px; font-size: $font-15px;
} }
.mx_SpaceRoomView_landing_joinButtons {
margin-top: 24px;
.mx_FormButton {
padding: 8px 22px;
}
}
.mx_SpaceRoomView_landing_adminButtons { .mx_SpaceRoomView_landing_adminButtons {
margin-top: 32px; margin-top: 32px;

View file

@ -26,7 +26,9 @@ limitations under the License.
padding: 7px 18px; padding: 7px 18px;
text-align: center; text-align: center;
border-radius: 8px; border-radius: 8px;
display: inline-block; display: inline-flex;
align-items: center;
justify-content: center;
font-size: $font-14px; font-size: $font-14px;
} }

View file

@ -33,4 +33,10 @@ limitations under the License.
color: $notice-primary-color; color: $notice-primary-color;
background-color: $notice-primary-bg-color; background-color: $notice-primary-bg-color;
} }
&.mx_AccessibleButton_kind_secondary {
color: $secondary-fg-color;
border: 1px solid $secondary-fg-color;
background-color: unset;
}
} }

View file

@ -14,13 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_UserInfo { .mx_EncryptionInfo_spinner {
.mx_EncryptionInfo_spinner {
.mx_Spinner { .mx_Spinner {
margin-top: 25px; margin-top: 25px;
margin-bottom: 15px; margin-bottom: 15px;
} }
text-align: center; text-align: center;
}
} }

View file

@ -1,5 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
const fs = require('fs'); const fs = require('fs');
const { promises: fsp } = fs;
const path = require('path'); const path = require('path');
const glob = require('glob'); const glob = require('glob');
const util = require('util'); const util = require('util');
@ -25,6 +26,8 @@ async function reskindex() {
const header = args.h || args.header; const header = args.h || args.header;
const strm = fs.createWriteStream(componentIndexTmp); const strm = fs.createWriteStream(componentIndexTmp);
// Wait for the open event to ensure the file descriptor is set
await new Promise(resolve => strm.once("open", resolve));
if (header) { if (header) {
strm.write(fs.readFileSync(header)); strm.write(fs.readFileSync(header));
@ -53,14 +56,9 @@ async function reskindex() {
strm.write("export {components};\n"); strm.write("export {components};\n");
// Ensure the file has been fully written to disk before proceeding // Ensure the file has been fully written to disk before proceeding
await util.promisify(fs.fsync)(strm.fd);
await util.promisify(strm.end); await util.promisify(strm.end);
fs.rename(componentIndexTmp, componentIndex, function(err) { await fsp.rename(componentIndexTmp, componentIndex);
if (err) {
console.error("Error moving new index into place: " + err);
} else {
console.log('Reskindex: completed');
}
});
} }
// Expects both arrays of file names to be sorted // Expects both arrays of file names to be sorted
@ -77,9 +75,17 @@ function filesHaveChanged(files, prevFiles) {
return false; return false;
} }
// Wrapper since await at the top level is not well supported yet
function run() {
(async function() {
await reskindex();
console.log("Reskindex completed");
})();
}
// -w indicates watch mode where any FS events will trigger reskindex // -w indicates watch mode where any FS events will trigger reskindex
if (!args.w) { if (!args.w) {
reskindex(); run();
return; return;
} }
@ -87,5 +93,5 @@ let watchDebouncer = null;
chokidar.watch(path.join(componentsDir, componentJsGlob)).on('all', (event, path) => { chokidar.watch(path.join(componentsDir, componentJsGlob)).on('all', (event, path) => {
if (path === componentIndex) return; if (path === componentIndex) return;
if (watchDebouncer) clearTimeout(watchDebouncer); if (watchDebouncer) clearTimeout(watchDebouncer);
watchDebouncer = setTimeout(reskindex, 1000); watchDebouncer = setTimeout(run, 1000);
}); });

View file

@ -1913,7 +1913,7 @@ export default class RoomView extends React.Component<IProps, IState> {
); );
} }
if (this.state.room?.isSpaceRoom()) { if (SettingsStore.getValue("feature_spaces") && this.state.room?.isSpaceRoom()) {
return <SpaceRoomView return <SpaceRoomView
space={this.state.room} space={this.state.room}
justCreatedOpts={this.props.justCreatedOpts} justCreatedOpts={this.props.justCreatedOpts}

View file

@ -94,26 +94,95 @@ const useMyRoomMembership = (room: Room) => {
return membership; return membership;
}; };
const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => { const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const myMembership = useMyRoomMembership(space); const myMembership = useMyRoomMembership(space);
const joinRule = space.getJoinRule();
const userId = cli.getUserId();
let inviterSection;
let joinButtons; let joinButtons;
if (myMembership === "invite") { if (myMembership === "invite") {
joinButtons = <div className="mx_SpaceRoomView_landing_joinButtons"> const inviteSender = space.getMember(cli.getUserId())?.events.member?.getSender();
<FormButton label={_t("Accept Invite")} onClick={onJoinButtonClicked} /> const inviter = inviteSender && space.getMember(inviteSender);
<AccessibleButton kind="link" onClick={onRejectButtonClicked}>
{_t("Decline")} if (inviteSender) {
</AccessibleButton> inviterSection = <div className="mx_SpaceRoomView_preview_inviter">
</div>; <MemberAvatar member={inviter} width={32} height={32} />
} else if (myMembership !== "join" && joinRule === "public") { <div>
joinButtons = <div className="mx_SpaceRoomView_landing_joinButtons"> <div className="mx_SpaceRoomView_preview_inviter_name">
<FormButton label={_t("Join")} onClick={onJoinButtonClicked} /> { _t("<inviter/> invites you", {}, {
inviter: () => <b>{ inviter.name || inviteSender }</b>,
}) }
</div>
{ inviter ? <div className="mx_SpaceRoomView_preview_inviter_mxid">
{ inviteSender }
</div> : null }
</div>
</div>; </div>;
} }
joinButtons = <>
<FormButton label={_t("Reject")} kind="secondary" onClick={onRejectButtonClicked} />
<FormButton label={_t("Accept")} onClick={onJoinButtonClicked} />
</>;
} else {
joinButtons = <FormButton label={_t("Join")} onClick={onJoinButtonClicked} />
}
let visibilitySection;
if (space.getJoinRule() === "public") {
visibilitySection = <span className="mx_SpaceRoomView_preview_info_public">
{ _t("Public space") }
</span>;
} else {
visibilitySection = <span className="mx_SpaceRoomView_preview_info_private">
{ _t("Private space") }
</span>;
}
return <div className="mx_SpaceRoomView_preview">
{ inviterSection }
<RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
<h1 className="mx_SpaceRoomView_preview_name">
<RoomName room={space} />
</h1>
<div className="mx_SpaceRoomView_preview_info">
{ visibilitySection }
<RoomMemberCount room={space}>
{(count) => count > 0 ? (
<AccessibleButton
className="mx_SpaceRoomView_preview_memberCount"
kind="link"
onClick={() => {
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.RoomMemberList,
refireParams: { space },
});
}}
>
{ _t("%(count)s members", { count }) }
</AccessibleButton>
) : null}
</RoomMemberCount>
</div>
<RoomTopic room={space}>
{(topic, ref) =>
<div className="mx_SpaceRoomView_preview_topic" ref={ref}>
{ topic }
</div>
}
</RoomTopic>
<div className="mx_SpaceRoomView_preview_joinButtons">
{ joinButtons }
</div>
</div>;
};
const SpaceLanding = ({ space }) => {
const cli = useContext(MatrixClientContext);
const myMembership = useMyRoomMembership(space);
const userId = cli.getUserId();
let inviteButton; let inviteButton;
if (myMembership === "join" && space.canInvite(userId)) { if (myMembership === "join" && space.canInvite(userId)) {
inviteButton = ( inviteButton = (
@ -227,26 +296,7 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
) : null} ) : null}
</RoomMemberCount> </RoomMemberCount>
</div> }; </div> };
if (myMembership === "invite") { if (shouldShowSpaceSettings(cli, space)) {
const inviteSender = space.getMember(userId)?.events.member?.getSender();
const inviter = inviteSender && space.getMember(inviteSender);
if (inviteSender) {
return _t("<inviter/> invited you to <name/>", {}, {
name: tags.name,
inviter: () => inviter
? <span className="mx_SpaceRoomView_landing_inviter">
<MemberAvatar member={inviter} width={26} height={26} viewUserOnClick={true} />
{ inviter.name }
</span>
: <span className="mx_SpaceRoomView_landing_inviter">
{ inviteSender }
</span>,
}) as JSX.Element;
} else {
return _t("You have been invited to <name/>", {}, tags) as JSX.Element;
}
} else if (shouldShowSpaceSettings(cli, space)) {
if (space.getJoinRule() === "public") { if (space.getJoinRule() === "public") {
return _t("Your public space <name/>", {}, tags) as JSX.Element; return _t("Your public space <name/>", {}, tags) as JSX.Element;
} else { } else {
@ -260,7 +310,6 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
<div className="mx_SpaceRoomView_landing_topic"> <div className="mx_SpaceRoomView_landing_topic">
<RoomTopic room={space} /> <RoomTopic room={space} />
</div> </div>
{ joinButtons }
<div className="mx_SpaceRoomView_landing_adminButtons"> <div className="mx_SpaceRoomView_landing_adminButtons">
{ inviteButton } { inviteButton }
{ addRoomButtons } { addRoomButtons }
@ -548,12 +597,15 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
private renderBody() { private renderBody() {
switch (this.state.phase) { switch (this.state.phase) {
case Phase.Landing: case Phase.Landing:
return <SpaceLanding if (this.props.space.getMyMembership() === "join") {
return <SpaceLanding space={this.props.space} />;
} else {
return <SpacePreview
space={this.props.space} space={this.props.space}
onJoinButtonClicked={this.props.onJoinButtonClicked} onJoinButtonClicked={this.props.onJoinButtonClicked}
onRejectButtonClicked={this.props.onRejectButtonClicked} onRejectButtonClicked={this.props.onRejectButtonClicked}
/>; />;
}
case Phase.PublicCreateRooms: case Phase.PublicCreateRooms:
return <SpaceSetupFirstRooms return <SpaceSetupFirstRooms
space={this.props.space} space={this.props.space}

View file

@ -20,6 +20,7 @@ import { _t } from '../../../languageHandler';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { import {
SetupEncryptionStore, SetupEncryptionStore,
PHASE_LOADING,
PHASE_INTRO, PHASE_INTRO,
PHASE_BUSY, PHASE_BUSY,
PHASE_DONE, PHASE_DONE,
@ -60,7 +61,9 @@ export default class CompleteSecurity extends React.Component {
let icon; let icon;
let title; let title;
if (phase === PHASE_INTRO) { if (phase === PHASE_LOADING) {
return null;
} else if (phase === PHASE_INTRO) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />; icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
title = _t("Verify this login"); title = _t("Verify this login");
} else if (phase === PHASE_DONE) { } else if (phase === PHASE_DONE) {

View file

@ -17,11 +17,13 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { import {
SetupEncryptionStore, SetupEncryptionStore,
PHASE_LOADING,
PHASE_INTRO, PHASE_INTRO,
PHASE_BUSY, PHASE_BUSY,
PHASE_DONE, PHASE_DONE,
@ -83,6 +85,22 @@ export default class SetupEncryptionBody extends React.Component {
store.usePassPhrase(); store.usePassPhrase();
} }
_onVerifyClick = () => {
const cli = MatrixClientPeg.get();
const userId = cli.getUserId();
const requestPromise = cli.requestVerification(userId);
this.props.onFinished(true);
Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, {
verificationRequestPromise: requestPromise,
member: cli.getUser(userId),
onFinished: async () => {
const request = await requestPromise;
request.cancel();
},
});
}
onSkipClick = () => { onSkipClick = () => {
const store = SetupEncryptionStore.sharedInstance(); const store = SetupEncryptionStore.sharedInstance();
store.skip(); store.skip();
@ -134,32 +152,22 @@ export default class SetupEncryptionBody extends React.Component {
</AccessibleButton>; </AccessibleButton>;
} }
const brand = SdkConfig.get().brand; let verifyButton;
if (store.hasDevicesToVerifyAgainst) {
verifyButton = <AccessibleButton kind="primary" onClick={this._onVerifyClick}>
{ _t("Verify with another session") }
</AccessibleButton>;
}
return ( return (
<div> <div>
<p>{_t( <p>{_t(
"Confirm your identity by verifying this login from one of your other sessions, " + "Verify this login to access your encrypted messages and " +
"granting it access to encrypted messages.", "prove to others that this login is really you.",
)}</p> )}</p>
<p>{_t(
"This requires the latest %(brand)s on your other devices:",
{ brand },
)}</p>
<div className="mx_CompleteSecurity_clients">
<div className="mx_CompleteSecurity_clients_desktop">
<div>{_t("%(brand)s Web", { brand })}</div>
<div>{_t("%(brand)s Desktop", { brand })}</div>
</div>
<div className="mx_CompleteSecurity_clients_mobile">
<div>{_t("%(brand)s iOS", { brand })}</div>
<div>{_t("%(brand)s Android", { brand })}</div>
</div>
<p>{_t("or another cross-signing capable Matrix client")}</p>
</div>
<div className="mx_CompleteSecurity_actionRow"> <div className="mx_CompleteSecurity_actionRow">
{verifyButton}
{useRecoveryKeyButton} {useRecoveryKeyButton}
<AccessibleButton kind="danger" onClick={this.onSkipClick}> <AccessibleButton kind="danger" onClick={this.onSkipClick}>
{_t("Skip")} {_t("Skip")}
@ -217,7 +225,7 @@ export default class SetupEncryptionBody extends React.Component {
</div> </div>
</div> </div>
); );
} else if (phase === PHASE_BUSY) { } else if (phase === PHASE_BUSY || phase === PHASE_LOADING) {
const Spinner = sdk.getComponent('views.elements.Spinner'); const Spinner = sdk.getComponent('views.elements.Spinner');
return <Spinner />; return <Spinner />;
} else { } else {

View file

@ -66,6 +66,10 @@ export default class NewSessionReviewDialog extends React.PureComponent {
Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, { Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, {
verificationRequestPromise: requestPromise, verificationRequestPromise: requestPromise,
member: cli.getUser(userId), member: cli.getUser(userId),
onFinished: async () => {
const request = await requestPromise;
request.cancel();
},
}); });
} }

View file

@ -27,11 +27,11 @@ export default class VerificationRequestDialog extends React.Component {
verificationRequest: PropTypes.object, verificationRequest: PropTypes.object,
verificationRequestPromise: PropTypes.object, verificationRequestPromise: PropTypes.object,
onFinished: PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
member: PropTypes.string,
}; };
constructor(...args) { constructor(...args) {
super(...args); super(...args);
this.onFinished = this.onFinished.bind(this);
this.state = {}; this.state = {};
if (this.props.verificationRequest) { if (this.props.verificationRequest) {
this.state.verificationRequest = this.props.verificationRequest; this.state.verificationRequest = this.props.verificationRequest;
@ -52,7 +52,7 @@ export default class VerificationRequestDialog extends React.Component {
const title = request && request.isSelfVerification ? const title = request && request.isSelfVerification ?
_t("Verify other session") : _t("Verification Request"); _t("Verify other session") : _t("Verification Request");
return <BaseDialog className="mx_InfoDialog" onFinished={this.onFinished} return <BaseDialog className="mx_InfoDialog" onFinished={this.props.onFinished}
contentId="mx_Dialog_content" contentId="mx_Dialog_content"
title={title} title={title}
hasCancel={true} hasCancel={true}
@ -66,13 +66,4 @@ export default class VerificationRequestDialog extends React.Component {
/> />
</BaseDialog>; </BaseDialog>;
} }
async onFinished() {
this.props.onFinished();
let request = this.props.verificationRequest;
if (!request && this.props.verificationRequestPromise) {
request = await this.props.verificationRequestPromise;
}
request.cancel();
}
} }

View file

@ -46,6 +46,7 @@ import EncryptionPanel from "./EncryptionPanel";
import {useAsyncMemo} from '../../../hooks/useAsyncMemo'; import {useAsyncMemo} from '../../../hooks/useAsyncMemo';
import {legacyVerifyUser, verifyDevice, verifyUser} from '../../../verification'; import {legacyVerifyUser, verifyDevice, verifyUser} from '../../../verification';
import {Action} from "../../../dispatcher/actions"; import {Action} from "../../../dispatcher/actions";
import { USER_SECURITY_TAB } from "../dialogs/UserSettingsDialog";
import {useIsEncrypted} from "../../../hooks/useIsEncrypted"; import {useIsEncrypted} from "../../../hooks/useIsEncrypted";
import BaseCard from "./BaseCard"; import BaseCard from "./BaseCard";
import {E2EStatus} from "../../../utils/ShieldUtils"; import {E2EStatus} from "../../../utils/ShieldUtils";
@ -1368,6 +1369,20 @@ const BasicUserInfo: React.FC<{
} }
} }
let editDevices;
if (member.userId == cli.getUserId()) {
editDevices = (<p>
<AccessibleButton className="mx_UserInfo_field" onClick={() => {
dis.dispatch({
action: Action.ViewUserSettings,
initialTabId: USER_SECURITY_TAB,
});
}}>
{ _t("Edit devices") }
</AccessibleButton>
</p>)
}
const securitySection = ( const securitySection = (
<div className="mx_UserInfo_container"> <div className="mx_UserInfo_container">
<h3>{ _t("Security") }</h3> <h3>{ _t("Security") }</h3>
@ -1377,6 +1392,7 @@ const BasicUserInfo: React.FC<{
loading={showDeviceListSpinner} loading={showDeviceListSpinner}
devices={devices} devices={devices}
userId={member.userId} /> } userId={member.userId} /> }
{ editDevices }
</div> </div>
); );

View file

@ -158,14 +158,14 @@ export default class ReadReceiptMarker extends React.PureComponent {
// then shift to the rightmost column, // then shift to the rightmost column,
// and then it will drop down to its resting position // and then it will drop down to its resting position
// //
// XXX: We use a fractional left value to trick velocity-animate into actually animating. // XXX: We use a small left value to trick velocity-animate into actually animating.
// This is a very annoying bug where if it thinks there's no change to `left` then it'll // This is a very annoying bug where if it thinks there's no change to `left` then it'll
// skip applying it, thus making our read receipt at +14px instead of +0px like it // skip applying it, thus making our read receipt at +14px instead of +0px like it
// should be. This does cause a tiny amount of drift for read receipts, however with a // should be. This does cause a tiny amount of drift for read receipts, however with a
// value so small it's not perceived by a user. // value so small it's not perceived by a user.
// Note: Any smaller values (or trying to interchange units) might cause read receipts to // Note: Any smaller values (or trying to interchange units) might cause read receipts to
// fail to fall down or cause gaps. // fail to fall down or cause gaps.
startStyles.push({ top: startTopOffset+'px', left: '0.001px' }); startStyles.push({ top: startTopOffset+'px', left: '1px' });
enterTransitionOpts.push({ enterTransitionOpts.push({
duration: bounce ? Math.min(Math.log(Math.abs(startTopOffset)) * 200, 3000) : 300, duration: bounce ? Math.min(Math.log(Math.abs(startTopOffset)) * 200, 3000) : 300,
easing: bounce ? 'easeOutBounce' : 'easeOutCubic', easing: bounce ? 'easeOutBounce' : 'easeOutCubic',

View file

@ -39,6 +39,7 @@ interface IProps {
interface IState { interface IState {
counter: number; counter: number;
device?: DeviceInfo; device?: DeviceInfo;
ip?: string;
} }
@replaceableComponent("views.toasts.VerificationRequestToast") @replaceableComponent("views.toasts.VerificationRequestToast")
@ -68,9 +69,15 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
// a toast hanging around after logging in if you did a verification as part of login). // a toast hanging around after logging in if you did a verification as part of login).
this._checkRequestIsPending(); this._checkRequestIsPending();
if (request.isSelfVerification) { if (request.isSelfVerification) {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
this.setState({device: cli.getStoredDevice(cli.getUserId(), request.channel.deviceId)}); const device = await cli.getDevice(request.channel.deviceId);
const ip = device.last_seen_ip;
this.setState({
device: cli.getStoredDevice(cli.getUserId(), request.channel.deviceId),
ip,
});
} }
} }
@ -120,6 +127,9 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
const VerificationRequestDialog = sdk.getComponent("views.dialogs.VerificationRequestDialog"); const VerificationRequestDialog = sdk.getComponent("views.dialogs.VerificationRequestDialog");
Modal.createTrackedDialog('Incoming Verification', '', VerificationRequestDialog, { Modal.createTrackedDialog('Incoming Verification', '', VerificationRequestDialog, {
verificationRequest: request, verificationRequest: request,
onFinished: () => {
request.cancel();
},
}, null, /* priority = */ false, /* static = */ true); }, null, /* priority = */ false, /* static = */ true);
} }
await request.accept(); await request.accept();
@ -133,9 +143,10 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
let nameLabel; let nameLabel;
if (request.isSelfVerification) { if (request.isSelfVerification) {
if (this.state.device) { if (this.state.device) {
nameLabel = _t("From %(deviceName)s (%(deviceId)s)", { nameLabel = _t("From %(deviceName)s (%(deviceId)s) at %(ip)s", {
deviceName: this.state.device.getDisplayName(), deviceName: this.state.device.getDisplayName(),
deviceId: this.state.device.deviceId, deviceId: this.state.device.deviceId,
ip: this.state.ip,
}); });
} }
} else { } else {

View file

@ -728,7 +728,7 @@
"Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.": "Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.", "Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.": "Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.",
"Yes": "Yes", "Yes": "Yes",
"No": "No", "No": "No",
"Review where youre logged in": "Review where youre logged in", "You have unverified logins": "You have unverified logins",
"Verify all your sessions to ensure your account & messages are safe": "Verify all your sessions to ensure your account & messages are safe", "Verify all your sessions to ensure your account & messages are safe": "Verify all your sessions to ensure your account & messages are safe",
"Review": "Review", "Review": "Review",
"Later": "Later", "Later": "Later",
@ -753,7 +753,8 @@
"Safeguard against losing access to encrypted messages & data": "Safeguard against losing access to encrypted messages & data", "Safeguard against losing access to encrypted messages & data": "Safeguard against losing access to encrypted messages & data",
"Other users may not trust it": "Other users may not trust it", "Other users may not trust it": "Other users may not trust it",
"New login. Was this you?": "New login. Was this you?", "New login. Was this you?": "New login. Was this you?",
"Verify the new login accessing your account: %(name)s": "Verify the new login accessing your account: %(name)s", "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s",
"Check your devices": "Check your devices",
"What's new?": "What's new?", "What's new?": "What's new?",
"What's New": "What's New", "What's New": "What's New",
"Update": "Update", "Update": "Update",
@ -980,7 +981,7 @@
"Folder": "Folder", "Folder": "Folder",
"Pin": "Pin", "Pin": "Pin",
"Your server isn't responding to some <a>requests</a>.": "Your server isn't responding to some <a>requests</a>.", "Your server isn't responding to some <a>requests</a>.": "Your server isn't responding to some <a>requests</a>.",
"From %(deviceName)s (%(deviceId)s)": "From %(deviceName)s (%(deviceId)s)", "From %(deviceName)s (%(deviceId)s) at %(ip)s": "From %(deviceName)s (%(deviceId)s) at %(ip)s",
"Decline (%(counter)s)": "Decline (%(counter)s)", "Decline (%(counter)s)": "Decline (%(counter)s)",
"Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:", "Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
"Delete": "Delete", "Delete": "Delete",
@ -1756,6 +1757,7 @@
"Failed to deactivate user": "Failed to deactivate user", "Failed to deactivate user": "Failed to deactivate user",
"Role": "Role", "Role": "Role",
"This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.", "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.",
"Edit devices": "Edit devices",
"Security": "Security", "Security": "Security",
"The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.": "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.", "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.": "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.",
"Verify by scanning": "Verify by scanning", "Verify by scanning": "Verify by scanning",
@ -2609,14 +2611,14 @@
"Promoted to users": "Promoted to users", "Promoted to users": "Promoted to users",
"Manage rooms": "Manage rooms", "Manage rooms": "Manage rooms",
"Find a room...": "Find a room...", "Find a room...": "Find a room...",
"Accept Invite": "Accept Invite", "<inviter/> invites you": "<inviter/> invites you",
"Public space": "Public space",
"Private space": "Private space",
"%(count)s members|other": "%(count)s members",
"%(count)s members|one": "%(count)s member",
"Add existing rooms & spaces": "Add existing rooms & spaces", "Add existing rooms & spaces": "Add existing rooms & spaces",
"Default Rooms": "Default Rooms", "Default Rooms": "Default Rooms",
"Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.", "Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.",
"%(count)s members|other": "%(count)s members",
"%(count)s members|one": "%(count)s member",
"<inviter/> invited you to <name/>": "<inviter/> invited you to <name/>",
"You have been invited to <name/>": "You have been invited to <name/>",
"Your public space <name/>": "Your public space <name/>", "Your public space <name/>": "Your public space <name/>",
"Your private space <name/>": "Your private space <name/>", "Your private space <name/>": "Your private space <name/>",
"Welcome to <name/>": "Welcome to <name/>", "Welcome to <name/>": "Welcome to <name/>",
@ -2720,13 +2722,8 @@
"Decide where your account is hosted": "Decide where your account is hosted", "Decide where your account is hosted": "Decide where your account is hosted",
"Use Security Key or Phrase": "Use Security Key or Phrase", "Use Security Key or Phrase": "Use Security Key or Phrase",
"Use Security Key": "Use Security Key", "Use Security Key": "Use Security Key",
"Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.": "Confirm your identity by verifying this login from one of your other sessions, granting it access to encrypted messages.", "Verify with another session": "Verify with another session",
"This requires the latest %(brand)s on your other devices:": "This requires the latest %(brand)s on your other devices:", "Verify this login to access your encrypted messages and prove to others that this login is really you.": "Verify this login to access your encrypted messages and prove to others that this login is really you.",
"%(brand)s Web": "%(brand)s Web",
"%(brand)s Desktop": "%(brand)s Desktop",
"%(brand)s iOS": "%(brand)s iOS",
"%(brand)s Android": "%(brand)s Android",
"or another cross-signing capable Matrix client": "or another cross-signing capable Matrix client",
"Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.", "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.",
"Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.", "Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.",
"Without completing security on this session, it wont have access to encrypted messages.": "Without completing security on this session, it wont have access to encrypted messages.", "Without completing security on this session, it wont have access to encrypted messages.": "Without completing security on this session, it wont have access to encrypted messages.",

View file

@ -19,11 +19,12 @@ import { MatrixClientPeg } from '../MatrixClientPeg';
import { accessSecretStorage, AccessCancelledError } from '../SecurityManager'; import { accessSecretStorage, AccessCancelledError } from '../SecurityManager';
import { PHASE_DONE as VERIF_PHASE_DONE } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import { PHASE_DONE as VERIF_PHASE_DONE } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
export const PHASE_INTRO = 0; export const PHASE_LOADING = 0;
export const PHASE_BUSY = 1; export const PHASE_INTRO = 1;
export const PHASE_DONE = 2; //final done stage, but still showing UX export const PHASE_BUSY = 2;
export const PHASE_CONFIRM_SKIP = 3; export const PHASE_DONE = 3; //final done stage, but still showing UX
export const PHASE_FINISHED = 4; //UX can be closed export const PHASE_CONFIRM_SKIP = 4;
export const PHASE_FINISHED = 5; //UX can be closed
export class SetupEncryptionStore extends EventEmitter { export class SetupEncryptionStore extends EventEmitter {
static sharedInstance() { static sharedInstance() {
@ -36,7 +37,7 @@ export class SetupEncryptionStore extends EventEmitter {
return; return;
} }
this._started = true; this._started = true;
this.phase = PHASE_BUSY; this.phase = PHASE_LOADING;
this.verificationRequest = null; this.verificationRequest = null;
this.backupInfo = null; this.backupInfo = null;
@ -75,7 +76,8 @@ export class SetupEncryptionStore extends EventEmitter {
} }
async fetchKeyInfo() { async fetchKeyInfo() {
const keys = await MatrixClientPeg.get().isSecretStored('m.cross_signing.master', false); const cli = MatrixClientPeg.get();
const keys = await cli.isSecretStored('m.cross_signing.master', false);
if (keys === null || Object.keys(keys).length === 0) { if (keys === null || Object.keys(keys).length === 0) {
this.keyId = null; this.keyId = null;
this.keyInfo = null; this.keyInfo = null;
@ -85,7 +87,20 @@ export class SetupEncryptionStore extends EventEmitter {
this.keyInfo = keys[this.keyId]; this.keyInfo = keys[this.keyId];
} }
// do we have any other devices which are E2EE which we can verify against?
const dehydratedDevice = await cli.getDehydratedDevice();
this.hasDevicesToVerifyAgainst = cli.getStoredDevicesForUser(cli.getUserId()).some(
device =>
device.getIdentityKey() &&
(!dehydratedDevice || (device.deviceId != dehydratedDevice.device_id)),
);
if (!this.hasDevicesToVerifyAgainst && !this.keyInfo) {
// skip before we can even render anything.
this.phase = PHASE_FINISHED;
} else {
this.phase = PHASE_INTRO; this.phase = PHASE_INTRO;
}
this.emit("update"); this.emit("update");
} }

View file

@ -39,7 +39,7 @@ export const showToast = (deviceIds: Set<string>) => {
ToastStore.sharedInstance().addOrReplaceToast({ ToastStore.sharedInstance().addOrReplaceToast({
key: TOAST_KEY, key: TOAST_KEY,
title: _t("Review where youre logged in"), title: _t("You have unverified logins"),
icon: "verification_warning", icon: "verification_warning",
props: { props: {
description: _t("Verify all your sessions to ensure your account & messages are safe"), description: _t("Verify all your sessions to ensure your account & messages are safe"),

View file

@ -15,38 +15,34 @@ limitations under the License.
*/ */
import { _t } from '../languageHandler'; import { _t } from '../languageHandler';
import dis from "../dispatcher/dispatcher";
import { MatrixClientPeg } from '../MatrixClientPeg'; import { MatrixClientPeg } from '../MatrixClientPeg';
import Modal from '../Modal';
import DeviceListener from '../DeviceListener'; import DeviceListener from '../DeviceListener';
import NewSessionReviewDialog from '../components/views/dialogs/NewSessionReviewDialog';
import ToastStore from "../stores/ToastStore"; import ToastStore from "../stores/ToastStore";
import GenericToast from "../components/views/toasts/GenericToast"; import GenericToast from "../components/views/toasts/GenericToast";
import { Action } from "../dispatcher/actions";
import { USER_SECURITY_TAB } from "../components/views/dialogs/UserSettingsDialog";
function toastKey(deviceId: string) { function toastKey(deviceId: string) {
return "unverified_session_" + deviceId; return "unverified_session_" + deviceId;
} }
export const showToast = (deviceId: string) => { export const showToast = async (deviceId: string) => {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const onAccept = () => { const onAccept = () => {
Modal.createTrackedDialog('New Session Review', 'Starting dialog', NewSessionReviewDialog, {
userId: cli.getUserId(),
device: cli.getStoredDevice(cli.getUserId(), deviceId),
onFinished: (r) => {
if (!r) {
/* This'll come back false if the user clicks "this wasn't me" and saw a warning dialog */
DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]); DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]);
} dis.dispatch({
}, action: Action.ViewUserSettings,
}, null, /* priority = */ false, /* static = */ true); initialTabId: USER_SECURITY_TAB,
});
}; };
const onReject = () => { const onReject = () => {
DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]); DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]);
}; };
const device = cli.getStoredDevice(cli.getUserId(), deviceId); const device = await cli.getDevice(deviceId);
ToastStore.sharedInstance().addOrReplaceToast({ ToastStore.sharedInstance().addOrReplaceToast({
key: toastKey(deviceId), key: toastKey(deviceId),
@ -54,8 +50,13 @@ export const showToast = (deviceId: string) => {
icon: "verification_warning", icon: "verification_warning",
props: { props: {
description: _t( description: _t(
"Verify the new login accessing your account: %(name)s", { name: device.getDisplayName()}), "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s", {
acceptLabel: _t("Verify"), name: device.display_name,
deviceID: deviceId,
ip: device.last_seen_ip,
},
),
acceptLabel: _t("Check your devices"),
onAccept, onAccept,
rejectLabel: _t("Later"), rejectLabel: _t("Later"),
onReject, onReject,