From 9eed423994b546dc01ecec75d4f8fae0df35d28b Mon Sep 17 00:00:00 2001 From: j <j@mailb.org> Date: Tue, 24 Dec 2019 15:06:50 +0100 Subject: [PATCH 01/21] support channel names with slash in name/alias Signed-off-by: Jan Gerber <j@mailb.org> --- src/components/structures/MatrixChat.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index fad57f5d52..af3f4d2598 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1569,9 +1569,17 @@ export default createReactClass({ action: 'start_post_registration', }); } else if (screen.indexOf('room/') == 0) { - const segments = screen.substring(5).split('/'); - const roomString = segments[0]; - let eventId = segments.splice(1).join("/"); // empty string if no event id given + // Rooms can have the following formats: + // #room_alias:domain or !opaque_id:domain + const room = screen.substring(5); + const domainOffset = room.indexOf(':') + 1; // 0 in case room does not contain a : + let eventOffset = room.length; + // room aliases can contain slashes only look for slash after domain + if (room.substring(domainOffset).indexOf('/') > -1) { + eventOffset = domainOffset + room.substring(domainOffset).indexOf('/'); + } + const roomString = room.substring(0, eventOffset); + let eventId = room.substring(eventOffset + 1); // empty string if no event id given // Previously we pulled the eventID from the segments in such a way // where if there was no eventId then we'd get undefined. However, we From ffba19bd613796090bdc899f5cb4b40e96bbeb47 Mon Sep 17 00:00:00 2001 From: David Baker <dave@matrix.org> Date: Thu, 2 Jan 2020 16:52:25 +0000 Subject: [PATCH 02/21] Remove E2eIcon onClick It displayed the Encrypted Event Info dialog, but this full of super advanced debug information and base64 strings that no normal users should ever have to see. It's still accessible via the comtext menu (ie. the same place as 'View Source'). --- res/css/views/rooms/_EventTile.scss | 2 +- src/components/views/rooms/EventTile.js | 23 ++++------------------- 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 5359992f84..fbac1e932a 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.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. @@ -353,7 +354,6 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { left: 46px; width: 15px; height: 15px; - cursor: pointer; display: block; bottom: 0; right: 0; diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 784c4071aa..e7696de841 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -1,7 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); @@ -443,15 +443,6 @@ module.exports = createReactClass({ }); }, - onCryptoClick: function(e) { - const event = this.props.mxEvent; - - Modal.createTrackedDialogAsync('Encrypted Event Dialog', '', - import('../../../async-components/views/dialogs/EncryptedEventDialog'), - {event}, - ); - }, - onRequestKeysClick: function() { this.setState({ // Indicate in the UI that the keys have been requested (this is expected to @@ -479,11 +470,10 @@ module.exports = createReactClass({ _renderE2EPadlock: function() { const ev = this.props.mxEvent; - const props = {onClick: this.onCryptoClick}; // event could not be decrypted if (ev.getContent().msgtype === 'm.bad.encrypted') { - return <E2ePadlockUndecryptable {...props} />; + return <E2ePadlockUndecryptable />; } // event is encrypted, display padlock corresponding to whether or not it is verified @@ -491,7 +481,7 @@ module.exports = createReactClass({ if (this.state.verified) { return; // no icon for verified } else { - return (<E2ePadlockUnverified {...props} />); + return (<E2ePadlockUnverified />); } } @@ -508,7 +498,7 @@ module.exports = createReactClass({ return; // we expect this to be unencrypted } // if the event is not encrypted, but it's an e2e room, show the open padlock - return <E2ePadlockUnencrypted {...props} />; + return <E2ePadlockUnencrypted />; } // no padlock needed @@ -920,7 +910,6 @@ class E2ePadlock extends React.Component { static propTypes = { icon: PropTypes.string.isRequired, title: PropTypes.string.isRequired, - onClick: PropTypes.func, }; constructor() { @@ -931,10 +920,6 @@ class E2ePadlock extends React.Component { }; } - onClick = (e) => { - if (this.props.onClick) this.props.onClick(e); - }; - onHoverStart = () => { this.setState({hover: true}); }; From 39777620a3d299943ed7d8f24fb34ff9263aa231 Mon Sep 17 00:00:00 2001 From: David Baker <dave@matrix.org> Date: Thu, 2 Jan 2020 16:58:00 +0000 Subject: [PATCH 03/21] order copyright lines by date --- src/components/views/rooms/EventTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index e7696de841..d8ca5ef7cd 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -1,8 +1,8 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 New Vector Ltd -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> +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. From 89ac476281763cea379fea4f23ed60a72ae4e031 Mon Sep 17 00:00:00 2001 From: David Baker <dave@matrix.org> Date: Thu, 2 Jan 2020 16:59:46 +0000 Subject: [PATCH 04/21] Unused import --- src/components/views/rooms/EventTile.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index d8ca5ef7cd..b71771a916 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -24,7 +24,6 @@ import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; const classNames = require("classnames"); import { _t, _td } from '../../../languageHandler'; -const Modal = require('../../../Modal'); const sdk = require('../../../index'); const TextForEvent = require('../../../TextForEvent'); From 4de0f7257a1846ac75d56bd75bc9941927c55a2b Mon Sep 17 00:00:00 2001 From: Travis Ralston <travpc@gmail.com> Date: Thu, 2 Jan 2020 17:40:18 -0700 Subject: [PATCH 05/21] Initial implementation of FTUE user lists design This covers the "recents" section and rough design exclusively. It is known that the Field does nothing and that there's a bunch of missing functionality - this is to be iterated upon in future PRs. Labs flag is to aide development and should be removed in a very near future PR. Also, this is focusing on DMs and not user lists in general because I misinterpreted the scope. I'll fix this in a future PR and instead make this the best DM invite dialog it can be. Closes https://github.com/vector-im/riot-web/issues/11197 --- package.json | 3 +- res/css/_components.scss | 1 + res/css/views/dialogs/_DMInviteDialog.scss | 81 +++++++ res/themes/dark/css/_dark.scss | 3 + res/themes/light/css/_light.scss | 3 + src/RoomInvite.js | 15 +- .../views/dialogs/DMInviteDialog.js | 212 ++++++++++++++++++ src/i18n/strings/en_EN.json | 6 + src/settings/Settings.js | 6 + src/utils/DMRoomMap.js | 9 + yarn.lock | 5 + 11 files changed, 342 insertions(+), 2 deletions(-) create mode 100644 res/css/views/dialogs/_DMInviteDialog.scss create mode 100644 src/components/views/dialogs/DMInviteDialog.js diff --git a/package.json b/package.json index 7ef14e6635..a1ebc6602d 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,6 @@ "file-saver": "^1.3.3", "filesize": "3.5.6", "flux": "2.1.1", - "react-focus-lock": "^2.2.1", "focus-visible": "^5.0.2", "fuse.js": "^2.2.0", "gemini-scrollbar": "github:matrix-org/gemini-scrollbar#91e1e566", @@ -82,6 +81,7 @@ "glob": "^5.0.14", "glob-to-regexp": "^0.4.1", "highlight.js": "^9.15.8", + "humanize": "^0.0.9", "is-ip": "^2.0.0", "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.6", @@ -99,6 +99,7 @@ "react-addons-css-transition-group": "15.6.2", "react-beautiful-dnd": "^4.0.1", "react-dom": "^16.9.0", + "react-focus-lock": "^2.2.1", "react-gemini-scrollbar": "github:matrix-org/react-gemini-scrollbar#9cf17f63b7c0b0ec5f31df27da0f82f7238dc594", "resize-observer-polyfill": "^1.5.0", "sanitize-html": "^1.18.4", diff --git a/res/css/_components.scss b/res/css/_components.scss index 7b8ca77739..7a9ebfdf26 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -56,6 +56,7 @@ @import "./views/dialogs/_ConfirmUserActionDialog.scss"; @import "./views/dialogs/_CreateGroupDialog.scss"; @import "./views/dialogs/_CreateRoomDialog.scss"; +@import "./views/dialogs/_DMInviteDialog.scss"; @import "./views/dialogs/_DeactivateAccountDialog.scss"; @import "./views/dialogs/_DeviceVerifyDialog.scss"; @import "./views/dialogs/_DevtoolsDialog.scss"; diff --git a/res/css/views/dialogs/_DMInviteDialog.scss b/res/css/views/dialogs/_DMInviteDialog.scss new file mode 100644 index 0000000000..50e9b0a15f --- /dev/null +++ b/res/css/views/dialogs/_DMInviteDialog.scss @@ -0,0 +1,81 @@ +/* +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. +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. +*/ + +.mx_DMInviteDialog_addressBar { + display: flex; + flex-direction: row; + + .mx_DMInviteDialog_editor { + flex: 1; + width: 100%; // Needed to make the Field inside grow + } + + .mx_Field { + margin: 0; + } + + .mx_DMInviteDialog_goButton { + width: 48px; + margin-left: 10px; + } +} + +.mx_DMInviteDialog_section { + padding-bottom: 10px; + + h3 { + font-size: 12px; + color: $muted-fg-color; + font-weight: bold; + text-transform: uppercase; + } +} + +.mx_DMInviteDialog_roomTile { + cursor: pointer; + padding: 5px 10px; + + &:hover { + background-color: $user-tile-hover-bg-color; + border-radius: 4px; + } + + * { + vertical-align: middle; + } + + .mx_DMInviteDialog_roomTile_name { + font-weight: 600; + font-size: 14px; + color: $primary-fg-color; + margin-left: 7px; + } + + .mx_DMInviteDialog_roomTile_userId { + font-size: 12px; + color: $muted-fg-color; + margin-left: 7px; + } + + .mx_DMInviteDialog_roomTile_time { + text-align: right; + font-size: 12px; + color: $muted-fg-color; + float: right; + line-height: 36px; // Height of the avatar to keep the time vertically aligned + } +} + diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index eadde4c672..d28efbb11f 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -16,6 +16,7 @@ $room-highlight-color: #343a46; // typical text (dark-on-white in light skin) $primary-fg-color: $text-primary-color; $primary-bg-color: $bg-color; +$muted-fg-color: $header-panel-text-primary-color; // used for dialog box text $light-fg-color: $header-panel-text-secondary-color; @@ -172,6 +173,8 @@ $interactive-tooltip-fg-color: #ffffff; $breadcrumb-placeholder-bg-color: #272c35; +$user-tile-hover-bg-color: $header-panel-bg-color; + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 0a3ef812b8..ac9cb261d3 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -21,6 +21,7 @@ $header-panel-bg-color: #f3f8fd; // typical text (dark-on-white in light skin) $primary-fg-color: #2e2f32; $primary-bg-color: #ffffff; +$muted-fg-color: #61708b; // Commonly used in headings and relevant alt text // used for dialog box text $light-fg-color: #747474; @@ -293,6 +294,8 @@ $interactive-tooltip-fg-color: #ffffff; $breadcrumb-placeholder-bg-color: #e8eef5; +$user-tile-hover-bg-color: $header-panel-bg-color; + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 48baad5d9f..ba9fe1f541 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -25,6 +25,7 @@ import sdk from './'; import dis from './dispatcher'; import DMRoomMap from './utils/DMRoomMap'; import { _t } from './languageHandler'; +import SettingsStore from "./settings/SettingsStore"; /** * Invites multiple addresses to a room @@ -41,6 +42,18 @@ function inviteMultipleToRoom(roomId, addrs) { } export function showStartChatInviteDialog() { + if (SettingsStore.isFeatureEnabled("feature_ftue_dms")) { + const DMInviteDialog = sdk.getComponent("dialogs.DMInviteDialog"); + Modal.createTrackedDialog('Start DM', '', DMInviteDialog, { + onFinished: (inviteIds) => { + // TODO: Replace _onStartDmFinished with less hacks + if (inviteIds.length > 0) _onStartDmFinished(true, inviteIds.map(i => ({address: i}))); + // else ignore and just do nothing + }, + }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); + return; + } + const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog"); Modal.createTrackedDialog('Start a chat', '', AddressPickerDialog, { @@ -99,7 +112,7 @@ export function isValid3pidInvite(event) { return true; } -// TODO: Immutable DMs replaces this +// TODO: Canonical DMs replaces this function _onStartDmFinished(shouldInvite, addrs) { if (!shouldInvite) return; diff --git a/src/components/views/dialogs/DMInviteDialog.js b/src/components/views/dialogs/DMInviteDialog.js new file mode 100644 index 0000000000..5b67112c14 --- /dev/null +++ b/src/components/views/dialogs/DMInviteDialog.js @@ -0,0 +1,212 @@ +/* +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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import {_t} from "../../../languageHandler"; +import sdk from "../../../index"; +import MatrixClientPeg from "../../../MatrixClientPeg"; +import {makeUserPermalink} from "../../../utils/permalinks/Permalinks"; +import DMRoomMap from "../../../utils/DMRoomMap"; +import {RoomMember} from "matrix-js-sdk/lib/matrix"; +import * as humanize from "humanize"; + +// TODO: [TravisR] Make this generic for all kinds of invites + +const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first +const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked + +class DMRoomTile extends React.Component { + static propTypes = { + member: PropTypes.object.isRequired, + lastActiveTs: PropTypes.number, + onToggle: PropTypes.func.isRequired, + }; + + constructor() { + super(); + } + + _onClick = (e) => { + // Stop the browser from highlighting text + e.preventDefault(); + e.stopPropagation(); + + this.props.onToggle(this.props.member.userId); + }; + + render() { + const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar"); + + let timestamp = null; + if (this.props.lastActiveTs) { + // TODO: [TravisR] Figure out how to i18n this + // `humanize` wants seconds for a timestamp, so divide by 1000 + const humanTs = humanize.relativeTime(this.props.lastActiveTs / 1000); + timestamp = <span className='mx_DMInviteDialog_roomTile_time'>{humanTs}</span>; + } + + return ( + <div className='mx_DMInviteDialog_roomTile' onClick={this._onClick}> + <MemberAvatar member={this.props.member} width={36} height={36} /> + <span className='mx_DMInviteDialog_roomTile_name'>{this.props.member.name}</span> + <span className='mx_DMInviteDialog_roomTile_userId'>{this.props.member.userId}</span> + {timestamp} + </div> + ); + } +} + +export default class DMInviteDialog extends React.Component { + static propTypes = { + // Takes an array of user IDs/emails to invite. + onFinished: PropTypes.func.isRequired, + }; + + constructor() { + super(); + + this.state = { + targets: [], // string[] of mxids/email addresses + filterText: "", + recents: this._buildRecents(), + numRecentsShown: INITIAL_ROOMS_SHOWN, + }; + } + + _buildRecents(): {userId: string, user: RoomMember, lastActive: number} { + const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); + const recents = []; + for (const userId in rooms) { + const room = rooms[userId]; + const member = room.getMember(userId); + if (!member) continue; // just skip people who don't have memberships for some reason + + const lastEventTs = room.timeline && room.timeline.length ? room.timeline[room.timeline.length - 1].getTs() : 0; + if (!lastEventTs) continue; // something weird is going on with this room + + recents.push({userId, user: member, lastActive: lastEventTs}); + } + + // Sort the recents by last active to save us time later + recents.sort((a, b) => b.lastActive - a.lastActive); + + return recents; + } + + _startDm = () => { + this.props.onFinished(this.state.targets); + }; + + _cancel = () => { + this.props.onFinished([]); + }; + + _updateFilter = (e) => { + this.setState({filterText: e.target.value}); + }; + + _showMoreRecents = () => { + this.setState({numRecentsShown: this.state.numRecentsShown + INCREMENT_ROOMS_SHOWN}); + }; + + _toggleMember = (userId) => { + const targets = this.state.targets.map(t => t); // cheap clone for mutation + const idx = targets.indexOf(userId); + if (idx >= 0) targets.splice(idx, 1); + else targets.push(userId); + this.setState({targets}); + }; + + _renderRecents() { + if (!this.state.recents || this.state.recents.length === 0) return null; + + // .slice() will return an incomplete array but won't error on us if we go too far + const toRender = this.state.recents.slice(0, this.state.numRecentsShown); + const hasMore = toRender.length < this.state.recents.length; + + const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); + let showMore = null; + if (hasMore) { + showMore = ( + <AccessibleButton onClick={this._showMoreRecents} kind="link"> + {_t("Show more")} + </AccessibleButton> + ) + } + + return ( + <div className='mx_DMInviteDialog_section'> + <h3>{_t("Recent Conversations")}</h3> + {toRender.map(r => <DMRoomTile member={r.user} lastActiveTs={r.lastActive} key={r.userId} onToggle={this._toggleMember} />)} + {showMore} + </div> + ) + } + + render() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const Field = sdk.getComponent("elements.Field"); + const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); + + // Dev note: The use of Field is temporary/incomplete pending https://github.com/vector-im/riot-web/issues/11197 + // For now, we just list who the targets are. + const editor = ( + <div className='mx_DMInviteDialog_editor'> + <Field + id="inviteTargets" + value={this.state.filterText} + onChange={this._updateFilter} + placeholder="TODO: Implement filtering/searching (https://github.com/vector-im/riot-web/issues/11199)" + /> + </div> + ); + const targets = this.state.targets.map(t => <div key={t}>{t}</div>); + + const userId = MatrixClientPeg.get().getUserId(); + return ( + <BaseDialog + className='mx_DMInviteDialog' + hasCancel={true} + onFinished={this._cancel} + title={_t("Direct Messages")} + > + <div className='mx_DMInviteDialog_content'> + <p> + {_t( + "If you can't find someone, ask them for their username, or share your " + + "username (%(userId)s) or <a>profile link</a>.", + {userId}, + {a: (sub) => <a href={makeUserPermalink(userId)} rel="noopener" target="_blank">{sub}</a>}, + )} + </p> + {targets} + <div className='mx_DMInviteDialog_addressBar'> + {editor} + <AccessibleButton + kind="primary" + onClick={this._startDm} + className='mx_DMInviteDialog_goButton' + > + {_t("Go")} + </AccessibleButton> + </div> + {this._renderRecents()} + </div> + </BaseDialog> + ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 6979759cd2..18c3d76e12 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -358,6 +358,7 @@ "Render simple counters in room header": "Render simple counters in room header", "Multiple integration managers": "Multiple integration managers", "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", + "New DM invite dialog (under development)": "New DM invite dialog (under development)", "Enable cross-signing to verify per-user instead of per-device (in development)": "Enable cross-signing to verify per-user instead of per-device (in development)", "Enable local event indexing and E2EE search (requires restart)": "Enable local event indexing and E2EE search (requires restart)", "Use the new, faster, composer for writing messages": "Use the new, faster, composer for writing messages", @@ -1431,6 +1432,11 @@ "View Servers in Room": "View Servers in Room", "Toolbox": "Toolbox", "Developer Tools": "Developer Tools", + "Show more": "Show more", + "Recent Conversations": "Recent Conversations", + "Direct Messages": "Direct Messages", + "If you can't find someone, ask them for their username, or share your username (%(userId)s) or <a>profile link</a>.": "If you can't find someone, ask them for their username, or share your username (%(userId)s) or <a>profile link</a>.", + "Go": "Go", "An error has occurred.": "An error has occurred.", "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.", "Verifying this user will mark their device as trusted, and also mark your device as trusted to them.": "Verifying this user will mark their device as trusted, and also mark your device as trusted to them.", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index f1299a9045..d606528ca3 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -128,6 +128,12 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_ftue_dms": { + isFeature: true, + displayName: _td("New DM invite dialog (under development)"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "mjolnirRooms": { supportedLevels: ['account'], default: [], diff --git a/src/utils/DMRoomMap.js b/src/utils/DMRoomMap.js index af65b6f001..498c073e0e 100644 --- a/src/utils/DMRoomMap.js +++ b/src/utils/DMRoomMap.js @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +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. @@ -16,6 +17,7 @@ limitations under the License. import MatrixClientPeg from '../MatrixClientPeg'; import _uniq from 'lodash/uniq'; +import {Room} from "matrix-js-sdk/lib/matrix"; /** * Class that takes a Matrix Client and flips the m.direct map @@ -144,6 +146,13 @@ export default class DMRoomMap { return this.roomToUser[roomId]; } + getUniqueRoomsWithIndividuals(): {[userId: string]: Room} { + return Object.keys(this.roomToUser) + .map(r => ({userId: this.getUserIdForRoomId(r), room: this.matrixClient.getRoom(r)})) + .filter(r => r.userId && r.room && r.room.getInvitedAndJoinedMemberCount() === 2) + .reduce((obj, r) => (obj[r.userId] = r.room) && obj, {}); + } + _getUserToRooms() { if (!this.userToRooms) { const userToRooms = this.mDirectEvent; diff --git a/yarn.lock b/yarn.lock index b8b877ab62..fc2b9e04c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4089,6 +4089,11 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" +humanize@^0.0.9: + version "0.0.9" + resolved "https://registry.yarnpkg.com/humanize/-/humanize-0.0.9.tgz#1994ffaecdfe9c441ed2bdac7452b7bb4c9e41a4" + integrity sha1-GZT/rs3+nEQe0r2sdFK3u0yeQaQ= + iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" From 3488eaba3cbed69f552d6a66d4e383b31768217a Mon Sep 17 00:00:00 2001 From: Travis Ralston <travpc@gmail.com> Date: Thu, 2 Jan 2020 17:44:19 -0700 Subject: [PATCH 06/21] Appease the linter --- src/components/views/dialogs/DMInviteDialog.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/views/dialogs/DMInviteDialog.js b/src/components/views/dialogs/DMInviteDialog.js index 5b67112c14..6456ef7083 100644 --- a/src/components/views/dialogs/DMInviteDialog.js +++ b/src/components/views/dialogs/DMInviteDialog.js @@ -95,7 +95,9 @@ export default class DMInviteDialog extends React.Component { const member = room.getMember(userId); if (!member) continue; // just skip people who don't have memberships for some reason - const lastEventTs = room.timeline && room.timeline.length ? room.timeline[room.timeline.length - 1].getTs() : 0; + const lastEventTs = room.timeline && room.timeline.length + ? room.timeline[room.timeline.length - 1].getTs() + : 0; if (!lastEventTs) continue; // something weird is going on with this room recents.push({userId, user: member, lastActive: lastEventTs}); @@ -145,16 +147,19 @@ export default class DMInviteDialog extends React.Component { <AccessibleButton onClick={this._showMoreRecents} kind="link"> {_t("Show more")} </AccessibleButton> - ) + ); } + const tiles = toRender.map(r => ( + <DMRoomTile member={r.user} lastActiveTs={r.lastActive} key={r.userId} onToggle={this._toggleMember} /> + )); return ( <div className='mx_DMInviteDialog_section'> <h3>{_t("Recent Conversations")}</h3> - {toRender.map(r => <DMRoomTile member={r.user} lastActiveTs={r.lastActive} key={r.userId} onToggle={this._toggleMember} />)} + {tiles} {showMore} </div> - ) + ); } render() { @@ -170,7 +175,7 @@ export default class DMInviteDialog extends React.Component { id="inviteTargets" value={this.state.filterText} onChange={this._updateFilter} - placeholder="TODO: Implement filtering/searching (https://github.com/vector-im/riot-web/issues/11199)" + placeholder="TODO: Implement filtering/searching (vector-im/riot-web#11199)" /> </div> ); From 6f1525c1f341c1dd7fc64727f6a4f22971df4ebc Mon Sep 17 00:00:00 2001 From: Travis Ralston <travpc@gmail.com> Date: Thu, 2 Jan 2020 17:47:26 -0700 Subject: [PATCH 07/21] Appease the scss linter --- res/css/views/dialogs/_DMInviteDialog.scss | 94 +++++++++++----------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/res/css/views/dialogs/_DMInviteDialog.scss b/res/css/views/dialogs/_DMInviteDialog.scss index 50e9b0a15f..1153ecb0d4 100644 --- a/res/css/views/dialogs/_DMInviteDialog.scss +++ b/res/css/views/dialogs/_DMInviteDialog.scss @@ -15,67 +15,67 @@ limitations under the License. */ .mx_DMInviteDialog_addressBar { - display: flex; - flex-direction: row; + display: flex; + flex-direction: row; - .mx_DMInviteDialog_editor { - flex: 1; - width: 100%; // Needed to make the Field inside grow - } + .mx_DMInviteDialog_editor { + flex: 1; + width: 100%; // Needed to make the Field inside grow + } - .mx_Field { - margin: 0; - } + .mx_Field { + margin: 0; + } - .mx_DMInviteDialog_goButton { - width: 48px; - margin-left: 10px; - } + .mx_DMInviteDialog_goButton { + width: 48px; + margin-left: 10px; + } } .mx_DMInviteDialog_section { - padding-bottom: 10px; + padding-bottom: 10px; - h3 { - font-size: 12px; - color: $muted-fg-color; - font-weight: bold; - text-transform: uppercase; - } + h3 { + font-size: 12px; + color: $muted-fg-color; + font-weight: bold; + text-transform: uppercase; + } } .mx_DMInviteDialog_roomTile { - cursor: pointer; - padding: 5px 10px; + cursor: pointer; + padding: 5px 10px; - &:hover { - background-color: $user-tile-hover-bg-color; - border-radius: 4px; - } + &:hover { + background-color: $user-tile-hover-bg-color; + border-radius: 4px; + } - * { - vertical-align: middle; - } + * { + vertical-align: middle; + } - .mx_DMInviteDialog_roomTile_name { - font-weight: 600; - font-size: 14px; - color: $primary-fg-color; - margin-left: 7px; - } + .mx_DMInviteDialog_roomTile_name { + font-weight: 600; + font-size: 14px; + color: $primary-fg-color; + margin-left: 7px; + } - .mx_DMInviteDialog_roomTile_userId { - font-size: 12px; - color: $muted-fg-color; - margin-left: 7px; - } + .mx_DMInviteDialog_roomTile_userId { + font-size: 12px; + color: $muted-fg-color; + margin-left: 7px; + } - .mx_DMInviteDialog_roomTile_time { - text-align: right; - font-size: 12px; - color: $muted-fg-color; - float: right; - line-height: 36px; // Height of the avatar to keep the time vertically aligned - } + .mx_DMInviteDialog_roomTile_time { + text-align: right; + font-size: 12px; + color: $muted-fg-color; + float: right; + line-height: 36px; // Height of the avatar to keep the time vertically aligned + } } From 557669b08e3734bf1e0e6fa871f17d76968af0fb Mon Sep 17 00:00:00 2001 From: David Baker <dave@matrix.org> Date: Fri, 3 Jan 2020 11:12:55 +0000 Subject: [PATCH 08/21] Don't crash if a keyshare request is removed ...during the time the dialog is displayed. Fixes https://github.com/vector-im/riot-web/issues/11745 (hopefully) --- src/KeyRequestHandler.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/KeyRequestHandler.js b/src/KeyRequestHandler.js index c3de7988b2..4ee258da53 100644 --- a/src/KeyRequestHandler.js +++ b/src/KeyRequestHandler.js @@ -111,6 +111,12 @@ export default class KeyRequestHandler { this._currentUser = null; this._currentDevice = null; + if (!this._pendingKeyRequests[userId] || !this._pendingKeyRequests[userId][deviceId]) { + // request was removed in the time the dialog was displayed + this._processNextRequest(); + return; + } + if (r) { for (const req of this._pendingKeyRequests[userId][deviceId]) { req.share(); From c2723176e403dcb16a11e57a40db5e800e0fc653 Mon Sep 17 00:00:00 2001 From: David Baker <dave@matrix.org> Date: Fri, 3 Jan 2020 12:08:35 +0000 Subject: [PATCH 09/21] Convert /verify to checkDeviceTrust Also de-promiseify the code a bit --- src/SlashCommands.js | 86 ++++++++++++++++++++++---------------------- 1 file changed, 42 insertions(+), 44 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index a9c015fdaf..21fa4a134e 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -780,54 +780,52 @@ export const CommandMap = { const deviceId = matches[2]; const fingerprint = matches[3]; - return success( - // Promise.resolve to handle transition from static result to promise; can be removed - // in future - Promise.resolve(cli.getStoredDevice(userId, deviceId)).then((device) => { - if (!device) { - throw new Error(_t('Unknown (user, device) pair:') + ` (${userId}, ${deviceId})`); - } + return success((async () => { + const device = await cli.getStoredDevice(userId, deviceId); + if (!device) { + throw new Error(_t('Unknown (user, device) pair:') + ` (${userId}, ${deviceId})`); + } + const deviceTrust = await cli.checkDeviceTrust(userId, deviceId); - if (device.isVerified()) { - if (device.getFingerprint() === fingerprint) { - throw new Error(_t('Device already verified!')); - } else { - throw new Error(_t('WARNING: Device already verified, but keys do NOT MATCH!')); - } + if (deviceTrust.isVerified()) { + if (device.getFingerprint() === fingerprint) { + throw new Error(_t('Device already verified!')); + } else { + throw new Error(_t('WARNING: Device 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' + - ' %(deviceId)s is "%(fprint)s" which does not match the provided key ' + - '"%(fingerprint)s". This could mean your communications are being intercepted!', - { - fprint, - userId, - deviceId, - fingerprint, - })); - } + if (device.getFingerprint() !== fingerprint) { + const fprint = device.getFingerprint(); + throw new Error( + _t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' + + ' %(deviceId)s is "%(fprint)s" which does not match the provided key ' + + '"%(fingerprint)s". This could mean your communications are being intercepted!', + { + fprint, + userId, + deviceId, + fingerprint, + })); + } - return cli.setDeviceVerified(userId, deviceId, true); - }).then(() => { - // Tell the user we verified everything - const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); - Modal.createTrackedDialog('Slash Commands', 'Verified key', InfoDialog, { - title: _t('Verified key'), - description: <div> - <p> - { - _t('The signing key you provided matches the signing key you received ' + - 'from %(userId)s\'s device %(deviceId)s. Device marked as verified.', - {userId, deviceId}) - } - </p> - </div>, - }); - }), - ); + await cli.setDeviceVerified(userId, deviceId, true); + + // Tell the user we verified everything + const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); + Modal.createTrackedDialog('Slash Commands', 'Verified key', InfoDialog, { + title: _t('Verified key'), + description: <div> + <p> + { + _t('The signing key you provided matches the signing key you received ' + + 'from %(userId)s\'s device %(deviceId)s. Device marked as verified.', + {userId, deviceId}) + } + </p> + </div>, + }); + })()); } } return reject(this.getUsage()); From 99559c5121e5759b6a6801b66db5d13a6f558388 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" <jryans@gmail.com> Date: Fri, 3 Jan 2020 13:33:32 +0000 Subject: [PATCH 10/21] Update backup restore paths for SSSS This updates all the various key backup entry points to ensure they use a flow that supports backups stored under secret storage. --- .../keybackup/NewRecoveryMethodDialog.js | 11 +++-- src/components/views/dialogs/LogoutDialog.js | 6 ++- .../keybackup/RestoreKeyBackupDialog.js | 47 +++++++++++++++++-- .../views/rooms/RoomRecoveryReminder.js | 6 ++- .../views/settings/KeyBackupPanel.js | 23 +++------ 5 files changed, 66 insertions(+), 27 deletions(-) diff --git a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js index 28281af771..147f109113 100644 --- a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js +++ b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js @@ -1,5 +1,6 @@ /* -Copyright 2018-2019 New Vector Ltd +Copyright 2018, 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. @@ -40,9 +41,11 @@ export default class NewRecoveryMethodDialog extends React.PureComponent { onSetupClick = async () => { const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); - Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, { - onFinished: this.props.onFinished, - }); + Modal.createTrackedDialog( + 'Restore Backup', '', RestoreKeyBackupDialog, { + onFinished: this.props.onFinished, + }, null, /* priority = */ false, /* static = */ true, + ); } render() { diff --git a/src/components/views/dialogs/LogoutDialog.js b/src/components/views/dialogs/LogoutDialog.js index 47d4153494..6e4f950830 100644 --- a/src/components/views/dialogs/LogoutDialog.js +++ b/src/components/views/dialogs/LogoutDialog.js @@ -1,5 +1,6 @@ /* Copyright 2018, 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. @@ -94,7 +95,10 @@ export default class LogoutDialog extends React.Component { // verified, so restore the backup which will give us the keys from it and // allow us to trust it (ie. upload keys to it) const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); - Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {}); + Modal.createTrackedDialog( + 'Restore Backup', '', RestoreKeyBackupDialog, null, null, + /* priority = */ false, /* static = */ true, + ); } else { Modal.createTrackedDialogAsync("Key Backup", "Key Backup", import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"), diff --git a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js index 45168c381e..2881cc920c 100644 --- a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js +++ b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js @@ -1,5 +1,6 @@ /* Copyright 2018, 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. @@ -15,17 +16,18 @@ limitations under the License. */ import React from 'react'; +import { MatrixClient } from 'matrix-js-sdk'; + import sdk from '../../../../index'; import MatrixClientPeg from '../../../../MatrixClientPeg'; import Modal from '../../../../Modal'; - -import { MatrixClient } from 'matrix-js-sdk'; - import { _t } from '../../../../languageHandler'; import {Key} from "../../../../Keyboard"; +import { accessSecretStorage } from '../../../../CrossSigningManager'; const RESTORE_TYPE_PASSPHRASE = 0; const RESTORE_TYPE_RECOVERYKEY = 1; +const RESTORE_TYPE_SECRET_STORAGE = 2; /* * Dialog for restoring e2e keys from a backup and the user's recovery key @@ -35,6 +37,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { super(props); this.state = { backupInfo: null, + backupKeyStored: null, loading: false, loadError: null, restoreError: null, @@ -148,6 +151,32 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { } } + async _restoreWithSecretStorage() { + this.setState({ + loading: true, + restoreError: null, + restoreType: RESTORE_TYPE_SECRET_STORAGE, + }); + try { + // `accessSecretStorage` may prompt for storage access as needed. + const recoverInfo = await accessSecretStorage(async () => { + return MatrixClientPeg.get().restoreKeyBackupWithSecretStorage( + this.state.backupInfo, + ); + }); + this.setState({ + loading: false, + recoverInfo, + }); + } catch (e) { + console.log("Error restoring backup", e); + this.setState({ + restoreError: e, + loading: false, + }); + } + } + async _loadBackupStatus() { this.setState({ loading: true, @@ -155,10 +184,20 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { }); try { const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); + const backupKeyStored = await MatrixClientPeg.get().isKeyBackupKeyStored(); + this.setState({ + backupInfo, + backupKeyStored, + }); + + // If the backup key is stored, we can proceed directly to restore. + if (backupKeyStored) { + return this._restoreWithSecretStorage(); + } + this.setState({ loadError: null, loading: false, - backupInfo, }); } catch (e) { console.log("Error loading backup status", e); diff --git a/src/components/views/rooms/RoomRecoveryReminder.js b/src/components/views/rooms/RoomRecoveryReminder.js index 6b7366bc4f..495364bf4c 100644 --- a/src/components/views/rooms/RoomRecoveryReminder.js +++ b/src/components/views/rooms/RoomRecoveryReminder.js @@ -1,5 +1,6 @@ /* Copyright 2018, 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. @@ -70,7 +71,10 @@ export default class RoomRecoveryReminder extends React.PureComponent { // verified, so restore the backup which will give us the keys from it and // allow us to trust it (ie. upload keys to it) const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); - Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {}); + Modal.createTrackedDialog( + 'Restore Backup', '', RestoreKeyBackupDialog, null, null, + /* priority = */ false, /* static = */ true, + ); } else { Modal.createTrackedDialogAsync("Key Backup", "Key Backup", import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"), diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index 559b1e0ba1..55bfadba88 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -1,6 +1,6 @@ /* Copyright 2018 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. @@ -181,22 +181,11 @@ export default class KeyBackupPanel extends React.PureComponent { } _restoreBackup = async () => { - // Use legacy path if backup key not stored in secret storage - if (!this.state.backupKeyStored) { - const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); - Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog); - return; - } - - try { - await accessSecretStorage(async () => { - await MatrixClientPeg.get().restoreKeyBackupWithSecretStorage( - this.state.backupInfo, - ); - }); - } catch (e) { - console.log("Error restoring backup", e); - } + const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); + Modal.createTrackedDialog( + 'Restore Backup', '', RestoreKeyBackupDialog, null, null, + /* priority = */ false, /* static = */ true, + ); } render() { From 43e4f2dcc0f5d970bd4b4021a713b58358e213f0 Mon Sep 17 00:00:00 2001 From: David Baker <dave@matrix.org> Date: Fri, 3 Jan 2020 13:34:43 +0000 Subject: [PATCH 11/21] Use deviceTrust when displaying key backup trust status Requires https://github.com/matrix-org/matrix-js-sdk/pull/1138 --- src/components/views/settings/KeyBackupPanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index 559b1e0ba1..9f20288fff 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -270,7 +270,7 @@ export default class KeyBackupPanel extends React.PureComponent { {sub} </span>; const verify = sub => - <span className={sig.device && sig.device.isVerified() ? 'mx_KeyBackupPanel_deviceVerified' : 'mx_KeyBackupPanel_deviceNotVerified'}> + <span className={sig.device && sig.deviceTrust.isVerified() ? 'mx_KeyBackupPanel_deviceVerified' : 'mx_KeyBackupPanel_deviceNotVerified'}> {sub} </span>; const device = sub => <span className="mx_KeyBackupPanel_deviceName">{deviceName}</span>; From d5a82a5fc2e9057ec94873e7c5f595ab4c79d569 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" <jryans@gmail.com> Date: Fri, 3 Jan 2020 13:45:52 +0000 Subject: [PATCH 12/21] Finish sentence in accessSecretStorage docs --- src/CrossSigningManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index ab0a22e4d5..f3953b1897 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -97,7 +97,7 @@ export const crossSigningCallbacks = { * * Additionally, the secret storage keys are cached during the scope of this function * to ensure the user is prompted only once for their secret storage - * passphrase. The cache is then + * passphrase. The cache is then cleared once the provided function completes. * * @param {Function} [func] An operation to perform once secret storage has been * bootstrapped. Optional. From 4211ec5063d1e531ffe2938dc2ea0b6088253bf4 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" <jryans@gmail.com> Date: Fri, 3 Jan 2020 13:51:42 +0000 Subject: [PATCH 13/21] Fix DOM structure in RoomRecoveryReminder Buttons (which end up as <div>s) aren't allowed inside <p>s. --- res/css/views/rooms/_RoomRecoveryReminder.scss | 1 + src/components/views/rooms/RoomRecoveryReminder.js | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/res/css/views/rooms/_RoomRecoveryReminder.scss b/res/css/views/rooms/_RoomRecoveryReminder.scss index 68e2bf861e..85d42ca4b4 100644 --- a/res/css/views/rooms/_RoomRecoveryReminder.scss +++ b/res/css/views/rooms/_RoomRecoveryReminder.scss @@ -40,4 +40,5 @@ limitations under the License. .mx_RoomRecoveryReminder_secondary { font-size: 90%; + margin-top: 1em; } diff --git a/src/components/views/rooms/RoomRecoveryReminder.js b/src/components/views/rooms/RoomRecoveryReminder.js index 495364bf4c..8554c804c0 100644 --- a/src/components/views/rooms/RoomRecoveryReminder.js +++ b/src/components/views/rooms/RoomRecoveryReminder.js @@ -154,14 +154,14 @@ export default class RoomRecoveryReminder extends React.PureComponent { onClick={this.onSetupClick}> {setupCaption} </AccessibleButton> - <p><AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton" + <AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton" onClick={this.onOnNotNowClick}> { _t("Not now") } - </AccessibleButton></p> - <p><AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton" + </AccessibleButton> + <AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton" onClick={this.onDontAskAgainClick}> { _t("Don't ask me again") } - </AccessibleButton></p> + </AccessibleButton> </div> </div> ); From 5897c8ca7ff5c20ef9f027097c6dae3735d52887 Mon Sep 17 00:00:00 2001 From: David Baker <dave@matrix.org> Date: Fri, 3 Jan 2020 15:00:51 +0000 Subject: [PATCH 14/21] Remove 'unverify' from UserInfoPanel It's not in the designs and it's not a thing we can do with cross-signing (at least not at the moment). --- src/components/views/right_panel/UserInfo.js | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 208c5e8906..809fdcb6d7 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -74,17 +74,6 @@ const _getE2EStatus = (cli, userId, devices) => { return "warning"; }; -async function unverifyUser(matrixClient, userId) { - const devices = await matrixClient.getStoredDevicesForUser(userId); - for (const device of devices) { - if (device.isVerified()) { - matrixClient.setDeviceVerified( - userId, device.deviceId, false, - ); - } - } -} - function openDMForUser(matrixClient, userId) { const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId); const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => { @@ -331,14 +320,6 @@ const UserOptionsSection = ({member, isIgnored, canInvite, devices}) => { </AccessibleButton> ); } - let unverifyButton; - if (devices && devices.some(device => device.isVerified())) { - unverifyButton = ( - <AccessibleButton onClick={() => unverifyUser(cli, member.userId)} className="mx_UserInfo_field mx_UserInfo_destructive"> - { _t('Unverify user') } - </AccessibleButton> - ); - } return ( <div className="mx_UserInfo_container"> @@ -350,7 +331,6 @@ const UserOptionsSection = ({member, isIgnored, canInvite, devices}) => { { insertPillButton } { inviteUserButton } { ignoreButton } - { unverifyButton } </div> </div> ); From 5faae1d2f2a0425997c08d5f03b9be80eb136152 Mon Sep 17 00:00:00 2001 From: David Baker <dave@matrix.org> Date: Fri, 3 Jan 2020 15:05:41 +0000 Subject: [PATCH 15/21] i18n --- src/i18n/strings/en_EN.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 6979759cd2..2065454525 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1114,7 +1114,6 @@ "%(count)s verified sessions|other": "%(count)s verified sessions", "%(count)s verified sessions|one": "1 verified session", "Direct message": "Direct message", - "Unverify user": "Unverify user", "Remove from community": "Remove from community", "Disinvite this user from community?": "Disinvite this user from community?", "Remove this user from community?": "Remove this user from community?", From 2970a9faaffde5f4eb22b33d15d481c9e83501ec Mon Sep 17 00:00:00 2001 From: David Baker <dave@matrix.org> Date: Fri, 3 Jan 2020 15:16:02 +0000 Subject: [PATCH 16/21] Don't fail if logs exists and is an empty dir --- scripts/ci/end-to-end-tests.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/ci/end-to-end-tests.sh b/scripts/ci/end-to-end-tests.sh index ae88ef70c7..a592888292 100755 --- a/scripts/ci/end-to-end-tests.sh +++ b/scripts/ci/end-to-end-tests.sh @@ -36,7 +36,8 @@ echo "--- Install synapse & other dependencies" ./install.sh # install static webserver to server symlinked local copy of riot ./riot/install-webserver.sh -mkdir logs || rm -r logs/* +rm -r logs || true +mkdir logs echo "+++ Running end-to-end tests" TESTS_STARTED=1 ./run.sh --no-sandbox --log-directory logs/ From b8683462e8f0bee0654defe92883e36ef99cef44 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" <jryans@gmail.com> Date: Fri, 3 Jan 2020 15:34:03 +0000 Subject: [PATCH 17/21] Update backup creation paths for SSSS This updates the various backup creation entry points to ensure they support creating with secret storage if the feature flag is enabled. --- .../keybackup/CreateKeyBackupDialog.js | 53 ++++++++++++++++--- .../keybackup/RecoveryMethodRemovedDialog.js | 2 + src/components/views/dialogs/LogoutDialog.js | 1 + .../keybackup/RestoreKeyBackupDialog.js | 2 +- .../views/rooms/RoomRecoveryReminder.js | 1 + .../views/settings/KeyBackupPanel.js | 34 ++++-------- 6 files changed, 61 insertions(+), 32 deletions(-) diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 3fac00c1b3..19720e077a 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -1,6 +1,6 @@ /* 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. @@ -17,11 +17,14 @@ limitations under the License. import React from 'react'; import FileSaver from 'file-saver'; +import PropTypes from 'prop-types'; import sdk from '../../../../index'; import MatrixClientPeg from '../../../../MatrixClientPeg'; import { scorePassword } from '../../../../utils/PasswordScorer'; import { _t } from '../../../../languageHandler'; +import { accessSecretStorage } from '../../../../CrossSigningManager'; +import SettingsStore from '../../../../../lib/settings/SettingsStore'; const PHASE_PASSPHRASE = 0; const PHASE_PASSPHRASE_CONFIRM = 1; @@ -49,10 +52,20 @@ function selectText(target) { * on the server. */ export default class CreateKeyBackupDialog extends React.PureComponent { + static propTypes = { + secureSecretStorage: PropTypes.bool, + onFinished: PropTypes.func.isRequired, + } + constructor(props) { super(props); + this._recoveryKeyNode = null; + this._keyBackupInfo = null; + this._setZxcvbnResultTimeout = null; + this.state = { + secureSecretStorage: props.secureSecretStorage, phase: PHASE_PASSPHRASE, passPhrase: '', passPhraseConfirm: '', @@ -61,12 +74,25 @@ export default class CreateKeyBackupDialog extends React.PureComponent { zxcvbnResult: null, setPassPhrase: false, }; + + if (this.state.secureSecretStorage === undefined) { + this.state.secureSecretStorage = + SettingsStore.isFeatureEnabled("feature_cross_signing"); + } + + // If we're using secret storage, skip ahead to the backing up step, as + // `accessSecretStorage` will handle passphrases as needed. + if (this.state.secureSecretStorage) { + this.state.phase = PHASE_BACKINGUP; + } } - componentWillMount() { - this._recoveryKeyNode = null; - this._keyBackupInfo = null; - this._setZxcvbnResultTimeout = null; + componentDidMount() { + // If we're using secret storage, skip ahead to the backing up step, as + // `accessSecretStorage` will handle passphrases as needed. + if (this.state.secureSecretStorage) { + this._createBackup(); + } } componentWillUnmount() { @@ -103,15 +129,26 @@ export default class CreateKeyBackupDialog extends React.PureComponent { } _createBackup = async () => { + const { secureSecretStorage } = this.state; this.setState({ phase: PHASE_BACKINGUP, error: null, }); let info; try { - info = await MatrixClientPeg.get().createKeyBackupVersion( - this._keyBackupInfo, - ); + if (secureSecretStorage) { + await accessSecretStorage(async () => { + info = await MatrixClientPeg.get().prepareKeyBackupVersion( + null /* random key */, + { secureSecretStorage: true }, + ); + info = await MatrixClientPeg.get().createKeyBackupVersion(info); + }); + } else { + info = await MatrixClientPeg.get().createKeyBackupVersion( + this._keyBackupInfo, + ); + } await MatrixClientPeg.get().scheduleAllGroupSessionsForBackup(); this.setState({ phase: PHASE_DONE, diff --git a/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js b/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js index 1975fbe6d6..4383908e23 100644 --- a/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js +++ b/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js @@ -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. @@ -35,6 +36,7 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent { this.props.onFinished(); Modal.createTrackedDialogAsync("Key Backup", "Key Backup", import("./CreateKeyBackupDialog"), + null, null, /* priority = */ false, /* static = */ true, ); } diff --git a/src/components/views/dialogs/LogoutDialog.js b/src/components/views/dialogs/LogoutDialog.js index 6e4f950830..ede03f13cc 100644 --- a/src/components/views/dialogs/LogoutDialog.js +++ b/src/components/views/dialogs/LogoutDialog.js @@ -102,6 +102,7 @@ export default class LogoutDialog extends React.Component { } else { Modal.createTrackedDialogAsync("Key Backup", "Key Backup", import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"), + null, null, /* priority = */ false, /* static = */ true, ); } diff --git a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js index 2881cc920c..106d8cd6f8 100644 --- a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js +++ b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js @@ -76,7 +76,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { onFinished: () => { this._loadBackupStatus(); }, - }, + }, null, /* priority = */ false, /* static = */ true, ); } diff --git a/src/components/views/rooms/RoomRecoveryReminder.js b/src/components/views/rooms/RoomRecoveryReminder.js index 8554c804c0..aa8134d680 100644 --- a/src/components/views/rooms/RoomRecoveryReminder.js +++ b/src/components/views/rooms/RoomRecoveryReminder.js @@ -78,6 +78,7 @@ export default class RoomRecoveryReminder extends React.PureComponent { } else { Modal.createTrackedDialogAsync("Key Backup", "Key Backup", import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"), + null, null, /* priority = */ false, /* static = */ true, ); } } diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index 55bfadba88..765dd16717 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -128,36 +128,24 @@ export default class KeyBackupPanel extends React.PureComponent { Modal.createTrackedDialogAsync('Key Backup', 'Key Backup', import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'), { + secureSecretStorage: false, onFinished: () => { this._loadBackupStatus(); }, - }, + }, null, /* priority = */ false, /* static = */ true, ); } _startNewBackupWithSecureSecretStorage = async () => { - const cli = MatrixClientPeg.get(); - let info; - try { - await accessSecretStorage(async () => { - info = await cli.prepareKeyBackupVersion( - null /* random key */, - { secureSecretStorage: true }, - ); - info = await cli.createKeyBackupVersion(info); - }); - await MatrixClientPeg.get().scheduleAllGroupSessionsForBackup(); - this._loadBackupStatus(); - } catch (e) { - console.error("Error creating key backup", e); - // TODO: If creating a version succeeds, but backup fails, should we - // delete the version, disable backup, or do nothing? If we just - // disable without deleting, we'll enable on next app reload since - // it is trusted. - if (info && info.version) { - MatrixClientPeg.get().deleteKeyBackupVersion(info.version); - } - } + Modal.createTrackedDialogAsync('Key Backup', 'Key Backup', + import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'), + { + secureSecretStorage: true, + onFinished: () => { + this._loadBackupStatus(); + }, + }, null, /* priority = */ false, /* static = */ true, + ); } _deleteBackup = () => { From 2125bcf5a67d37142615b839d5c67cd0e77c082f Mon Sep 17 00:00:00 2001 From: David Baker <dave@matrix.org> Date: Fri, 3 Jan 2020 15:38:59 +0000 Subject: [PATCH 18/21] Comment remaining non-cross-signing-compliant components Fixes https://github.com/vector-im/riot-web/issues/11748 --- src/async-components/views/dialogs/EncryptedEventDialog.js | 3 +++ src/components/views/elements/DeviceVerifyButtons.js | 2 ++ src/components/views/rooms/MemberDeviceInfo.js | 2 ++ 3 files changed, 7 insertions(+) diff --git a/src/async-components/views/dialogs/EncryptedEventDialog.js b/src/async-components/views/dialogs/EncryptedEventDialog.js index 15bb1e046b..ea3c109e05 100644 --- a/src/async-components/views/dialogs/EncryptedEventDialog.js +++ b/src/async-components/views/dialogs/EncryptedEventDialog.js @@ -23,6 +23,9 @@ import { _t } from '../../../languageHandler'; const sdk = require('../../../index'); const MatrixClientPeg = require("../../../MatrixClientPeg"); +// XXX: This component is not cross-signing aware. +// https://github.com/vector-im/riot-web/issues/11752 tracks either updating this +// component or taking it out to pasture. module.exports = createReactClass({ displayName: 'EncryptedEventDialog', diff --git a/src/components/views/elements/DeviceVerifyButtons.js b/src/components/views/elements/DeviceVerifyButtons.js index bb08f8b234..14b4ad1760 100644 --- a/src/components/views/elements/DeviceVerifyButtons.js +++ b/src/components/views/elements/DeviceVerifyButtons.js @@ -22,6 +22,8 @@ import sdk from '../../../index'; import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; +// XXX: This component is *not* cross-signing aware. Once everything is +// cross-signing, this component should just go away. export default createReactClass({ displayName: 'DeviceVerifyButtons', diff --git a/src/components/views/rooms/MemberDeviceInfo.js b/src/components/views/rooms/MemberDeviceInfo.js index ff88c6f6e6..ba90850b35 100644 --- a/src/components/views/rooms/MemberDeviceInfo.js +++ b/src/components/views/rooms/MemberDeviceInfo.js @@ -23,6 +23,8 @@ import classNames from 'classnames'; export default class MemberDeviceInfo extends React.Component { render() { const DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons'); + // XXX: These checks are not cross-signing aware but this component is only used + // from the old, pre-cross-signing memberinfopanel const iconClasses = classNames({ mx_MemberDeviceInfo_icon: true, mx_MemberDeviceInfo_icon_blacklisted: this.props.device.isBlocked(), From e12ed04da8cf2914f002a599ec0d9e5ca079434d Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" <jryans@gmail.com> Date: Fri, 3 Jan 2020 15:59:14 +0000 Subject: [PATCH 19/21] Remove unused import --- src/components/views/settings/KeyBackupPanel.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index 765dd16717..30d0416968 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -22,7 +22,6 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; import SettingsStore from '../../../../lib/settings/SettingsStore'; -import { accessSecretStorage } from '../../../CrossSigningManager'; export default class KeyBackupPanel extends React.PureComponent { constructor(props) { From 752482964a4c5ce98825011c4797bf4c878b58d5 Mon Sep 17 00:00:00 2001 From: Travis Ralston <travpc@gmail.com> Date: Fri, 3 Jan 2020 10:24:07 -0700 Subject: [PATCH 20/21] Purify the components --- src/components/views/dialogs/DMInviteDialog.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/DMInviteDialog.js b/src/components/views/dialogs/DMInviteDialog.js index 6456ef7083..ff498e3e75 100644 --- a/src/components/views/dialogs/DMInviteDialog.js +++ b/src/components/views/dialogs/DMInviteDialog.js @@ -29,7 +29,7 @@ import * as humanize from "humanize"; const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked -class DMRoomTile extends React.Component { +class DMRoomTile extends React.PureComponent { static propTypes = { member: PropTypes.object.isRequired, lastActiveTs: PropTypes.number, @@ -70,7 +70,7 @@ class DMRoomTile extends React.Component { } } -export default class DMInviteDialog extends React.Component { +export default class DMInviteDialog extends React.PureComponent { static propTypes = { // Takes an array of user IDs/emails to invite. onFinished: PropTypes.func.isRequired, From 814c0aa4c2469fa71c2372609d85c4185a4320ca Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 5 Jan 2020 20:52:54 +0000 Subject: [PATCH 21/21] Send enabled_labs over rageshake as comma delimited list Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/rageshake/submit-rageshake.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/rageshake/submit-rageshake.js b/src/rageshake/submit-rageshake.js index 457958eb82..44f1039016 100644 --- a/src/rageshake/submit-rageshake.js +++ b/src/rageshake/submit-rageshake.js @@ -27,6 +27,7 @@ import rageshake from './rageshake'; // polyfill textencoder if necessary import * as TextEncodingUtf8 from 'text-encoding-utf-8'; +import SettingsStore from "../settings/SettingsStore"; let TextEncoder = window.TextEncoder; if (!TextEncoder) { TextEncoder = TextEncodingUtf8.TextEncoder; @@ -85,6 +86,12 @@ export default async function sendBugReport(bugReportEndpoint, opts) { body.append('label', opts.label); } + // add labs options + const enabledLabs = SettingsStore.getLabsFeatures().filter(SettingsStore.isFeatureEnabled); + if (enabledLabs.length) { + body.append('enabled_labs', enabledLabs.join(', ')); + } + if (opts.sendLogs) { progressCallback(_t("Collecting logs")); const logs = await rageshake.getLogsForReport();