diff --git a/res/css/_common.scss b/res/css/_common.scss index 8ae1cc6641..36a81e6651 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -489,54 +489,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { 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 // used by SenderProfile & RoomPreviewBar .mx_Username_color1 { diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index 38310d39a9..60abe36c29 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -20,6 +20,8 @@ $SpaceRoomViewInnerWidth: 428px; .mx_MainSplit > div:first-child { padding: 80px 60px; flex-grow: 1; + max-height: 100%; + overflow-y: auto; h1 { margin: 0; @@ -69,9 +71,116 @@ $SpaceRoomViewInnerWidth: 428px; } } - .mx_SpaceRoomView_landing { - overflow-y: auto; + .mx_SpaceRoomView_preview { + 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 > .mx_BaseAvatar_image { border-radius: 12px; @@ -128,14 +237,6 @@ $SpaceRoomViewInnerWidth: 428px; font-size: $font-15px; } - .mx_SpaceRoomView_landing_joinButtons { - margin-top: 24px; - - .mx_FormButton { - padding: 8px 22px; - } - } - .mx_SpaceRoomView_landing_adminButtons { margin-top: 32px; diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss index 9c26f8f120..0075dcb511 100644 --- a/res/css/views/elements/_AccessibleButton.scss +++ b/res/css/views/elements/_AccessibleButton.scss @@ -26,7 +26,9 @@ limitations under the License. padding: 7px 18px; text-align: center; border-radius: 8px; - display: inline-block; + display: inline-flex; + align-items: center; + justify-content: center; font-size: $font-14px; } diff --git a/res/css/views/elements/_FormButton.scss b/res/css/views/elements/_FormButton.scss index 7ec01f17e6..eda201ff03 100644 --- a/res/css/views/elements/_FormButton.scss +++ b/res/css/views/elements/_FormButton.scss @@ -33,4 +33,10 @@ limitations under the License. color: $notice-primary-color; background-color: $notice-primary-bg-color; } + + &.mx_AccessibleButton_kind_secondary { + color: $secondary-fg-color; + border: 1px solid $secondary-fg-color; + background-color: unset; + } } diff --git a/res/css/views/right_panel/_EncryptionInfo.scss b/res/css/views/right_panel/_EncryptionInfo.scss index e13b1b6802..b3d4275f60 100644 --- a/res/css/views/right_panel/_EncryptionInfo.scss +++ b/res/css/views/right_panel/_EncryptionInfo.scss @@ -14,13 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_UserInfo { - .mx_EncryptionInfo_spinner { - .mx_Spinner { - margin-top: 25px; - margin-bottom: 15px; - } - - text-align: center; +.mx_EncryptionInfo_spinner { + .mx_Spinner { + margin-top: 25px; + margin-bottom: 15px; } + + text-align: center; } diff --git a/scripts/reskindex.js b/scripts/reskindex.js index 12310b77c1..5eaec4d1d5 100755 --- a/scripts/reskindex.js +++ b/scripts/reskindex.js @@ -1,5 +1,6 @@ #!/usr/bin/env node const fs = require('fs'); +const { promises: fsp } = fs; const path = require('path'); const glob = require('glob'); const util = require('util'); @@ -25,6 +26,8 @@ async function reskindex() { const header = args.h || args.header; 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) { strm.write(fs.readFileSync(header)); @@ -53,14 +56,9 @@ async function reskindex() { strm.write("export {components};\n"); // Ensure the file has been fully written to disk before proceeding + await util.promisify(fs.fsync)(strm.fd); await util.promisify(strm.end); - fs.rename(componentIndexTmp, componentIndex, function(err) { - if (err) { - console.error("Error moving new index into place: " + err); - } else { - console.log('Reskindex: completed'); - } - }); + await fsp.rename(componentIndexTmp, componentIndex); } // Expects both arrays of file names to be sorted @@ -77,9 +75,17 @@ function filesHaveChanged(files, prevFiles) { 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 if (!args.w) { - reskindex(); + run(); return; } @@ -87,5 +93,5 @@ let watchDebouncer = null; chokidar.watch(path.join(componentsDir, componentJsGlob)).on('all', (event, path) => { if (path === componentIndex) return; if (watchDebouncer) clearTimeout(watchDebouncer); - watchDebouncer = setTimeout(reskindex, 1000); + watchDebouncer = setTimeout(run, 1000); }); diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index b57638413b..706cd5ded8 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -1913,7 +1913,7 @@ export default class RoomView extends React.Component { ); } - if (this.state.room?.isSpaceRoom()) { + if (SettingsStore.getValue("feature_spaces") && this.state.room?.isSpaceRoom()) { return { return membership; }; -const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => { +const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => { const cli = useContext(MatrixClientContext); const myMembership = useMyRoomMembership(space); - const joinRule = space.getJoinRule(); - const userId = cli.getUserId(); + let inviterSection; let joinButtons; if (myMembership === "invite") { - joinButtons =
- - - {_t("Decline")} - -
; - } else if (myMembership !== "join" && joinRule === "public") { - joinButtons =
- -
; + const inviteSender = space.getMember(cli.getUserId())?.events.member?.getSender(); + const inviter = inviteSender && space.getMember(inviteSender); + + if (inviteSender) { + inviterSection =
+ +
+
+ { _t(" invites you", {}, { + inviter: () => { inviter.name || inviteSender }, + }) } +
+ { inviter ?
+ { inviteSender } +
: null } +
+
; + } + + joinButtons = <> + + + ; + } else { + joinButtons = } + let visibilitySection; + if (space.getJoinRule() === "public") { + visibilitySection = + { _t("Public space") } + ; + } else { + visibilitySection = + { _t("Private space") } + ; + } + + return
+ { inviterSection } + +

+ +

+
+ { visibilitySection } + + {(count) => count > 0 ? ( + { + defaultDispatcher.dispatch({ + action: Action.SetRightPanelPhase, + phase: RightPanelPhases.RoomMemberList, + refireParams: { space }, + }); + }} + > + { _t("%(count)s members", { count }) } + + ) : null} + +
+ + {(topic, ref) => +
+ { topic } +
+ } +
+
+ { joinButtons } +
+
; +}; + +const SpaceLanding = ({ space }) => { + const cli = useContext(MatrixClientContext); + const myMembership = useMyRoomMembership(space); + const userId = cli.getUserId(); + let inviteButton; if (myMembership === "join" && space.canInvite(userId)) { inviteButton = ( @@ -227,26 +296,7 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => ) : null} }; - if (myMembership === "invite") { - const inviteSender = space.getMember(userId)?.events.member?.getSender(); - const inviter = inviteSender && space.getMember(inviteSender); - - if (inviteSender) { - return _t(" invited you to ", {}, { - name: tags.name, - inviter: () => inviter - ? - - { inviter.name } - - : - { inviteSender } - , - }) as JSX.Element; - } else { - return _t("You have been invited to ", {}, tags) as JSX.Element; - } - } else if (shouldShowSpaceSettings(cli, space)) { + if (shouldShowSpaceSettings(cli, space)) { if (space.getJoinRule() === "public") { return _t("Your public space ", {}, tags) as JSX.Element; } else { @@ -260,7 +310,6 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
- { joinButtons }
{ inviteButton } { addRoomButtons } @@ -548,12 +597,15 @@ export default class SpaceRoomView extends React.PureComponent { private renderBody() { switch (this.state.phase) { case Phase.Landing: - return ; - + if (this.props.space.getMyMembership() === "join") { + return ; + } else { + return ; + } case Phase.PublicCreateRooms: return ; title = _t("Verify this login"); } else if (phase === PHASE_DONE) { diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js index f66c434edd..e246b9cbd0 100644 --- a/src/components/structures/auth/SetupEncryptionBody.js +++ b/src/components/structures/auth/SetupEncryptionBody.js @@ -17,11 +17,13 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; -import SdkConfig from '../../../SdkConfig'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; +import Modal from '../../../Modal'; +import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog'; import * as sdk from '../../../index'; import { SetupEncryptionStore, + PHASE_LOADING, PHASE_INTRO, PHASE_BUSY, PHASE_DONE, @@ -83,6 +85,22 @@ export default class SetupEncryptionBody extends React.Component { 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 = () => { const store = SetupEncryptionStore.sharedInstance(); store.skip(); @@ -134,32 +152,22 @@ export default class SetupEncryptionBody extends React.Component { ; } - const brand = SdkConfig.get().brand; + let verifyButton; + if (store.hasDevicesToVerifyAgainst) { + verifyButton = + { _t("Verify with another session") } + ; + } return (

{_t( - "Confirm your identity by verifying this login from one of your other sessions, " + - "granting it access to encrypted messages.", + "Verify this login to access your encrypted messages and " + + "prove to others that this login is really you.", )}

-

{_t( - "This requires the latest %(brand)s on your other devices:", - { brand }, - )}

- -
-
-
{_t("%(brand)s Web", { brand })}
-
{_t("%(brand)s Desktop", { brand })}
-
-
-
{_t("%(brand)s iOS", { brand })}
-
{_t("%(brand)s Android", { brand })}
-
-

{_t("or another cross-signing capable Matrix client")}

-
+ {verifyButton} {useRecoveryKeyButton} {_t("Skip")} @@ -217,7 +225,7 @@ export default class SetupEncryptionBody extends React.Component {
); - } else if (phase === PHASE_BUSY) { + } else if (phase === PHASE_BUSY || phase === PHASE_LOADING) { const Spinner = sdk.getComponent('views.elements.Spinner'); return ; } else { diff --git a/src/components/views/dialogs/NewSessionReviewDialog.js b/src/components/views/dialogs/NewSessionReviewDialog.js index e17501da40..5172f29405 100644 --- a/src/components/views/dialogs/NewSessionReviewDialog.js +++ b/src/components/views/dialogs/NewSessionReviewDialog.js @@ -66,6 +66,10 @@ export default class NewSessionReviewDialog extends React.PureComponent { Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, { verificationRequestPromise: requestPromise, member: cli.getUser(userId), + onFinished: async () => { + const request = await requestPromise; + request.cancel(); + }, }); } diff --git a/src/components/views/dialogs/VerificationRequestDialog.js b/src/components/views/dialogs/VerificationRequestDialog.js index 574beebbc6..308dc6d622 100644 --- a/src/components/views/dialogs/VerificationRequestDialog.js +++ b/src/components/views/dialogs/VerificationRequestDialog.js @@ -27,11 +27,11 @@ export default class VerificationRequestDialog extends React.Component { verificationRequest: PropTypes.object, verificationRequestPromise: PropTypes.object, onFinished: PropTypes.func.isRequired, + member: PropTypes.string, }; constructor(...args) { super(...args); - this.onFinished = this.onFinished.bind(this); this.state = {}; if (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 ? _t("Verify other session") : _t("Verification Request"); - return ; } - - async onFinished() { - this.props.onFinished(); - let request = this.props.verificationRequest; - if (!request && this.props.verificationRequestPromise) { - request = await this.props.verificationRequestPromise; - } - request.cancel(); - } } diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index aa11b70e50..12a6a2a311 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -46,6 +46,7 @@ import EncryptionPanel from "./EncryptionPanel"; import {useAsyncMemo} from '../../../hooks/useAsyncMemo'; import {legacyVerifyUser, verifyDevice, verifyUser} from '../../../verification'; import {Action} from "../../../dispatcher/actions"; +import { USER_SECURITY_TAB } from "../dialogs/UserSettingsDialog"; import {useIsEncrypted} from "../../../hooks/useIsEncrypted"; import BaseCard from "./BaseCard"; import {E2EStatus} from "../../../utils/ShieldUtils"; @@ -1368,6 +1369,20 @@ const BasicUserInfo: React.FC<{ } } + let editDevices; + if (member.userId == cli.getUserId()) { + editDevices = (

+ { + dis.dispatch({ + action: Action.ViewUserSettings, + initialTabId: USER_SECURITY_TAB, + }); + }}> + { _t("Edit devices") } + +

) + } + const securitySection = (

{ _t("Security") }

@@ -1377,6 +1392,7 @@ const BasicUserInfo: React.FC<{ loading={showDeviceListSpinner} devices={devices} userId={member.userId} /> } + { editDevices }
); diff --git a/src/components/views/rooms/ReadReceiptMarker.js b/src/components/views/rooms/ReadReceiptMarker.js index ade84cbef3..7473aac7cd 100644 --- a/src/components/views/rooms/ReadReceiptMarker.js +++ b/src/components/views/rooms/ReadReceiptMarker.js @@ -158,14 +158,14 @@ export default class ReadReceiptMarker extends React.PureComponent { // then shift to the rightmost column, // 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 // 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 // value so small it's not perceived by a user. // Note: Any smaller values (or trying to interchange units) might cause read receipts to // fail to fall down or cause gaps. - startStyles.push({ top: startTopOffset+'px', left: '0.001px' }); + startStyles.push({ top: startTopOffset+'px', left: '1px' }); enterTransitionOpts.push({ duration: bounce ? Math.min(Math.log(Math.abs(startTopOffset)) * 200, 3000) : 300, easing: bounce ? 'easeOutBounce' : 'easeOutCubic', diff --git a/src/components/views/toasts/VerificationRequestToast.tsx b/src/components/views/toasts/VerificationRequestToast.tsx index 010c8fd12f..d3da282c1c 100644 --- a/src/components/views/toasts/VerificationRequestToast.tsx +++ b/src/components/views/toasts/VerificationRequestToast.tsx @@ -39,6 +39,7 @@ interface IProps { interface IState { counter: number; device?: DeviceInfo; + ip?: string; } @replaceableComponent("views.toasts.VerificationRequestToast") @@ -68,9 +69,15 @@ export default class VerificationRequestToast extends React.PureComponent { + request.cancel(); + }, }, null, /* priority = */ false, /* static = */ true); } await request.accept(); @@ -133,9 +143,10 @@ export default class VerificationRequestToast extends React.PureComponentanonymous usage data which helps us improve %(brand)s. This will use a cookie.": "Send anonymous usage data which helps us improve %(brand)s. This will use a cookie.", "Yes": "Yes", "No": "No", - "Review where you’re logged in": "Review where you’re 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", "Review": "Review", "Later": "Later", @@ -753,7 +753,8 @@ "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", "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", "Update": "Update", @@ -980,7 +981,7 @@ "Folder": "Folder", "Pin": "Pin", "Your server isn't responding to some requests.": "Your server isn't responding to some requests.", - "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)", "Accept to continue:": "Accept to continue:", "Delete": "Delete", @@ -1756,6 +1757,7 @@ "Failed to deactivate user": "Failed to deactivate user", "Role": "Role", "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.", + "Edit devices": "Edit devices", "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.", "Verify by scanning": "Verify by scanning", @@ -2609,14 +2611,14 @@ "Promoted to users": "Promoted to users", "Manage rooms": "Manage rooms", "Find a room...": "Find a room...", - "Accept Invite": "Accept Invite", + " invites you": " 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", "Default Rooms": "Default Rooms", "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", - " invited you to ": " invited you to ", - "You have been invited to ": "You have been invited to ", "Your public space ": "Your public space ", "Your private space ": "Your private space ", "Welcome to ": "Welcome to ", @@ -2720,13 +2722,8 @@ "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": "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.", - "This requires the latest %(brand)s on your other devices:": "This requires the latest %(brand)s on your other devices:", - "%(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", + "Verify with another session": "Verify with another session", + "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.", "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.", "Without completing security on this session, it won’t have access to encrypted messages.": "Without completing security on this session, it won’t have access to encrypted messages.", diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.js index 981ce6eca9..3839f27a77 100644 --- a/src/stores/SetupEncryptionStore.js +++ b/src/stores/SetupEncryptionStore.js @@ -19,11 +19,12 @@ import { MatrixClientPeg } from '../MatrixClientPeg'; import { accessSecretStorage, AccessCancelledError } from '../SecurityManager'; import { PHASE_DONE as VERIF_PHASE_DONE } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; -export const PHASE_INTRO = 0; -export const PHASE_BUSY = 1; -export const PHASE_DONE = 2; //final done stage, but still showing UX -export const PHASE_CONFIRM_SKIP = 3; -export const PHASE_FINISHED = 4; //UX can be closed +export const PHASE_LOADING = 0; +export const PHASE_INTRO = 1; +export const PHASE_BUSY = 2; +export const PHASE_DONE = 3; //final done stage, but still showing UX +export const PHASE_CONFIRM_SKIP = 4; +export const PHASE_FINISHED = 5; //UX can be closed export class SetupEncryptionStore extends EventEmitter { static sharedInstance() { @@ -36,7 +37,7 @@ export class SetupEncryptionStore extends EventEmitter { return; } this._started = true; - this.phase = PHASE_BUSY; + this.phase = PHASE_LOADING; this.verificationRequest = null; this.backupInfo = null; @@ -75,7 +76,8 @@ export class SetupEncryptionStore extends EventEmitter { } 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) { this.keyId = null; this.keyInfo = null; @@ -85,7 +87,20 @@ export class SetupEncryptionStore extends EventEmitter { this.keyInfo = keys[this.keyId]; } - this.phase = PHASE_INTRO; + // 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.emit("update"); } diff --git a/src/toasts/BulkUnverifiedSessionsToast.ts b/src/toasts/BulkUnverifiedSessionsToast.ts index 41717e0804..bc129ebd54 100644 --- a/src/toasts/BulkUnverifiedSessionsToast.ts +++ b/src/toasts/BulkUnverifiedSessionsToast.ts @@ -39,7 +39,7 @@ export const showToast = (deviceIds: Set) => { ToastStore.sharedInstance().addOrReplaceToast({ key: TOAST_KEY, - title: _t("Review where you’re logged in"), + title: _t("You have unverified logins"), icon: "verification_warning", props: { description: _t("Verify all your sessions to ensure your account & messages are safe"), diff --git a/src/toasts/UnverifiedSessionToast.ts b/src/toasts/UnverifiedSessionToast.ts index 9dedd2b137..e0ea323033 100644 --- a/src/toasts/UnverifiedSessionToast.ts +++ b/src/toasts/UnverifiedSessionToast.ts @@ -15,38 +15,34 @@ limitations under the License. */ import { _t } from '../languageHandler'; +import dis from "../dispatcher/dispatcher"; import { MatrixClientPeg } from '../MatrixClientPeg'; -import Modal from '../Modal'; import DeviceListener from '../DeviceListener'; -import NewSessionReviewDialog from '../components/views/dialogs/NewSessionReviewDialog'; import ToastStore from "../stores/ToastStore"; 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) { return "unverified_session_" + deviceId; } -export const showToast = (deviceId: string) => { +export const showToast = async (deviceId: string) => { const cli = MatrixClientPeg.get(); 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]); - } - }, - }, null, /* priority = */ false, /* static = */ true); + DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]); + dis.dispatch({ + action: Action.ViewUserSettings, + initialTabId: USER_SECURITY_TAB, + }); }; const onReject = () => { DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]); }; - const device = cli.getStoredDevice(cli.getUserId(), deviceId); + const device = await cli.getDevice(deviceId); ToastStore.sharedInstance().addOrReplaceToast({ key: toastKey(deviceId), @@ -54,8 +50,13 @@ export const showToast = (deviceId: string) => { icon: "verification_warning", props: { description: _t( - "Verify the new login accessing your account: %(name)s", { name: device.getDisplayName()}), - acceptLabel: _t("Verify"), + "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s", { + name: device.display_name, + deviceID: deviceId, + ip: device.last_seen_ip, + }, + ), + acceptLabel: _t("Check your devices"), onAccept, rejectLabel: _t("Later"), onReject,