diff --git a/CHANGELOG.md b/CHANGELOG.md index 5390cad319..3ae2711e25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,193 @@ +Changes in [1.7.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.6) (2020-01-13) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.6-rc.2...v1.7.6) + + * Repair community member info panel + [\#3834](https://github.com/matrix-org/matrix-react-sdk/pull/3834) + * Add feature flag around the presence indicator in room list + [\#3833](https://github.com/matrix-org/matrix-react-sdk/pull/3833) + +Changes in [1.7.6-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.6-rc.2) (2020-01-08) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.6-rc.1...v1.7.6-rc.2) + + * Strip all variation selectors on emoji + [\#3818](https://github.com/matrix-org/matrix-react-sdk/pull/3818) + +Changes in [1.7.6-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.6-rc.1) (2020-01-06) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.5...v1.7.6-rc.1) + + * Deduplicate recent emoji + [\#3806](https://github.com/matrix-org/matrix-react-sdk/pull/3806) + * Fix ability to remove avatars + [\#3803](https://github.com/matrix-org/matrix-react-sdk/pull/3803) + * Update from Weblate + [\#3810](https://github.com/matrix-org/matrix-react-sdk/pull/3810) + * User Info fetch latest RoomMember instead of showing historical data + [\#3788](https://github.com/matrix-org/matrix-react-sdk/pull/3788) + * Remove all usages of slate in favour of CIDER + [\#3808](https://github.com/matrix-org/matrix-react-sdk/pull/3808) + * Use display name when pinned messages are changed + [\#3809](https://github.com/matrix-org/matrix-react-sdk/pull/3809) + * Fix inverted diff line highlighting in dark theme + [\#3790](https://github.com/matrix-org/matrix-react-sdk/pull/3790) + * Bridge info settings tab + [\#3693](https://github.com/matrix-org/matrix-react-sdk/pull/3693) + * Send the labs flags the client is running with in rageshake + [\#3805](https://github.com/matrix-org/matrix-react-sdk/pull/3805) + * Initial implementation of FTUE user lists design + [\#3792](https://github.com/matrix-org/matrix-react-sdk/pull/3792) + * Update key backup creation and recovery paths for SSSS + [\#3800](https://github.com/matrix-org/matrix-react-sdk/pull/3800) + * Don't fail if logs exists and is an empty dir + [\#3798](https://github.com/matrix-org/matrix-react-sdk/pull/3798) + * Comment remaining non-cross-signing-compliant components + [\#3799](https://github.com/matrix-org/matrix-react-sdk/pull/3799) + * Remove 'unverify' from UserInfoPanel + [\#3797](https://github.com/matrix-org/matrix-react-sdk/pull/3797) + * Use deviceTrust when displaying key backup trust status + [\#3795](https://github.com/matrix-org/matrix-react-sdk/pull/3795) + * Don't crash if a keyshare request is removed + [\#3793](https://github.com/matrix-org/matrix-react-sdk/pull/3793) + * Convert /verify to checkDeviceTrust + [\#3794](https://github.com/matrix-org/matrix-react-sdk/pull/3794) + * Remove E2eIcon onClick + [\#3791](https://github.com/matrix-org/matrix-react-sdk/pull/3791) + * support channel names with slash in name/alias + [\#3778](https://github.com/matrix-org/matrix-react-sdk/pull/3778) + * Fix NPE when filtering the room list + [\#3787](https://github.com/matrix-org/matrix-react-sdk/pull/3787) + * Turn RoomAliasField into properly controlled and use in RoomSettings + [\#3782](https://github.com/matrix-org/matrix-react-sdk/pull/3782) + * fuzzy-sort MemberList + [\#3783](https://github.com/matrix-org/matrix-react-sdk/pull/3783) + * Serialize file uploads into room to match confirmation dialog order + [\#3786](https://github.com/matrix-org/matrix-react-sdk/pull/3786) + * Do not show Top Unread Messages Bar and Jump to bottom button if searching + [\#3785](https://github.com/matrix-org/matrix-react-sdk/pull/3785) + * Fix sticker picker chevron offset calculation + [\#3784](https://github.com/matrix-org/matrix-react-sdk/pull/3784) + * Fix not being able to promote others to the same power level as your own + [\#3781](https://github.com/matrix-org/matrix-react-sdk/pull/3781) + * Room Tile DMs online/active green dot + [\#3751](https://github.com/matrix-org/matrix-react-sdk/pull/3751) + * Fix spelling and grammar in README + [\#3780](https://github.com/matrix-org/matrix-react-sdk/pull/3780) + * Reintroduce working resizer code for right panel + [\#3776](https://github.com/matrix-org/matrix-react-sdk/pull/3776) + * Fix wrong scope binding on openHelp for TopLeftMenu + [\#3775](https://github.com/matrix-org/matrix-react-sdk/pull/3775) + * UserInfo hide kick/mute buttons if they make no sense + [\#3774](https://github.com/matrix-org/matrix-react-sdk/pull/3774) + * Fix duplicate Incoming Call prompt on Community Invite sublist + [\#3773](https://github.com/matrix-org/matrix-react-sdk/pull/3773) + * Apply new design to highlighted tags and add toggle mechanic + [\#3755](https://github.com/matrix-org/matrix-react-sdk/pull/3755) + * stop using ReactDOM.findDOMNode in componentWillUnmount, use refs + [\#3771](https://github.com/matrix-org/matrix-react-sdk/pull/3771) + * Add alt="" to presentational images + [\#3772](https://github.com/matrix-org/matrix-react-sdk/pull/3772) + * Fix room list filtering weird case sensitivity + [\#3759](https://github.com/matrix-org/matrix-react-sdk/pull/3759) + * Don't show the 'verify' button if the user is verified + [\#3758](https://github.com/matrix-org/matrix-react-sdk/pull/3758) + * Switch to using checkDeviceTrust + [\#3757](https://github.com/matrix-org/matrix-react-sdk/pull/3757) + * Migrate away from React Legacy contexts API + [\#3743](https://github.com/matrix-org/matrix-react-sdk/pull/3743) + * Migrate key backups to SSSS + [\#3749](https://github.com/matrix-org/matrix-react-sdk/pull/3749) + * Get rid of stripped-emoji.json in favour of an in-memory single source of + truth + [\#3745](https://github.com/matrix-org/matrix-react-sdk/pull/3745) + * Combine cross signing and verification over DM feature flags + [\#3753](https://github.com/matrix-org/matrix-react-sdk/pull/3753) + * apply unhomoglyph when filtering room list to fuzzify it + [\#3754](https://github.com/matrix-org/matrix-react-sdk/pull/3754) + * Make EmojiPicker an unmanaged Context Menu as it is too complex to be + managed + [\#3746](https://github.com/matrix-org/matrix-react-sdk/pull/3746) + * Internationalise M_TOO_LARGE error from Synapse + [\#3750](https://github.com/matrix-org/matrix-react-sdk/pull/3750) + * Replace UserInfo avatar with for fallback logic + [\#3748](https://github.com/matrix-org/matrix-react-sdk/pull/3748) + * Dropdown stop keyboard propagation if key handled + [\#3741](https://github.com/matrix-org/matrix-react-sdk/pull/3741) + * Fix right panel for multiple member info viewings + [\#3742](https://github.com/matrix-org/matrix-react-sdk/pull/3742) + * Fix Field validation tooltip sticking if blurred before async validation + resolved + [\#3740](https://github.com/matrix-org/matrix-react-sdk/pull/3740) + * Fix UserInfo exploding without a room being passed to it + [\#3738](https://github.com/matrix-org/matrix-react-sdk/pull/3738) + * Fix room directory maintaining and error state + [\#3737](https://github.com/matrix-org/matrix-react-sdk/pull/3737) + * Stop trapping tab in AddressPickerDialog + [\#3735](https://github.com/matrix-org/matrix-react-sdk/pull/3735) + * Stop using KeyboardEvent.keyCode as it is deprecated + [\#3736](https://github.com/matrix-org/matrix-react-sdk/pull/3736) + * Implement new design for uploading/removing avatars + [\#3733](https://github.com/matrix-org/matrix-react-sdk/pull/3733) + * Fix aspect ratio on room/profile avatar preview + [\#3731](https://github.com/matrix-org/matrix-react-sdk/pull/3731) + * Switch to react-focus-lock for it to comprehend Portals + [\#3732](https://github.com/matrix-org/matrix-react-sdk/pull/3732) + * Make combobox dropdown keyboard and screen reader accessible + [\#3729](https://github.com/matrix-org/matrix-react-sdk/pull/3729) + * Verify users when cross-signing enabled + [\#3728](https://github.com/matrix-org/matrix-react-sdk/pull/3728) + * Update from Weblate + [\#3730](https://github.com/matrix-org/matrix-react-sdk/pull/3730) + * Improve a11y of the unignore button in Settings + [\#3727](https://github.com/matrix-org/matrix-react-sdk/pull/3727) + * Fix ToggleSwitch A11Y (trapping tab and switch v. checkbox) + [\#3726](https://github.com/matrix-org/matrix-react-sdk/pull/3726) + * Make URL previews dismissable via keyboard and accessible to screen readers + [\#3725](https://github.com/matrix-org/matrix-react-sdk/pull/3725) + * Create new key backups using secret storage + [\#3720](https://github.com/matrix-org/matrix-react-sdk/pull/3720) + * Replace sign-ins with sessions + [\#3721](https://github.com/matrix-org/matrix-react-sdk/pull/3721) + * Refactor RightPanel to match expected behaviour + [\#3703](https://github.com/matrix-org/matrix-react-sdk/pull/3703) + * Render policy room event updates in the timeline + [\#3716](https://github.com/matrix-org/matrix-react-sdk/pull/3716) + * Wrap the await call for unknown device lookups + [\#3718](https://github.com/matrix-org/matrix-react-sdk/pull/3718) + * Add testing flow to bootstrap secret storage + [\#3640](https://github.com/matrix-org/matrix-react-sdk/pull/3640) + * Fix remaining context menu regressions + [\#3715](https://github.com/matrix-org/matrix-react-sdk/pull/3715) + * Migrate away from React Legacy string refs + [\#3712](https://github.com/matrix-org/matrix-react-sdk/pull/3712) + * Update copy for DM invites + [\#3706](https://github.com/matrix-org/matrix-react-sdk/pull/3706) + * Fix message action bar reaction picker regression + [\#3714](https://github.com/matrix-org/matrix-react-sdk/pull/3714) + * Add what-input to allow different scoping to focus-visible for MAB a11y + [\#3709](https://github.com/matrix-org/matrix-react-sdk/pull/3709) + * Mark the This/All Rooms scope buttons as radios for a11y + [\#3708](https://github.com/matrix-org/matrix-react-sdk/pull/3708) + * Switch ReactionsRowButton to an AccessibleButton for space/enter handling + [\#3707](https://github.com/matrix-org/matrix-react-sdk/pull/3707) + * Change the (edited) link to an AccessibleButton for a11y + [\#3710](https://github.com/matrix-org/matrix-react-sdk/pull/3710) + * Update from Weblate + [\#3713](https://github.com/matrix-org/matrix-react-sdk/pull/3713) + * Fix ?via= args in SpecPermalinkConstructor.js + [\#3694](https://github.com/matrix-org/matrix-react-sdk/pull/3694) + * Don't mark a room as unread when server ACLs are set + [\#3705](https://github.com/matrix-org/matrix-react-sdk/pull/3705) + * Make reaction buttons more accessible + [\#3704](https://github.com/matrix-org/matrix-react-sdk/pull/3704) + * yarn upgrade + [\#3701](https://github.com/matrix-org/matrix-react-sdk/pull/3701) + * Make CI scripts executable + [\#3698](https://github.com/matrix-org/matrix-react-sdk/pull/3698) + * ARIA compliant context menus + [\#3611](https://github.com/matrix-org/matrix-react-sdk/pull/3611) + Changes in [1.7.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.5) (2019-12-09) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.5-rc.1...v1.7.5) diff --git a/package.json b/package.json index ad446e26cc..4cbfd84882 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "1.7.5", + "version": "1.7.6", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -81,13 +81,14 @@ "glob": "^5.0.14", "glob-to-regexp": "^0.4.1", "highlight.js": "^9.15.8", + "html-entities": "^1.2.1", "humanize": "^0.0.9", "is-ip": "^2.0.0", "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.6", "lodash": "^4.17.14", "lolex": "4.2", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "3.0.0", "optimist": "^0.6.1", "pako": "^1.0.5", "png-chunks-extract": "^1.0.0", @@ -127,6 +128,8 @@ "babel-preset-react": "^6.24.1", "chokidar": "^2.1.2", "concurrently": "^4.0.1", + "enzyme": "^3.10.0", + "enzyme-adapter-react-16": "^1.15.1", "eslint": "^5.12.0", "eslint-config-google": "^0.7.1", "eslint-plugin-babel": "^5.2.1", diff --git a/res/css/views/dialogs/_DMInviteDialog.scss b/res/css/views/dialogs/_DMInviteDialog.scss index 364c796f16..f806e85120 100644 --- a/res/css/views/dialogs/_DMInviteDialog.scss +++ b/res/css/views/dialogs/_DMInviteDialog.scss @@ -21,15 +21,51 @@ limitations under the License. .mx_DMInviteDialog_editor { flex: 1; width: 100%; // Needed to make the Field inside grow - } + background-color: $user-tile-hover-bg-color; + border-radius: 4px; + min-height: 25px; + padding-left: 8px; + overflow-x: hidden; + overflow-y: auto; - .mx_Field { - margin: 0; + .mx_DMInviteDialog_userTile { + display: inline-block; + float: left; + position: relative; + top: 7px; + } + + // Using a textarea for this element, to circumvent autofill + // Mostly copied from AddressPickerDialog + textarea, + textarea:focus { + height: 34px; + line-height: 34px; + font-size: 14px; + padding-left: 12px; + margin: 0 !important; + border: 0 !important; + outline: 0 !important; + resize: none; + overflow: hidden; + box-sizing: border-box; + word-wrap: nowrap; + + // Roughly fill about 2/5ths of the available space. This is to try and 'fill' the + // remaining space after a bunch of pills, but is a bit hacky. Ideally we'd have + // support for "fill remaining width", but traditional tricks don't work with what + // we're pushing into this "field". Flexbox just makes things worse. The theory is + // that users won't need more than about 2/5ths of the input to find the person + // they're looking for. + width: 40%; + } } .mx_DMInviteDialog_goButton { width: 48px; margin-left: 10px; + height: 25px; + line-height: 25px; } } @@ -57,6 +93,43 @@ limitations under the License. vertical-align: middle; } + .mx_DMInviteDialog_roomTile_avatarStack { + display: inline-block; + position: relative; + width: 36px; + height: 36px; + + & > * { + position: absolute; + top: 0; + left: 0; + } + } + + .mx_DMInviteDialog_roomTile_selected { + width: 36px; + height: 36px; + border-radius: 36px; + background-color: $username-variant1-color; + display: inline-block; + position: relative; + + &::before { + content: ""; + width: 24px; + height: 24px; + grid-column: 1; + grid-row: 1; + mask-image: url('$(res)/img/feather-customised/check.svg'); + mask-size: 100%; + mask-repeat: no-repeat; + position: absolute; + top: 6px; // 50% + left: 6px; // 50% + background-color: #ffffff; // this is fine without a var because it's for both themes + } + } + .mx_DMInviteDialog_roomTile_name { font-weight: 600; font-size: 14px; @@ -83,3 +156,42 @@ limitations under the License. } } +// Many of these styles are stolen from mx_UserPill, but adjusted for the invite dialog. +.mx_DMInviteDialog_userTile { + margin-right: 8px; + + .mx_DMInviteDialog_userTile_pill { + background-color: $username-variant1-color; + border-radius: 12px; + display: inline-block; + height: 24px; + line-height: 24px; + padding-left: 8px; + padding-right: 8px; + color: #ffffff; // this is fine without a var because it's for both themes + + .mx_DMInviteDialog_userTile_avatar { + border-radius: 20px; + position: relative; + left: -5px; + top: 2px; + } + + img.mx_DMInviteDialog_userTile_avatar { + vertical-align: top; + } + + .mx_DMInviteDialog_userTile_name { + vertical-align: top; + } + + .mx_DMInviteDialog_userTile_threepidAvatar { + background-color: #ffffff; // this is fine without a var because it's for both themes + } + } + + .mx_DMInviteDialog_userTile_remove { + display: inline-block; + margin-left: 4px; + } +} diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index f1e4456cc1..45b9733faa 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -263,3 +263,24 @@ limitations under the License. .mx_RoomHeader_pinsIndicatorUnread { background-color: $pinned-unread-color; } + +.mx_RoomHeader_PrivateIcon.mx_RoomHeader_isPrivate { + width: 12px; + height: 12px; + position: relative; + display: block !important; + + &::before { + background-color: $roomtile-name-color; + mask-image: url('$(res)/img/feather-customised/lock-solid.svg'); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + } +} diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index e5c7948216..cb1137bb2f 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -200,3 +200,31 @@ limitations under the License. .mx_GroupInviteTile .mx_RoomTile_name { flex: 1; } + +.mx_RoomTile.mx_RoomTile.mx_RoomTile_isPrivate .mx_RoomTile_name { + // Scoot the padding in a bit from 6px to make it look better + padding-left: 3px; +} + +.mx_RoomTile.mx_RoomTile_isPrivate .mx_RoomTile_PrivateIcon { + width: 12px; + height: 12px; + position: relative; + display: block !important; + // Align the padlock with unencrypted room names + margin-left: 6px; + + &::before { + background-color: $roomtile-name-color; + mask-image: url('$(res)/img/feather-customised/lock-solid.svg'); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + } +} diff --git a/res/img/feather-customised/lock-solid.svg b/res/img/feather-customised/lock-solid.svg new file mode 100644 index 0000000000..9eb8b6a4c5 --- /dev/null +++ b/res/img/feather-customised/lock-solid.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/icon-email-pill-avatar.svg b/res/img/icon-email-pill-avatar.svg new file mode 100644 index 0000000000..6b0ac200a5 --- /dev/null +++ b/res/img/icon-email-pill-avatar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/img/icon-pill-remove.svg b/res/img/icon-pill-remove.svg new file mode 100644 index 0000000000..adf6fd4771 --- /dev/null +++ b/res/img/icon-pill-remove.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Avatar.js b/src/Avatar.js index 17860698cb..a529ca5588 100644 --- a/src/Avatar.js +++ b/src/Avatar.js @@ -21,14 +21,17 @@ import DMRoomMap from './utils/DMRoomMap'; module.exports = { avatarUrlForMember: function(member, width, height, resizeMethod) { - let url = member.getAvatarUrl( - MatrixClientPeg.get().getHomeserverUrl(), - Math.floor(width * window.devicePixelRatio), - Math.floor(height * window.devicePixelRatio), - resizeMethod, - false, - false, - ); + let url; + if (member && member.getAvatarUrl) { + url = member.getAvatarUrl( + MatrixClientPeg.get().getHomeserverUrl(), + Math.floor(width * window.devicePixelRatio), + Math.floor(height * window.devicePixelRatio), + resizeMethod, + false, + false, + ); + } if (!url) { // member can be null here currently since on invites, the JS SDK // does not have enough info to build a RoomMember object for diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 7cdff26a21..e2b7c61a12 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -377,6 +377,7 @@ class TextHighlighter extends BaseHighlighter { * opts.stripReplyFallback: optional argument specifying the event is a reply and so fallback needs removing * opts.returnString: return an HTML string rather than JSX elements * opts.forComposerQuote: optional param to lessen the url rewriting done by sanitization, for quoting into composer + * opts.ref: React ref to attach to any React components returned (not compatible with opts.returnString) */ export function bodyToHtml(content, highlights, opts={}) { const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body; @@ -459,8 +460,8 @@ export function bodyToHtml(content, highlights, opts={}) { }); return isDisplayedWithHtml ? - : - { strippedBody }; + : + { strippedBody }; } /** diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 19720e077a..1095fb4aee 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -24,7 +24,7 @@ import MatrixClientPeg from '../../../../MatrixClientPeg'; import { scorePassword } from '../../../../utils/PasswordScorer'; import { _t } from '../../../../languageHandler'; import { accessSecretStorage } from '../../../../CrossSigningManager'; -import SettingsStore from '../../../../../lib/settings/SettingsStore'; +import SettingsStore from '../../../../settings/SettingsStore'; const PHASE_PASSPHRASE = 0; const PHASE_PASSPHRASE_CONFIRM = 1; diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 25bc8cdfda..b6d314aab1 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -24,14 +24,15 @@ import { _t } from '../../../../languageHandler'; import Modal from '../../../../Modal'; const PHASE_LOADING = 0; -const PHASE_MIGRATE = 1; -const PHASE_PASSPHRASE = 2; -const PHASE_PASSPHRASE_CONFIRM = 3; -const PHASE_SHOWKEY = 4; -const PHASE_KEEPITSAFE = 5; -const PHASE_STORING = 6; -const PHASE_DONE = 7; -const PHASE_OPTOUT_CONFIRM = 8; +const PHASE_RESTORE_KEY_BACKUP = 1; +const PHASE_MIGRATE = 2; +const PHASE_PASSPHRASE = 3; +const PHASE_PASSPHRASE_CONFIRM = 4; +const PHASE_SHOWKEY = 5; +const PHASE_KEEPITSAFE = 6; +const PHASE_STORING = 7; +const PHASE_DONE = 8; +const PHASE_OPTOUT_CONFIRM = 9; const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms. @@ -67,6 +68,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { downloaded: false, zxcvbnResult: null, setPassPhrase: false, + backupInfo: null, + backupSigStatus: null, }; this._fetchBackupInfo(); @@ -80,10 +83,16 @@ export default class CreateSecretStorageDialog extends React.PureComponent { async _fetchBackupInfo() { const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); + const backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo); + + const phase = backupInfo ? + (backupSigStatus.usable ? PHASE_MIGRATE : PHASE_RESTORE_KEY_BACKUP) : + PHASE_PASSPHRASE; this.setState({ - phase: backupInfo ? PHASE_MIGRATE: PHASE_PASSPHRASE, + phase, backupInfo, + backupSigStatus, }); } @@ -161,6 +170,14 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this.props.onFinished(true); } + _onRestoreKeyBackupClick = () => { + const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); + Modal.createTrackedDialog( + 'Restore Backup', '', RestoreKeyBackupDialog, null, null, + /* priority = */ false, /* static = */ true, + ); + } + _onOptOutClick = () => { this.setState({phase: PHASE_OPTOUT_CONFIRM}); } @@ -268,6 +285,23 @@ export default class CreateSecretStorageDialog extends React.PureComponent { return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE; } + _renderPhaseRestoreKeyBackup() { + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
+

{_t( + "Key Backup is enabled on your account but has not been set " + + "up from this session. To set up secret storage, " + + "restore your key backup.", + )}

+ + +
; + } + _renderPhaseMigrate() { // TODO: This is a temporary screen so people who have the labs flag turned on and // click the button are aware they're making a change to their account. @@ -277,7 +311,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return

{_t( - "Secret Storage will be set up using your existing key backup details." + + "Secret Storage will be set up using your existing key backup details. " + "Your secret storage passphrase and recovery key will be the same as " + " they were for your key backup", )}

@@ -527,6 +561,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { _titleForPhase(phase) { switch (phase) { + case PHASE_RESTORE_KEY_BACKUP: + return _t('Restore your Key Backup'); case PHASE_MIGRATE: return _t('Migrate from Key Backup'); case PHASE_PASSPHRASE: @@ -569,6 +605,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent { case PHASE_LOADING: content = this._renderBusyPhase(); break; + case PHASE_RESTORE_KEY_BACKUP: + content = this._renderPhaseRestoreKeyBackup(); + break; case PHASE_MIGRATE: content = this._renderPhaseMigrate(); break; diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 9df4630136..50b63b94b1 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -1299,7 +1299,7 @@ export default createReactClass({ ); } - const rightPanel = !RightPanelStore.getSharedInstance().isOpenForGroup + const rightPanel = RightPanelStore.getSharedInstance().isOpenForGroup ? : undefined; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index af3f4d2598..2515035996 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1505,6 +1505,15 @@ export default createReactClass({ "blacklistUnverifiedDevices", ); cli.setGlobalBlacklistUnverifiedDevices(blacklistEnabled); + + // With cross-signing enabled, we send to unknown devices + // without prompting. Any bad-device status the user should + // be aware of will be signalled through the room shield + // changing colour. More advanced behaviour will come once + // we implement more settings. + cli.setGlobalErrorOnUnknownDevices( + !SettingsStore.isFeatureEnabled("feature_cross_signing"), + ); } }, diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index c552e2f8f5..a717f485f0 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -485,6 +485,7 @@ module.exports = createReactClass({ MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData); + MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents); MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership); MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember); MatrixClientPeg.get().removeListener("accountData", this.onAccountData); diff --git a/src/components/views/avatars/MemberAvatar.js b/src/components/views/avatars/MemberAvatar.js index 383bab5e79..8b6d41eb9f 100644 --- a/src/components/views/avatars/MemberAvatar.js +++ b/src/components/views/avatars/MemberAvatar.js @@ -55,7 +55,7 @@ module.exports = createReactClass({ }, _getState: function(props) { - if (props.member) { + if (props.member && props.member.name) { return { name: props.member.name, title: props.title || props.member.userId, diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index efbfc4322f..ef1822b555 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -422,7 +422,7 @@ module.exports = createReactClass({ ); - if (this.props.eventTileOps && this.props.eventTileOps.getInnerText) { + if (this.props.eventTileOps) { // this event is rendered using TextualBody quoteButton = ( { _t('Quote') } diff --git a/src/components/views/dialogs/DMInviteDialog.js b/src/components/views/dialogs/DMInviteDialog.js index c5e9c92131..371768eb4e 100644 --- a/src/components/views/dialogs/DMInviteDialog.js +++ b/src/components/views/dialogs/DMInviteDialog.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, {createRef} from 'react'; import PropTypes from 'prop-types'; import {_t} from "../../../languageHandler"; import sdk from "../../../index"; @@ -25,24 +25,56 @@ import {RoomMember} from "matrix-js-sdk/lib/matrix"; import * as humanize from "humanize"; import SdkConfig from "../../../SdkConfig"; import {getHttpUriForMxc} from "matrix-js-sdk/lib/content-repo"; +import * as Email from "../../../email"; +import {getDefaultIdentityServerUrl, useDefaultIdentityServer} from "../../../utils/IdentityServerUtils"; +import {abbreviateUrl} from "../../../utils/UrlUtils"; +import dis from "../../../dispatcher"; +import IdentityAuthClient from "../../../IdentityAuthClient"; +import Modal from "../../../Modal"; // 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 DirectoryMember { +// This is the interface that is expected by various components in this file. It is a bit +// awkward because it also matches the RoomMember class from the js-sdk with some extra support +// for 3PIDs/email addresses. +// +// XXX: We should use TypeScript interfaces instead of this weird "abstract" class. +class Member { + /** + * The display name of this Member. For users this should be their profile's display + * name or user ID if none set. For 3PIDs this should be the 3PID address (email). + */ + get name(): string { throw new Error("Member class not implemented"); } + + /** + * The ID of this Member. For users this should be their user ID. For 3PIDs this should + * be the 3PID address (email). + */ + get userId(): string { throw new Error("Member class not implemented"); } + + /** + * Gets the MXC URL of this Member's avatar. For users this should be their profile's + * avatar MXC URL or null if none set. For 3PIDs this should always be null. + */ + getMxcAvatarUrl(): string { throw new Error("Member class not implemented"); } +} + +class DirectoryMember extends Member { _userId: string; _displayName: string; _avatarUrl: string; constructor(userDirResult: {user_id: string, display_name: string, avatar_url: string}) { + super(); this._userId = userDirResult.user_id; this._displayName = userDirResult.display_name; this._avatarUrl = userDirResult.avatar_url; } - // These next members are to implement the contract expected by DMRoomTile + // These next class members are for the Member interface get name(): string { return this._displayName || this._userId; } @@ -56,13 +88,93 @@ class DirectoryMember { } } +class ThreepidMember extends Member { + _id: string; + + constructor(id: string) { + super(); + this._id = id; + } + + // This is a getter that would be falsey on all other implementations. Until we have + // better type support in the react-sdk we can use this trick to determine the kind + // of 3PID we're dealing with, if any. + get isEmail(): boolean { + return this._id.includes('@'); + } + + // These next class members are for the Member interface + get name(): string { + return this._id; + } + + get userId(): string { + return this._id; + } + + getMxcAvatarUrl(): string { + return null; + } +} + +class DMUserTile extends React.PureComponent { + static propTypes = { + member: PropTypes.object.isRequired, // Should be a Member (see interface above) + onRemove: PropTypes.func.isRequired, // takes 1 argument, the member being removed + }; + + _onRemove = (e) => { + // Stop the browser from highlighting text + e.preventDefault(); + e.stopPropagation(); + + this.props.onRemove(this.props.member); + }; + + render() { + const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar"); + const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); + + const avatarSize = 20; + const avatar = this.props.member.isEmail + ? + : ; + + return ( + + + {avatar} + {this.props.member.name} + + + {_t('Remove')} + + + ); + } +} + class DMRoomTile extends React.PureComponent { static propTypes = { - // Has properties to match RoomMember: userId (str), name (str), getMxcAvatarUrl(): string - member: PropTypes.object.isRequired, + member: PropTypes.object.isRequired, // Should be a Member (see interface above) lastActiveTs: PropTypes.number, - onToggle: PropTypes.func.isRequired, + onToggle: PropTypes.func.isRequired, // takes 1 argument, the member being toggled highlightWord: PropTypes.string, + isSelected: PropTypes.bool, }; _onClick = (e) => { @@ -70,7 +182,7 @@ class DMRoomTile extends React.PureComponent { e.preventDefault(); e.stopPropagation(); - this.props.onToggle(this.props.member.userId); + this.props.onToggle(this.props.member); }; _highlightName(str: string) { @@ -121,19 +233,37 @@ class DMRoomTile extends React.PureComponent { } const avatarSize = 36; - const avatarUrl = getHttpUriForMxc( - MatrixClientPeg.get().getHomeserverUrl(), this.props.member.getMxcAvatarUrl(), - avatarSize, avatarSize, "crop"); + const avatar = this.props.member.isEmail + ? + : ; + + let checkmark = null; + if (this.props.isSelected) { + // To reduce flickering we put the 'selected' room tile above the real avatar + checkmark =
; + } + + // To reduce flickering we put the checkmark on top of the actual avatar (prevents + // the browser from reloading the image source when the avatar remounts). + const stackedAvatar = ( + + {avatar} + {checkmark} + + ); return (
- + {stackedAvatar} {this._highlightName(this.props.member.name)} {this._highlightName(this.props.member.userId)} {timestamp} @@ -149,19 +279,25 @@ export default class DMInviteDialog extends React.PureComponent { }; _debounceTimer: number = null; + _editorRef: any = null; constructor() { super(); this.state = { - targets: [], // string[] of mxids/email addresses + targets: [], // array of Member objects (see interface above) filterText: "", recents: this._buildRecents(), numRecentsShown: INITIAL_ROOMS_SHOWN, suggestions: this._buildSuggestions(), numSuggestionsShown: INITIAL_ROOMS_SHOWN, serverResultsMixin: [], // { user: DirectoryMember, userId: string }[], like recents and suggestions + threepidResultsMixin: [], // { user: ThreepidMember, userId: string}[], like recents and suggestions + canUseIdentityServer: !!MatrixClientPeg.get().getIdentityServerUrl(), + tryingIdentityServer: false, }; + + this._editorRef = createRef(); } _buildRecents(): {userId: string, user: RoomMember, lastActive: number} { @@ -245,7 +381,7 @@ export default class DMInviteDialog extends React.PureComponent { } _startDm = () => { - this.props.onFinished(this.state.targets); + this.props.onFinished(this.state.targets.map(t => t.userId)); }; _cancel = () => { @@ -262,7 +398,7 @@ export default class DMInviteDialog extends React.PureComponent { if (this._debounceTimer) { clearTimeout(this._debounceTimer); } - this._debounceTimer = setTimeout(() => { + this._debounceTimer = setTimeout(async () => { MatrixClientPeg.get().searchUserDirectory({term}).then(r => { if (term !== this.state.filterText) { // Discard the results - we were probably too slow on the server-side to make @@ -281,6 +417,62 @@ export default class DMInviteDialog extends React.PureComponent { console.error(e); this.setState({serverResultsMixin: []}); // clear results because it's moderately fatal }); + + // Whenever we search the directory, also try to search the identity server. It's + // all debounced the same anyways. + if (!this.state.canUseIdentityServer) { + // The user doesn't have an identity server set - warn them of that. + this.setState({tryingIdentityServer: true}); + return; + } + if (term.indexOf('@') > 0 && Email.looksValid(term)) { + // Start off by suggesting the plain email while we try and resolve it + // to a real account. + this.setState({ + // per above: the userId is a lie here - it's just a regular identifier + threepidResultsMixin: [{user: new ThreepidMember(term), userId: term}], + }); + try { + const authClient = new IdentityAuthClient(); + const token = await authClient.getAccessToken(); + if (term !== this.state.filterText) return; // abandon hope + + const lookup = await MatrixClientPeg.get().lookupThreePid( + 'email', + term, + undefined, // callback + token, + ); + if (term !== this.state.filterText) return; // abandon hope + + if (!lookup || !lookup.mxid) { + // We weren't able to find anyone - we're already suggesting the plain email + // as an alternative, so do nothing. + return; + } + + // We append the user suggestion to give the user an option to click + // the email anyways, and so we don't cause things to jump around. In + // theory, the user would see the user pop up and think "ah yes, that + // person!" + const profile = await MatrixClientPeg.get().getProfileInfo(lookup.mxid); + if (term !== this.state.filterText || !profile) return; // abandon hope + this.setState({ + threepidResultsMixin: [...this.state.threepidResultsMixin, { + user: new DirectoryMember({ + user_id: lookup.mxid, + display_name: profile.displayname, + avatar_url: profile.avatar_url, + }), + userId: lookup.mxid, + }], + }); + } catch (e) { + console.error("Error searching identity server:"); + console.error(e); + this.setState({threepidResultsMixin: []}); // clear results because it's moderately fatal + } + } }, 150); // 150ms debounce (human reaction time + some) }; @@ -292,14 +484,112 @@ export default class DMInviteDialog extends React.PureComponent { this.setState({numSuggestionsShown: this.state.numSuggestionsShown + INCREMENT_ROOMS_SHOWN}); }; - _toggleMember = (userId) => { + _toggleMember = (member: Member) => { const targets = this.state.targets.map(t => t); // cheap clone for mutation - const idx = targets.indexOf(userId); + const idx = targets.indexOf(member); if (idx >= 0) targets.splice(idx, 1); - else targets.push(userId); + else targets.push(member); this.setState({targets}); }; + _removeMember = (member: Member) => { + const targets = this.state.targets.map(t => t); // cheap clone for mutation + const idx = targets.indexOf(member); + if (idx >= 0) { + targets.splice(idx, 1); + this.setState({targets}); + } + }; + + _onPaste = async (e) => { + // Prevent the text being pasted into the textarea + e.preventDefault(); + + // Process it as a list of addresses to add instead + const text = e.clipboardData.getData("text"); + const possibleMembers = [ + // If we can avoid hitting the profile endpoint, we should. + ...this.state.recents, + ...this.state.suggestions, + ...this.state.serverResultsMixin, + ...this.state.threepidResultsMixin, + ]; + const toAdd = []; + const failed = []; + const potentialAddresses = text.split(/[\s,]+/); + for (const address of potentialAddresses) { + const member = possibleMembers.find(m => m.userId === address); + if (member) { + toAdd.push(member.user); + continue; + } + + if (address.indexOf('@') > 0 && Email.looksValid(address)) { + toAdd.push(new ThreepidMember(address)); + continue; + } + + if (address[0] !== '@') { + failed.push(address); // not a user ID + continue; + } + + try { + const profile = await MatrixClientPeg.get().getProfileInfo(address); + const displayName = profile ? profile.displayname : null; + const avatarUrl = profile ? profile.avatar_url : null; + toAdd.push(new DirectoryMember({ + user_id: address, + display_name: displayName, + avatar_url: avatarUrl, + })); + } catch (e) { + console.error("Error looking up profile for " + address); + console.error(e); + failed.push(address); + } + } + + if (failed.length > 0) { + const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); + Modal.createTrackedDialog('Invite Paste Fail', '', QuestionDialog, { + title: _t('Failed to find the following users'), + description: _t( + "The following users might not exist or are invalid, and cannot be invited: %(csvNames)s", + {csvNames: failed.join(", ")}, + ), + button: _t('OK'), + }); + } + + this.setState({targets: [...this.state.targets, ...toAdd]}); + }; + + _onClickInputArea = (e) => { + // Stop the browser from highlighting text + e.preventDefault(); + e.stopPropagation(); + + if (this._editorRef && this._editorRef.current) { + this._editorRef.current.focus(); + } + }; + + _onUseDefaultIdentityServerClick = (e) => { + e.preventDefault(); + + // Update the IS in account data. Actually using it may trigger terms. + // eslint-disable-next-line react-hooks/rules-of-hooks + useDefaultIdentityServer(); + this.setState({canUseIdentityServer: true, tryingIdentityServer: false}); + }; + + _onManageSettingsClick = (e) => { + e.preventDefault(); + dis.dispatch({ action: 'view_user_settings' }); + this._cancel(); + }; + _renderSection(kind: "recents"|"suggestions") { let sourceMembers = kind === 'recents' ? this.state.recents : this.state.suggestions; let showNum = kind === 'recents' ? this.state.numRecentsShown : this.state.numSuggestionsShown; @@ -307,17 +597,27 @@ export default class DMInviteDialog extends React.PureComponent { const lastActive = (m) => kind === 'recents' ? m.lastActive : null; const sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions"); - // Mix in the server results if we have any, but only if we're searching - if (this.state.filterText && this.state.serverResultsMixin && kind === 'suggestions') { - // only pick out the server results that aren't already covered though - const uniqueServerResults = this.state.serverResultsMixin - .filter(u => !sourceMembers.some(m => m.userId === u.userId)); + // Mix in the server results if we have any, but only if we're searching. We track the additional + // members separately because we want to filter sourceMembers but trust the mixin arrays to have + // the right members in them. + let additionalMembers = []; + const hasMixins = this.state.serverResultsMixin || this.state.threepidResultsMixin; + if (this.state.filterText && hasMixins && kind === 'suggestions') { + // We don't want to duplicate members though, so just exclude anyone we've already seen. + const notAlreadyExists = (u: Member): boolean => { + return !sourceMembers.some(m => m.userId === u.userId) + && !additionalMembers.some(m => m.userId === u.userId); + }; - sourceMembers = sourceMembers.concat(uniqueServerResults); + const uniqueServerResults = this.state.serverResultsMixin.filter(notAlreadyExists); + additionalMembers = additionalMembers.concat(...uniqueServerResults); + + const uniqueThreepidResults = this.state.threepidResultsMixin.filter(notAlreadyExists); + additionalMembers = additionalMembers.concat(...uniqueThreepidResults); } // Hide the section if there's nothing to filter by - if (!sourceMembers || sourceMembers.length === 0) return null; + if (sourceMembers.length === 0 && additionalMembers.length === 0) return null; // Do some simple filtering on the input before going much further. If we get no results, say so. if (this.state.filterText) { @@ -325,7 +625,7 @@ export default class DMInviteDialog extends React.PureComponent { sourceMembers = sourceMembers .filter(m => m.user.name.toLowerCase().includes(filterBy) || m.userId.toLowerCase().includes(filterBy)); - if (sourceMembers.length === 0) { + if (sourceMembers.length === 0 && additionalMembers.length === 0) { return (

{sectionName}

@@ -335,6 +635,10 @@ export default class DMInviteDialog extends React.PureComponent { } } + // Now we mix in the additional members. Again, we presume these have already been filtered. We + // also assume they are more relevant than our suggestions and prepend them to the list. + sourceMembers = [...additionalMembers, ...sourceMembers]; + // If we're going to hide one member behind 'show more', just use up the space of the button // with the member's tile instead. if (showNum === sourceMembers.length - 1) showNum++; @@ -360,6 +664,7 @@ export default class DMInviteDialog extends React.PureComponent { key={r.userId} onToggle={this._toggleMember} highlightWord={this.state.filterText} + isSelected={this.state.targets.some(t => t.userId === r.userId)} /> )); return ( @@ -371,23 +676,65 @@ export default class DMInviteDialog extends React.PureComponent { ); } - 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 = ( -
- + _renderEditor() { + const targets = this.state.targets.map(t => ( + + )); + const input = ( +