diff --git a/CHANGELOG.md b/CHANGELOG.md index 07e478fa02..02c085d0b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,159 @@ +Changes in [2.3.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.3.0) (2020-03-30) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.3.0-rc.1...v2.3.0) + + * Upgrade JS SDK to 5.2.0 + +Changes in [2.3.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.3.0-rc.1) (2020-03-26) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.2.3...v2.3.0-rc.1) + + * Upgrade JS SDK to 5.2.0-rc.1 + * Add a flag to control whether cross-signing signatures are trusted + [\#4277](https://github.com/matrix-org/matrix-react-sdk/pull/4277) + * Update from Weblate + [\#4282](https://github.com/matrix-org/matrix-react-sdk/pull/4282) + * Update copy on SSSS symmetric upgrade toast + [\#4281](https://github.com/matrix-org/matrix-react-sdk/pull/4281) + * Wait for SSSS upgrade to complete + [\#4270](https://github.com/matrix-org/matrix-react-sdk/pull/4270) + * Update cross-signing verification copy and fix i18n + [\#4278](https://github.com/matrix-org/matrix-react-sdk/pull/4278) + * Fix soft-crash on bad permalinks + [\#4280](https://github.com/matrix-org/matrix-react-sdk/pull/4280) + * Fix: make self-verification wait for incoming request + [\#4267](https://github.com/matrix-org/matrix-react-sdk/pull/4267) + * Fall back to non-standard persisted api for Safari + [\#4272](https://github.com/matrix-org/matrix-react-sdk/pull/4272) + * Respond to backup key sharing requests + [\#4275](https://github.com/matrix-org/matrix-react-sdk/pull/4275) + * Log and display secret sharing cache state + [\#4268](https://github.com/matrix-org/matrix-react-sdk/pull/4268) + * Support sending config and ready events to capable widgets (Jitsi) + [\#4266](https://github.com/matrix-org/matrix-react-sdk/pull/4266) + * If cached keys are present in the key backup dialog, use them + [\#4273](https://github.com/matrix-org/matrix-react-sdk/pull/4273) + * Fix formatbar not hidden on highlighted message sent + [\#4265](https://github.com/matrix-org/matrix-react-sdk/pull/4265) + * Support Jitsi conferences sent/received on Riot Mobile and older Riot Webs + [\#4252](https://github.com/matrix-org/matrix-react-sdk/pull/4252) + * Use unified function to check cross-signing is ready + [\#4263](https://github.com/matrix-org/matrix-react-sdk/pull/4263) + * Migrate SSSS to symmetric + [\#4224](https://github.com/matrix-org/matrix-react-sdk/pull/4224) + * Migration to symmetric SSSS + [\#4242](https://github.com/matrix-org/matrix-react-sdk/pull/4242) + * Always display verification request toasts on top + [\#4262](https://github.com/matrix-org/matrix-react-sdk/pull/4262) + * Fix: assume SAS is supported when starting request with .start + [\#4249](https://github.com/matrix-org/matrix-react-sdk/pull/4249) + * Fix logout when Olm failed to load. + [\#4261](https://github.com/matrix-org/matrix-react-sdk/pull/4261) + * Improve naming of Jitsi conferences + [\#4251](https://github.com/matrix-org/matrix-react-sdk/pull/4251) + * Handle matrix.to user permalink in-room rather than solo + [\#4245](https://github.com/matrix-org/matrix-react-sdk/pull/4245) + * Fix: filter room list (again) by canonical and alternative aliases + [\#4260](https://github.com/matrix-org/matrix-react-sdk/pull/4260) + * EventIndex: Add some logging to the file panel populating. + [\#4250](https://github.com/matrix-org/matrix-react-sdk/pull/4250) + * Update from Weblate + [\#4259](https://github.com/matrix-org/matrix-react-sdk/pull/4259) + * Migrate RoomView to React Contexts in the hope for better temporal stability + [\#4258](https://github.com/matrix-org/matrix-react-sdk/pull/4258) + * Update WidgetUtils.js fix Jitsi path + [\#4256](https://github.com/matrix-org/matrix-react-sdk/pull/4256) + * Fix local jitsi build url fail and missing argument + [\#4255](https://github.com/matrix-org/matrix-react-sdk/pull/4255) + * Add shortcut CmdOrCtrl+. to toggle right panel + [\#4244](https://github.com/matrix-org/matrix-react-sdk/pull/4244) + * Improve Keyboard Shortcuts. Add alt-arrows & alt-shift-arrows + [\#4241](https://github.com/matrix-org/matrix-react-sdk/pull/4241) + * Bring back legacy verification by comparing public device keys + [\#4240](https://github.com/matrix-org/matrix-react-sdk/pull/4240) + * Searching: Return an empty result if the search term is an empty string. + [\#4248](https://github.com/matrix-org/matrix-react-sdk/pull/4248) + * Break continuation on showHiddenEvents-rendered events + [\#4247](https://github.com/matrix-org/matrix-react-sdk/pull/4247) + * Watch for show-RR settings changes, use room-specific and fix margins + [\#4246](https://github.com/matrix-org/matrix-react-sdk/pull/4246) + * Register Mac electron specific Cmd+, shortcut to User Settings + [\#4243](https://github.com/matrix-org/matrix-react-sdk/pull/4243) + * Use a local wrapper for Jitsi calls + [\#4234](https://github.com/matrix-org/matrix-react-sdk/pull/4234) + * Invite Dialog fixes + [\#4233](https://github.com/matrix-org/matrix-react-sdk/pull/4233) + * RoomPreviewBar word-break the sender name too + [\#4239](https://github.com/matrix-org/matrix-react-sdk/pull/4239) + * Report to the user when a key signature upload fails + [\#4229](https://github.com/matrix-org/matrix-react-sdk/pull/4229) + * pre-send megolm keys when possible when a user starts typing + [\#4235](https://github.com/matrix-org/matrix-react-sdk/pull/4235) + * we don't do mx_fadable anymore so get rid of broken RightPanel disabling + [\#4238](https://github.com/matrix-org/matrix-react-sdk/pull/4238) + * Fix left left panel overflowing vertically + [\#4237](https://github.com/matrix-org/matrix-react-sdk/pull/4237) + * Fix custom tags causing left panel to over-expand + [\#4236](https://github.com/matrix-org/matrix-react-sdk/pull/4236) + * Add Keyboard shortcuts dialog + [\#4231](https://github.com/matrix-org/matrix-react-sdk/pull/4231) + * Don't use buildkite agent to upload logs + [\#4232](https://github.com/matrix-org/matrix-react-sdk/pull/4232) + * Remove Gemini Scrollbars + [\#4217](https://github.com/matrix-org/matrix-react-sdk/pull/4217) + * Room Directory Explore Servers redesign + [\#4209](https://github.com/matrix-org/matrix-react-sdk/pull/4209) + * Fix redo keyboard shortcut on macOS + [\#4110](https://github.com/matrix-org/matrix-react-sdk/pull/4110) + * Fix: ensure local state for aliases doesn't get garbled up + [\#4230](https://github.com/matrix-org/matrix-react-sdk/pull/4230) + * Rename 'jump to bottom' to avoid ublock block + [\#4208](https://github.com/matrix-org/matrix-react-sdk/pull/4208) + * Restore key backup in background after complete security + [\#4225](https://github.com/matrix-org/matrix-react-sdk/pull/4225) + * Fix key backup trust text for cross-signing + [\#4223](https://github.com/matrix-org/matrix-react-sdk/pull/4223) + * Add default on config setting to control call button in composer + [\#4227](https://github.com/matrix-org/matrix-react-sdk/pull/4227) + * Fix: make alternative addresses UX less confusing + [\#4221](https://github.com/matrix-org/matrix-react-sdk/pull/4221) + * Wait for verification request on login + [\#4222](https://github.com/matrix-org/matrix-react-sdk/pull/4222) + * EventIndex: Add support to delete events from the index. + [\#4204](https://github.com/matrix-org/matrix-react-sdk/pull/4204) + * EventIndex: Remove a checkpoint if the HTTP request returns a 403. + [\#4214](https://github.com/matrix-org/matrix-react-sdk/pull/4214) + * Move to composer when typing letters with Shift held + [\#4216](https://github.com/matrix-org/matrix-react-sdk/pull/4216) + * Wrap large room names when previewing them + [\#4213](https://github.com/matrix-org/matrix-react-sdk/pull/4213) + * Rename Review Devices to Review Sessions + [\#4219](https://github.com/matrix-org/matrix-react-sdk/pull/4219) + * Fix typo in tabIndex to make React happy + [\#4215](https://github.com/matrix-org/matrix-react-sdk/pull/4215) + * Proof of concept for custom theme adding + [\#4148](https://github.com/matrix-org/matrix-react-sdk/pull/4148) + * Remove stuff that yarn install doesn't think we need + [\#4205](https://github.com/matrix-org/matrix-react-sdk/pull/4205) + * Declare jsx in tsconfig for IDEs + [\#4207](https://github.com/matrix-org/matrix-react-sdk/pull/4207) + * Fix: best-effort to join room without canonical alias over federation from + room directory + [\#4210](https://github.com/matrix-org/matrix-react-sdk/pull/4210) + * Test for cross-signing homeserver support during login, toasts + [\#4206](https://github.com/matrix-org/matrix-react-sdk/pull/4206) + * Send verification request to a single device in a way compatible with non- + cross-signing + [\#4202](https://github.com/matrix-org/matrix-react-sdk/pull/4202) + * Fixes for removing local alias + [\#4199](https://github.com/matrix-org/matrix-react-sdk/pull/4199) + * yarn upgrade + [\#4201](https://github.com/matrix-org/matrix-react-sdk/pull/4201) + * Support TypeScript for React components + [\#4203](https://github.com/matrix-org/matrix-react-sdk/pull/4203) + * When room name is changed, show both the old and new name + [\#4183](https://github.com/matrix-org/matrix-react-sdk/pull/4183) + Changes in [2.2.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.2.3) (2020-03-17) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.2.3-rc.1...v2.2.3) diff --git a/package.json b/package.json index 1ff0fb6f55..7001b1cb21 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "2.2.3", + "version": "2.3.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index 472831c0d9..c1c5d92d3c 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -137,7 +137,7 @@ limitations under the License. top: -8px; border-radius: 8px; background-color: $neutral-badge-color; - color: #ffffff; + color: #000; font-weight: 600; font-size: 10px; text-align: center; diff --git a/src/CallHandler.js b/src/CallHandler.js index 362db939a3..8284e788b4 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -430,7 +430,7 @@ async function _startCallApp(roomId, type) { return; } - const confId = `JitsiConference_${generateHumanReadableId()}`; + const confId = `JitsiConference${generateHumanReadableId()}`; const jitsiDomain = SdkConfig.get()['jitsi']['preferredDomain']; let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl(); diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 8351013a5a..9fb0a151cf 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -85,7 +85,7 @@ class Command { aliases = [], args = '', description, - runFn=undefined, + runFn = undefined, category = CommandCategories.other, hideCompletionAfterSpace = false, }: { @@ -160,6 +160,15 @@ export const Commands = [ }, category: CommandCategories.messages, }), + new Command({ + command: 'html', + args: '', + description: _td('Sends a message as html, without interpreting it as markdown'), + runFn: function(roomId, messages) { + return success(MatrixClientPeg.get().sendHtmlMessage(roomId, messages, messages)); + }, + category: CommandCategories.messages, + }), new Command({ command: 'ddg', args: '', diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 78e750b817..01a2856df0 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -67,8 +67,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { constructor(props) { super(props); - this._keyInfo = null; - this._encodedRecoveryKey = null; + this._recoveryKey = null; this._recoveryKeyNode = null; this._setZxcvbnResultTimeout = null; @@ -180,7 +179,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } _onDownloadClick = () => { - const blob = new Blob([this._encodedRecoveryKey], { + const blob = new Blob([this._recoveryKey.encodedPrivateKey], { type: 'text/plain;charset=us-ascii', }); FileSaver.saveAs(blob, 'recovery-key.txt'); @@ -234,14 +233,14 @@ export default class CreateSecretStorageDialog extends React.PureComponent { if (force) { await cli.bootstrapSecretStorage({ authUploadDeviceSigningKeys: this._doBootstrapUIAuth, - createSecretStorageKey: async () => this._keyInfo, + createSecretStorageKey: async () => this._recoveryKey, setupNewKeyBackup: true, setupNewSecretStorage: true, }); } else { await cli.bootstrapSecretStorage({ authUploadDeviceSigningKeys: this._doBootstrapUIAuth, - createSecretStorageKey: async () => this._keyInfo, + createSecretStorageKey: async () => this._recoveryKey, keyBackupInfo: this.state.backupInfo, setupNewKeyBackup: !this.state.backupInfo && this.state.useKeyBackup, getKeyBackupPassphrase: promptForBackupPassphrase, @@ -299,10 +298,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } _onSkipPassPhraseClick = async () => { - const [keyInfo, encodedRecoveryKey] = + this._recoveryKey = await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(); - this._keyInfo = keyInfo; - this._encodedRecoveryKey = encodedRecoveryKey; this.setState({ copied: false, downloaded: false, @@ -335,10 +332,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { if (this.state.passPhrase !== this.state.passPhraseConfirm) return; - const [keyInfo, encodedRecoveryKey] = + this._recoveryKey = await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(this.state.passPhrase); - this._keyInfo = keyInfo; - this._encodedRecoveryKey = encodedRecoveryKey; this.setState({ copied: false, downloaded: false, @@ -613,7 +608,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
- {this._encodedRecoveryKey} + {this._recoveryKey.encodedPrivateKey}
diff --git a/src/components/structures/EmbeddedPage.js b/src/components/structures/EmbeddedPage.js index f854dc955f..a0a95ac6f1 100644 --- a/src/components/structures/EmbeddedPage.js +++ b/src/components/structures/EmbeddedPage.js @@ -37,6 +37,8 @@ export default class EmbeddedPage extends React.PureComponent { className: PropTypes.string, // Whether to wrap the page in a scrollbar scrollbar: PropTypes.bool, + // Map of keys to replace with values, e.g {$placeholder: "value"} + replaceMap: PropTypes.object, }; static contextType = MatrixClientContext; @@ -81,6 +83,13 @@ export default class EmbeddedPage extends React.PureComponent { } body = body.replace(/_t\(['"]([\s\S]*?)['"]\)/mg, (match, g1)=>this.translate(g1)); + + if (this.props.replaceMap) { + Object.keys(this.props.replaceMap).forEach(key => { + body = body.split(key).join(this.props.replaceMap[key]); + }); + } + this.setState({ page: body }); }, ); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 9d17cd8647..258972d18d 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -2022,7 +2022,7 @@ export default createReactClass({ } } else if (this.state.view === VIEWS.WELCOME) { const Welcome = sdk.getComponent('auth.Welcome'); - view = ; + view = ; } else if (this.state.view === VIEWS.REGISTER) { const Registration = sdk.getComponent('structures.auth.Registration'); view = ( diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 5fd5f42f78..1a1f60c7bd 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -55,6 +55,7 @@ import RightPanelStore from "../../stores/RightPanelStore"; import {haveTileForEvent} from "../views/rooms/EventTile"; import RoomContext from "../../contexts/RoomContext"; import MatrixClientContext from "../../contexts/MatrixClientContext"; +import { shieldStatusForRoom } from '../../utils/ShieldUtils'; const DEBUG = false; let debuglog = function() {}; @@ -817,40 +818,9 @@ export default createReactClass({ return; } - // Duplication between here and _updateE2eStatus in RoomTile /* At this point, the user has encryption on and cross-signing on */ - const e2eMembers = await room.getEncryptionTargetMembers(); - const verified = []; - const unverified = []; - e2eMembers.map(({userId}) => userId) - .filter((userId) => userId !== this.context.getUserId()) - .forEach((userId) => { - (this.context.checkUserTrust(userId).isCrossSigningVerified() ? - verified : unverified).push(userId); - }); - - debuglog("e2e verified", verified, "unverified", unverified); - - /* Check all verified user devices. */ - /* Don't alarm if no other users are verified */ - const targets = (verified.length > 0) ? [...verified, this.context.getUserId()] : verified; - for (const userId of targets) { - const devices = await this.context.getStoredDevicesForUser(userId); - const anyDeviceNotVerified = devices.some(({deviceId}) => { - return !this.context.checkDeviceTrust(userId, deviceId).isVerified(); - }); - if (anyDeviceNotVerified) { - this.setState({ - e2eStatus: "warning", - }); - debuglog("e2e status set to warning as not all users trust all of their sessions." + - " Aborted on user", userId); - return; - } - } - this.setState({ - e2eStatus: unverified.length === 0 ? "verified" : "normal", + e2eStatus: await shieldStatusForRoom(this.context, room), }); }, diff --git a/src/components/views/auth/Welcome.js b/src/components/views/auth/Welcome.js index 58f117ea36..7cbcf65d3c 100644 --- a/src/components/views/auth/Welcome.js +++ b/src/components/views/auth/Welcome.js @@ -18,6 +18,12 @@ import React from 'react'; import * as sdk from '../../../index'; import SdkConfig from '../../../SdkConfig'; import AuthPage from "./AuthPage"; +import * as Matrix from "matrix-js-sdk"; +import {_td} from "../../../languageHandler"; +import PlatformPeg from "../../../PlatformPeg"; + +// translatable strings for Welcome pages +_td("Sign in with SSO"); export default class Welcome extends React.PureComponent { render() { @@ -33,11 +39,24 @@ export default class Welcome extends React.PureComponent { pageUrl = 'welcome.html'; } + const {hsUrl, isUrl} = this.props.serverConfig; + const tmpClient = Matrix.createClient({ + baseUrl: hsUrl, + idBaseUrl: isUrl, + }); + const plaf = PlatformPeg.get(); + const callbackUrl = plaf.getSSOCallbackUrl(tmpClient.getHomeserverUrl(), tmpClient.getIdentityServerUrl()); + return (
-
diff --git a/src/components/views/elements/EventListSummary.js b/src/components/views/elements/EventListSummary.js index 7a69398071..79c84293c2 100644 --- a/src/components/views/elements/EventListSummary.js +++ b/src/components/views/elements/EventListSummary.js @@ -20,6 +20,7 @@ import MemberAvatar from '../avatars/MemberAvatar'; import { _t } from '../../../languageHandler'; import {MatrixEvent, RoomMember} from "matrix-js-sdk"; import {useStateToggle} from "../../../hooks/useStateToggle"; +import AccessibleButton from "./AccessibleButton"; const EventListSummary = ({events, children, threshold=3, onToggle, startExpanded, summaryMembers=[], summaryText}) => { const [expanded, toggleExpanded] = useStateToggle(startExpanded); @@ -42,24 +43,15 @@ const EventListSummary = ({events, children, threshold=3, onToggle, startExpande ); } + let body; if (expanded) { - return ( -
-
- { _t('collapse') } -
-
 
- { children } -
- ); - } - - const avatars = summaryMembers.map((m) => ); - return ( -
-
- { _t('expand') } -
+ body = +
 
+ { children } +
; + } else { + const avatars = summaryMembers.map((m) => ); + body = (
@@ -70,6 +62,15 @@ const EventListSummary = ({events, children, threshold=3, onToggle, startExpande
+ ); + } + + return ( +
+ + { expanded ? _t('collapse') : _t('expand') } + + { body }
); }; diff --git a/src/components/views/elements/Field.js b/src/components/views/elements/Field.js index 797c83bc06..2ebb90da26 100644 --- a/src/components/views/elements/Field.js +++ b/src/components/views/elements/Field.js @@ -32,7 +32,7 @@ function getId() { export default class Field extends React.PureComponent { static propTypes = { // The field's ID, which binds the input and label together. Immutable. - id: PropTypes.string.isRequired, + id: PropTypes.string, // The element to create. Defaults to "input". // To define options for a select, use element: PropTypes.oneOf(["input", "select", "textarea"]), diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index ee47f08aa2..a2081dc9e4 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -68,8 +68,10 @@ export const getE2EStatus = (cli, userId, devices) => { return hasUnverifiedDevice ? "warning" : "verified"; } const isMe = userId === cli.getUserId(); - const userVerified = cli.checkUserTrust(userId).isCrossSigningVerified(); - if (!userVerified) return "normal"; + const userTrust = cli.checkUserTrust(userId); + if (!userTrust.isCrossSigningVerified()) { + return userTrust.wasCrossSigningVerified() ? "warning" : "normal"; + } const anyDeviceUnverified = devices.some(device => { const { deviceId } = device; diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js index a0e900b5fc..bf2a1bee23 100644 --- a/src/components/views/rooms/MemberTile.js +++ b/src/components/views/rooms/MemberTile.js @@ -121,10 +121,10 @@ export default createReactClass({ const cli = MatrixClientPeg.get(); const { userId } = this.props.member; const isMe = userId === cli.getUserId(); - const userVerified = cli.checkUserTrust(userId).isCrossSigningVerified(); - if (!userVerified) { + const userTrust = cli.checkUserTrust(userId); + if (!userTrust.isCrossSigningVerified()) { this.setState({ - e2eStatus: "normal", + e2eStatus: userTrust.wasCrossSigningVerified() ? "warning" : "normal", }); return; } diff --git a/src/components/views/rooms/MessageComposerFormatBar.js b/src/components/views/rooms/MessageComposerFormatBar.js index 79ae9f34e8..42d54f5987 100644 --- a/src/components/views/rooms/MessageComposerFormatBar.js +++ b/src/components/views/rooms/MessageComposerFormatBar.js @@ -19,12 +19,13 @@ import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import * as sdk from '../../../index'; import classNames from 'classnames'; +import AccessibleButton from "../elements/AccessibleButton"; export default class MessageComposerFormatBar extends React.PureComponent { static propTypes = { onAction: PropTypes.func.isRequired, shortcuts: PropTypes.object.isRequired, - } + }; constructor(props) { super(props); @@ -64,7 +65,7 @@ class FormatButton extends React.PureComponent { icon: PropTypes.string.isRequired, shortcut: PropTypes.string, visible: PropTypes.bool, - } + }; render() { const InteractiveTooltip = sdk.getComponent('elements.InteractiveTooltip'); @@ -82,11 +83,12 @@ class FormatButton extends React.PureComponent { return ( - - + ); } diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 0f44f5077a..0c913b32da 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -37,6 +37,7 @@ import E2EIcon from './E2EIcon'; import InviteOnlyIcon from './InviteOnlyIcon'; // eslint-disable-next-line camelcase import rate_limited_func from '../../../ratelimitedfunc'; +import { shieldStatusForRoom } from '../../../utils/ShieldUtils'; export default createReactClass({ displayName: 'RoomTile', @@ -154,35 +155,9 @@ export default createReactClass({ return; } - // Duplication between here and _updateE2eStatus in RoomView - const e2eMembers = await this.props.room.getEncryptionTargetMembers(); - const verified = []; - const unverified = []; - e2eMembers.map(({userId}) => userId) - .filter((userId) => userId !== cli.getUserId()) - .forEach((userId) => { - (cli.checkUserTrust(userId).isCrossSigningVerified() ? - verified : unverified).push(userId); - }); - - /* Check all verified user devices. */ - /* Don't alarm if no other users are verified */ - const targets = (verified.length > 0) ? [...verified, cli.getUserId()] : verified; - for (const userId of targets) { - const devices = await cli.getStoredDevicesForUser(userId); - const allDevicesVerified = devices.every(({deviceId}) => { - return cli.checkDeviceTrust(userId, deviceId).isVerified(); - }); - if (!allDevicesVerified) { - this.setState({ - e2eStatus: "warning", - }); - return; - } - } - + /* At this point, the user has encryption on and cross-signing on */ this.setState({ - e2eStatus: unverified.length === 0 ? "verified" : "normal", + e2eStatus: await shieldStatusForRoom(cli, this.props.room), }); }, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 151d5a2b0d..0e9aa3c83f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -153,6 +153,7 @@ "Usage": "Usage", "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Prepends ¯\\_(ツ)_/¯ to a plain-text message", "Sends a message as plain text, without interpreting it as markdown": "Sends a message as plain text, without interpreting it as markdown", + "Sends a message as html, without interpreting it as markdown": "Sends a message as html, without interpreting it as markdown", "Searches DuckDuckGo for results": "Searches DuckDuckGo for results", "/ddg is not a command": "/ddg is not a command", "To use it, just wait for autocomplete results to load and tab through them.": "To use it, just wait for autocomplete results to load and tab through them.", @@ -1880,6 +1881,7 @@ "Find other public servers or use a custom server": "Find other public servers or use a custom server", "Sign in to your Matrix account on %(serverName)s": "Sign in to your Matrix account on %(serverName)s", "Sign in to your Matrix account on ": "Sign in to your Matrix account on ", + "Sign in with SSO": "Sign in with SSO", "Sorry, your browser is not able to run Riot.": "Sorry, your browser is not able to run Riot.", "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.": "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.", "Please install Chrome, Firefox, or Safari for the best experience.": "Please install Chrome, Firefox, or Safari for the best experience.", diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index 9e27451a78..14257af014 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -469,6 +469,9 @@ export default class EventIndex extends EventEmitter { // decryption keys, do we want to retry this checkpoint at a later // stage? const filteredEvents = matrixEvents.filter(this.isValidEvent); + const undecryptableEvents = matrixEvents.filter((ev) => { + return ev.isDecryptionFailure(); + }); // Collect the redaction events so we can delete the redacted events // from the index. @@ -503,7 +506,10 @@ export default class EventIndex extends EventEmitter { console.log( "EventIndex: Crawled room", client.getRoom(checkpoint.roomId).name, - "and fetched", events.length, "events.", + "and fetched total", matrixEvents.length, "events of which", + events.length, "are being added,", redactionEvents.length, + "are redacted,", matrixEvents.length - events.length, + "are being skipped, undecryptable", undecryptableEvents.length, ); try { diff --git a/src/utils/ShieldUtils.ts b/src/utils/ShieldUtils.ts new file mode 100644 index 0000000000..9bf6fe2327 --- /dev/null +++ b/src/utils/ShieldUtils.ts @@ -0,0 +1,58 @@ +import DMRoomMap from './DMRoomMap'; + +/* For now, a cut-down type spec for the client */ +interface Client { + getUserId: () => string; + checkUserTrust: (userId: string) => { + isCrossSigningVerified: () => boolean + wasCrossSigningVerified: () => boolean + }; + getStoredDevicesForUser: (userId: string) => Promise<[{ deviceId: string }]>; + checkDeviceTrust: (userId: string, deviceId: string) => { + isVerified: () => boolean + } +} + +interface Room { + getEncryptionTargetMembers: () => Promise<[{userId: string}]>; + roomId: string; +} + +export async function shieldStatusForRoom(client: Client, room: Room): Promise { + const members = (await room.getEncryptionTargetMembers()).map(({userId}) => userId); + const inDMMap = !!DMRoomMap.shared().getUserIdForRoomId(room.roomId); + + const verified: string[] = []; + const unverified: string[] = []; + members.filter((userId) => userId !== client.getUserId()) + .forEach((userId) => { + (client.checkUserTrust(userId).isCrossSigningVerified() ? + verified : unverified).push(userId); + }); + + /* Alarm if any unverified users were verified before. */ + for (const userId of unverified) { + if (client.checkUserTrust(userId).wasCrossSigningVerified()) { + return "warning"; + } + } + + /* Check all verified user devices. */ + /* Don't alarm if no other users are verified */ + const includeUser = (verified.length > 0) && // Don't alarm for self in rooms where nobody else is verified + !inDMMap && // Don't alarm for self in DMs with other users + (members.length !== 2) || // Don't alarm for self in 1:1 chats with other users + (members.length === 1); // Do alarm for self if we're alone in a room + const targets = includeUser ? [...verified, client.getUserId()] : verified; + for (const userId of targets) { + const devices = await client.getStoredDevicesForUser(userId); + const anyDeviceNotVerified = devices.some(({deviceId}) => { + return !client.checkDeviceTrust(userId, deviceId).isVerified(); + }); + if (anyDeviceNotVerified) { + return "warning"; + } + } + + return unverified.length === 0 ? "verified" : "normal"; +} diff --git a/test/utils/ShieldUtils-test.js b/test/utils/ShieldUtils-test.js new file mode 100644 index 0000000000..5f676579fa --- /dev/null +++ b/test/utils/ShieldUtils-test.js @@ -0,0 +1,183 @@ +import { shieldStatusForRoom } from '../../src/utils/ShieldUtils'; +import DMRoomMap from '../../src/utils/DMRoomMap'; + +function mkClient(selfTrust) { + return { + getUserId: () => "@self:localhost", + checkUserTrust: (userId) => ({ + isCrossSigningVerified: () => userId[1] == "T", + wasCrossSigningVerified: () => userId[1] == "T" || userId[1] == "W", + }), + checkDeviceTrust: (userId, deviceId) => ({ + isVerified: () => userId === "@self:localhost" ? selfTrust : userId[2] == "T", + }), + getStoredDevicesForUser: async (userId) => ["DEVICE"], + }; +} + +describe("mkClient self-test", function() { + test.each([true, false])("behaves well for self-trust=%s", (v) => { + const client = mkClient(v); + expect(client.checkDeviceTrust("@self:localhost", "DEVICE").isVerified()).toBe(v); + }); + + test.each([ + ["@TT:h", true], + ["@TF:h", true], + ["@FT:h", false], + ["@FF:h", false]], + )("behaves well for user trust %s", (userId, trust) => { + expect(mkClient().checkUserTrust(userId).isCrossSigningVerified()).toBe(trust); + }); + + test.each([ + ["@TT:h", true], + ["@TF:h", false], + ["@FT:h", true], + ["@FF:h", false]], + )("behaves well for device trust %s", (userId, trust) => { + expect(mkClient().checkDeviceTrust(userId, "device").isVerified()).toBe(trust); + }); +}); + +describe("shieldStatusForMembership self-trust behaviour", function() { + beforeAll(() => { + DMRoomMap._sharedInstance = { + getUserIdForRoomId: (roomId) => roomId === "DM" ? "@any:h" : null, + }; + }); + + it.each( + [[true, true], [true, false], + [false, true], [false, false]], + )("2 unverified: returns 'normal', self-trust = %s, DM = %s", async (trusted, dm) => { + const client = mkClient(trusted); + const room = { + roomId: dm ? "DM" : "other", + getEncryptionTargetMembers: () => ["@self:localhost", "@FF1:h", "@FF2:h"].map((userId) => ({userId})), + }; + const status = await shieldStatusForRoom(client, room); + expect(status).toEqual("normal"); + }); + + it.each( + [["verified", true, true], ["verified", true, false], + ["verified", false, true], ["warning", false, false]], + )("2 verified: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => { + const client = mkClient(trusted); + const room = { + roomId: dm ? "DM" : "other", + getEncryptionTargetMembers: () => ["@self:localhost", "@TT1:h", "@TT2:h"].map((userId) => ({userId})), + }; + const status = await shieldStatusForRoom(client, room); + expect(status).toEqual(result); + }); + + it.each( + [["normal", true, true], ["normal", true, false], + ["normal", false, true], ["warning", false, false]], + )("2 mixed: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => { + const client = mkClient(trusted); + const room = { + roomId: dm ? "DM" : "other", + getEncryptionTargetMembers: () => ["@self:localhost", "@TT1:h", "@FF2:h"].map((userId) => ({userId})), + }; + const status = await shieldStatusForRoom(client, room); + expect(status).toEqual(result); + }); + + it.each( + [["verified", true, true], ["verified", true, false], + ["warning", false, true], ["warning", false, false]], + )("0 others: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => { + const client = mkClient(trusted); + const room = { + roomId: dm ? "DM" : "other", + getEncryptionTargetMembers: () => ["@self:localhost"].map((userId) => ({userId})), + }; + const status = await shieldStatusForRoom(client, room); + expect(status).toEqual(result); + }); + + it.each( + [["verified", true, true], ["verified", true, false], + ["verified", false, true], ["verified", false, false]], + )("1 verified: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => { + const client = mkClient(trusted); + const room = { + roomId: dm ? "DM" : "other", + getEncryptionTargetMembers: () => ["@self:localhost", "@TT:h"].map((userId) => ({userId})), + }; + const status = await shieldStatusForRoom(client, room); + expect(status).toEqual(result); + }); + + it.each( + [["normal", true, true], ["normal", true, false], + ["normal", false, true], ["normal", false, false]], + )("1 unverified: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => { + const client = mkClient(trusted); + const room = { + roomId: dm ? "DM" : "other", + getEncryptionTargetMembers: () => ["@self:localhost", "@FF:h"].map((userId) => ({userId})), + }; + const status = await shieldStatusForRoom(client, room); + expect(status).toEqual(result); + }); +}); + +describe("shieldStatusForMembership other-trust behaviour", function() { + beforeAll(() => { + DMRoomMap._sharedInstance = { + getUserIdForRoomId: (roomId) => roomId === "DM" ? "@any:h" : null, + }; + }); + + it.each( + [["warning", true], ["warning", false]], + )("1 verified/untrusted: returns '%s', DM = %s", async (result, dm) => { + const client = mkClient(true); + const room = { + roomId: dm ? "DM" : "other", + getEncryptionTargetMembers: () => ["@self:localhost", "@TF:h"].map((userId) => ({userId})), + }; + const status = await shieldStatusForRoom(client, room); + expect(status).toEqual(result); + }); + + it.each( + [["warning", true], ["warning", false]], + )("2 verified/untrusted: returns '%s', DM = %s", async (result, dm) => { + const client = mkClient(true); + const room = { + roomId: dm ? "DM" : "other", + getEncryptionTargetMembers: () => ["@self:localhost", "@TF:h", "@TT:h"].map((userId) => ({userId})), + }; + const status = await shieldStatusForRoom(client, room); + expect(status).toEqual(result); + }); + + it.each( + [["normal", true], ["normal", false]], + )("2 unverified/untrusted: returns '%s', DM = %s", async (result, dm) => { + const client = mkClient(true); + const room = { + roomId: dm ? "DM" : "other", + getEncryptionTargetMembers: () => ["@self:localhost", "@FF:h", "@FT:h"].map((userId) => ({userId})), + }; + const status = await shieldStatusForRoom(client, room); + expect(status).toEqual(result); + }); + + it.each( + [["warning", true], ["warning", false]], + )("2 was verified: returns '%s', DM = %s", async (result, dm) => { + const client = mkClient(true); + const room = { + roomId: dm ? "DM" : "other", + getEncryptionTargetMembers: () => ["@self:localhost", "@WF:h", "@FT:h"].map((userId) => ({userId})), + }; + const status = await shieldStatusForRoom(client, room); + expect(status).toEqual(result); + }); +}); diff --git a/yarn.lock b/yarn.lock index 582d89137e..c5fc8268a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5690,8 +5690,8 @@ mathml-tag-names@^2.0.1: integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": - version "5.1.1" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/b2e154377a4268441a3b27b183dd7f7018187035" + version "5.2.0" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/223d37ffce674a23ca73702f04b9ba31cfd84196" dependencies: "@babel/runtime" "^7.8.3" another-json "^0.2.0"