diff --git a/package.json b/package.json index 78bbb5b4c6..2c4d0144d4 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "linkifyjs": "^2.1.6", "lodash": "^4.17.14", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "minimist": "^1.2.0", "pako": "^1.0.5", "png-chunks-extract": "^1.0.0", "prop-types": "^15.5.8", diff --git a/res/css/_components.scss b/res/css/_components.scss index 22c9b73dca..bc636eb3c6 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -128,7 +128,6 @@ @import "./views/messages/_MEmoteBody.scss"; @import "./views/messages/_MFileBody.scss"; @import "./views/messages/_MImageBody.scss"; -@import "./views/messages/_MKeyVerificationRequest.scss"; @import "./views/messages/_MNoticeBody.scss"; @import "./views/messages/_MStickerBody.scss"; @import "./views/messages/_MTextBody.scss"; @@ -143,6 +142,7 @@ @import "./views/messages/_TextualEvent.scss"; @import "./views/messages/_UnknownBody.scss"; @import "./views/messages/_ViewSourceEvent.scss"; +@import "./views/messages/_common_CryptoEvent.scss"; @import "./views/right_panel/_EncryptionInfo.scss"; @import "./views/right_panel/_UserInfo.scss"; @import "./views/right_panel/_VerificationPanel.scss"; diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss index 973f6fe9b3..3c373e8883 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,7 +19,7 @@ limitations under the License. overflow-x: hidden; flex: 0 0 auto; position: relative; - min-width: 250px; + min-width: 264px; display: flex; flex-direction: column; } diff --git a/res/css/structures/_RoomDirectory.scss b/res/css/structures/_RoomDirectory.scss index 4b49332af7..5ae8df7176 100644 --- a/res/css/structures/_RoomDirectory.scss +++ b/res/css/structures/_RoomDirectory.scss @@ -119,6 +119,16 @@ limitations under the License. display: inline-block; } +.mx_RoomDirectory_perm { + border-radius: 10px; + display: inline-block; + height: 20px; + line-height: 20px; + padding: 0 5px; + color: $accent-fg-color; + background-color: $rte-room-pill-color; +} + .mx_RoomDirectory_topic { cursor: initial; color: $light-fg-color; diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss index 5b5c49f357..d1687743d6 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -98,5 +98,9 @@ limitations under the License. margin: 4px 0 11px 0; font-size: 12px; } + + .mx_Toast_deviceID { + font-size: 10px; + } } } diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss index 71fab50339..5e0893b8fd 100644 --- a/res/css/views/dialogs/_InviteDialog.scss +++ b/res/css/views/dialogs/_InviteDialog.scss @@ -62,7 +62,7 @@ limitations under the License. } .mx_InviteDialog_goButton { - width: 48px; + min-width: 48px; margin-left: 10px; height: 25px; line-height: 25px; @@ -131,7 +131,7 @@ limitations under the License. height: 24px; grid-column: 1; grid-row: 1; - mask-image: url('$(res)/img/feather-customised/check.svg'); + mask-image: url("$(res)/img/feather-customised/check.svg"); mask-size: 100%; mask-repeat: no-repeat; position: absolute; diff --git a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss index bbbf3fc1d3..a9ebd54b31 100644 --- a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss +++ b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss @@ -37,6 +37,10 @@ limitations under the License. flex: 0 0 auto; margin-left: 30px; } + + details .mx_AccessibleButton { + margin: 1em 0; // emulate paragraph spacing because we can't put this button in a paragraph due to HTML rules + } } .mx_CreateSecretStorageDialog .mx_Dialog_title { diff --git a/res/css/views/messages/_MKeyVerificationRequest.scss b/res/css/views/messages/_common_CryptoEvent.scss similarity index 61% rename from res/css/views/messages/_MKeyVerificationRequest.scss rename to res/css/views/messages/_common_CryptoEvent.scss index ee20751083..98e1e97e39 100644 --- a/res/css/views/messages/_MKeyVerificationRequest.scss +++ b/res/css/views/messages/_common_CryptoEvent.scss @@ -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. @@ -14,60 +14,62 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_KeyVerification { +.mx_cryptoEvent { display: grid; grid-template-columns: 24px minmax(0, 1fr) min-content; - &.mx_KeyVerification_icon::after { + &.mx_cryptoEvent_icon::after { grid-column: 1; grid-row: 1 / 3; - width: 12px; + width: 16px; height: 16px; content: ""; - mask-image: url("$(res)/img/e2e/normal.svg"); - mask-repeat: no-repeat; - mask-size: 100%; + background-image: url("$(res)/img/e2e/normal.svg"); + background-repeat: no-repeat; + background-size: 100%; margin-top: 4px; - background-color: $primary-fg-color; } - &.mx_KeyVerification_icon_verified::after { - mask-image: url("$(res)/img/e2e/verified.svg"); - background-color: $accent-color; + &.mx_cryptoEvent_icon_verified::after { + background-image: url("$(res)/img/e2e/verified.svg"); } - .mx_KeyVerification_title, .mx_KeyVerification_subtitle, .mx_KeyVerification_state { + &.mx_cryptoEvent_icon_warning::after { + background-image: url("$(res)/img/e2e/warning.svg"); + } + + .mx_cryptoEvent_title, .mx_cryptoEvent_subtitle, .mx_cryptoEvent_state { overflow-wrap: break-word; } - .mx_KeyVerification_title { + .mx_cryptoEvent_title { font-weight: 600; font-size: 15px; grid-column: 2; grid-row: 1; } - .mx_KeyVerification_subtitle { + .mx_cryptoEvent_subtitle { grid-column: 2; grid-row: 2; } - .mx_KeyVerification_state, .mx_KeyVerification_subtitle { + .mx_cryptoEvent_state, .mx_cryptoEvent_subtitle { font-size: 12px; } - .mx_KeyVerification_state, .mx_KeyVerification_buttons { + .mx_cryptoEvent_state, .mx_cryptoEvent_buttons { grid-column: 3; grid-row: 1 / 3; } - .mx_KeyVerification_buttons { + .mx_cryptoEvent_buttons { align-items: center; display: flex; } - .mx_KeyVerification_state { + .mx_cryptoEvent_state { width: 130px; padding: 10px 20px; margin: auto 0; diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index dc22de4713..46d5e99d64 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -211,7 +211,7 @@ limitations under the License. padding-bottom: 16px; } - .mx_UserInfo_scrollContainer:not(.mx_UserInfo_separator) { + .mx_UserInfo_container:not(.mx_UserInfo_separator) { padding-top: 16px; padding-bottom: 0; @@ -256,15 +256,14 @@ limitations under the License. .mx_UserInfo_expand { display: flex; margin-top: 11px; - color: $accent-color; } } - .mx_UserInfo_verify { + .mx_UserInfo_wideButton { display: block; margin: 16px 0; } - button.mx_UserInfo_verify { + button.mx_UserInfo_wideButton { width: 100%; // FIXME get rid of this once we get rid of DialogButtons here } } diff --git a/res/css/views/right_panel/_VerificationPanel.scss b/res/css/views/right_panel/_VerificationPanel.scss index 75b469cef9..827f2a2c49 100644 --- a/res/css/views/right_panel/_VerificationPanel.scss +++ b/res/css/views/right_panel/_VerificationPanel.scss @@ -16,7 +16,8 @@ limitations under the License. .mx_UserInfo { .mx_VerificationPanel_verified_section .mx_E2EIcon { - margin: 0 auto; + // Override general user info margin + margin: 0 auto !important; } .mx_VerificationPanel_qrCode { @@ -25,7 +26,8 @@ limitations under the License. border-radius: 4px; width: max-content; max-width: 100%; - margin: 0 auto; + // Override general user info margin + margin: 0 auto !important; canvas { // override height and width which are set on the element directly diff --git a/res/css/views/rooms/_EntityTile.scss b/res/css/views/rooms/_EntityTile.scss index 2b6b31acb4..a2867de3a7 100644 --- a/res/css/views/rooms/_EntityTile.scss +++ b/res/css/views/rooms/_EntityTile.scss @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,6 +20,15 @@ limitations under the License. align-items: center; color: $primary-fg-color; cursor: pointer; + + .mx_E2EIcon { + margin: 0; + position: absolute; + bottom: 2px; + right: 7px; + height: 15px; + width: 15px; + } } .mx_EntityTile:hover { @@ -30,7 +40,7 @@ limitations under the License. content: ""; position: absolute; top: calc(50% - 8px); // center - right: 10px; + right: -8px; mask: url('$(res)/img/member_chevron.png'); mask-repeat: no-repeat; width: 16px; @@ -64,14 +74,6 @@ limitations under the License. position: relative; } -.mx_EntityTile_power { - position: absolute; - width: 16px; - height: 17px; - top: 0px; - right: 6px; -} - .mx_EntityTile_name, .mx_GroupRoomTile_name { flex: 1 1 0; @@ -83,6 +85,7 @@ limitations under the License. .mx_EntityTile_details { overflow: hidden; + flex: 1; } .mx_EntityTile_ellipsis .mx_EntityTile_name { @@ -112,10 +115,6 @@ limitations under the License. opacity: 0.25; } -.mx_EntityTile:not(.mx_EntityTile_noHover):hover .mx_EntityTile_name { - font-size: 13px; -} - .mx_EntityTile_subtext { font-size: 11px; opacity: 0.5; @@ -123,3 +122,17 @@ limitations under the License. white-space: nowrap; text-overflow: clip; } + +.mx_EntityTile_power { + padding-inline-start: 6px; + font-size: 10px; + color: $notice-secondary-color; + max-width: 6em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.mx_EntityTile:hover .mx_EntityTile_power { + display: none; +} diff --git a/res/css/views/rooms/_InviteOnlyIcon.scss b/res/css/views/rooms/_InviteOnlyIcon.scss index e70586bb73..6943d1797b 100644 --- a/res/css/views/rooms/_InviteOnlyIcon.scss +++ b/res/css/views/rooms/_InviteOnlyIcon.scss @@ -20,7 +20,7 @@ limitations under the License. position: relative; display: block !important; // Align the padlock with unencrypted room names - margin-left: 6px; + margin: 0 4px; &::before { background-color: $roomtile-name-color; @@ -34,5 +34,7 @@ limitations under the License. bottom: 0; left: 0; right: 0; + width: 12px; + height: 12px; } } diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index fae9d0dfe3..a05b4c0c0e 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -76,8 +76,8 @@ limitations under the License. left: 60px; margin-right: 0; // Counteract the E2EIcon class margin-left: 3px; // Counteract the E2EIcon class - width: 12px; - height: 12px; + width: 15px; + height: 15px; } .mx_MessageComposer_noperm_error { diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index 6f0377b29c..47b8131ef0 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -21,10 +21,10 @@ limitations under the License. .mx_E2EIcon { margin: 0; position: absolute; - bottom: -1px; - right: -2px; - height: 10px; - width: 10px; + bottom: -2px; + right: -6px; + height: 15px; + width: 15px; } } diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index a24fdf2629..31d887cbbb 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -101,19 +101,19 @@ limitations under the License. // Note we match .mx_E2EIcon to make sure this matches more tightly than just // .mx_E2EIcon on its own .mx_RoomTile_e2eIcon.mx_E2EIcon { - height: 10px; - width: 10px; + height: 14px; + width: 14px; display: block; position: absolute; - bottom: -1px; - right: -2px; + bottom: -2px; + right: -5px; z-index: 1; margin: 0; } .mx_RoomTile_name { font-size: 14px; - padding: 0 6px; + padding: 0 4px; color: $roomtile-name-color; white-space: nowrap; overflow-x: hidden; @@ -214,8 +214,3 @@ limitations under the License. .mx_GroupInviteTile .mx_RoomTile_name { flex: 1; } - -.mx_InviteOnlyIcon + .mx_RoomTile_nameContainer .mx_RoomTile_name { - // Scoot the padding in a bit from 6px to make it look better - padding-left: 3px; -} diff --git a/res/css/views/settings/_DevicesPanel.scss b/res/css/views/settings/_DevicesPanel.scss index 581ff47fc1..49debe842f 100644 --- a/res/css/views/settings/_DevicesPanel.scss +++ b/res/css/views/settings/_DevicesPanel.scss @@ -18,7 +18,7 @@ limitations under the License. display: table; table-layout: fixed; width: 880px; - border-spacing: 2px; + border-spacing: 10px; } .mx_DevicesPanel_header { @@ -32,7 +32,11 @@ limitations under the License. .mx_DevicesPanel_header > div { display: table-cell; - vertical-align: bottom; + vertical-align: middle; +} + +.mx_DevicesPanel_header .mx_DevicesPanel_deviceName { + width: 50%; } .mx_DevicesPanel_header .mx_DevicesPanel_deviceLastSeen { diff --git a/res/css/views/verification/_VerificationShowSas.scss b/res/css/views/verification/_VerificationShowSas.scss index a0da7e2539..5038d40b73 100644 --- a/res/css/views/verification/_VerificationShowSas.scss +++ b/res/css/views/verification/_VerificationShowSas.scss @@ -1,5 +1,6 @@ /* Copyright 2019 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. @@ -28,21 +29,35 @@ limitations under the License. .mx_VerificationShowSas_emojiSas { text-align: center; + display: flex; + flex-wrap: wrap; + justify-content: center; + margin: 25px 0; } .mx_VerificationShowSas_emojiSas_block { display: inline-block; - margin-left: 15px; - margin-right: 15px; - text-align: center; - margin-bottom: 20px; + margin-bottom: 16px; + position: relative; + width: 52px; +} + +.mx_Dialog .mx_VerificationShowSas_emojiSas_block, +.mx_AuthPage_modal .mx_VerificationShowSas_emojiSas_block { + width: 60px; } .mx_VerificationShowSas_emojiSas_emoji { - font-size: 48px; + font-size: 32px; } .mx_VerificationShowSas_emojiSas_label { - text-align: center; - font-weight: bold; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + font-size: 12px; +} + +.mx_VerificationShowSas_emojiSas_break { + flex-basis: 100%; } diff --git a/res/img/admin.svg b/res/img/admin.svg deleted file mode 100644 index 7ea7459304..0000000000 --- a/res/img/admin.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - icons_owner - Created with sketchtool. - - - - - - - - - - - - diff --git a/res/img/mod.svg b/res/img/mod.svg deleted file mode 100644 index 847baf98f9..0000000000 --- a/res/img/mod.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - icons_admin - Created with sketchtool. - - - - - - - - - - - diff --git a/scripts/reskindex.js b/scripts/reskindex.js index 3919295078..81ab111f46 100755 --- a/scripts/reskindex.js +++ b/scripts/reskindex.js @@ -2,7 +2,7 @@ var fs = require('fs'); var path = require('path'); var glob = require('glob'); -var args = require('optimist').argv; +var args = require('minimist')(process.argv); var chokidar = require('chokidar'); var componentIndex = path.join('src', 'component-index.js'); diff --git a/src/CallHandler.js b/src/CallHandler.js index 33e15d3cc9..1551b57313 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -139,7 +139,7 @@ function _setCallListeners(call) { Modal.createTrackedDialog('Call Failed', '', QuestionDialog, { title: _t('Call Failed'), description: _t( - "There are unknown devices in this room: "+ + "There are unknown sessions in this room: "+ "if you proceed without verifying them, it will be "+ "possible for someone to eavesdrop on your call.", ), diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index 085764214f..a560c956f1 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -20,6 +20,7 @@ import {MatrixClientPeg} from './MatrixClientPeg'; import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase'; import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; import { _t } from './languageHandler'; +import SettingsStore from './settings/SettingsStore'; // This stores the secret storage private keys in memory for the JS SDK. This is // only meant to act as a cache to avoid prompting the user multiple times @@ -27,7 +28,20 @@ import { _t } from './languageHandler'; // single secret storage operation, as it will clear the cached keys once the // operation ends. let secretStorageKeys = {}; -let cachingAllowed = false; +let secretStorageBeingAccessed = false; + +function isCachingAllowed() { + return ( + secretStorageBeingAccessed || + SettingsStore.getValue("keepSecretStoragePassphraseForSession") + ); +} + +export class AccessCancelledError extends Error { + constructor() { + super("Secret storage access canceled"); + } +} async function getSecretStorageKey({ keys: keyInfos }) { const keyInfoEntries = Object.entries(keyInfos); @@ -37,7 +51,7 @@ async function getSecretStorageKey({ keys: keyInfos }) { const [name, info] = keyInfoEntries[0]; // Check the in-memory cache - if (cachingAllowed && secretStorageKeys[name]) { + if (isCachingAllowed() && secretStorageKeys[name]) { return [name, secretStorageKeys[name]]; } @@ -66,12 +80,12 @@ async function getSecretStorageKey({ keys: keyInfos }) { ); const [input] = await finished; if (!input) { - throw new Error("Secret storage access canceled"); + throw new AccessCancelledError(); } const key = await inputToKey(input); // Save to cache to avoid future prompts in the current session - if (cachingAllowed) { + if (isCachingAllowed()) { secretStorageKeys[name] = key; } @@ -104,7 +118,7 @@ export const crossSigningCallbacks = { */ export async function accessSecretStorage(func = async () => { }) { const cli = MatrixClientPeg.get(); - cachingAllowed = true; + secretStorageBeingAccessed = true; try { if (!await cli.hasSecretStorageKey()) { @@ -125,7 +139,7 @@ export async function accessSecretStorage(func = async () => { }) { const { finished } = Modal.createTrackedDialog( 'Cross-signing keys dialog', '', InteractiveAuthDialog, { - title: _t("Send cross-signing keys to homeserver"), + title: _t("Setting up keys"), matrixClient: MatrixClientPeg.get(), makeRequest, }, @@ -143,7 +157,9 @@ export async function accessSecretStorage(func = async () => { }) { return await func(); } finally { // Clear secret storage key cache now that work is complete - cachingAllowed = false; - secretStorageKeys = {}; + secretStorageBeingAccessed = false; + if (!isCachingAllowed()) { + secretStorageKeys = {}; + } } } diff --git a/src/DeviceListener.js b/src/DeviceListener.js index 630d1a61c0..4e7bc8470d 100644 --- a/src/DeviceListener.js +++ b/src/DeviceListener.js @@ -21,7 +21,7 @@ import { _t } from './languageHandler'; import ToastStore from './stores/ToastStore'; function toastKey(deviceId) { - return 'newsession_' + deviceId; + return 'unverified_session_' + deviceId; } const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000; @@ -77,8 +77,8 @@ export default class DeviceListener { this._recheck(); } - _onDeviceVerificationChanged = (users) => { - if (!users.includes(MatrixClientPeg.get().getUserId())) return; + _onDeviceVerificationChanged = (userId) => { + if (userId !== MatrixClientPeg.get().getUserId()) return; this._recheck(); } @@ -160,10 +160,10 @@ export default class DeviceListener { this._activeNagToasts.add(device.deviceId); ToastStore.sharedInstance().addOrReplaceToast({ key: toastKey(device.deviceId), - title: _t("New Session"), + title: _t("Unverified session"), icon: "verification_warning", - props: {deviceId: device.deviceId}, - component: sdk.getComponent("toasts.NewSessionToast"), + props: { device }, + component: sdk.getComponent("toasts.UnverifiedSessionToast"), }); newActiveToasts.add(device.deviceId); } diff --git a/src/Entities.js b/src/Entities.js deleted file mode 100644 index 872a837f3a..0000000000 --- a/src/Entities.js +++ /dev/null @@ -1,137 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd - -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 * as sdk from './index'; - -function isMatch(query, name, uid) { - query = query.toLowerCase(); - name = name.toLowerCase(); - uid = uid.toLowerCase(); - - // direct prefix matches - if (name.indexOf(query) === 0 || uid.indexOf(query) === 0) { - return true; - } - - // strip @ on uid and try matching again - if (uid.length > 1 && uid[0] === "@" && uid.substring(1).indexOf(query) === 0) { - return true; - } - - // split spaces in name and try matching constituent parts - const parts = name.split(" "); - for (let i = 0; i < parts.length; i++) { - if (parts[i].indexOf(query) === 0) { - return true; - } - } - return false; -} - -/* - * Converts various data models to Entity objects. - * - * Entity objects provide an interface for UI components to use to display - * members in a data-agnostic way. This means they don't need to care if the - * underlying data model is a RoomMember, User or 3PID data structure, it just - * cares about rendering. - */ - -class Entity { - constructor(model) { - this.model = model; - } - - getJsx() { - return null; - } - - matches(queryString) { - return false; - } -} - -class MemberEntity extends Entity { - getJsx() { - const MemberTile = sdk.getComponent("rooms.MemberTile"); - return ( - - ); - } - - matches(queryString) { - return isMatch(queryString, this.model.name, this.model.userId); - } -} - -class UserEntity extends Entity { - constructor(model, showInviteButton, inviteFn) { - super(model); - this.showInviteButton = Boolean(showInviteButton); - this.inviteFn = inviteFn; - this.onClick = this.onClick.bind(this); - } - - onClick() { - if (this.inviteFn) { - this.inviteFn(this.model.userId); - } - } - - getJsx() { - const UserTile = sdk.getComponent("rooms.UserTile"); - return ( - - ); - } - - matches(queryString) { - const name = this.model.displayName || this.model.userId; - return isMatch(queryString, name, this.model.userId); - } -} - -export function newEntity(jsx, matchFn) { - const entity = new Entity(); - entity.getJsx = function() { - return jsx; - }; - entity.matches = matchFn; - return entity; -} - -/** - * @param {RoomMember[]} members - * @return {Entity[]} - */ -export function fromRoomMembers(members) { - return members.map(function(m) { - return new MemberEntity(m); - }); -} - -/** - * @param {User[]} users - * @param {boolean} showInviteButton - * @param {Function} inviteFn Called with the user ID. - * @return {Entity[]} - */ -export function fromUsers(users, showInviteButton, inviteFn) { - return users.map(function(u) { - return new UserEntity(u, showInviteButton, inviteFn); - }); -} diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 7488488dd8..303bae42b1 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -378,7 +378,7 @@ export function hydrateSession(credentials) { const overwrite = credentials.userId !== oldUserId || credentials.deviceId !== oldDeviceId; if (overwrite) { - console.warn("Clearing all data: Old session belongs to a different user/device"); + console.warn("Clearing all data: Old session belongs to a different user/session"); } return _doSetLoggedIn(credentials, overwrite); diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 450bec8e77..448c6d9e9b 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -32,6 +32,7 @@ import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientB import * as StorageManager from './utils/StorageManager'; import IdentityAuthClient from './IdentityAuthClient'; import { crossSigningCallbacks } from './CrossSigningManager'; +import {SCAN_QR_CODE_METHOD, SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode"; interface MatrixClientCreds { homeserverUrl: string, @@ -217,7 +218,12 @@ class _MatrixClientPeg { timelineSupport: true, forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false), fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'), - verificationMethods: [verificationMethods.SAS, verificationMethods.QR_CODE_SHOW], + verificationMethods: [ + verificationMethods.SAS, + SHOW_QR_CODE_METHOD, + SCAN_QR_CODE_METHOD, // XXX: We don't actually support scanning yet! + verificationMethods.RECIPROCATE_QR_CODE, + ], unstableClientRelationAggregation: true, identityServer: new IdentityAuthClient(), }; diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 2eb34576ac..b39b8fb9ac 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -771,7 +771,7 @@ export const CommandMap = { verify: new Command({ name: 'verify', args: ' ', - description: _td('Verifies a user, device, and pubkey tuple'), + description: _td('Verifies a user, session, and pubkey tuple'), runFn: function(roomId, args) { if (args) { const matches = args.match(/^(\S+) +(\S+) +(\S+)$/); @@ -785,22 +785,22 @@ export const CommandMap = { return success((async () => { const device = await cli.getStoredDevice(userId, deviceId); if (!device) { - throw new Error(_t('Unknown (user, device) pair:') + ` (${userId}, ${deviceId})`); + throw new Error(_t('Unknown (user, session) pair:') + ` (${userId}, ${deviceId})`); } const deviceTrust = await cli.checkDeviceTrust(userId, deviceId); if (deviceTrust.isVerified()) { if (device.getFingerprint() === fingerprint) { - throw new Error(_t('Device already verified!')); + throw new Error(_t('Session already verified!')); } else { - throw new Error(_t('WARNING: Device already verified, but keys do NOT MATCH!')); + throw new Error(_t('WARNING: Session already verified, but keys do NOT MATCH!')); } } if (device.getFingerprint() !== fingerprint) { const fprint = device.getFingerprint(); throw new Error( - _t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' + + _t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session' + ' %(deviceId)s is "%(fprint)s" which does not match the provided key ' + '"%(fingerprint)s". This could mean your communications are being intercepted!', { @@ -821,7 +821,7 @@ export const CommandMap = {

{ _t('The signing key you provided matches the signing key you received ' + - 'from %(userId)s\'s device %(deviceId)s. Device marked as verified.', + 'from %(userId)s\'s session %(deviceId)s. Session marked as verified.', {userId, deviceId}) }

diff --git a/src/TextForEvent.js b/src/TextForEvent.js index cdfea45ad7..d4003058c8 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -442,23 +442,6 @@ function textForHistoryVisibilityEvent(event) { } } -function textForEncryptionEvent(event) { - const senderName = event.sender ? event.sender.name : event.getSender(); - if (event.getContent().algorithm === "m.megolm.v1.aes-sha2") { - return _t('%(senderName)s turned on end-to-end encryption.', { - senderName, - }); - } - return _t( - '%(senderName)s turned on end-to-end encryption ' + - '(unrecognised algorithm %(algorithm)s).', - { - senderName, - algorithm: event.getContent().algorithm, - }, - ); -} - // Currently will only display a change if a user's power level is changed function textForPowerEvent(event) { const senderName = event.sender ? event.sender.name : event.getSender(); @@ -636,7 +619,6 @@ const stateHandlers = { 'm.room.member': textForMemberEvent, 'm.room.third_party_invite': textForThreePidInviteEvent, 'm.room.history_visibility': textForHistoryVisibilityEvent, - 'm.room.encryption': textForEncryptionEvent, 'm.room.power_levels': textForPowerEvent, 'm.room.pinned_events': textForPinnedEvent, 'm.room.server_acl': textForServerACLEvent, diff --git a/src/async-components/views/dialogs/EncryptedEventDialog.js b/src/async-components/views/dialogs/EncryptedEventDialog.js index f6e17b1c84..b602cf60fe 100644 --- a/src/async-components/views/dialogs/EncryptedEventDialog.js +++ b/src/async-components/views/dialogs/EncryptedEventDialog.js @@ -191,7 +191,7 @@ export default createReactClass({

{ _t('Event information') }

{ this._renderEventInfo() } -

{ _t('Sender device information') }

+

{ _t('Sender session information') }

{ this._renderDeviceInfo() }
diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js index b7ea87b1b2..b98fecf22f 100644 --- a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js +++ b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js @@ -18,6 +18,7 @@ import React from 'react'; import * as sdk from '../../../../index'; import PropTypes from 'prop-types'; import { _t } from '../../../../languageHandler'; +import SettingsStore, {SettingLevel} from "../../../../settings/SettingsStore"; import Modal from '../../../../Modal'; import {formatBytes, formatCountLong} from "../../../../utils/FormattingUtils"; @@ -37,8 +38,11 @@ export default class ManageEventIndexDialog extends React.Component { this.state = { eventIndexSize: 0, eventCount: 0, + crawlingRoomsCount: 0, roomCount: 0, currentRoom: null, + crawlerSleepTime: + SettingsStore.getValueAt(SettingLevel.DEVICE, 'crawlerSleepTime'), }; } @@ -48,11 +52,15 @@ export default class ManageEventIndexDialog extends React.Component { let currentRoom = null; if (room) currentRoom = room.name; + const roomStats = eventIndex.crawlingRooms(); + const crawlingRoomsCount = roomStats.crawlingRooms.size; + const roomCount = roomStats.totalRooms.size; this.setState({ eventIndexSize: stats.size, - roomCount: stats.roomCount, eventCount: stats.eventCount, + crawlingRoomsCount: crawlingRoomsCount, + roomCount: roomCount, currentRoom: currentRoom, }); } @@ -67,6 +75,7 @@ export default class ManageEventIndexDialog extends React.Component { async componentWillMount(): void { let eventIndexSize = 0; + let crawlingRoomsCount = 0; let roomCount = 0; let eventCount = 0; let currentRoom = null; @@ -77,8 +86,10 @@ export default class ManageEventIndexDialog extends React.Component { eventIndex.on("changedCheckpoint", this.updateCurrentRoom.bind(this)); const stats = await eventIndex.getStats(); + const roomStats = eventIndex.crawlingRooms(); eventIndexSize = stats.size; - roomCount = stats.roomCount; + crawlingRoomsCount = roomStats.crawlingRooms.size; + roomCount = roomStats.totalRooms.size; eventCount = stats.eventCount; const room = eventIndex.currentRoom(); @@ -88,6 +99,7 @@ export default class ManageEventIndexDialog extends React.Component { this.setState({ eventIndexSize, eventCount, + crawlingRoomsCount, roomCount, currentRoom, }); @@ -104,6 +116,11 @@ export default class ManageEventIndexDialog extends React.Component { this.props.onFinished(true); } + _onCrawlerSleepTimeChange = (e) => { + this.setState({crawlerSleepTime: e.target.value}); + SettingsStore.setValue("crawlerSleepTime", null, SettingLevel.DEVICE, e.target.value); + } + render() { let crawlerState; @@ -115,6 +132,8 @@ export default class ManageEventIndexDialog extends React.Component { ); } + const Field = sdk.getComponent('views.elements.Field'); + const eventIndexingSettings = (
{ @@ -125,8 +144,15 @@ export default class ManageEventIndexDialog extends React.Component {
{_t("Space used:")} {formatBytes(this.state.eventIndexSize, 0)}
{_t("Indexed messages:")} {formatCountLong(this.state.eventCount)}
- {_t("Number of rooms:")} {formatCountLong(this.state.roomCount)}
+ {_t("Number of rooms:")} {formatCountLong(this.state.crawlingRoomsCount)} {_t("of ")} + {formatCountLong(this.state.roomCount)}
{crawlerState}
+
); diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 1557159e5c..2d6a07ad2f 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -71,7 +71,6 @@ export default class CreateKeyBackupDialog extends React.PureComponent { copied: false, downloaded: false, zxcvbnResult: null, - setPassPhrase: false, }; if (this.state.secureSecretStorage === undefined) { @@ -215,7 +214,6 @@ export default class CreateKeyBackupDialog extends React.PureComponent { this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(this.state.passPhrase); this.setState({ - setPassPhrase: true, copied: false, downloaded: false, phase: PHASE_SHOWKEY, @@ -329,7 +327,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
{_t("Advanced")}

; @@ -393,28 +391,17 @@ export default class CreateKeyBackupDialog extends React.PureComponent { } _renderPhaseShowKey() { - let bodyText; - if (this.state.setPassPhrase) { - bodyText = _t( - "As a safety net, you can use it to restore your encrypted message " + - "history if you forget your Recovery Passphrase.", - ); - } else { - bodyText = _t("As a safety net, you can use it to restore your encrypted message history."); - } - return

{_t( "Your recovery key is a safety net - you can use it to restore " + "access to your encrypted messages if you forget your passphrase.", )}

{_t( - "Keep your recovery key somewhere very secure, like a password manager (or a safe).", + "Keep a copy of it somewhere secure, like a password manager or even a safe.", )}

-

{bodyText}

- {_t("Your Recovery Key")} + {_t("Your recovery key")}
@@ -422,7 +409,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
@@ -487,7 +474,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent { return
{_t( "Without setting up Secure Message Recovery, you won't be able to restore your " + - "encrypted message history if you log out or use another device.", + "encrypted message history if you log out or use another session.", )} {newMethodDetected}

{_t( - "This device is encrypting history using the new recovery method.", + "This session is encrypting history using the new recovery method.", )}

{hackWarning}

{_t( - "This device has detected that your recovery passphrase and key " + + "This session has detected that your recovery passphrase and key " + "for Secure Messages have been removed.", )}

{_t( "If you did this accidentally, you can setup Secure Messages on " + - "this device which will re-encrypt this device's message " + + "this session which will re-encrypt this session's message " + "history with a new recovery method.", )}

{_t( diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 35cd5aa819..12ca752421 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -76,7 +76,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { copied: false, downloaded: false, zxcvbnResult: null, - setPassPhrase: false, backupInfo: null, backupSigStatus: null, // does the server offer a UI auth flow with just m.login.password @@ -84,9 +83,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { canUploadKeysWithPasswordOnly: null, accountPassword: props.accountPassword, accountPasswordCorrect: null, - // set if we are 'upgrading' encryption (making an SSSS store from - // an existing key backup secret). - doingUpgrade: null, // status of the key backup toggle switch useKeyBackup: true, }; @@ -117,8 +113,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { phase, backupInfo, backupSigStatus, - // remember this after this phase so we can use appropriate copy - doingUpgrade: phase === PHASE_MIGRATE, }); } @@ -205,7 +199,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { const { finished } = Modal.createTrackedDialog( 'Cross-signing keys dialog', '', InteractiveAuthDialog, { - title: _t("Send cross-signing keys to homeserver"), + title: _t("Setting up keys"), matrixClient: MatrixClientPeg.get(), makeRequest, }, @@ -321,7 +315,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this._keyInfo = keyInfo; this._encodedRecoveryKey = encodedRecoveryKey; this.setState({ - setPassPhrase: true, copied: false, downloaded: false, phase: PHASE_SHOWKEY, @@ -415,7 +408,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { return

{_t( - "Upgrade this device to allow it to verify other devices, " + + "Upgrade this session to allow it to verify other sessions, " + "granting them access to encrypted messages and marking them " + "as trusted for other users.", )}

@@ -444,14 +437,19 @@ export default class CreateSecretStorageDialog extends React.PureComponent { if (this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE) { helpText = _t("Great! This passphrase looks strong enough."); } else { - const suggestions = []; - for (let i = 0; i < this.state.zxcvbnResult.feedback.suggestions.length; ++i) { - suggestions.push(
{this.state.zxcvbnResult.feedback.suggestions[i]}
); - } - const suggestionBlock =
{suggestions.length > 0 ? suggestions : _t("Keep going...")}
; + // We take the warning from zxcvbn or failing that, the first + // suggestion. In practice The first is generally the most relevant + // and it's probably better to present the user with one thing to + // improve about their password than a whole collection - it can + // spit out a warning and multiple suggestions which starts getting + // very information-dense. + const suggestion = ( + this.state.zxcvbnResult.feedback.warning || + this.state.zxcvbnResult.feedback.suggestions[0] + ); + const suggestionBlock =
{suggestion || _t("Keep going...")}
; helpText =
- {this.state.zxcvbnResult.feedback.warning} {suggestionBlock}
; } @@ -462,7 +460,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { return

{_t( - "Set up encryption on this device to allow it to verify other devices, " + + "Set up encryption on this session to allow it to verify other sessions, " + "granting them access to encrypted messages and marking them as trusted for other users.", )}

{_t( @@ -473,6 +471,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {

{_t("Advanced")} -

+ {_t("Set up with a recovery key")} -

+ ; } @@ -578,19 +577,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } _renderPhaseShowKey() { - let bodyText; - if (this.state.setPassPhrase) { - bodyText = _t( - "As a safety net, you can use it to restore your access to encrypted " + - "messages if you forget your passphrase.", - ); - } else { - bodyText = _t( - "As a safety net, you can use it to restore your access to encrypted " + - "messages.", - ); - } - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); return

{_t( @@ -598,12 +584,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent { "access to your encrypted messages if you forget your passphrase.", )}

{_t( - "Keep your recovery key somewhere very secure, like a password manager (or a safe).", + "Keep a copy of it somewhere secure, like a password manager or even a safe.", )}

-

{bodyText}

- {_t("Your Recovery Key")} + {_t("Your recovery key")}
@@ -611,7 +596,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
- {_t("Copy to clipboard")} + {_t("Copy")} {_t("Download")} @@ -643,7 +628,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
  • {_t("Save it on a USB key or backup drive", {}, {b: s => {s}})}
  • {_t("Copy it to your personal cloud storage", {}, {b: s => {s}})}
  • - @@ -662,11 +647,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return

    {_t( - "This device can now verify other devices, granting them access " + - "to encrypted messages and marking them as trusted for other users.", - )}

    -

    {_t( - "Verify other users in their profile.", + "You can now verify your other devices, " + + "and other users to keep your chats safe.", )}

    {_t( - "Without completing security on this device, it won’t have " + + "Without completing security on this session, it won’t have " + "access to encrypted messages.", )} !!room && !!getDisplayAliasForRoom(room), ).map((room) => { return { diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 1b4d0e9609..3ccc4627e1 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -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()} /> ); diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 4ad75eb700..a13278cf68 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -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( { - s.publicRooms.push(...data.chunk); + s.publicRooms.push(...(data.chunk || [])); s.loading = false; return s; }); diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index d9ce032ba8..13b73ec02b 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -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( - "Show devices, send anyway or cancel.", + "Show sessions, send anyway or cancel.", {}, { - 'showDevicesText': (sub) => { sub }, + 'showSessionsText': (sub) => { sub }, 'sendAnywayText': (sub) => { sub }, 'cancelText': (sub) => { sub }, }, diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 2d669f9243..acc87d9616 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -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; } diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index cefb60653f..622e63d8ce 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -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(); } }, diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index e708fad6a4..25526c3139 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -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") { diff --git a/src/components/structures/auth/CompleteSecurity.js b/src/components/structures/auth/CompleteSecurity.js index 0f30d9cf61..2126590736 100644 --- a/src/components/structures/auth/CompleteSecurity.js +++ b/src/components/structures/auth/CompleteSecurity.js @@ -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 = (

    {_t( - "Without completing security on this device, it won’t have " + + "Without completing security on this session, it won’t have " + "access to encrypted messages.", )}

    @@ -220,7 +223,7 @@ export default class CompleteSecurity extends React.Component { } else if (phase === PHASE_BUSY) { const Spinner = sdk.getComponent('views.elements.Spinner'); icon = ; - title = ''; + title = _t("Complete security"); body = ; } else { throw new Error(`Unknown phase ${phase}`); diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 4576067caa..e921951512 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -152,8 +152,8 @@ export default createReactClass({
    { _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.", ) }
    , @@ -358,7 +358,7 @@ export default createReactClass({ return

    {_t("Your password has been reset.")}

    {_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.", )}

    diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 171d3ada26..8593a4b1e2 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -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() { diff --git a/src/components/structures/auth/SoftLogout.js b/src/components/structures/auth/SoftLogout.js index 40800ad907..8481b3fc43 100644 --- a/src/components/structures/auth/SoftLogout.js +++ b/src/components/structures/auth/SoftLogout.js @@ -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 won’t 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 won’t 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 {

    {_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.", )}

    diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index 801420da95..6f6eb7e2a1 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -142,7 +142,7 @@ export const PasswordAuthEntry = createReactClass({ return (
    -

    { _t("To continue, please enter your password.") }

    +

    { _t("Confirm your identity by entering your account password below.") }

    ; 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} >