diff --git a/.buildkite/pipeline.yaml b/.buildkite/pipeline.yaml deleted file mode 100644 index a8ce1273fb..0000000000 --- a/.buildkite/pipeline.yaml +++ /dev/null @@ -1,126 +0,0 @@ -steps: - - label: ":eslint: JS Lint" - command: - - "echo '--- Install js-sdk'" - - "./scripts/ci/install-deps.sh" - - "yarn lint:js" - plugins: - - docker#v3.0.1: - image: "node:12" - - - label: ":eslint: TS Lint" - command: - - "echo '--- Install js-sdk'" - - "./scripts/ci/install-deps.sh" - - "yarn lint:ts" - plugins: - - docker#v3.0.1: - image: "node:12" - - - label: ":eslint: Types Lint" - command: - - "echo '--- Install js-sdk'" - - "./scripts/ci/install-deps.sh" - - "yarn lint:types" - plugins: - - docker#v3.0.1: - image: "node:12" - - - label: ":jest: Tests" - agents: - # We use a medium sized instance instead of the normal small ones because - # webpack loves to gorge itself on resources. - queue: "medium" - command: - - "echo '--- Install js-sdk'" - # TODO: Remove hacky chmod for BuildKite - - "chmod +x ./scripts/ci/*.sh" - - "chmod +x ./scripts/*" - - "echo '--- Installing Dependencies'" - - "./scripts/ci/install-deps.sh" - - "echo '--- Running initial build steps'" - - "yarn build" - - "echo '+++ Running Tests'" - - "yarn test" - plugins: - - docker#v3.0.1: - image: "node:12" - - - label: "🛠 Build" - command: - - "echo '--- Install js-sdk'" - - "./scripts/ci/install-deps.sh" - - "echo '+++ Building Project'" - - "yarn build" - plugins: - - docker#v3.0.1: - image: "node:12" - - - label: ":chains: End-to-End Tests" - agents: - # We use a xlarge sized instance instead of the normal small ones because - # e2e tests otherwise take +-8min - queue: "xlarge" - command: - # TODO: Remove hacky chmod for BuildKite - - "echo '--- Setup'" - - "chmod +x ./scripts/ci/*.sh" - - "chmod +x ./scripts/*" - - "echo '--- Install js-sdk'" - - "./scripts/ci/install-deps.sh" - - "echo '--- Running initial build steps'" - - "yarn build" - - "echo '+++ Running Tests'" - - "./scripts/ci/end-to-end-tests.sh" - plugins: - - docker#v3.0.1: - image: "matrixdotorg/riotweb-ci-e2etests-env:latest" - propagate-environment: true - - - label: "🔧 Riot Tests" - agents: - # We use a medium sized instance instead of the normal small ones because - # webpack loves to gorge itself on resources. - queue: "medium" - command: - # Install chrome - - "echo '--- Installing Chrome'" - - "wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -" - - "sh -c 'echo \"deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main\" >> /etc/apt/sources.list.d/google.list'" - - "apt-get update" - - "apt-get install -y google-chrome-stable" - # TODO: Remove hacky chmod for BuildKite - - "chmod +x ./scripts/ci/*.sh" - - "chmod +x ./scripts/*" - - "echo '--- Installing Dependencies'" - - "./scripts/ci/install-deps.sh" - - "echo '--- Running initial build steps'" - - "yarn build" - - "echo '+++ Running Tests'" - - "./scripts/ci/riot-unit-tests.sh" - env: - CHROME_BIN: "/usr/bin/google-chrome-stable" - plugins: - - docker#v3.0.1: - image: "node:10" - propagate-environment: true - - - label: "🌐 i18n" - command: - - "echo '--- Fetching Dependencies'" - - "yarn install" - - "echo '+++ Testing i18n output'" - - "yarn diff-i18n" - plugins: - - docker#v3.0.1: - image: "node:10" - - - wait - - - label: "🐴 Trigger riot-web" - trigger: "riot-web" - branches: "develop" - build: - branch: "develop" - message: "[react-sdk] ${BUILDKITE_MESSAGE}" - async: true diff --git a/.eslintignore b/.eslintignore index c4c7fe5067..c4f7298047 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,4 @@ src/component-index.js +test/end-to-end-tests/node_modules/ +test/end-to-end-tests/riot/ +test/end-to-end-tests/synapse/ diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index 36b03b121c..9973cfb120 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -1,6 +1,5 @@ # autogenerated file: run scripts/generate-eslint-error-ignore-file to update. -src/component-index.js src/components/structures/RoomDirectory.js src/components/structures/RoomStatusBar.js src/components/structures/RoomView.js @@ -10,7 +9,6 @@ src/components/structures/UploadBar.js src/components/views/avatars/BaseAvatar.js src/components/views/avatars/MemberAvatar.js src/components/views/create_room/RoomAlias.js -src/components/views/dialogs/DeactivateAccountDialog.js src/components/views/dialogs/SetPasswordDialog.js src/components/views/dialogs/UnknownDeviceDialog.js src/components/views/elements/AddressSelector.js @@ -31,7 +29,6 @@ src/components/views/rooms/MemberInfo.js src/components/views/rooms/MemberList.js src/components/views/rooms/RoomList.js src/components/views/rooms/RoomPreviewBar.js -src/components/views/rooms/SearchBar.js src/components/views/rooms/SearchResultTile.js src/components/views/settings/ChangeAvatar.js src/components/views/settings/ChangePassword.js @@ -44,7 +41,6 @@ src/notifications/ContentRules.js src/notifications/PushRuleVectorState.js src/PlatformPeg.js src/rageshake/rageshake.js -src/rageshake/submit-rageshake.js src/ratelimitedfunc.js src/Rooms.js src/Unread.js @@ -60,4 +56,7 @@ test/components/views/dialogs/InteractiveAuthDialog-test.js test/mock-clock.js test/notifications/ContentRules-test.js test/notifications/PushRuleVectorState-test.js -test/stores/RoomViewStore-test.js +src/component-index.js +test/end-to-end-tests/node_modules/ +test/end-to-end-tests/riot/ +test/end-to-end-tests/synapse/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ae2711e25..02c085d0b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,994 @@ +Changes in [2.3.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.3.0) (2020-03-30) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.3.0-rc.1...v2.3.0) + + * Upgrade JS SDK to 5.2.0 + +Changes in [2.3.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.3.0-rc.1) (2020-03-26) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.2.3...v2.3.0-rc.1) + + * Upgrade JS SDK to 5.2.0-rc.1 + * Add a flag to control whether cross-signing signatures are trusted + [\#4277](https://github.com/matrix-org/matrix-react-sdk/pull/4277) + * Update from Weblate + [\#4282](https://github.com/matrix-org/matrix-react-sdk/pull/4282) + * Update copy on SSSS symmetric upgrade toast + [\#4281](https://github.com/matrix-org/matrix-react-sdk/pull/4281) + * Wait for SSSS upgrade to complete + [\#4270](https://github.com/matrix-org/matrix-react-sdk/pull/4270) + * Update cross-signing verification copy and fix i18n + [\#4278](https://github.com/matrix-org/matrix-react-sdk/pull/4278) + * Fix soft-crash on bad permalinks + [\#4280](https://github.com/matrix-org/matrix-react-sdk/pull/4280) + * Fix: make self-verification wait for incoming request + [\#4267](https://github.com/matrix-org/matrix-react-sdk/pull/4267) + * Fall back to non-standard persisted api for Safari + [\#4272](https://github.com/matrix-org/matrix-react-sdk/pull/4272) + * Respond to backup key sharing requests + [\#4275](https://github.com/matrix-org/matrix-react-sdk/pull/4275) + * Log and display secret sharing cache state + [\#4268](https://github.com/matrix-org/matrix-react-sdk/pull/4268) + * Support sending config and ready events to capable widgets (Jitsi) + [\#4266](https://github.com/matrix-org/matrix-react-sdk/pull/4266) + * If cached keys are present in the key backup dialog, use them + [\#4273](https://github.com/matrix-org/matrix-react-sdk/pull/4273) + * Fix formatbar not hidden on highlighted message sent + [\#4265](https://github.com/matrix-org/matrix-react-sdk/pull/4265) + * Support Jitsi conferences sent/received on Riot Mobile and older Riot Webs + [\#4252](https://github.com/matrix-org/matrix-react-sdk/pull/4252) + * Use unified function to check cross-signing is ready + [\#4263](https://github.com/matrix-org/matrix-react-sdk/pull/4263) + * Migrate SSSS to symmetric + [\#4224](https://github.com/matrix-org/matrix-react-sdk/pull/4224) + * Migration to symmetric SSSS + [\#4242](https://github.com/matrix-org/matrix-react-sdk/pull/4242) + * Always display verification request toasts on top + [\#4262](https://github.com/matrix-org/matrix-react-sdk/pull/4262) + * Fix: assume SAS is supported when starting request with .start + [\#4249](https://github.com/matrix-org/matrix-react-sdk/pull/4249) + * Fix logout when Olm failed to load. + [\#4261](https://github.com/matrix-org/matrix-react-sdk/pull/4261) + * Improve naming of Jitsi conferences + [\#4251](https://github.com/matrix-org/matrix-react-sdk/pull/4251) + * Handle matrix.to user permalink in-room rather than solo + [\#4245](https://github.com/matrix-org/matrix-react-sdk/pull/4245) + * Fix: filter room list (again) by canonical and alternative aliases + [\#4260](https://github.com/matrix-org/matrix-react-sdk/pull/4260) + * EventIndex: Add some logging to the file panel populating. + [\#4250](https://github.com/matrix-org/matrix-react-sdk/pull/4250) + * Update from Weblate + [\#4259](https://github.com/matrix-org/matrix-react-sdk/pull/4259) + * Migrate RoomView to React Contexts in the hope for better temporal stability + [\#4258](https://github.com/matrix-org/matrix-react-sdk/pull/4258) + * Update WidgetUtils.js fix Jitsi path + [\#4256](https://github.com/matrix-org/matrix-react-sdk/pull/4256) + * Fix local jitsi build url fail and missing argument + [\#4255](https://github.com/matrix-org/matrix-react-sdk/pull/4255) + * Add shortcut CmdOrCtrl+. to toggle right panel + [\#4244](https://github.com/matrix-org/matrix-react-sdk/pull/4244) + * Improve Keyboard Shortcuts. Add alt-arrows & alt-shift-arrows + [\#4241](https://github.com/matrix-org/matrix-react-sdk/pull/4241) + * Bring back legacy verification by comparing public device keys + [\#4240](https://github.com/matrix-org/matrix-react-sdk/pull/4240) + * Searching: Return an empty result if the search term is an empty string. + [\#4248](https://github.com/matrix-org/matrix-react-sdk/pull/4248) + * Break continuation on showHiddenEvents-rendered events + [\#4247](https://github.com/matrix-org/matrix-react-sdk/pull/4247) + * Watch for show-RR settings changes, use room-specific and fix margins + [\#4246](https://github.com/matrix-org/matrix-react-sdk/pull/4246) + * Register Mac electron specific Cmd+, shortcut to User Settings + [\#4243](https://github.com/matrix-org/matrix-react-sdk/pull/4243) + * Use a local wrapper for Jitsi calls + [\#4234](https://github.com/matrix-org/matrix-react-sdk/pull/4234) + * Invite Dialog fixes + [\#4233](https://github.com/matrix-org/matrix-react-sdk/pull/4233) + * RoomPreviewBar word-break the sender name too + [\#4239](https://github.com/matrix-org/matrix-react-sdk/pull/4239) + * Report to the user when a key signature upload fails + [\#4229](https://github.com/matrix-org/matrix-react-sdk/pull/4229) + * pre-send megolm keys when possible when a user starts typing + [\#4235](https://github.com/matrix-org/matrix-react-sdk/pull/4235) + * we don't do mx_fadable anymore so get rid of broken RightPanel disabling + [\#4238](https://github.com/matrix-org/matrix-react-sdk/pull/4238) + * Fix left left panel overflowing vertically + [\#4237](https://github.com/matrix-org/matrix-react-sdk/pull/4237) + * Fix custom tags causing left panel to over-expand + [\#4236](https://github.com/matrix-org/matrix-react-sdk/pull/4236) + * Add Keyboard shortcuts dialog + [\#4231](https://github.com/matrix-org/matrix-react-sdk/pull/4231) + * Don't use buildkite agent to upload logs + [\#4232](https://github.com/matrix-org/matrix-react-sdk/pull/4232) + * Remove Gemini Scrollbars + [\#4217](https://github.com/matrix-org/matrix-react-sdk/pull/4217) + * Room Directory Explore Servers redesign + [\#4209](https://github.com/matrix-org/matrix-react-sdk/pull/4209) + * Fix redo keyboard shortcut on macOS + [\#4110](https://github.com/matrix-org/matrix-react-sdk/pull/4110) + * Fix: ensure local state for aliases doesn't get garbled up + [\#4230](https://github.com/matrix-org/matrix-react-sdk/pull/4230) + * Rename 'jump to bottom' to avoid ublock block + [\#4208](https://github.com/matrix-org/matrix-react-sdk/pull/4208) + * Restore key backup in background after complete security + [\#4225](https://github.com/matrix-org/matrix-react-sdk/pull/4225) + * Fix key backup trust text for cross-signing + [\#4223](https://github.com/matrix-org/matrix-react-sdk/pull/4223) + * Add default on config setting to control call button in composer + [\#4227](https://github.com/matrix-org/matrix-react-sdk/pull/4227) + * Fix: make alternative addresses UX less confusing + [\#4221](https://github.com/matrix-org/matrix-react-sdk/pull/4221) + * Wait for verification request on login + [\#4222](https://github.com/matrix-org/matrix-react-sdk/pull/4222) + * EventIndex: Add support to delete events from the index. + [\#4204](https://github.com/matrix-org/matrix-react-sdk/pull/4204) + * EventIndex: Remove a checkpoint if the HTTP request returns a 403. + [\#4214](https://github.com/matrix-org/matrix-react-sdk/pull/4214) + * Move to composer when typing letters with Shift held + [\#4216](https://github.com/matrix-org/matrix-react-sdk/pull/4216) + * Wrap large room names when previewing them + [\#4213](https://github.com/matrix-org/matrix-react-sdk/pull/4213) + * Rename Review Devices to Review Sessions + [\#4219](https://github.com/matrix-org/matrix-react-sdk/pull/4219) + * Fix typo in tabIndex to make React happy + [\#4215](https://github.com/matrix-org/matrix-react-sdk/pull/4215) + * Proof of concept for custom theme adding + [\#4148](https://github.com/matrix-org/matrix-react-sdk/pull/4148) + * Remove stuff that yarn install doesn't think we need + [\#4205](https://github.com/matrix-org/matrix-react-sdk/pull/4205) + * Declare jsx in tsconfig for IDEs + [\#4207](https://github.com/matrix-org/matrix-react-sdk/pull/4207) + * Fix: best-effort to join room without canonical alias over federation from + room directory + [\#4210](https://github.com/matrix-org/matrix-react-sdk/pull/4210) + * Test for cross-signing homeserver support during login, toasts + [\#4206](https://github.com/matrix-org/matrix-react-sdk/pull/4206) + * Send verification request to a single device in a way compatible with non- + cross-signing + [\#4202](https://github.com/matrix-org/matrix-react-sdk/pull/4202) + * Fixes for removing local alias + [\#4199](https://github.com/matrix-org/matrix-react-sdk/pull/4199) + * yarn upgrade + [\#4201](https://github.com/matrix-org/matrix-react-sdk/pull/4201) + * Support TypeScript for React components + [\#4203](https://github.com/matrix-org/matrix-react-sdk/pull/4203) + * When room name is changed, show both the old and new name + [\#4183](https://github.com/matrix-org/matrix-react-sdk/pull/4183) + +Changes in [2.2.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.2.3) (2020-03-17) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.2.3-rc.1...v2.2.3) + + * Upgrade JS SDK to 5.1.1 + * Add default on config setting to control call button in composer + [\#4228](https://github.com/matrix-org/matrix-react-sdk/pull/4228) + * Fix: make alternative addresses UX less confusing + [\#4226](https://github.com/matrix-org/matrix-react-sdk/pull/4226) + * Fix: best-effort to join room without canonical alias over federation from + room directory + [\#4211](https://github.com/matrix-org/matrix-react-sdk/pull/4211) + +Changes in [2.2.3-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.2.3-rc.1) (2020-03-11) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.2.1...v2.2.3-rc.1) + + * Update from Weblate + [\#4200](https://github.com/matrix-org/matrix-react-sdk/pull/4200) + * Revert "enable 4s when accepting a verification request" + [\#4198](https://github.com/matrix-org/matrix-react-sdk/pull/4198) + * Don't remount main split children on rhs collapse + [\#4197](https://github.com/matrix-org/matrix-react-sdk/pull/4197) + * Add fallback label for canonical alias events that dont change anything + [\#4195](https://github.com/matrix-org/matrix-react-sdk/pull/4195) + * Immediately switch to verification dialog when clicking [Continue] from new + session dialog + [\#4196](https://github.com/matrix-org/matrix-react-sdk/pull/4196) + * Enable 4S if needed when trying to verify or accepting verification + [\#4194](https://github.com/matrix-org/matrix-react-sdk/pull/4194) + * Remove extraneous tab stop from room tree view. + [\#4193](https://github.com/matrix-org/matrix-react-sdk/pull/4193) + * Remove v1 identity server fallbacks + [\#4191](https://github.com/matrix-org/matrix-react-sdk/pull/4191) + * Allow editing of alt_aliases according to MSC2432 + [\#4187](https://github.com/matrix-org/matrix-react-sdk/pull/4187) + * Update timeline rendering of aliases + [\#4189](https://github.com/matrix-org/matrix-react-sdk/pull/4189) + * Fix mark as read button for dark theme + [\#4190](https://github.com/matrix-org/matrix-react-sdk/pull/4190) + * Un-linkify version in settings + [\#4188](https://github.com/matrix-org/matrix-react-sdk/pull/4188) + * Make Mjolnir stop more robust + [\#4186](https://github.com/matrix-org/matrix-react-sdk/pull/4186) + * Fix secret sharing names to match spec + [\#4185](https://github.com/matrix-org/matrix-react-sdk/pull/4185) + * Share secrets with another device on request + [\#4172](https://github.com/matrix-org/matrix-react-sdk/pull/4172) + * Fall back to to_device verification if other user hasn't uploaded cross- + signing keys + [\#4181](https://github.com/matrix-org/matrix-react-sdk/pull/4181) + * Disable edits on redacted events + [\#4182](https://github.com/matrix-org/matrix-react-sdk/pull/4182) + * Use crypto.verification.request even when xsign is disabled + [\#4180](https://github.com/matrix-org/matrix-react-sdk/pull/4180) + * Reword the status for the currently indexing rooms. + [\#4084](https://github.com/matrix-org/matrix-react-sdk/pull/4084) + * Moved read receipts to the bottom of the message + [\#3892](https://github.com/matrix-org/matrix-react-sdk/pull/3892) + * Include a mark as read X under the scroll to unread button + [\#4159](https://github.com/matrix-org/matrix-react-sdk/pull/4159) + * Show the room presence indicator, even when cross-singing is enabled + [\#4178](https://github.com/matrix-org/matrix-react-sdk/pull/4178) + * Add local echo when clicking "Manually Verify" in unverified session dialog + [\#4179](https://github.com/matrix-org/matrix-react-sdk/pull/4179) + * link to matrix.org/security-disclosure-policy in help screen + [\#4129](https://github.com/matrix-org/matrix-react-sdk/pull/4129) + * only show verify button if user has uploaded cross-signing keys + [\#4174](https://github.com/matrix-org/matrix-react-sdk/pull/4174) + * Fix room alias references in topics + [\#4176](https://github.com/matrix-org/matrix-react-sdk/pull/4176) + * Fix not being able to start chats when you have no rooms + [\#4177](https://github.com/matrix-org/matrix-react-sdk/pull/4177) + * Disable registration flows on SSO servers + [\#4170](https://github.com/matrix-org/matrix-react-sdk/pull/4170) + * Don't group blank membership changes + [\#4160](https://github.com/matrix-org/matrix-react-sdk/pull/4160) + * Ensure the room list always triggers updates on itself + [\#4175](https://github.com/matrix-org/matrix-react-sdk/pull/4175) + * Fix composer touch bar flickering on keypress in Chrome + [\#4173](https://github.com/matrix-org/matrix-react-sdk/pull/4173) + * Document scrollpanel and BACAT scrolling + [\#4167](https://github.com/matrix-org/matrix-react-sdk/pull/4167) + * riot-desktop open SSO in browser so user doesn't have to auth twice + [\#4158](https://github.com/matrix-org/matrix-react-sdk/pull/4158) + * Lock login and registration buttons after submit + [\#4165](https://github.com/matrix-org/matrix-react-sdk/pull/4165) + * Suggest the server's results as lower quality in the invite dialog + [\#4149](https://github.com/matrix-org/matrix-react-sdk/pull/4149) + * Adjust scroll offset with relative scrolling + [\#4166](https://github.com/matrix-org/matrix-react-sdk/pull/4166) + * only automatically download in usercontent if user requested it + [\#4163](https://github.com/matrix-org/matrix-react-sdk/pull/4163) + * Fix having to decrypt & download in two steps + [\#4162](https://github.com/matrix-org/matrix-react-sdk/pull/4162) + * Use bash for release script + [\#4161](https://github.com/matrix-org/matrix-react-sdk/pull/4161) + * Revert to manual sorting for custom tag rooms + [\#4157](https://github.com/matrix-org/matrix-react-sdk/pull/4157) + * Fix the last char of people's names being cut off in the invite dialog + [\#4150](https://github.com/matrix-org/matrix-react-sdk/pull/4150) + * Add /whois SlashCommand to open UserInfo + [\#4154](https://github.com/matrix-org/matrix-react-sdk/pull/4154) + * word-break in pills and wrap the background correctly + [\#4155](https://github.com/matrix-org/matrix-react-sdk/pull/4155) + * don't show "This alias is available to use" if the alias is invalid + [\#4153](https://github.com/matrix-org/matrix-react-sdk/pull/4153) + * Don't ask to enable analytics when Do Not Track is enabled + [\#4098](https://github.com/matrix-org/matrix-react-sdk/pull/4098) + * Fix MELS not breaking on day boundaries regression + [\#4152](https://github.com/matrix-org/matrix-react-sdk/pull/4152) + * Fix Quote on search results page + [\#4151](https://github.com/matrix-org/matrix-react-sdk/pull/4151) + * Ensure errors when creating a DM are raised to the user + [\#4144](https://github.com/matrix-org/matrix-react-sdk/pull/4144) + * Add a Login button to startAnyRegistrationFlow + [\#3829](https://github.com/matrix-org/matrix-react-sdk/pull/3829) + * Use latest backup status directly rather than via state + [\#4147](https://github.com/matrix-org/matrix-react-sdk/pull/4147) + * Prefer account password variation of upgrading + [\#4146](https://github.com/matrix-org/matrix-react-sdk/pull/4146) + * Hide user avatars from screen readers in group and room user lists. + [\#4145](https://github.com/matrix-org/matrix-react-sdk/pull/4145) + * Room List sorting algorithms + [\#4085](https://github.com/matrix-org/matrix-react-sdk/pull/4085) + * Clear selected tags when disabling tag panel + [\#4143](https://github.com/matrix-org/matrix-react-sdk/pull/4143) + * Ignore cursor jumping shortcuts with shift + [\#4142](https://github.com/matrix-org/matrix-react-sdk/pull/4142) + * add local echo for clicking 'start verification' button + [\#4138](https://github.com/matrix-org/matrix-react-sdk/pull/4138) + * Fix formatting buttons not marking the composer as modified + [\#4141](https://github.com/matrix-org/matrix-react-sdk/pull/4141) + * Upgrade deps + [\#4136](https://github.com/matrix-org/matrix-react-sdk/pull/4136) + * Remove debug line from Analytics + [\#4137](https://github.com/matrix-org/matrix-react-sdk/pull/4137) + * Use the right function for creating binary verification QR codes + [\#4140](https://github.com/matrix-org/matrix-react-sdk/pull/4140) + * Ensure verification QR codes use the right buffer size + [\#4139](https://github.com/matrix-org/matrix-react-sdk/pull/4139) + * Don't prefix QR codes with the length of the static marker string + [\#4128](https://github.com/matrix-org/matrix-react-sdk/pull/4128) + * Solve fixed-width digit display in flowed text + [\#4127](https://github.com/matrix-org/matrix-react-sdk/pull/4127) + * Limit UserInfo Displayname to 3 lines to get rid of scrollbars + [\#4135](https://github.com/matrix-org/matrix-react-sdk/pull/4135) + +Changes in [2.2.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.2.1) (2020-03-04) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.2.0...v2.2.1) + + * Adjust scroll offset with relative scrolling + [\#4171](https://github.com/matrix-org/matrix-react-sdk/pull/4171) + * Disable registration flows on SSO servers + [\#4169](https://github.com/matrix-org/matrix-react-sdk/pull/4169) + +Changes in [2.2.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.2.0) (2020-03-02) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.2.0-rc.1...v2.2.0) + + * Upgrade JS SDK to 5.1.0 + * Ignore cursor jumping shortcuts with shift + [\#4142](https://github.com/matrix-org/matrix-react-sdk/pull/4142) + +Changes in [2.2.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.2.0-rc.1) (2020-02-26) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.1.1...v2.2.0-rc.1) + + * Upgrade JS SDK to 5.1.0-rc.1 + * Fix message context menu breaking on invalid m.room.pinned_events event + [\#4133](https://github.com/matrix-org/matrix-react-sdk/pull/4133) + * Update from Weblate + [\#4134](https://github.com/matrix-org/matrix-react-sdk/pull/4134) + * Notify platform of language changes + [\#4121](https://github.com/matrix-org/matrix-react-sdk/pull/4121) + * Handle errors when previewing rooms more safely + [\#4132](https://github.com/matrix-org/matrix-react-sdk/pull/4132) + * Don't try to collapse zero events with a group + [\#4131](https://github.com/matrix-org/matrix-react-sdk/pull/4131) + * Don't print errors when the tab is used with no autocomplete present + [\#4130](https://github.com/matrix-org/matrix-react-sdk/pull/4130) + * Improve UI feedback while waiting for network + [\#4126](https://github.com/matrix-org/matrix-react-sdk/pull/4126) + * Ensure DMs tagged outside of account data work in the invite dialog + [\#4123](https://github.com/matrix-org/matrix-react-sdk/pull/4123) + * Show a warning dialog when user indicates a new session wasn't them + [\#4125](https://github.com/matrix-org/matrix-react-sdk/pull/4125) + * Show cancel events as hidden events if we wouldn't usually render them + [\#4120](https://github.com/matrix-org/matrix-react-sdk/pull/4120) + * Collapsed room list has unaligned room tiles #4030 version 2 + [\#4033](https://github.com/matrix-org/matrix-react-sdk/pull/4033) + * Check for cross-signing homeserver support + [\#4118](https://github.com/matrix-org/matrix-react-sdk/pull/4118) + * Don't leak if show_sas never comes (or already came) + [\#4119](https://github.com/matrix-org/matrix-react-sdk/pull/4119) + * Add verification request viewer in devtools + [\#4106](https://github.com/matrix-org/matrix-react-sdk/pull/4106) + * update phase when request prop changes + [\#4117](https://github.com/matrix-org/matrix-react-sdk/pull/4117) + * Handle file downloading locally in electron rather than sending to browser + [\#4113](https://github.com/matrix-org/matrix-react-sdk/pull/4113) + * Remove unused CIDER setting watcher + [\#4116](https://github.com/matrix-org/matrix-react-sdk/pull/4116) + * Use alt_aliases for pills and autocomplete + [\#4102](https://github.com/matrix-org/matrix-react-sdk/pull/4102) + * Add shortcuts for beginning / end of composer + [\#4108](https://github.com/matrix-org/matrix-react-sdk/pull/4108) + * Update from Weblate + [\#4115](https://github.com/matrix-org/matrix-react-sdk/pull/4115) + * Revert "Fix escaped markdown passing backslashes through" + [\#4114](https://github.com/matrix-org/matrix-react-sdk/pull/4114) + * Fix a couple of React warnings/errors + [\#4112](https://github.com/matrix-org/matrix-react-sdk/pull/4112) + * Fix two big DOM leaks which were locking Chrome solid. + [\#4111](https://github.com/matrix-org/matrix-react-sdk/pull/4111) + * Filter out empty strings when pasting IDs into the invite dialog + [\#4109](https://github.com/matrix-org/matrix-react-sdk/pull/4109) + * Remove buildkite pipeline + [\#4107](https://github.com/matrix-org/matrix-react-sdk/pull/4107) + * Use binary packing for verification QR codes + [\#4091](https://github.com/matrix-org/matrix-react-sdk/pull/4091) + * Fix several small bugs with the invite/DM dialog + [\#4099](https://github.com/matrix-org/matrix-react-sdk/pull/4099) + * ignore e2e tests node_modules during linting + [\#4103](https://github.com/matrix-org/matrix-react-sdk/pull/4103) + * Apply null-guard to room pills for when we can't fetch the room + [\#4104](https://github.com/matrix-org/matrix-react-sdk/pull/4104) + * Fix theme being overridden to light even after login is completed + [\#4105](https://github.com/matrix-org/matrix-react-sdk/pull/4105) + * Fix bug where SSSS could be overwritten if user never cross-signs + [\#4100](https://github.com/matrix-org/matrix-react-sdk/pull/4100) + * Accept canonical alias for pills + [\#4096](https://github.com/matrix-org/matrix-react-sdk/pull/4096) + * Fix: don't advertise ability to scan a QR code for verification + [\#4094](https://github.com/matrix-org/matrix-react-sdk/pull/4094) + * Fixes for printing event indexing stats. + [\#4082](https://github.com/matrix-org/matrix-react-sdk/pull/4082) + * Remove exec so release script continues + [\#4095](https://github.com/matrix-org/matrix-react-sdk/pull/4095) + * Use Persistent Storage where possible + [\#4092](https://github.com/matrix-org/matrix-react-sdk/pull/4092) + * Fix user page (missing null check) + [\#4088](https://github.com/matrix-org/matrix-react-sdk/pull/4088) + * Cancel verification request on dialog close + [\#4081](https://github.com/matrix-org/matrix-react-sdk/pull/4081) + * Fix various memory leaks due to method re-binding + [\#4093](https://github.com/matrix-org/matrix-react-sdk/pull/4093) + * Fix share message context menu option keyboard a11y + [\#4073](https://github.com/matrix-org/matrix-react-sdk/pull/4073) + +Changes in [2.1.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.1.1) (2020-02-19) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.1.0...v2.1.1) + + * show spinner while loading local aliases + [\#4090](https://github.com/matrix-org/matrix-react-sdk/pull/4090) + * Don't index key verification events. + [\#4083](https://github.com/matrix-org/matrix-react-sdk/pull/4083) + * Get rid of dependence on usercontent.riot.im + [\#4046](https://github.com/matrix-org/matrix-react-sdk/pull/4046) + * also detect aliases using new /aliases endpoint for room access settings + [\#4089](https://github.com/matrix-org/matrix-react-sdk/pull/4089) + * get local aliases from /aliases in room settings + [\#4086](https://github.com/matrix-org/matrix-react-sdk/pull/4086) + * Start verification sessions in an E2E DM where possible + [\#4080](https://github.com/matrix-org/matrix-react-sdk/pull/4080) + * Only show supported verification methods + [\#4077](https://github.com/matrix-org/matrix-react-sdk/pull/4077) + * Use local echo in VerificationRequest for accepting/declining a verification + request + [\#4072](https://github.com/matrix-org/matrix-react-sdk/pull/4072) + * Report installed PWA, touch input status in rageshakes, analytics + [\#4078](https://github.com/matrix-org/matrix-react-sdk/pull/4078) + * refactor event grouping into separate helper classes + [\#4059](https://github.com/matrix-org/matrix-react-sdk/pull/4059) + * Find existing requests when starting a new verification request + [\#4070](https://github.com/matrix-org/matrix-react-sdk/pull/4070) + * Always speak the full text of the typing indicator when it updates. + [\#4074](https://github.com/matrix-org/matrix-react-sdk/pull/4074) + * Fix escaped markdown passing backslashes through + [\#4008](https://github.com/matrix-org/matrix-react-sdk/pull/4008) + * Move the sidebar to below the sidebar tab buttons for screen readers. + [\#4071](https://github.com/matrix-org/matrix-react-sdk/pull/4071) + +Changes in [2.1.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.1.0) (2020-02-17) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.1.0-rc.2...v2.1.0) + + * Automate SDK dep upgrades for release + [\#4076](https://github.com/matrix-org/matrix-react-sdk/pull/4076) + +Changes in [2.1.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.1.0-rc.2) (2020-02-13) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.1.0-rc.1...v2.1.0-rc.2) + + * Fix error in previous attempt to upgrade JS SDK + +Changes in [2.1.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.1.0-rc.1) (2020-02-13) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.0.0...v2.1.0-rc.1) + + * Upgrade JS SDK to 5.0.0-rc.1 + * don't show tooltips on big icons + [\#4067](https://github.com/matrix-org/matrix-react-sdk/pull/4067) + * Update from Weblate + [\#4069](https://github.com/matrix-org/matrix-react-sdk/pull/4069) + * Fix sending of visit variables to Matomo + [\#4068](https://github.com/matrix-org/matrix-react-sdk/pull/4068) + * Use embedded piwik script rather than piwik.js to respect CSP + [\#4066](https://github.com/matrix-org/matrix-react-sdk/pull/4066) + * remove methods arg to requestVerification(DM) + [\#4058](https://github.com/matrix-org/matrix-react-sdk/pull/4058) + * Check for null config settings a bit safer + [\#4061](https://github.com/matrix-org/matrix-react-sdk/pull/4061) + * Score user ID searches higher when they match nearly exactly + [\#4060](https://github.com/matrix-org/matrix-react-sdk/pull/4060) + * Fix uncentered letter inside avatar for currently typing users + [\#4051](https://github.com/matrix-org/matrix-react-sdk/pull/4051) + * Disable 'start' button after clicking in VerificationPanel + [\#4065](https://github.com/matrix-org/matrix-react-sdk/pull/4065) + * Fixed bug where key reset didn't always return the right key + [\#4057](https://github.com/matrix-org/matrix-react-sdk/pull/4057) + * Don't render avatars in pills for screen readers. + [\#4062](https://github.com/matrix-org/matrix-react-sdk/pull/4062) + * Make QR self-verification compatible with RiotX + [\#4044](https://github.com/matrix-org/matrix-react-sdk/pull/4044) + * Verify single device from other user in right panel & Not Trusted dialog + [\#4043](https://github.com/matrix-org/matrix-react-sdk/pull/4043) + * Disable verification buttons after clicking to avoid double submission + [\#4049](https://github.com/matrix-org/matrix-react-sdk/pull/4049) + * Verification toast fixes + [\#4048](https://github.com/matrix-org/matrix-react-sdk/pull/4048) + * Use EncryptionPanel everywhere, part I + [\#4042](https://github.com/matrix-org/matrix-react-sdk/pull/4042) + * quick fix for cross-signing reset bug + [\#4056](https://github.com/matrix-org/matrix-react-sdk/pull/4056) + * Fix error message rendering for key entry + [\#4055](https://github.com/matrix-org/matrix-react-sdk/pull/4055) + * Fix recaptcha blocked by CSP for non-SSL origins + [\#4052](https://github.com/matrix-org/matrix-react-sdk/pull/4052) + * Fix watcher for showTypingNotifications setting + [\#4054](https://github.com/matrix-org/matrix-react-sdk/pull/4054) + * Allow custom hs url submission on enter + [\#4053](https://github.com/matrix-org/matrix-react-sdk/pull/4053) + * Support keepSecretStoragePassphraseForSession at the config level too + [\#4045](https://github.com/matrix-org/matrix-react-sdk/pull/4045) + * Add setting to allow hiding of typing indicator + [\#4047](https://github.com/matrix-org/matrix-react-sdk/pull/4047) + * Button to reset cross-signing and SSSS keys + [\#4041](https://github.com/matrix-org/matrix-react-sdk/pull/4041) + * Use forms to wrap password fields so Chrome doesn't go wild + [\#3974](https://github.com/matrix-org/matrix-react-sdk/pull/3974) + * Update QR code rendering to support VerificationRequests + [\#4001](https://github.com/matrix-org/matrix-react-sdk/pull/4001) + * Differentiate AccessSecretStorageDialog dismiss dialog based on which key we + want to read + [\#4038](https://github.com/matrix-org/matrix-react-sdk/pull/4038) + * Only emit in RoomViewStore when state actually changes + [\#4039](https://github.com/matrix-org/matrix-react-sdk/pull/4039) + * Mark AccessSecretStorageDialog to not be closed by clicking background + [\#4029](https://github.com/matrix-org/matrix-react-sdk/pull/4029) + * Let pointer events fall through to scroll button + [\#4037](https://github.com/matrix-org/matrix-react-sdk/pull/4037) + * Improve event indexing status strings for translation + [\#4035](https://github.com/matrix-org/matrix-react-sdk/pull/4035) + * Button size reviewed for word consuming languages & Settings showing devices + are a bit too tight + [\#4024](https://github.com/matrix-org/matrix-react-sdk/pull/4024) + * Only enumerate settings handlers which are supported + [\#4034](https://github.com/matrix-org/matrix-react-sdk/pull/4034) + * Fix listener removal in verification tile + [\#4036](https://github.com/matrix-org/matrix-react-sdk/pull/4036) + * Do not show alarming red shields on large encrypted rooms for your own + device + [\#4028](https://github.com/matrix-org/matrix-react-sdk/pull/4028) + * Add a class for styling room directory permissions + [\#4007](https://github.com/matrix-org/matrix-react-sdk/pull/4007) + * double-check user verification + [\#4010](https://github.com/matrix-org/matrix-react-sdk/pull/4010) + * Use minimist instead of optimist as it is deprecated + [\#4031](https://github.com/matrix-org/matrix-react-sdk/pull/4031) + * SettingsStore, use a counter instead of wall clock for watcher ids + [\#4032](https://github.com/matrix-org/matrix-react-sdk/pull/4032) + * Don't crash immediately if the room directory chunk is null/empty + [\#4027](https://github.com/matrix-org/matrix-react-sdk/pull/4027) + * Fix verification toast to close at 0s + [\#3998](https://github.com/matrix-org/matrix-react-sdk/pull/3998) + * Fix listener leak in TagPanel + [\#4026](https://github.com/matrix-org/matrix-react-sdk/pull/4026) + * Update from Weblate + [\#4025](https://github.com/matrix-org/matrix-react-sdk/pull/4025) + * Honour the isLogin flag in theme.js + [\#4023](https://github.com/matrix-org/matrix-react-sdk/pull/4023) + * ManageEventIndexDialog: Show how many rooms are being currently crawled. + [\#4022](https://github.com/matrix-org/matrix-react-sdk/pull/4022) + * Advertise that we can scan QR codes even though we can't + [\#4021](https://github.com/matrix-org/matrix-react-sdk/pull/4021) + * Checkpoint addition fixes and return of the crawler sleep time setting. + [\#4020](https://github.com/matrix-org/matrix-react-sdk/pull/4020) + * Truncate SAS emoji labels to fit + [\#4018](https://github.com/matrix-org/matrix-react-sdk/pull/4018) + * Apply copy edits to security setup flow + [\#4017](https://github.com/matrix-org/matrix-react-sdk/pull/4017) + * Fix user trust text to match what was checked + [\#4016](https://github.com/matrix-org/matrix-react-sdk/pull/4016) + * Fix size of invite only icon + [\#4015](https://github.com/matrix-org/matrix-react-sdk/pull/4015) + * Add temporary feature flag to control padlocks + [\#4013](https://github.com/matrix-org/matrix-react-sdk/pull/4013) + * Add an override for the theme + [\#4014](https://github.com/matrix-org/matrix-react-sdk/pull/4014) + * Add title to complete security loading + [\#4011](https://github.com/matrix-org/matrix-react-sdk/pull/4011) + * Only display the first zxcvbn warning/suggestion + [\#4012](https://github.com/matrix-org/matrix-react-sdk/pull/4012) + * Log exceptions from accessSecretStorage + [\#4009](https://github.com/matrix-org/matrix-react-sdk/pull/4009) + * Add advanced option to keep secret storage in memory for session + [\#3995](https://github.com/matrix-org/matrix-react-sdk/pull/3995) + * Add shields to member list, move power label to text + [\#4006](https://github.com/matrix-org/matrix-react-sdk/pull/4006) + * Make encryption events into bubble-style tiles + [\#4005](https://github.com/matrix-org/matrix-react-sdk/pull/4005) + * Update copy when the user verifies their own devices + [\#4000](https://github.com/matrix-org/matrix-react-sdk/pull/4000) + * Use Sets instead of array scans and simplify hiding of invalid users when + inviting + [\#4004](https://github.com/matrix-org/matrix-react-sdk/pull/4004) + * Fix room completion for invited rooms and upgraded rooms + [\#4003](https://github.com/matrix-org/matrix-react-sdk/pull/4003) + * Make shields in UserInfo black if user isn't verified + [\#3999](https://github.com/matrix-org/matrix-react-sdk/pull/3999) + * Change verify user text + [\#3994](https://github.com/matrix-org/matrix-react-sdk/pull/3994) + * Disable all inputs in login form while busy, not just the submit button + [\#3996](https://github.com/matrix-org/matrix-react-sdk/pull/3996) + * fix SAS dialog width + [\#3993](https://github.com/matrix-org/matrix-react-sdk/pull/3993) + * Update placeholder in the composer when it gets changed + [\#3990](https://github.com/matrix-org/matrix-react-sdk/pull/3990) + * Send initial device display name on register + [\#3992](https://github.com/matrix-org/matrix-react-sdk/pull/3992) + * Update QR code handling for new spec + [\#3959](https://github.com/matrix-org/matrix-react-sdk/pull/3959) + * Apply the Olympic effect to SAS Emoji Verification + [\#3989](https://github.com/matrix-org/matrix-react-sdk/pull/3989) + * Pass an ID to the as needed and fix div inside p nesting + [\#3988](https://github.com/matrix-org/matrix-react-sdk/pull/3988) + * Update user info for device and trust changes + [\#3987](https://github.com/matrix-org/matrix-react-sdk/pull/3987) + * Relax secret storage account data check + [\#3985](https://github.com/matrix-org/matrix-react-sdk/pull/3985) + * Fix various races that prevented the right panel being in the right state + for verifications + [\#3984](https://github.com/matrix-org/matrix-react-sdk/pull/3984) + * Fix verifying individual devices + [\#3986](https://github.com/matrix-org/matrix-react-sdk/pull/3986) + * Update from Weblate + [\#3982](https://github.com/matrix-org/matrix-react-sdk/pull/3982) + * Replace device with session in UI text + [\#3980](https://github.com/matrix-org/matrix-react-sdk/pull/3980) + * Add missing await causing promises to be leaked as room IDs + [\#3981](https://github.com/matrix-org/matrix-react-sdk/pull/3981) + * Change new session toast to unverified + [\#3978](https://github.com/matrix-org/matrix-react-sdk/pull/3978) + * Replace Verify button in UserInfo verification with "Learn more" + [\#3975](https://github.com/matrix-org/matrix-react-sdk/pull/3975) + * Don't peek until the matrix client is ready + [\#3979](https://github.com/matrix-org/matrix-react-sdk/pull/3979) + * Verification: don't block UI update on verification finishing + [\#3976](https://github.com/matrix-org/matrix-react-sdk/pull/3976) + * Adjust icons with in person with design + [\#3977](https://github.com/matrix-org/matrix-react-sdk/pull/3977) + * Update copy for right panel verification + [\#3973](https://github.com/matrix-org/matrix-react-sdk/pull/3973) + * Check for timeline in pre-join UISI path + [\#3972](https://github.com/matrix-org/matrix-react-sdk/pull/3972) + * Let users paste text if they've already started filtering invite targets + [\#3970](https://github.com/matrix-org/matrix-react-sdk/pull/3970) + * Filter event types when deciding on activity metrics for DM suggestions + [\#3969](https://github.com/matrix-org/matrix-react-sdk/pull/3969) + * Revert a change causing a login loop + [\#3971](https://github.com/matrix-org/matrix-react-sdk/pull/3971) + * Improve the docs for the event index and fix some type hints. + [\#3960](https://github.com/matrix-org/matrix-react-sdk/pull/3960) + * Automatically focus on the invite dialog input + [\#3968](https://github.com/matrix-org/matrix-react-sdk/pull/3968) + * Restore key backup in Complete Security dialog + [\#3966](https://github.com/matrix-org/matrix-react-sdk/pull/3966) + * Right Panel Verification improvements + [\#3967](https://github.com/matrix-org/matrix-react-sdk/pull/3967) + * Cross Signing Right Panel Verification Decoration + [\#3950](https://github.com/matrix-org/matrix-react-sdk/pull/3950) + * Passing refireParams actually prevented this from working + [\#3965](https://github.com/matrix-org/matrix-react-sdk/pull/3965) + * Start new key backup in security setup flow + [\#3964](https://github.com/matrix-org/matrix-react-sdk/pull/3964) + * Tweak styling of the unread indicator circle. + [\#3958](https://github.com/matrix-org/matrix-react-sdk/pull/3958) + * Add device IDs in user info tooltips + [\#3963](https://github.com/matrix-org/matrix-react-sdk/pull/3963) + * Improve encryption upgrade on login flow + [\#3962](https://github.com/matrix-org/matrix-react-sdk/pull/3962) + * Switch back to legacy decorators + [\#3961](https://github.com/matrix-org/matrix-react-sdk/pull/3961) + * Style bridge settings tab according to design + [\#3894](https://github.com/matrix-org/matrix-react-sdk/pull/3894) + * Fix skinning and babel targets + [\#3957](https://github.com/matrix-org/matrix-react-sdk/pull/3957) + * Enable cross-signing lab when key in storage + [\#3956](https://github.com/matrix-org/matrix-react-sdk/pull/3956) + * Add new session verification details dialog + [\#3953](https://github.com/matrix-org/matrix-react-sdk/pull/3953) + * Fix issue where we don't notice if our own devices shouldn't be trusted + [\#3949](https://github.com/matrix-org/matrix-react-sdk/pull/3949) + * Add separate component for post-auth security flows + [\#3951](https://github.com/matrix-org/matrix-react-sdk/pull/3951) + * Add more logging to settings watchers + [\#3952](https://github.com/matrix-org/matrix-react-sdk/pull/3952) + * Use https for recaptcha for all non-http protocols + [\#3944](https://github.com/matrix-org/matrix-react-sdk/pull/3944) + * Add status and management UI for the event indexer + [\#3672](https://github.com/matrix-org/matrix-react-sdk/pull/3672) + * Remove DM icons if `feature_cross_signing` is enabled; hide padlocks in DM + room headers + [\#3948](https://github.com/matrix-org/matrix-react-sdk/pull/3948) + * Stop rogue verification toast if you verify during login + [\#3943](https://github.com/matrix-org/matrix-react-sdk/pull/3943) + * Show incoming verification requests in the 'complete security' phase + [\#3942](https://github.com/matrix-org/matrix-react-sdk/pull/3942) + * Dismiss logged out device toasts + [\#3941](https://github.com/matrix-org/matrix-react-sdk/pull/3941) + * Verification nag toasts + [\#3940](https://github.com/matrix-org/matrix-react-sdk/pull/3940) + * Update from Weblate + [\#3947](https://github.com/matrix-org/matrix-react-sdk/pull/3947) + * Remember password for e2e bootstrapping + [\#3939](https://github.com/matrix-org/matrix-react-sdk/pull/3939) + * fix compound emoji + [\#3946](https://github.com/matrix-org/matrix-react-sdk/pull/3946) + * Setup flow for cross-signing on login / registration + [\#3937](https://github.com/matrix-org/matrix-react-sdk/pull/3937) + * Update profile avatar letter size + [\#3935](https://github.com/matrix-org/matrix-react-sdk/pull/3935) + * Hide default encryption algorithm + [\#3936](https://github.com/matrix-org/matrix-react-sdk/pull/3936) + * Resolve default export warnings from Webpack + [\#3938](https://github.com/matrix-org/matrix-react-sdk/pull/3938) + * Add null check for cross-signing info in verification panel + [\#3934](https://github.com/matrix-org/matrix-react-sdk/pull/3934) + * Add trace logging to figure out which component is causing weird events + [\#3926](https://github.com/matrix-org/matrix-react-sdk/pull/3926) + * Remove user lists feature flag, making it the default + [\#3906](https://github.com/matrix-org/matrix-react-sdk/pull/3906) + * Last bit of polish for user lists + [\#3925](https://github.com/matrix-org/matrix-react-sdk/pull/3925) + * QR code verification + [\#3871](https://github.com/matrix-org/matrix-react-sdk/pull/3871) + * Do less unnecessary work on CI + [\#3933](https://github.com/matrix-org/matrix-react-sdk/pull/3933) + * Re-enable stylelint on CI + [\#3932](https://github.com/matrix-org/matrix-react-sdk/pull/3932) + * Design pass for room icons + [\#3931](https://github.com/matrix-org/matrix-react-sdk/pull/3931) + * Populate the file panel using the event index if available. + [\#3858](https://github.com/matrix-org/matrix-react-sdk/pull/3858) + * Split AsyncWrapper out from Modal + [\#3928](https://github.com/matrix-org/matrix-react-sdk/pull/3928) + * Fix error in verification code on develop + [\#3930](https://github.com/matrix-org/matrix-react-sdk/pull/3930) + * Seperates out the padlock icon, and adds a tooltip + [\#3929](https://github.com/matrix-org/matrix-react-sdk/pull/3929) + * Cross Signing redesign for composer + [\#3910](https://github.com/matrix-org/matrix-react-sdk/pull/3910) + * Fix verifying your own devices with to_device messages + [\#3927](https://github.com/matrix-org/matrix-react-sdk/pull/3927) + * Room list reflects encryption state + [\#3908](https://github.com/matrix-org/matrix-react-sdk/pull/3908) + * Make the entire User Info scrollable, sticky close button + [\#3914](https://github.com/matrix-org/matrix-react-sdk/pull/3914) + * Remove riot logo from the security setup screens + [\#3916](https://github.com/matrix-org/matrix-react-sdk/pull/3916) + * Only say the session is verified if it is now verified + [\#3917](https://github.com/matrix-org/matrix-react-sdk/pull/3917) + * Hide password section if you can't change your password + [\#3924](https://github.com/matrix-org/matrix-react-sdk/pull/3924) + * Ensure a plaintext version of the composer ends up on the clipboard + [\#3922](https://github.com/matrix-org/matrix-react-sdk/pull/3922) + * Move & upgrade babel runtime into dependencies (like it wants) + [\#3920](https://github.com/matrix-org/matrix-react-sdk/pull/3920) + * Don't list every single alias when there's many + [\#3918](https://github.com/matrix-org/matrix-react-sdk/pull/3918) + * Try to populate user IDs even when the server's directory fails us + [\#3907](https://github.com/matrix-org/matrix-react-sdk/pull/3907) + * Remove .event property on verification request + [\#3912](https://github.com/matrix-org/matrix-react-sdk/pull/3912) + * Attempt to fix Safari + VoiceOver misunderstanding the timeline list + [\#3911](https://github.com/matrix-org/matrix-react-sdk/pull/3911) + * Enable encryption in DMs with device keys + [\#3913](https://github.com/matrix-org/matrix-react-sdk/pull/3913) + * Fix scrollable area and padding in user lists dialog + [\#3905](https://github.com/matrix-org/matrix-react-sdk/pull/3905) + * Add Reject & Ignore user button to invites view + [\#3909](https://github.com/matrix-org/matrix-react-sdk/pull/3909) + * Fix paragraph-awareness of the composer formatting features + [\#3891](https://github.com/matrix-org/matrix-react-sdk/pull/3891) + * Updated visuals for cross-signing bootstrap + [\#3903](https://github.com/matrix-org/matrix-react-sdk/pull/3903) + * Implement some parts of new cross signing bootstrap UI + [\#3897](https://github.com/matrix-org/matrix-react-sdk/pull/3897) + * Treat links as external in report content admin message + [\#3904](https://github.com/matrix-org/matrix-react-sdk/pull/3904) + * Be consistent about our settings svg, free the other one + [\#3902](https://github.com/matrix-org/matrix-react-sdk/pull/3902) + * Change prepublish script to prepare + [\#3899](https://github.com/matrix-org/matrix-react-sdk/pull/3899) + * Remove the react-sdk version + [\#3901](https://github.com/matrix-org/matrix-react-sdk/pull/3901) + * BuildKite: Retry end-to-end tests automatically once if they fail + [\#3900](https://github.com/matrix-org/matrix-react-sdk/pull/3900) + * Slash Command improvements around sending messages with leading slash + [\#3893](https://github.com/matrix-org/matrix-react-sdk/pull/3893) + * Support admin configurable message when reporting content + [\#3898](https://github.com/matrix-org/matrix-react-sdk/pull/3898) + * Don't warn on unverified users; ensured behavior stays the same with flags + off + [\#3896](https://github.com/matrix-org/matrix-react-sdk/pull/3896) + * Fix roving room list for resizer and ff tabstop a11y + [\#3895](https://github.com/matrix-org/matrix-react-sdk/pull/3895) + * Verify individual messages via cross-signing + [\#3875](https://github.com/matrix-org/matrix-react-sdk/pull/3875) + * Fix layering of dependencies in riot-web and e2e tests + [\#3882](https://github.com/matrix-org/matrix-react-sdk/pull/3882) + * Implement Roving Tab Index and Room List as TreeView + [\#3844](https://github.com/matrix-org/matrix-react-sdk/pull/3844) + * Move room header shields over the avatar for the room + [\#3888](https://github.com/matrix-org/matrix-react-sdk/pull/3888) + * Fix toast icon to prevent clipping + [\#3890](https://github.com/matrix-org/matrix-react-sdk/pull/3890) + * Only show devices and verify actions in E2EE rooms + [\#3889](https://github.com/matrix-org/matrix-react-sdk/pull/3889) + * Change user info verification checks to use cross-signing + [\#3887](https://github.com/matrix-org/matrix-react-sdk/pull/3887) + * Fix click-to-ping not inserting colon if composer non-empty + [\#3886](https://github.com/matrix-org/matrix-react-sdk/pull/3886) + * Fix emoticon space completion for upper case emoticons like :D xD + [\#3884](https://github.com/matrix-org/matrix-react-sdk/pull/3884) + * Repair cross-signing panel with async status + [\#3880](https://github.com/matrix-org/matrix-react-sdk/pull/3880) + * Remove temporary key backup button + [\#3878](https://github.com/matrix-org/matrix-react-sdk/pull/3878) + * Score users who have recently spoken higher in invite suggestions + [\#3866](https://github.com/matrix-org/matrix-react-sdk/pull/3866) + * Initial support for verification in right panel + [\#3796](https://github.com/matrix-org/matrix-react-sdk/pull/3796) + * Prevent the invite dialog from jumping around when elements change + [\#3868](https://github.com/matrix-org/matrix-react-sdk/pull/3868) + * Add prepublish script + [\#3876](https://github.com/matrix-org/matrix-react-sdk/pull/3876) + +Changes in [2.0.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.0.0) (2020-01-27) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.0.0-rc.2...v2.0.0) + + * Ensure a plaintext version of the composer ends up on the clipboard + [\#3923](https://github.com/matrix-org/matrix-react-sdk/pull/3923) + * Move & upgrade babel runtime into dependencies (like it wants) + [\#3921](https://github.com/matrix-org/matrix-react-sdk/pull/3921) + * Don't list every single alias when there's many + [\#3919](https://github.com/matrix-org/matrix-react-sdk/pull/3919) + +Changes in [2.0.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.0.0-rc.2) (2020-01-20) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.0.0-rc.1...v2.0.0-rc.2) + + * Add prepublish script + [\#3877](https://github.com/matrix-org/matrix-react-sdk/pull/3877) + +Changes in [2.0.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.0.0-rc.1) (2020-01-20) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.6...v2.0.0-rc.1) + +BREAKING CHANGES +================ + * The react-sdk node module now exports ES6 rather than ES5. If you + wish to supports target that aren't compatible with ES6, you + will need to transpile the react-sdk to a suitable dialect. + +All Changes +=========== + * Fix arrows keys moving through edit history + [\#3874](https://github.com/matrix-org/matrix-react-sdk/pull/3874) + * Fix error about MessagePanel not being available for read markers + [\#3867](https://github.com/matrix-org/matrix-react-sdk/pull/3867) + * Adjust secret storage to work before sync + [\#3864](https://github.com/matrix-org/matrix-react-sdk/pull/3864) + * Update from Weblate + [\#3872](https://github.com/matrix-org/matrix-react-sdk/pull/3872) + * Remove unused deps and dev-deps + [\#3870](https://github.com/matrix-org/matrix-react-sdk/pull/3870) + * Tidy Jest test stuff and dependencies + [\#3869](https://github.com/matrix-org/matrix-react-sdk/pull/3869) + * Move feature flag check for new session toast + [\#3865](https://github.com/matrix-org/matrix-react-sdk/pull/3865) + * Catch exception in checkTerms if no ID server + [\#3863](https://github.com/matrix-org/matrix-react-sdk/pull/3863) + * Catch exception if passphrase dialog cancelled + [\#3862](https://github.com/matrix-org/matrix-react-sdk/pull/3862) + * Disable key request dialogs with cross-signing + [\#3860](https://github.com/matrix-org/matrix-react-sdk/pull/3860) + * Toasts for new, unverified sessions + [\#3859](https://github.com/matrix-org/matrix-react-sdk/pull/3859) + * Check for a matrixclient before trying to use it + [\#3861](https://github.com/matrix-org/matrix-react-sdk/pull/3861) + * Room header & message box shields now reflect cross-signing state + [\#3850](https://github.com/matrix-org/matrix-react-sdk/pull/3850) + * Fix Array.concat undefined + [\#3857](https://github.com/matrix-org/matrix-react-sdk/pull/3857) + * Update chokidar to fix reskindex not working + [\#3856](https://github.com/matrix-org/matrix-react-sdk/pull/3856) + * Make the new DM invite dialog work for regular invites too + [\#3854](https://github.com/matrix-org/matrix-react-sdk/pull/3854) + * Fix event handler leak in MemberStatusMessageAvatar + [\#3855](https://github.com/matrix-org/matrix-react-sdk/pull/3855) + * Move DM creation logic into DMInviteDialog + [\#3843](https://github.com/matrix-org/matrix-react-sdk/pull/3843) + * Remove all text when cutting in the composer + [\#3848](https://github.com/matrix-org/matrix-react-sdk/pull/3848) + * Add a ToastStore + [\#3853](https://github.com/matrix-org/matrix-react-sdk/pull/3853) + * 'Members' button always toggle the right panel + [\#3804](https://github.com/matrix-org/matrix-react-sdk/pull/3804) + * Fix timing of when Composer considers itself to be modified + [\#3842](https://github.com/matrix-org/matrix-react-sdk/pull/3842) + * Compute download file icon immediately + [\#3851](https://github.com/matrix-org/matrix-react-sdk/pull/3851) + * Fix not being able to open profiles from the timeline + [\#3852](https://github.com/matrix-org/matrix-react-sdk/pull/3852) + * Add post-login complete security flow + [\#3847](https://github.com/matrix-org/matrix-react-sdk/pull/3847) + * Added cut/copy and pasting user pills from editor. + [\#3828](https://github.com/matrix-org/matrix-react-sdk/pull/3828) + * Fix imports for help & support tab + [\#3846](https://github.com/matrix-org/matrix-react-sdk/pull/3846) + * Humanize the recent DM rooms ourselves for translations + [\#3841](https://github.com/matrix-org/matrix-react-sdk/pull/3841) + * Improve the quality of invite suggestions by filtering out DMs + [\#3840](https://github.com/matrix-org/matrix-react-sdk/pull/3840) + * Fix linter and tests on develop + [\#3845](https://github.com/matrix-org/matrix-react-sdk/pull/3845) + * Fix sourcemaps by refactoring the build system + [\#3839](https://github.com/matrix-org/matrix-react-sdk/pull/3839) + * Don't error on unverified/unknown devices. + [\#3837](https://github.com/matrix-org/matrix-react-sdk/pull/3837) + * Padlock icons in room header + [\#3835](https://github.com/matrix-org/matrix-react-sdk/pull/3835) + * Don't allow upgrade from untrusted key backup. + [\#3822](https://github.com/matrix-org/matrix-react-sdk/pull/3822) + * Emoji verification: Change name of 🔒 to lock + [\#3825](https://github.com/matrix-org/matrix-react-sdk/pull/3825) + * Room padlock decorations only if cross-signing is enabled + [\#3838](https://github.com/matrix-org/matrix-react-sdk/pull/3838) + * Enable end-to-end tests for sourcemaps (+Windows instructions) + [\#3827](https://github.com/matrix-org/matrix-react-sdk/pull/3827) + * Repair community member info panel + [\#3832](https://github.com/matrix-org/matrix-react-sdk/pull/3832) + * Add feature flag around the presence indicator in room list + [\#3831](https://github.com/matrix-org/matrix-react-sdk/pull/3831) + * Display a padlock icon beside invite-only rooms in the room list + [\#3821](https://github.com/matrix-org/matrix-react-sdk/pull/3821) + * Update from Weblate + [\#3830](https://github.com/matrix-org/matrix-react-sdk/pull/3830) + * Fix listener leak on RoomView + [\#3826](https://github.com/matrix-org/matrix-react-sdk/pull/3826) + * Regenerate i18n for sourcemaps branch + [\#3824](https://github.com/matrix-org/matrix-react-sdk/pull/3824) + * Fix tests for sourcemaps branch + [\#3823](https://github.com/matrix-org/matrix-react-sdk/pull/3823) + * Jest + [\#3724](https://github.com/matrix-org/matrix-react-sdk/pull/3724) + * Sourcemaps: develop -> feature branch + [\#3817](https://github.com/matrix-org/matrix-react-sdk/pull/3817) + * Support pasting a bunch of identifiers into the invite dialog + [\#3820](https://github.com/matrix-org/matrix-react-sdk/pull/3820) + * Support 3PIDs (email addresses) in the invite dialog + [\#3819](https://github.com/matrix-org/matrix-react-sdk/pull/3819) + * Placeholder PR for cleaner diffs: ES6 + [\#3765](https://github.com/matrix-org/matrix-react-sdk/pull/3765) + * Misc fixes for ES6 imports/exports + [\#3766](https://github.com/matrix-org/matrix-react-sdk/pull/3766) + * Wire up the invite targets dialog to a real composer and show selections + [\#3815](https://github.com/matrix-org/matrix-react-sdk/pull/3815) + * Change ref handling in TextualBody to prevent it parsing generated nodes + [\#3711](https://github.com/matrix-org/matrix-react-sdk/pull/3711) + * Render encoded html entities in og:description + [\#3789](https://github.com/matrix-org/matrix-react-sdk/pull/3789) + * Update package.json for new build process + cosmetics + [\#3767](https://github.com/matrix-org/matrix-react-sdk/pull/3767) + * Convert CommonJS exports to ES6 exports + [\#3761](https://github.com/matrix-org/matrix-react-sdk/pull/3761) + * Round 2 of CommonJS to ES6 imports + [\#3764](https://github.com/matrix-org/matrix-react-sdk/pull/3764) + * Strip all variation selectors on emoji + [\#3814](https://github.com/matrix-org/matrix-react-sdk/pull/3814) + * Use the new js-sdk imports and import from src + [\#3763](https://github.com/matrix-org/matrix-react-sdk/pull/3763) + * Convert many imports to handle ES6 exports + [\#3762](https://github.com/matrix-org/matrix-react-sdk/pull/3762) + * Fix userinfo for users not in the room + [\#3812](https://github.com/matrix-org/matrix-react-sdk/pull/3812) + * Attempt to fix e2e tests + [\#3811](https://github.com/matrix-org/matrix-react-sdk/pull/3811) + * Add bunch of null-guards and similar to fix React Errors/complaints + [\#3752](https://github.com/matrix-org/matrix-react-sdk/pull/3752) + * Delegate all room alias validation to the RoomAliasField validator + [\#3807](https://github.com/matrix-org/matrix-react-sdk/pull/3807) + * Support filtering and searching for users to invite in DMs + [\#3802](https://github.com/matrix-org/matrix-react-sdk/pull/3802) + * Add suggestions for which users to invite to chat + [\#3801](https://github.com/matrix-org/matrix-react-sdk/pull/3801) + * Use `flex-start` instead of `start` for postcss + [\#3760](https://github.com/matrix-org/matrix-react-sdk/pull/3760) + * Define getLanguageFromBrowser() for LanguageDropdown + [\#3769](https://github.com/matrix-org/matrix-react-sdk/pull/3769) + * Introduce babel's export-default-from plugin to fix build errors + [\#3768](https://github.com/matrix-org/matrix-react-sdk/pull/3768) + * Add a bit of debugging to incorrect components in the Skinner + [\#3770](https://github.com/matrix-org/matrix-react-sdk/pull/3770) + * [BREAKING] Refactor the entire build process for babel@7 and TypeScript + (chunk 1 of many) + [\#3722](https://github.com/matrix-org/matrix-react-sdk/pull/3722) + * Implementation of new potential skinning mechanism + [\#3723](https://github.com/matrix-org/matrix-react-sdk/pull/3723) + 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) diff --git a/README.md b/README.md index 0fbed22030..d6fd6db1b7 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ All code lands on the `develop` branch - `master` is only used for stable releas **Please file PRs against `develop`!!** Please follow the standard Matrix contributor's guide: -https://github.com/matrix-org/synapse/tree/master/CONTRIBUTING.rst +https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.rst Please follow the Matrix JS/React code style as per: https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md diff --git a/__mocks__/browser-request.js b/__mocks__/browser-request.js index 45f83a1763..7d231fb9db 100644 --- a/__mocks__/browser-request.js +++ b/__mocks__/browser-request.js @@ -1,16 +1,17 @@ const en = require("../src/i18n/strings/en_EN"); module.exports = jest.fn((opts, cb) => { - if (opts.url.endsWith("languages.json")) { + const url = opts.url || opts.uri; + if (url && url.endsWith("languages.json")) { cb(undefined, {status: 200}, JSON.stringify({ "en": { "fileName": "en_EN.json", "label": "English", }, })); - } else if (opts.url.endsWith("en_EN.json")) { + } else if (url && url.endsWith("en_EN.json")) { cb(undefined, {status: 200}, JSON.stringify(en)); } else { - cb(undefined, {status: 404}, ""); + cb(true, {status: 404}, ""); } }); diff --git a/babel.config.js b/babel.config.js index c83be72518..d5a97d56ce 100644 --- a/babel.config.js +++ b/babel.config.js @@ -2,19 +2,16 @@ module.exports = { "sourceMaps": "inline", "presets": [ ["@babel/preset-env", { - "targets": { - "browsers": [ - "last 2 versions" - ] - }, - "modules": "commonjs" + "targets": [ + "last 2 Chrome versions", "last 2 Firefox versions", "last 2 Safari versions" + ], }], "@babel/preset-typescript", "@babel/preset-flow", "@babel/preset-react" ], "plugins": [ - ["@babel/plugin-proposal-decorators", { "legacy": true }], + ["@babel/plugin-proposal-decorators", {legacy: true}], "@babel/plugin-proposal-export-default-from", "@babel/plugin-proposal-numeric-separator", "@babel/plugin-proposal-class-properties", diff --git a/docs/jitsi.md b/docs/jitsi.md new file mode 100644 index 0000000000..779ef79d3a --- /dev/null +++ b/docs/jitsi.md @@ -0,0 +1,31 @@ +# Jitsi Wrapper + +**Note**: These are developer docs. Please consult your client's documentation for +instructions on setting up Jitsi. + +The react-sdk wraps all Jitsi call widgets in a local wrapper called `jitsi.html` +which takes several parameters: + +*Query string*: +* `widgetId`: The ID of the widget. This is needed for communication back to the + react-sdk. +* `parentUrl`: The URL of the parent window. This is also needed for + communication back to the react-sdk. + +*Hash/fragment (formatted as a query string)*: +* `conferenceDomain`: The domain to connect Jitsi Meet to. +* `conferenceId`: The room or conference ID to connect Jitsi Meet to. +* `isAudioOnly`: Boolean for whether this is a voice-only conference. May not + be present, should default to `false`. +* `displayName`: The display name of the user viewing the widget. May not + be present or could be null. +* `avatarUrl`: The HTTP(S) URL for the avatar of the user viewing the widget. May + not be present or could be null. +* `userId`: The MXID of the user viewing the widget. May not be present or could + be null. + +The react-sdk will assume that `jitsi.html` is at the path of wherever it is currently +being served. For example, `https://riot.im/develop/jitsi.html` or `vector://webapp/jitsi.html`. + +The `jitsi.html` wrapper can use the react-sdk's `WidgetApi` to communicate, making +it easier to actually implement the feature. diff --git a/docs/scrolling.md b/docs/scrolling.md new file mode 100644 index 0000000000..71329e5c32 --- /dev/null +++ b/docs/scrolling.md @@ -0,0 +1,28 @@ +# ScrollPanel + +## Updates + +During an onscroll event, we check whether we're getting close to the top or bottom edge of the loaded content. If close enough, we fire a request to load more through the callback passed in the `onFillRequest` prop. This returns a promise is passed down from `TimelinePanel`, where it will call paginate on the `TimelineWindow` and once the events are received back, update its state with the new events. This update trickles down to the `MessagePanel`, which rerenders all tiles and passed that to `ScrollPanel`. ScrollPanels `componentDidUpdate` method gets called, and we do the scroll housekeeping there (read below). Once the rerender has completed, the `setState` callback is called and we resolve the promise returned by `onFillRequest`. Now we check the DOM to see if we need more fill requests. + +## Prevent Shrinking + +ScrollPanel supports a mode to prevent it shrinking. This is used to prevent a jump when at the bottom of the timeline and people start and stop typing. It gets cleared automatically when 200px above the bottom of the timeline. + + +## BACAT (Bottom-Aligned, Clipped-At-Top) scrolling + +BACAT scrolling implements a different way of restoring the scroll position in the timeline while tiles out of view are changing height or tiles are being added or removed. It was added in https://github.com/matrix-org/matrix-react-sdk/pull/2842. + +The motivation for the changes is having noticed that setting scrollTop while scrolling tends to not work well, with it interrupting ongoing scrolling and also querying scrollTop reporting outdated values and consecutive scroll adjustments cancelling each out previous ones. This seems to be worse on macOS than other platforms, presumably because of a higher resolution in scroll events there. Also see https://github.com/vector-im/riot-web/issues/528. The BACAT approach allows to only have to change the scroll offset when adding or removing tiles. + +The approach taken instead is to vertically align the timeline tiles to the bottom of the scroll container (using flexbox) and give the timeline inside the scroll container an explicit height, initially set to a multiple of the PAGE_SIZE (400px at time of writing) as needed by the content. When scrolled up, we can compensate for anything that grew below the viewport by changing the height of the timeline to maintain what's currently visible in the viewport without adjusting the scrollTop and hence without jumping. + +For anything above the viewport growing or shrinking, we don't need to do anything as the timeline is bottom-aligned. We do need to update the height manually to keep all content visible as more is loaded. To maintain scroll position after the portion above the viewport changes height, we need to set the scrollTop, as we cannot balance it out with more height changes. We do this 100ms after the user has stopped scrolling, so setting scrollTop has not nasty side-effects. + +As of https://github.com/matrix-org/matrix-react-sdk/pull/4166, we are scrolling to compensate for height changes by calling `scrollBy(0, x)` rather than reading and than setting `scrollTop`, as reading `scrollTop` can (again, especially on macOS) easily return values that are out of sync with what is on the screen, probably because scrolling can be done [off the main thread](https://wiki.mozilla.org/Platform/GFX/APZ) in some circumstances. This seems to further prevent jumps. + +### How does it work? + +`componentDidUpdate` is called when a tile in the timeline is updated (as we rerender the whole timeline) or tiles are added or removed (see Updates section before). From here, `checkScroll` is called, which calls `_restoreSavedScrollState`. Now, we increase the timeline height if something below the viewport grew by adjusting `this._bottomGrowth`. `bottomGrowth` is the height added to the timeline (on top of the height from the number of pages calculated at the last `_updateHeight` run) to compensate for growth below the viewport. This is cleared during the next run of `_updateHeight`. Remember that the tiles in the timeline are aligned to the bottom. + +From `_restoreSavedScrollState` we also call `_updateHeight` which waits until the user stops scrolling for 100ms and then recalculates the amount of pages of 400px the timeline should be sized to, to be able to show all of its (newly added) content. We have to adjust the scroll offset (which is why we wait until scrolling has stopped) now because the space above the viewport has likely changed. diff --git a/docs/usercontent.md b/docs/usercontent.md new file mode 100644 index 0000000000..e54851dd0d --- /dev/null +++ b/docs/usercontent.md @@ -0,0 +1,27 @@ +# Usercontent + +While decryption itself is safe to be done without a sandbox, +letting the browser and user interact with the resulting data may be dangerous, +previously `usercontent.riot.im` was used to act as a sandbox on a different origin to close the attack surface, +it is now possible to do by using a combination of a sandboxed iframe and some code written into the app which consumes this SDK. + +Usercontent is an iframe sandbox target for allowing a user to safely download a decrypted attachment from a sandboxed origin where it cannot be used to XSS your riot session out from under you. + +Its function is to create an Object URL for the user/browser to use but bound to an origin different to that of the riot instance to protect against XSS. + +It exposes a function over a postMessage API, when sent an object with the matching fields to render a download link with the Object URL: + +```json5 +{ + "imgSrc": "", // the src of the image to display in the download link + "imgStyle": "", // the style to apply to the image + "style": "", // the style to apply to the download link + "download": "", // download attribute to pass to the tag + "textContent": "", // the text to put inside the download link + "blob": "", // the data blob to wrap in an object url and allow the user to download +} +``` + +If only imgSrc, imgStyle and style are passed then just update the existing link without overwriting other things about it. + +It is expected that this target be available at `usercontent/` relative to the root of the app, this can be seen in riot-web's webpack config. diff --git a/package.json b/package.json index b6ec44ebd0..07ed0ccf78 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "1.7.6", + "version": "2.3.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -31,6 +31,7 @@ "typings": "./lib/index.d.ts", "matrix_src_main": "./src/index.js", "scripts": { + "prepare": "yarn build", "i18n": "matrix-gen-i18n", "prunei18n": "matrix-prune-i18n", "diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && ./scripts/gen-i18n.js && node scripts/compare-file.js src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json", @@ -39,20 +40,21 @@ "rethemendex": "res/css/rethemendex.sh", "clean": "rimraf lib", "build": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types", - "build:compile": "yarn reskindex && babel -d lib --verbose --extensions \".ts,.js\" src", - "build:types": "tsc --emitDeclarationOnly", + "build:compile": "yarn reskindex && babel -d lib --verbose --extensions \".ts,.js,.tsx\" src", + "build:types": "tsc --emitDeclarationOnly --jsx react", "start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && yarn start:all", "start:all": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n build,reskindex \"yarn start:build\" \"yarn reskindex:watch\"", "start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"", "lint": "yarn lint:types && yarn lint:ts && yarn lint:js && yarn lint:style", "lint:js": "eslint --max-warnings 0 --ignore-path .eslintignore.errorfiles src test", "lint:ts": "tslint --project ./tsconfig.json -t stylish", - "lint:types": "tsc --noEmit", + "lint:types": "tsc --noEmit --jsx react", "lint:style": "stylelint 'res/css/**/*.scss'", "test": "jest", "test:e2e": "./test/end-to-end-tests/run.sh --riot-url http://localhost:8080" }, "dependencies": { + "@babel/runtime": "^7.8.3", "blueimp-canvas-to-blob": "^3.5.0", "browser-encrypt-attachment": "^0.3.0", "browser-request": "^0.3.3", @@ -70,31 +72,27 @@ "flux": "2.1.1", "focus-visible": "^5.0.2", "fuse.js": "^2.2.0", - "gemini-scrollbar": "github:matrix-org/gemini-scrollbar#91e1e566", "gfm.css": "^1.1.1", - "glob": "^5.0.14", "glob-to-regexp": "^0.4.1", "highlight.js": "^9.15.8", "html-entities": "^1.2.1", "is-ip": "^2.0.0", - "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.6", "lodash": "^4.17.14", - "lolex": "4.2", - "matrix-js-sdk": "3.0.0", - "optimist": "^0.6.1", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "minimist": "^1.2.0", "pako": "^1.0.5", "png-chunks-extract": "^1.0.0", + "project-name-generator": "^2.1.7", "prop-types": "^15.5.8", + "qrcode": "^1.4.4", "qrcode-react": "^0.1.16", "qs": "^6.6.0", - "querystring": "^0.2.0", "react": "^16.9.0", "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", "tar-js": "^0.3.0", @@ -102,7 +100,6 @@ "url": "^0.11.0", "velocity-animate": "^1.5.2", "what-input": "^5.2.6", - "whatwg-fetch": "^1.1.1", "zxcvbn": "^4.4.2" }, "devDependencies": { @@ -114,17 +111,18 @@ "@babel/plugin-proposal-numeric-separator": "^7.7.4", "@babel/plugin-proposal-object-rest-spread": "^7.7.4", "@babel/plugin-transform-flow-comments": "^7.7.4", - "@babel/plugin-transform-runtime": "^7.7.6", + "@babel/plugin-transform-runtime": "^7.8.3", "@babel/preset-env": "^7.7.6", "@babel/preset-flow": "^7.7.4", "@babel/preset-react": "^7.7.4", "@babel/preset-typescript": "^7.7.4", "@babel/register": "^7.7.4", - "@babel/runtime": "^7.7.6", "@peculiar/webcrypto": "^1.0.22", + "@types/classnames": "^2.2.10", + "@types/react": "16.9", "babel-eslint": "^10.0.3", "babel-jest": "^24.9.0", - "chokidar": "^2.1.2", + "chokidar": "^3.3.1", "concurrently": "^4.0.1", "enzyme": "^3.10.0", "enzyme-adapter-react-16": "^1.15.1", @@ -138,11 +136,12 @@ "estree-walker": "^0.5.0", "file-loader": "^3.0.1", "flow-parser": "^0.57.3", + "glob": "^5.0.14", "jest": "^24.9.0", + "lolex": "^5.1.2", "matrix-mock-request": "^1.2.3", "matrix-react-test-utils": "^0.2.2", "react-test-renderer": "^16.9.0", - "require-json": "0.0.1", "rimraf": "^2.4.3", "source-map-loader": "^0.2.3", "stylelint": "^9.10.1", @@ -158,7 +157,9 @@ "testMatch": [ "/test/**/*-test.js" ], - "setupTestFrameworkScriptFile": "/test/setupTests.js", + "setupFilesAfterEnv": [ + "/test/setupTests.js" + ], "moduleNameMapper": { "\\.(gif|png|svg|ttf|woff2)$": "/__mocks__/imageMock.js", "\\$webapp/i18n/languages.json": "/__mocks__/languages.json" diff --git a/release.sh b/release.sh index 1f287bc839..23b8822041 100755 --- a/release.sh +++ b/release.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash # # Script to perform a release of matrix-react-sdk. # @@ -9,4 +9,52 @@ set -e cd `dirname $0` -exec ./node_modules/matrix-js-sdk/release.sh -z "$@" +for i in matrix-js-sdk +do + echo "Checking version of $i..." + depver=`cat package.json | jq -r .dependencies[\"$i\"]` + latestver=`yarn info -s $i dist-tags.next` + if [ "$depver" != "$latestver" ] + then + echo "The latest version of $i is $latestver but package.json depends on $depver." + echo -n "Type 'u' to auto-upgrade, 'c' to continue anyway, or 'a' to abort:" + read resp + if [ "$resp" != "u" ] && [ "$resp" != "c" ] + then + echo "Aborting." + exit 1 + fi + if [ "$resp" == "u" ] + then + echo "Upgrading $i to $latestver..." + yarn add -E $i@$latestver + git add -u + # The `-e` flag opens the editor and gives you a chance to check + # the upgrade for correctness. + git commit -m "Upgrade $i to $latestver" -e + fi + fi +done + +./node_modules/matrix-js-sdk/release.sh -z "$@" + +release="${1#v}" +prerelease=0 +# We check if this build is a prerelease by looking to +# see if the version has a hyphen in it. Crude, +# but semver doesn't support postreleases so anything +# with a hyphen is a prerelease. +echo $release | grep -q '-' && prerelease=1 + +if [ $prerelease -eq 0 ] +then + # For a release, reset SDK deps back to the `develop` branch. + for i in matrix-js-sdk + do + echo "Resetting $i to develop branch..." + yarn add github:matrix-org/$i#develop + git add -u + git commit -m "Reset $i back to develop branch" + done + git push origin develop +fi diff --git a/res/css/_common.scss b/res/css/_common.scss index 51d985efb7..ad64aced50 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -42,10 +42,15 @@ pre, code { font-size: 100% !important; } -.error, .warning { +.error, .warning, +.text-error, .text-warning { color: $warning-color; } +.text-success { + color: $accent-color; +} + b { // On Firefox, the default weight for `` is `bolder` which results in no bold // effect since we only have specific weights of our fonts available. @@ -202,37 +207,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { transition: opacity 0.2s ease-in-out; } -/* XXX: critical hack to GeminiScrollbar to allow them to work in FF 42 and Chrome 48. - Stop the scrollbar view from pushing out the container's overall sizing, which causes - flexbox to adapt to the new size and cause the view to keep growing. - */ -.gm-scrollbar-container .gm-scroll-view { - position: absolute; -} - -/* Expand thumbs on hoverover */ -.gm-scrollbar { - border-radius: 5px !important; -} -.gm-scrollbar.-vertical { - width: 6px; - transition: width 120ms ease-out !important; -} -.gm-scrollbar.-vertical:hover, -.gm-scrollbar.-vertical:active { - width: 8px; - transition: width 120ms ease-out !important; -} -.gm-scrollbar.-horizontal { - height: 6px; - transition: height 120ms ease-out !important; -} -.gm-scrollbar.-horizontal:hover, -.gm-scrollbar.-horizontal:active { - height: 8px; - transition: height 120ms ease-out !important; -} - // These are magic constants which are excluded from tinting, to let themes // (which only have CSS, unlike skins) tell the app what their non-tinted // colourscheme is by inspecting the stylesheet DOM. @@ -338,6 +312,14 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { margin-bottom: 10px; } +.mx_Dialog_titleImage { + vertical-align: middle; + width: 25px; + height: 25px; + margin-left: -2px; + margin-right: 4px; +} + .mx_Dialog_title { font-size: 22px; line-height: 36px; @@ -378,7 +360,13 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { text-align: right; } -.mx_Dialog button, .mx_Dialog input[type="submit"] { +/* XXX: Our button style are a mess: buttons that happen to appear in dialogs get special styles applied + * to them that no button anywhere else in the app gets by default. In practice, buttons in other places + * in the app look the same by being AccessibleButtons, or possibly by having explict button classes. + * We should go through and have one consistent set of styles for buttons throughout the app. + * For now, I am duplicating the selectors here for mx_Dialog and mx_DialogButtons. + */ +.mx_Dialog button, .mx_Dialog input[type="submit"], .mx_Dialog_buttons button, .mx_Dialog_buttons input[type="submit"] { @mixin mx_DialogButton; margin-left: 0px; margin-right: 8px; @@ -394,27 +382,32 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { margin-right: 0px; } -.mx_Dialog button:hover, .mx_Dialog input[type="submit"]:hover { +.mx_Dialog button:hover, .mx_Dialog input[type="submit"]:hover, .mx_Dialog_buttons button:hover, .mx_Dialog_buttons input[type="submit"]:hover { @mixin mx_DialogButton_hover; } -.mx_Dialog button:focus, .mx_Dialog input[type="submit"]:focus { +.mx_Dialog button:focus, .mx_Dialog input[type="submit"]:focus, .mx_Dialog_buttons button:focus, .mx_Dialog_buttons input[type="submit"]:focus { filter: brightness($focus-brightness); } -.mx_Dialog button.mx_Dialog_primary, .mx_Dialog input[type="submit"].mx_Dialog_primary { +.mx_Dialog button.mx_Dialog_primary, .mx_Dialog input[type="submit"].mx_Dialog_primary, .mx_Dialog_buttons button.mx_Dialog_primary, .mx_Dialog_buttons input[type="submit"].mx_Dialog_primary { color: $accent-fg-color; background-color: $accent-color; min-width: 156px; } -.mx_Dialog button.danger, .mx_Dialog input[type="submit"].danger { +.mx_Dialog button.danger, .mx_Dialog input[type="submit"].danger, .mx_Dialog_buttons button.danger, .mx_Dialog_buttons input[type="submit"].danger { background-color: $warning-color; border: solid 1px $warning-color; color: $accent-fg-color; } -.mx_Dialog button:disabled, .mx_Dialog input[type="submit"]:disabled { +.mx_Dialog button.warning, .mx_Dialog input[type="submit"].warning { + border: solid 1px $warning-color; + color: $warning-color; +} + +.mx_Dialog button:disabled, .mx_Dialog input[type="submit"]:disabled, .mx_Dialog_buttons button:disabled, .mx_Dialog_buttons input[type="submit"]:disabled { background-color: $light-fg-color; border: solid 1px $light-fg-color; opacity: 0.7; diff --git a/res/css/_components.scss b/res/css/_components.scss index 7a9ebfdf26..b959b1f1cd 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -28,6 +28,7 @@ @import "./structures/_TopLeftMenuButton.scss"; @import "./structures/_UploadBar.scss"; @import "./structures/_ViewSource.scss"; +@import "./structures/auth/_CompleteSecurity.scss"; @import "./structures/auth/_Login.scss"; @import "./views/auth/_AuthBody.scss"; @import "./views/auth/_AuthButtons.scss"; @@ -35,6 +36,7 @@ @import "./views/auth/_AuthHeader.scss"; @import "./views/auth/_AuthHeaderLogo.scss"; @import "./views/auth/_AuthPage.scss"; +@import "./views/auth/_CompleteSecurityBody.scss"; @import "./views/auth/_CountryDropdown.scss"; @import "./views/auth/_InteractiveAuthEntryComponents.scss"; @import "./views/auth/_LanguageSelector.scss"; @@ -56,15 +58,18 @@ @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"; @import "./views/dialogs/_EncryptedEventDialog.scss"; @import "./views/dialogs/_GroupAddressPicker.scss"; @import "./views/dialogs/_IncomingSasDialog.scss"; +@import "./views/dialogs/_InviteDialog.scss"; +@import "./views/dialogs/_KeyboardShortcutsDialog.scss"; @import "./views/dialogs/_MessageEditHistoryDialog.scss"; +@import "./views/dialogs/_NewSessionReviewDialog.scss"; @import "./views/dialogs/_RoomSettingsDialog.scss"; +@import "./views/dialogs/_RoomSettingsDialogBridges.scss"; @import "./views/dialogs/_RoomUpgradeDialog.scss"; @import "./views/dialogs/_RoomUpgradeWarningDialog.scss"; @import "./views/dialogs/_SetEmailDialog.scss"; @@ -124,7 +129,6 @@ @import "./views/messages/_MEmoteBody.scss"; @import "./views/messages/_MFileBody.scss"; @import "./views/messages/_MImageBody.scss"; -@import "./views/messages/_MKeyVerificationRequest.scss"; @import "./views/messages/_MNoticeBody.scss"; @import "./views/messages/_MStickerBody.scss"; @import "./views/messages/_MTextBody.scss"; @@ -139,7 +143,10 @@ @import "./views/messages/_TextualEvent.scss"; @import "./views/messages/_UnknownBody.scss"; @import "./views/messages/_ViewSourceEvent.scss"; +@import "./views/messages/_common_CryptoEvent.scss"; +@import "./views/right_panel/_EncryptionInfo.scss"; @import "./views/right_panel/_UserInfo.scss"; +@import "./views/right_panel/_VerificationPanel.scss"; @import "./views/room_settings/_AliasSettings.scss"; @import "./views/room_settings/_ColorSettings.scss"; @import "./views/rooms/_AppsDrawer.scss"; @@ -150,6 +157,7 @@ @import "./views/rooms/_EditMessageComposer.scss"; @import "./views/rooms/_EntityTile.scss"; @import "./views/rooms/_EventTile.scss"; +@import "./views/rooms/_InviteOnlyIcon.scss"; @import "./views/rooms/_JumpToBottomButton.scss"; @import "./views/rooms/_LinkPreviewWidget.scss"; @import "./views/rooms/_MemberDeviceInfo.scss"; @@ -170,7 +178,6 @@ @import "./views/rooms/_RoomTile.scss"; @import "./views/rooms/_RoomUpgradeWarningBar.scss"; @import "./views/rooms/_SearchBar.scss"; -@import "./views/rooms/_SearchableEntityList.scss"; @import "./views/rooms/_SendMessageComposer.scss"; @import "./views/rooms/_Stickers.scss"; @import "./views/rooms/_TopUnreadMessagesBar.scss"; @@ -179,6 +186,7 @@ @import "./views/settings/_AvatarSetting.scss"; @import "./views/settings/_CrossSigningPanel.scss"; @import "./views/settings/_DevicesPanel.scss"; +@import "./views/settings/_E2eAdvancedPanel.scss"; @import "./views/settings/_EmailAddresses.scss"; @import "./views/settings/_IntegrationManager.scss"; @import "./views/settings/_KeyBackupPanel.scss"; diff --git a/res/css/structures/_AutoHideScrollbar.scss b/res/css/structures/_AutoHideScrollbar.scss index 6e4484157c..50842c71bc 100644 --- a/res/css/structures/_AutoHideScrollbar.scss +++ b/res/css/structures/_AutoHideScrollbar.scss @@ -14,69 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -/* This file has CSS for both native and non-native scrollbars in an order - * that's fairly logical to read but duplicates a selector to separate the - * hiding/showing from the sizing. - */ -/* stylelint-disable no-duplicate-selectors */ - -/* -1. for browsers that support native overlay auto-hiding scrollbars -*/ -.mx_AutoHideScrollbar { - overflow-x: hidden; - overflow-y: auto; - -ms-overflow-style: -ms-autohiding-scrollbar; -} -/* -2. webkit also supports overflow:overlay where the scrollbars don't take any space -in the layout but they don't autohide, so do that only on hover -*/ -body.mx_scrollbar_overlay_noautohide .mx_AutoHideScrollbar { - overflow-y: hidden; -} - -body.mx_scrollbar_overlay_noautohide .mx_AutoHideScrollbar:hover { - overflow-y: overlay; -} -/* -3. as a last fallback, compensate for the scrollbar taking up space in the layout -by having giving the child element (.mx_AutoHideScrollbar_offset) a -negative right margin of the width of the scrollbar when the container -is overflowing. This is what Firefox ends up using. Overflow is detected -in javascript, and adds the mx_AutoHideScrollbar_overflow class to the container. -This only works in Firefox, which should be fine as this fallback is only needed there. -*/ -body.mx_scrollbar_nooverlay { - .mx_AutoHideScrollbar { - overflow-y: hidden; - } - - .mx_AutoHideScrollbar:hover { - overflow-y: auto; - } - - /* - offset scrollbar width with negative margin-right - - include before and after psuedo-elements here so they can - be used to do something interesting like scroll-indicating - gradients (see IndicatorScrollBar) - */ - .mx_AutoHideScrollbar:hover.mx_AutoHideScrollbar_overflow > .mx_AutoHideScrollbar_offset, - .mx_AutoHideScrollbar:hover.mx_AutoHideScrollbar_overflow::before, - .mx_AutoHideScrollbar:hover.mx_AutoHideScrollbar_overflow::after { - margin-right: calc(-1 * var(--scrollbar-width)); - } -} - -// style the native scrollbars ... -// ... standard css scrollbars (firefox at time of writing) -.mx_AutoHideScrollbar { +// make any scrollbar grey and thin +html { scrollbar-color: $scrollbar-thumb-color $scrollbar-track-color; +} +// scrollbar-width is not inherited (but -color is, why?!), +// so declare it on every element +* { scrollbar-width: thin; } -// or fallback for webkit browsers + ::-webkit-scrollbar { width: 6px; height: 6px; @@ -84,6 +31,37 @@ body.mx_scrollbar_nooverlay { } ::-webkit-scrollbar-thumb { - background-color: $scrollbar-thumb-color; border-radius: 3px; + background-color: $scrollbar-thumb-color; +} + +// make auto-hide scrollbars not transparent again on hover +.mx_AutoHideScrollbar:hover { + scrollbar-color: $scrollbar-thumb-color $scrollbar-track-color; + + &::-webkit-scrollbar { + background-color: $scrollbar-track-color; + } + + &::-webkit-scrollbar-thumb { + background-color: $scrollbar-thumb-color; + } +} + +// make scrollbars transparent for autohide scrollbars +.mx_AutoHideScrollbar { + overflow-x: hidden; + overflow-y: auto; + overflow-y: overlay; // where supported + -ms-overflow-style: -ms-autohiding-scrollbar; + + &::-webkit-scrollbar { + background-color: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: transparent; + } + + scrollbar-color: transparent transparent; } diff --git a/res/css/structures/_GroupView.scss b/res/css/structures/_GroupView.scss index 4ec53a3c9a..72a1132c15 100644 --- a/res/css/structures/_GroupView.scss +++ b/res/css/structures/_GroupView.scss @@ -63,7 +63,7 @@ limitations under the License. } .mx_GroupHeader_editButton::before { - mask-image: url('$(res)/img/icons-settings-room.svg'); + mask-image: url('$(res)/img/feather-customised/settings.svg'); } .mx_GroupHeader_shareButton::before { @@ -180,10 +180,6 @@ limitations under the License. line-height: 2em; } -.mx_GroupView > .mx_MainSplit { - flex: 1; -} - .mx_GroupView_body { flex-grow: 1; } @@ -341,8 +337,8 @@ limitations under the License. display: none; } -.mx_GroupView_body .gm-scroll-view > * { - margin: 11px 50px 0px 68px; +.mx_GroupView_body .mx_AutoHideScrollbar > * { + margin: 11px 50px 50px 68px; } .mx_GroupView_groupDesc textarea { @@ -370,7 +366,7 @@ limitations under the License. padding: 40px 20px; } -.mx_GroupView .mx_MemberInfo .gm-scroll-view > :not(.mx_MemberInfo_avatar) { +.mx_GroupView .mx_MemberInfo .mx_AutoHideScrollbar > :not(.mx_MemberInfo_avatar) { padding-left: 16px; padding-right: 16px; } diff --git a/res/css/structures/_MainSplit.scss b/res/css/structures/_MainSplit.scss index 4d73953cd7..25e1153fce 100644 --- a/res/css/structures/_MainSplit.scss +++ b/res/css/structures/_MainSplit.scss @@ -18,6 +18,7 @@ limitations under the License. display: flex; flex-direction: row; min-width: 0; + height: 100%; } // move hit area 5px to the right so it doesn't overlap with the timeline scrollbar diff --git a/res/css/structures/_MatrixChat.scss b/res/css/structures/_MatrixChat.scss index f2ce7e1d5c..c5a5d50068 100644 --- a/res/css/structures/_MatrixChat.scss +++ b/res/css/structures/_MatrixChat.scss @@ -76,13 +76,6 @@ limitations under the License. flex: 1 1 0; min-width: 0; - /* Experimental fix for https://github.com/vector-im/vector-web/issues/947 - and https://github.com/vector-im/vector-web/issues/946. - Empirically this stops the MessagePanel's width exploding outwards when - gemini is in 'prevented' mode - */ - overflow-x: auto; - /* To fix https://github.com/vector-im/riot-web/issues/3298 where Safari needed height 100% all the way down to the HomePage. Height does not have to be auto, empirically. diff --git a/res/css/structures/_MyGroups.scss b/res/css/structures/_MyGroups.scss index d25789ab94..36150c33a5 100644 --- a/res/css/structures/_MyGroups.scss +++ b/res/css/structures/_MyGroups.scss @@ -67,9 +67,6 @@ limitations under the License. } } - - - .mx_MyGroups_headerCard_header { font-weight: bold; margin-bottom: 10px; @@ -98,6 +95,11 @@ limitations under the License. display: flex; flex-direction: column; + overflow-y: auto; +} + +.mx_MyGroups_scrollable { + overflow-y: inherit; } .mx_MyGroups_placeholder { diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss index 973f6fe9b3..3c373e8883 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,7 +19,7 @@ limitations under the License. overflow-x: hidden; flex: 0 0 auto; position: relative; - min-width: 250px; + min-width: 264px; display: flex; flex-direction: column; } diff --git a/res/css/structures/_RoomDirectory.scss b/res/css/structures/_RoomDirectory.scss index 4b49332af7..f3a7b0e243 100644 --- a/res/css/structures/_RoomDirectory.scss +++ b/res/css/structures/_RoomDirectory.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. @@ -45,9 +46,8 @@ limitations under the License. } .mx_RoomDirectory_listheader { - display: flex; - margin-top: 12px; - margin-bottom: 12px; + display: block; + margin-top: 13px; } .mx_RoomDirectory_searchbox { @@ -64,7 +64,7 @@ limitations under the License. } .mx_RoomDirectory_table { - font-size: 14px; + font-size: 12px; color: $primary-fg-color; width: 100%; text-align: left; @@ -112,6 +112,7 @@ limitations under the License. .mx_RoomDirectory_name { display: inline-block; + font-size: 18px; font-weight: 600; } @@ -119,6 +120,16 @@ limitations under the License. display: inline-block; } +.mx_RoomDirectory_perm { + border-radius: 10px; + display: inline-block; + height: 20px; + line-height: 20px; + padding: 0 5px; + color: $accent-fg-color; + background-color: $rte-room-pill-color; +} + .mx_RoomDirectory_topic { cursor: initial; color: $light-fg-color; @@ -138,8 +149,8 @@ limitations under the License. padding: 0; } -.mx_RoomDirectory p { - font-size: 14px; +.mx_RoomDirectory > span { + font-size: 15px; margin-top: 0; .mx_AccessibleButton { diff --git a/res/css/structures/_RoomSubList.scss b/res/css/structures/_RoomSubList.scss index be44563cfb..1934e681c2 100644 --- a/res/css/structures/_RoomSubList.scss +++ b/res/css/structures/_RoomSubList.scss @@ -166,41 +166,22 @@ limitations under the License. // overflow indicators .mx_RoomSubList:not(.resized-all) > .mx_RoomSubList_scroll { - &.mx_IndicatorScrollbar_topOverflow::before, - &.mx_IndicatorScrollbar_bottomOverflow::after { + &.mx_IndicatorScrollbar_topOverflow::before { position: sticky; + content: ""; + top: 0; left: 0; right: 0; height: 8px; - content: ""; - display: block; z-index: 100; + display: block; pointer-events: none; - } - - &.mx_IndicatorScrollbar_topOverflow > .mx_AutoHideScrollbar_offset { - margin-top: -8px; - } - &.mx_IndicatorScrollbar_bottomOverflow > .mx_AutoHideScrollbar_offset { - margin-bottom: -8px; - } - - &.mx_IndicatorScrollbar_topOverflow::before { - top: 0; transition: background-image 0.1s ease-in; background: linear-gradient(to top, $panel-gradient); } - /* - // for now, we remove the bottomOverflow entirely as we don't want to - // lose the screen real-estate due to a bg-colored gradient, but we also - // don't want to use drop shadows and risk a confusing hierarchy of cards. - // so, instead, we hard-clip at the bottom but soft-clip at the top. - &.mx_IndicatorScrollbar_bottomOverflow::after { - bottom: 0; - transition: background-image 0.1s ease-in; - margin: 0px -8px; - background: linear-gradient(to bottom, rgba(0,0,0,0.1), rgba(0,0,0,0.0)); + + &.mx_IndicatorScrollbar_topOverflow { + margin-top: -8px; } - */ } diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index dddd2e324c..472831c0d9 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -23,6 +23,7 @@ limitations under the License. flex-direction: column; align-items: center; justify-content: space-between; + min-height: 0; } .mx_TagPanel_items_selected { @@ -57,6 +58,7 @@ limitations under the License. .mx_TagPanel .mx_TagPanel_scroller { flex-grow: 1; + width: 100%; } .mx_TagPanel .mx_TagPanel_tagTileContainer { diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss index 4c5e746e66..d1687743d6 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -51,8 +51,8 @@ limitations under the License. &.mx_Toast_hasIcon { &::after { content: ""; - width: 20px; - height: 20px; + width: 22px; + height: 22px; grid-column: 1; grid-row: 1; mask-size: 100%; @@ -64,6 +64,10 @@ limitations under the License. background-color: $primary-fg-color; } + &.mx_Toast_icon_verification_warning::after { + background-image: url("$(res)/img/e2e/warning.svg"); + } + h2, .mx_Toast_body { grid-column: 2; } @@ -94,5 +98,9 @@ limitations under the License. margin: 4px 0 11px 0; font-size: 12px; } + + .mx_Toast_deviceID { + font-size: 10px; + } } } diff --git a/res/css/structures/auth/_CompleteSecurity.scss b/res/css/structures/auth/_CompleteSecurity.scss new file mode 100644 index 0000000000..601492d43c --- /dev/null +++ b/res/css/structures/auth/_CompleteSecurity.scss @@ -0,0 +1,55 @@ +/* +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. +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_CompleteSecurity_header { + display: flex; + align-items: center; +} + +.mx_CompleteSecurity_headerIcon { + width: 24px; + height: 24px; + margin-right: 4px; + position: relative; +} + +.mx_CompleteSecurity_heroIcon { + width: 128px; + height: 128px; + position: relative; + margin: 0 auto; +} + +.mx_CompleteSecurity_body { + font-size: 15px; +} + +.mx_CompleteSecurity_waiting { + color: $notice-secondary-color; +} + +.mx_CompleteSecurity_actionRow { + display: flex; + justify-content: flex-end; + + .mx_AccessibleButton { + margin-inline-start: 18px; + + &.warning { + color: $warning-color; + } + } +} diff --git a/res/css/views/auth/_AuthBody.scss b/res/css/views/auth/_AuthBody.scss index b05629003e..7c5b008535 100644 --- a/res/css/views/auth/_AuthBody.scss +++ b/res/css/views/auth/_AuthBody.scss @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,12 +17,12 @@ limitations under the License. .mx_AuthBody { width: 500px; + font-size: 12px; + color: $authpage-secondary-color; background-color: $authpage-body-bg-color; border-radius: 0 4px 4px 0; padding: 25px 60px; box-sizing: border-box; - font-size: 12px; - color: $authpage-secondary-color; h2 { font-size: 24px; diff --git a/res/css/views/auth/_CompleteSecurityBody.scss b/res/css/views/auth/_CompleteSecurityBody.scss new file mode 100644 index 0000000000..c7860fbe74 --- /dev/null +++ b/res/css/views/auth/_CompleteSecurityBody.scss @@ -0,0 +1,42 @@ +/* +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. +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_CompleteSecurityBody { + width: 600px; + color: $authpage-primary-color; + background-color: $authpage-body-bg-color; + border-radius: 4px; + padding: 20px; + box-sizing: border-box; + + h2 { + font-size: 24px; + font-weight: 600; + margin-top: 0; + } + + h3 { + font-size: 14px; + font-weight: 600; + } + + a:link, + a:hover, + a:visited { + @mixin mx_Dialog_link; + } +} diff --git a/res/css/views/dialogs/_DevtoolsDialog.scss b/res/css/views/dialogs/_DevtoolsDialog.scss index 9d58c999c3..500c46b5fd 100644 --- a/res/css/views/dialogs/_DevtoolsDialog.scss +++ b/res/css/views/dialogs/_DevtoolsDialog.scss @@ -189,3 +189,37 @@ limitations under the License. } } } + +.mx_DevTools_VerificationRequest { + border: 1px solid #cccccc; + border-radius: 3px; + padding: 1px 5px; + margin-bottom: 6px; + font-family: $monospace-font-family; + + dl { + display: grid; + grid-template-columns: max-content auto; + margin: 0; + } + + dd { + grid-column-start: 2; + } + + dd:empty { + color: #666666; + &::after { + content: "(empty)"; + } + } + + dt { + font-weight: bold; + grid-column-start: 1; + } + + dt::after { + content: ":"; + } +} diff --git a/res/css/views/dialogs/_DMInviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss similarity index 72% rename from res/css/views/dialogs/_DMInviteDialog.scss rename to res/css/views/dialogs/_InviteDialog.scss index f806e85120..5e0893b8fd 100644 --- a/res/css/views/dialogs/_DMInviteDialog.scss +++ b/res/css/views/dialogs/_InviteDialog.scss @@ -14,11 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_DMInviteDialog_addressBar { +.mx_InviteDialog_addressBar { display: flex; flex-direction: row; - .mx_DMInviteDialog_editor { + .mx_InviteDialog_editor { flex: 1; width: 100%; // Needed to make the Field inside grow background-color: $user-tile-hover-bg-color; @@ -28,7 +28,7 @@ limitations under the License. overflow-x: hidden; overflow-y: auto; - .mx_DMInviteDialog_userTile { + .mx_InviteDialog_userTile { display: inline-block; float: left; position: relative; @@ -61,15 +61,26 @@ limitations under the License. } } - .mx_DMInviteDialog_goButton { - width: 48px; + .mx_InviteDialog_goButton { + min-width: 48px; margin-left: 10px; height: 25px; line-height: 25px; } + + .mx_InviteDialog_buttonAndSpinner { + .mx_Spinner { + // Width and height are required to trick the layout engine. + width: 20px; + height: 20px; + margin-left: 5px; + display: inline-block; + vertical-align: middle; + } + } } -.mx_DMInviteDialog_section { +.mx_InviteDialog_section { padding-bottom: 10px; h3 { @@ -80,7 +91,7 @@ limitations under the License. } } -.mx_DMInviteDialog_roomTile { +.mx_InviteDialog_roomTile { cursor: pointer; padding: 5px 10px; @@ -93,7 +104,7 @@ limitations under the License. vertical-align: middle; } - .mx_DMInviteDialog_roomTile_avatarStack { + .mx_InviteDialog_roomTile_avatarStack { display: inline-block; position: relative; width: 36px; @@ -106,7 +117,7 @@ limitations under the License. } } - .mx_DMInviteDialog_roomTile_selected { + .mx_InviteDialog_roomTile_selected { width: 36px; height: 36px; border-radius: 36px; @@ -120,7 +131,7 @@ limitations under the License. height: 24px; grid-column: 1; grid-row: 1; - mask-image: url('$(res)/img/feather-customised/check.svg'); + mask-image: url("$(res)/img/feather-customised/check.svg"); mask-size: 100%; mask-repeat: no-repeat; position: absolute; @@ -130,20 +141,20 @@ limitations under the License. } } - .mx_DMInviteDialog_roomTile_name { + .mx_InviteDialog_roomTile_name { font-weight: 600; font-size: 14px; color: $primary-fg-color; margin-left: 7px; } - .mx_DMInviteDialog_roomTile_userId { + .mx_InviteDialog_roomTile_userId { font-size: 12px; color: $muted-fg-color; margin-left: 7px; } - .mx_DMInviteDialog_roomTile_time { + .mx_InviteDialog_roomTile_time { text-align: right; font-size: 12px; color: $muted-fg-color; @@ -151,16 +162,16 @@ limitations under the License. line-height: 36px; // Height of the avatar to keep the time vertically aligned } - .mx_DMInviteDialog_roomTile_highlight { + .mx_InviteDialog_roomTile_highlight { font-weight: 900; } } // Many of these styles are stolen from mx_UserPill, but adjusted for the invite dialog. -.mx_DMInviteDialog_userTile { +.mx_InviteDialog_userTile { margin-right: 8px; - .mx_DMInviteDialog_userTile_pill { + .mx_InviteDialog_userTile_pill { background-color: $username-variant1-color; border-radius: 12px; display: inline-block; @@ -170,28 +181,48 @@ limitations under the License. padding-right: 8px; color: #ffffff; // this is fine without a var because it's for both themes - .mx_DMInviteDialog_userTile_avatar { + .mx_InviteDialog_userTile_avatar { border-radius: 20px; position: relative; left: -5px; top: 2px; } - img.mx_DMInviteDialog_userTile_avatar { + img.mx_InviteDialog_userTile_avatar { vertical-align: top; } - .mx_DMInviteDialog_userTile_name { + .mx_InviteDialog_userTile_name { vertical-align: top; } - .mx_DMInviteDialog_userTile_threepidAvatar { + .mx_InviteDialog_userTile_threepidAvatar { background-color: #ffffff; // this is fine without a var because it's for both themes } } - .mx_DMInviteDialog_userTile_remove { + .mx_InviteDialog_userTile_remove { display: inline-block; margin-left: 4px; } } + +.mx_InviteDialog { + // Prevent the dialog from jumping around randomly when elements change. + height: 590px; + padding-left: 20px; // the design wants some padding on the left +} + +.mx_InviteDialog_userSections { + margin-top: 10px; + overflow-y: auto; + padding-right: 45px; + height: 455px; // mx_InviteDialog's height minus some for the upper elements +} + +// Right margin for the design. We could apply this to the whole dialog, but then the scrollbar +// for the user section gets weird. +.mx_InviteDialog_helpText, +.mx_InviteDialog_addressBar { + margin-right: 45px; +} diff --git a/res/css/views/dialogs/_KeyboardShortcutsDialog.scss b/res/css/views/dialogs/_KeyboardShortcutsDialog.scss new file mode 100644 index 0000000000..638cacd41f --- /dev/null +++ b/res/css/views/dialogs/_KeyboardShortcutsDialog.scss @@ -0,0 +1,65 @@ +/* +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. +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_KeyboardShortcutsDialog { + display: flex; + flex-wrap: wrap; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + flex-direction: column; + margin-bottom: -50px; + max-height: 1100px; // XXX: this may need adjusting when adding new shortcuts + + .mx_KeyboardShortcutsDialog_category { + width: 33.3333%; // 3 columns + margin: 0 0 40px; + + & > div { + padding-left: 5px; + } + } + + h3 { + margin: 0 0 10px; + } + + h5 { + margin: 15px 0 5px; + font-weight: normal; + } + + kbd { + padding: 5px; + border-radius: 4px; + background-color: $reaction-row-button-bg-color; + margin-right: 5px; + min-width: 20px; + text-align: center; + display: inline-block; + border: 1px solid $kbd-border-color; + box-shadow: 0 2px $kbd-border-color; + margin-bottom: 4px; + text-transform: capitalize; + + & + kbd { + margin-left: 5px; + } + } + + .mx_KeyboardShortcutsDialog_inline div { + display: inline; + } +} diff --git a/res/css/views/dialogs/_NewSessionReviewDialog.scss b/res/css/views/dialogs/_NewSessionReviewDialog.scss new file mode 100644 index 0000000000..7e35fe941e --- /dev/null +++ b/res/css/views/dialogs/_NewSessionReviewDialog.scss @@ -0,0 +1,37 @@ +/* +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. +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_NewSessionReviewDialog_header { + display: flex; + align-items: center; + margin-top: 0; +} + +.mx_NewSessionReviewDialog_headerIcon { + width: 24px; + height: 24px; + margin-right: 4px; + position: relative; +} + +.mx_NewSessionReviewDialog_deviceName { + font-weight: 600; +} + +.mx_NewSessionReviewDialog_deviceID { + font-size: 12px; + color: $notice-secondary-color; +} diff --git a/res/css/views/dialogs/_RoomSettingsDialog.scss b/res/css/views/dialogs/_RoomSettingsDialog.scss index aa66e97f9e..2a4e62f9aa 100644 --- a/res/css/views/dialogs/_RoomSettingsDialog.scss +++ b/res/css/views/dialogs/_RoomSettingsDialog.scss @@ -56,16 +56,3 @@ limitations under the License. mask-position: center; } -.mx_RoomSettingsDialog_BridgeList { - padding: 0; -} - -.mx_RoomSettingsDialog_BridgeList li { - list-style-type: none; - padding: 5px; - margin-bottom: 5px; - border-width: 1px 0px; - border-color: #dee1f3; - border-style: solid; -} - diff --git a/res/css/views/dialogs/_RoomSettingsDialogBridges.scss b/res/css/views/dialogs/_RoomSettingsDialogBridges.scss new file mode 100644 index 0000000000..a1793cc75e --- /dev/null +++ b/res/css/views/dialogs/_RoomSettingsDialogBridges.scss @@ -0,0 +1,112 @@ +/* +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. +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_RoomSettingsDialog_BridgeList { + padding: 0; + + .mx_AccessibleButton { + display: inline; + margin: 0; + padding: 0; + } +} + +.mx_RoomSettingsDialog_BridgeList li { + list-style-type: none; + padding: 5px; + margin-bottom: 8px; + border-width: 1px 1px; + border-color: $primary-hairline-color; + border-style: solid; + border-radius: 5px; + + .column-icon { + float: left; + padding-right: 10px; + + * { + border-radius: 5px; + border: 1px solid $input-darker-bg-color; + } + + .noProtocolIcon { + width: 48px; + height: 48px; + background: $input-darker-bg-color; + border-radius: 5px; + } + + .protocol-icon { + float: left; + margin-right: 5px; + img { + border-radius: 5px; + border-width: 1px 1px; + border-color: $primary-hairline-color; + } + span { + /* Correct letter placement */ + left: auto; + } + } + } + + .column-data { + display: inline-block; + width: 85%; + + > h3 { + margin-top: 0px; + margin-bottom: 0px; + font-size: 16pt; + color: $primary-fg-color; + } + + > * { + margin-top: 4px; + margin-bottom: 0; + } + + .workspace-channel-details { + color: $primary-fg-color; + font-weight: 600; + + .channel { + margin-left: 5px; + } + } + + .mx_showMore { + display: block; + text-align: left; + margin-top: 10px; + } + + .metadata { + color: $muted-fg-color; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-bottom: 0; + } + + .metadata.visible { + overflow-y: visible; + text-overflow: ellipsis; + white-space: normal; + } + } +} diff --git a/res/css/views/dialogs/_UnknownDeviceDialog.scss b/res/css/views/dialogs/_UnknownDeviceDialog.scss index 02e0fb1fe5..2b0f8dceca 100644 --- a/res/css/views/dialogs/_UnknownDeviceDialog.scss +++ b/res/css/views/dialogs/_UnknownDeviceDialog.scss @@ -14,14 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -// CSS voodoo to support a gemini-scrollbar for the contents of the dialog -.mx_Dialog_unknownDevice .mx_Dialog { - // ideally we'd shrink the height to fit when needed, but in practice this - // is a pain in the ass. plus might as well make the dialog big given how - // important it is. - height: 100%; -} - .mx_UnknownDeviceDialog { height: 100%; display: flex; @@ -44,6 +36,7 @@ limitations under the License. .mx_UnknownDeviceDialog .mx_Dialog_content { margin-bottom: 24px; + overflow-y: scroll; } .mx_UnknownDeviceDialog_deviceList > li { diff --git a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss index 04ee575867..b9babd05f5 100644 --- a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss +++ b/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss @@ -85,3 +85,9 @@ limitations under the License. flex: 1; white-space: nowrap; } + +.mx_CreateKeyBackupDialog { + details .mx_AccessibleButton { + margin: 1em 0; // emulate paragraph spacing because we can't put this button in a paragraph due to HTML rules + } +} diff --git a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss index 5899abdf73..a9ebd54b31 100644 --- a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss +++ b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss @@ -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. @@ -15,6 +15,34 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_CreateSecretStorageDialog { + // Why you ask? Because CompleteSecurityBody is 600px so this is the width + // we end up when in there, but when in our own dialog we set our own width + // so need to fix it to something sensible as otherwise we'd end up either + // really wide or really narrow depending on the phase. I bet you wish you + // never asked. + width: 560px; + + .mx_SettingsFlag { + display: flex; + } + + .mx_SettingsFlag_label { + flex: 1 1 0; + min-width: 0; + font-weight: 600; + } + + .mx_ToggleSwitch { + flex: 0 0 auto; + margin-left: 30px; + } + + details .mx_AccessibleButton { + margin: 1em 0; // emulate paragraph spacing because we can't put this button in a paragraph due to HTML rules + } +} + .mx_CreateSecretStorageDialog .mx_Dialog_title { /* TODO: Consider setting this for all dialog titles. */ margin-bottom: 1em; @@ -22,7 +50,7 @@ limitations under the License. .mx_CreateSecretStorageDialog_primaryContainer { /* FIXME: plinth colour in new theme(s). background-color: $accent-color; */ - padding: 20px; + padding-top: 20px; } .mx_CreateSecretStorageDialog_primaryContainer::after { @@ -36,9 +64,13 @@ limitations under the License. align-items: flex-start; } +.mx_Field.mx_CreateSecretStorageDialog_passPhraseField { + margin-top: 0px; +} + .mx_CreateSecretStorageDialog_passPhraseHelp { flex: 1; - height: 85px; + height: 64px; margin-left: 20px; font-size: 80%; } @@ -47,16 +79,8 @@ limitations under the License. width: 100%; } -.mx_CreateSecretStorageDialog_passPhraseInput { - flex: none; - width: 250px; - border: 1px solid $accent-color; - border-radius: 5px; - padding: 10px; - margin-bottom: 1em; -} - .mx_CreateSecretStorageDialog_passPhraseMatch { + width: 200px; margin-left: 20px; } @@ -82,6 +106,10 @@ limitations under the License. align-items: center; } +.mx_CreateSecretStorageDialog_recoveryKeyButtons .mx_AccessibleButton { + margin-right: 10px; +} + .mx_CreateSecretStorageDialog_recoveryKeyButtons button { flex: 1; white-space: nowrap; diff --git a/res/css/views/directory/_NetworkDropdown.scss b/res/css/views/directory/_NetworkDropdown.scss index d402f6c48f..106392f880 100644 --- a/res/css/views/directory/_NetworkDropdown.scss +++ b/res/css/views/directory/_NetworkDropdown.scss @@ -1,5 +1,5 @@ /* -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. @@ -15,70 +15,149 @@ limitations under the License. */ .mx_NetworkDropdown { + height: 32px; position: relative; -} + width: max-content; + padding-right: 32px; + margin-left: auto; + margin-right: 9px; + margin-top: 12px; -.mx_NetworkDropdown_input { - position: relative; - border-radius: 3px; - border: 1px solid $strong-input-border-color; - font-weight: 300; - font-size: 13px; - user-select: none; -} - -.mx_NetworkDropdown_arrow { - border-color: $primary-fg-color transparent transparent; - border-style: solid; - border-width: 5px 5px 0; - display: block; - height: 0; - position: absolute; - right: 10px; - top: 16px; - width: 0; -} - -.mx_NetworkDropdown_networkoption { - height: 37px; - line-height: 37px; - padding-left: 8px; - padding-right: 8px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - -.mx_NetworkDropdown_networkoption img { - margin: 5px; - width: 25px; - vertical-align: middle; -} - -input.mx_NetworkDropdown_networkoption, input.mx_NetworkDropdown_networkoption:focus { - border: 0; - padding-top: 0; - padding-bottom: 0; + .mx_AccessibleButton { + width: max-content; + } } .mx_NetworkDropdown_menu { - position: absolute; - left: -1px; - right: -1px; - top: 100%; - z-index: 2; + min-width: 204px; margin: 0; - padding: 0px; - border-radius: 3px; - border: 1px solid $accent-color; + box-sizing: border-box; + border-radius: 4px; + border: 1px solid $dialog-close-fg-color; background-color: $primary-bg-color; } -.mx_NetworkDropdown_menu .mx_NetworkDropdown_networkoption:hover { - background-color: $focus-bg-color; -} - .mx_NetworkDropdown_menu_network { font-weight: bold; } +.mx_NetworkDropdown_server { + padding: 12px 0; + border-bottom: 1px solid $input-darker-fg-color; + + .mx_NetworkDropdown_server_title { + padding: 0 10px; + font-size: 15px; + font-weight: 600; + line-height: 20px; + margin-bottom: 4px; + + // remove server button + .mx_AccessibleButton { + position: absolute; + display: inline; + right: 12px; + height: 16px; + width: 16px; + margin-top: 4px; + + &::after { + content: ""; + position: absolute; + width: 16px; + height: 16px; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + mask-image: url('$(res)/img/feather-customised/x.svg'); + background-color: $notice-primary-color; + } + } + } + + .mx_NetworkDropdown_server_subtitle { + padding: 0 10px; + font-size: 10px; + line-height: 14px; + margin-top: -4px; + margin-bottom: 4px; + color: $muted-fg-color; + } + + .mx_NetworkDropdown_server_network { + font-size: 12px; + line-height: 16px; + padding: 4px 10px; + cursor: pointer; + position: relative; + + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + &[aria-checked=true]::after { + content: ""; + position: absolute; + width: 16px; + height: 16px; + right: 10px; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + mask-image: url('$(res)/img/feather-customised/check.svg'); + background-color: $input-valid-border-color; + } + } +} + +.mx_NetworkDropdown_server_add, +.mx_NetworkDropdown_server_network { + &:hover { + background-color: $header-panel-bg-color; + } +} + +.mx_NetworkDropdown_server_add { + padding: 16px 10px 16px 32px; + position: relative; + border-radius: 0 0 4px 4px; + + &::before { + content: ""; + position: absolute; + width: 16px; + height: 16px; + left: 7px; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + mask-image: url('$(res)/img/feather-customised/plus.svg'); + background-color: $muted-fg-color; + } +} + +.mx_NetworkDropdown_handle { + position: relative; + + &::after { + content: ""; + position: absolute; + width: 24px; + height: 24px; + right: -28px; // - (24 + 4) + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); + background-color: $primary-fg-color; + } + + .mx_NetworkDropdown_handle_server { + color: $muted-fg-color; + font-size: 12px; + } +} + +.mx_NetworkDropdown_dialog .mx_Dialog { + width: 45vw; +} diff --git a/res/css/views/elements/_DirectorySearchBox.scss b/res/css/views/elements/_DirectorySearchBox.scss index ef944f6fa0..75ef3fbabd 100644 --- a/res/css/views/elements/_DirectorySearchBox.scss +++ b/res/css/views/elements/_DirectorySearchBox.scss @@ -18,7 +18,6 @@ limitations under the License. display: flex; padding-left: 9px; padding-right: 9px; - margin: 0 5px 0 0 !important; } .mx_DirectorySearchBox_joinButton { diff --git a/res/css/views/elements/_EditableItemList.scss b/res/css/views/elements/_EditableItemList.scss index 51fa4c4423..ef60f006cc 100644 --- a/res/css/views/elements/_EditableItemList.scss +++ b/res/css/views/elements/_EditableItemList.scss @@ -20,14 +20,21 @@ limitations under the License. } .mx_EditableItem { + display: flex; margin-bottom: 5px; - margin-left: 15px; } .mx_EditableItem_delete { + order: 3; margin-right: 5px; cursor: pointer; vertical-align: middle; + width: 14px; + height: 14px; + mask-image: url('$(res)/img/feather-customised/cancel.svg'); + mask-repeat: no-repeat; + background-color: $warning-color; + mask-size: 100%; } .mx_EditableItem_email { @@ -36,12 +43,19 @@ limitations under the License. .mx_EditableItem_promptText { margin-right: 10px; + order: 2; } .mx_EditableItem_confirmBtn { margin-right: 5px; } +.mx_EditableItem_item { + flex: auto 1 0; + order: 1; +} + .mx_EditableItemList_label { margin-bottom: 5px; } + diff --git a/res/css/views/elements/_RichText.scss b/res/css/views/elements/_RichText.scss index 73f0be291f..5066ee10f3 100644 --- a/res/css/views/elements/_RichText.scss +++ b/res/css/views/elements/_RichText.scss @@ -13,6 +13,11 @@ padding-left: 5px; } +a.mx_Pill { + word-break: break-all; + display: inline; +} + /* More specific to override `.markdown-body a` text-decoration */ .mx_EventTile_content .markdown-body a.mx_Pill { text-decoration: none; diff --git a/res/css/views/messages/_MKeyVerificationRequest.scss b/res/css/views/messages/_common_CryptoEvent.scss similarity index 61% rename from res/css/views/messages/_MKeyVerificationRequest.scss rename to res/css/views/messages/_common_CryptoEvent.scss index ee20751083..98e1e97e39 100644 --- a/res/css/views/messages/_MKeyVerificationRequest.scss +++ b/res/css/views/messages/_common_CryptoEvent.scss @@ -1,5 +1,5 @@ /* -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,60 +14,62 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_KeyVerification { +.mx_cryptoEvent { display: grid; grid-template-columns: 24px minmax(0, 1fr) min-content; - &.mx_KeyVerification_icon::after { + &.mx_cryptoEvent_icon::after { grid-column: 1; grid-row: 1 / 3; - width: 12px; + width: 16px; height: 16px; content: ""; - mask-image: url("$(res)/img/e2e/normal.svg"); - mask-repeat: no-repeat; - mask-size: 100%; + background-image: url("$(res)/img/e2e/normal.svg"); + background-repeat: no-repeat; + background-size: 100%; margin-top: 4px; - background-color: $primary-fg-color; } - &.mx_KeyVerification_icon_verified::after { - mask-image: url("$(res)/img/e2e/verified.svg"); - background-color: $accent-color; + &.mx_cryptoEvent_icon_verified::after { + background-image: url("$(res)/img/e2e/verified.svg"); } - .mx_KeyVerification_title, .mx_KeyVerification_subtitle, .mx_KeyVerification_state { + &.mx_cryptoEvent_icon_warning::after { + background-image: url("$(res)/img/e2e/warning.svg"); + } + + .mx_cryptoEvent_title, .mx_cryptoEvent_subtitle, .mx_cryptoEvent_state { overflow-wrap: break-word; } - .mx_KeyVerification_title { + .mx_cryptoEvent_title { font-weight: 600; font-size: 15px; grid-column: 2; grid-row: 1; } - .mx_KeyVerification_subtitle { + .mx_cryptoEvent_subtitle { grid-column: 2; grid-row: 2; } - .mx_KeyVerification_state, .mx_KeyVerification_subtitle { + .mx_cryptoEvent_state, .mx_cryptoEvent_subtitle { font-size: 12px; } - .mx_KeyVerification_state, .mx_KeyVerification_buttons { + .mx_cryptoEvent_state, .mx_cryptoEvent_buttons { grid-column: 3; grid-row: 1 / 3; } - .mx_KeyVerification_buttons { + .mx_cryptoEvent_buttons { align-items: center; display: flex; } - .mx_KeyVerification_state { + .mx_cryptoEvent_state { width: 130px; padding: 10px 20px; margin: auto 0; diff --git a/res/css/views/right_panel/_EncryptionInfo.scss b/res/css/views/right_panel/_EncryptionInfo.scss new file mode 100644 index 0000000000..e13b1b6802 --- /dev/null +++ b/res/css/views/right_panel/_EncryptionInfo.scss @@ -0,0 +1,26 @@ +/* +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. +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_UserInfo { + .mx_EncryptionInfo_spinner { + .mx_Spinner { + margin-top: 25px; + margin-bottom: 15px; + } + + text-align: center; + } +} diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index e87fe06a94..0e4b1bda9e 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -23,15 +23,23 @@ limitations under the License. font-size: 12px; .mx_UserInfo_cancel { - height: 16px; - width: 16px; - padding: 10px 0 10px 10px; cursor: pointer; - mask-image: url('$(res)/img/minimise.svg'); - mask-repeat: no-repeat; - mask-position: 16px center; - background-color: $rightpanel-button-color; position: absolute; + top: 0; + border-radius: 4px; + background-color: $dark-panel-bg-color; + margin: 9px; + z-index: 1; // render on top of the right panel + + div { + height: 16px; + width: 16px; + padding: 4px; + mask-image: url('$(res)/img/minimise.svg'); + mask-repeat: no-repeat; + mask-position: 7px center; + background-color: $rightpanel-button-color; + } } h2 { @@ -41,12 +49,17 @@ limitations under the License. } .mx_UserInfo_container { - padding: 0 16px 16px 16px; + padding: 8px 16px; + } + + .mx_UserInfo_separator { border-bottom: 1px solid lightgray; } .mx_UserInfo_memberDetailsContainer { + padding-top: 0; padding-bottom: 0; + margin-bottom: 8px; } .mx_RoomTile_nameContainer { @@ -68,6 +81,7 @@ limitations under the License. .mx_UserInfo_avatar > div { max-width: 30vh; margin: 0 auto; + transition: 0.5s; } .mx_UserInfo_avatar > div > div { @@ -95,8 +109,9 @@ limitations under the License. justify-content: center; // override the calculated sizes so that the letter isn't HUGE - font-size: 26px !important; + font-size: 56px !important; width: 100% !important; + transition: font-size 0.5s; } .mx_UserInfo_avatar .mx_BaseAvatar.mx_BaseAvatar_image { @@ -122,12 +137,19 @@ limitations under the License. font-size: 18px; line-height: 25px; flex: 1; - overflow-x: auto; - max-height: 50px; - display: flex; justify-content: center; align-items: center; + // limit to 2 lines, show an ellipsis if it overflows + // this looks webkit specific but is supported by Firefox 68+ + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + + overflow: hidden; + word-break: break-all; + text-overflow: ellipsis; + .mx_E2EIcon { margin: 5px; } @@ -196,10 +218,9 @@ limitations under the License. padding-bottom: 16px; } - .mx_UserInfo_scrollContainer .mx_UserInfo_container { + .mx_UserInfo_container:not(.mx_UserInfo_separator) { padding-top: 16px; padding-bottom: 0; - border-bottom: none; > :not(h3) { margin-left: 8px; @@ -242,17 +263,25 @@ limitations under the License. .mx_UserInfo_expand { display: flex; margin-top: 11px; - color: $accent-color; } } - .mx_UserInfo_verify { + .mx_UserInfo_wideButton { display: block; - background-color: $accent-color; - color: $accent-fg-color; - border-radius: 4px; - padding: 7px 1.5em; - text-align: center; margin: 16px 0; } + button.mx_UserInfo_wideButton { + width: 100%; // FIXME get rid of this once we get rid of DialogButtons here + } +} + +.mx_UserInfo.mx_UserInfo_smallAvatar { + .mx_UserInfo_avatar > div { + max-width: 72px; + margin: 0 auto; + } + + .mx_UserInfo_avatar .mx_BaseAvatar_initial { + font-size: 40px !important; // override the other override because here the avatar is smaller + } } diff --git a/res/css/views/right_panel/_VerificationPanel.scss b/res/css/views/right_panel/_VerificationPanel.scss new file mode 100644 index 0000000000..459622b277 --- /dev/null +++ b/res/css/views/right_panel/_VerificationPanel.scss @@ -0,0 +1,121 @@ +/* +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. +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_UserInfo { + .mx_EncryptionPanel_cancel { + mask: url('$(res)/img/feather-customised/cancel.svg'); + mask-repeat: no-repeat; + mask-position: center; + mask-size: cover; + width: 14px; + height: 14px; + background-color: $settings-subsection-fg-color; + cursor: pointer; + position: absolute; + z-index: 100; + top: 14px; + right: 14px; + } + + .mx_VerificationPanel_verified_section .mx_E2EIcon { + // Override general user info margin + margin: 0 auto !important; + } + + .mx_VerificationPanel_qrCode { + padding: 4px 4px 0 4px; + background: white; + border-radius: 4px; + width: max-content; + max-width: 100%; + // Override general user info margin + margin: 0 auto !important; + + canvas { + // override height and width which are set on the element directly + height: auto !important; + width: 100% !important; + max-width: 240px; + } + } +} + +// Special case styling for EncryptionPanel in a Modal dialog +.mx_Dialog, .mx_CompleteSecurity_body { + .mx_VerificationPanel_QRPhase_startOptions { + display: flex; + margin-top: 10px; + margin-bottom: 10px; + align-items: stretch; + + > .mx_VerificationPanel_QRPhase_betweenText { + width: 50px; + vertical-align: middle; + text-align: center; + display: flex; + align-items: center; + justify-content: center; + } + + .mx_VerificationPanel_QRPhase_startOption { + background-color: $user-tile-hover-bg-color; + border-radius: 10px; + flex: 1; + display: flex; + padding: 10px; + align-items: center; + flex-direction: column; + position: relative; + + canvas, .mx_VerificationPanel_QRPhase_noQR { + width: 220px !important; + height: 220px !important; + background-color: #fff; + border-radius: 4px; + vertical-align: middle; + text-align: center; + padding: 10px; + } + + > p { + font-weight: 700; + } + + .mx_VerificationPanel_QRPhase_helpText { + font-size: 14px; + margin-top: 71px; + text-align: center; + } + + .mx_AccessibleButton { + position: absolute; + bottom: 30px; + } + } + } + + // EncryptionPanel when verification is done + .mx_VerificationPanel_verified_section { + // center the big shield icon + .mx_E2EIcon { + margin: 0 auto; + } + // right align the "Got it" button + .mx_AccessibleButton { + float: right; + } + } +} diff --git a/res/css/views/room_settings/_AliasSettings.scss b/res/css/views/room_settings/_AliasSettings.scss index d4ae58e5b0..f8d92e7828 100644 --- a/res/css/views/room_settings/_AliasSettings.scss +++ b/res/css/views/room_settings/_AliasSettings.scss @@ -26,3 +26,21 @@ limitations under the License. outline: none; box-shadow: none; } + +.mx_AliasSettings { + summary { + cursor: pointer; + color: $accent-color; + font-weight: 600; + list-style: none; + + // list-style doesn't do it for webkit + &::-webkit-details-marker { + display: none; + } + } + + .mx_AliasSettings_localAliasHeader { + margin-top: 35px; + } +} diff --git a/res/css/views/rooms/_EntityTile.scss b/res/css/views/rooms/_EntityTile.scss index 2b6b31acb4..a2867de3a7 100644 --- a/res/css/views/rooms/_EntityTile.scss +++ b/res/css/views/rooms/_EntityTile.scss @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,6 +20,15 @@ limitations under the License. align-items: center; color: $primary-fg-color; cursor: pointer; + + .mx_E2EIcon { + margin: 0; + position: absolute; + bottom: 2px; + right: 7px; + height: 15px; + width: 15px; + } } .mx_EntityTile:hover { @@ -30,7 +40,7 @@ limitations under the License. content: ""; position: absolute; top: calc(50% - 8px); // center - right: 10px; + right: -8px; mask: url('$(res)/img/member_chevron.png'); mask-repeat: no-repeat; width: 16px; @@ -64,14 +74,6 @@ limitations under the License. position: relative; } -.mx_EntityTile_power { - position: absolute; - width: 16px; - height: 17px; - top: 0px; - right: 6px; -} - .mx_EntityTile_name, .mx_GroupRoomTile_name { flex: 1 1 0; @@ -83,6 +85,7 @@ limitations under the License. .mx_EntityTile_details { overflow: hidden; + flex: 1; } .mx_EntityTile_ellipsis .mx_EntityTile_name { @@ -112,10 +115,6 @@ limitations under the License. opacity: 0.25; } -.mx_EntityTile:not(.mx_EntityTile_noHover):hover .mx_EntityTile_name { - font-size: 13px; -} - .mx_EntityTile_subtext { font-size: 11px; opacity: 0.5; @@ -123,3 +122,17 @@ limitations under the License. white-space: nowrap; text-overflow: clip; } + +.mx_EntityTile_power { + padding-inline-start: 6px; + font-size: 10px; + color: $notice-secondary-color; + max-width: 6em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.mx_EntityTile:hover .mx_EntityTile_power { + display: none; +} diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index fbac1e932a..2fa9994155 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -112,8 +112,6 @@ limitations under the License. .mx_EventTile_line, .mx_EventTile_reply { position: relative; - /* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */ - margin-right: 110px; padding-left: 65px; /* left gutter */ padding-top: 4px; padding-bottom: 2px; @@ -122,6 +120,13 @@ limitations under the License. line-height: 22px; } +.mx_RoomView_timeline_rr_enabled { + .mx_EventTile_line, .mx_EventTile_reply { + /* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */ + margin-right: 110px; + } +} + .mx_EventTile_bubbleContainer { display: grid; grid-template-columns: 1fr 100px; @@ -367,6 +372,11 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { opacity: 1; } +.mx_EventTile_e2eIcon_unknown { + background-image: url('$(res)/img/e2e/warning.svg'); + opacity: 1; +} + .mx_EventTile_e2eIcon_unencrypted { background-image: url('$(res)/img/e2e/warning.svg'); opacity: 1; @@ -415,7 +425,8 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { } .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line, -.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line { +.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line, +.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { padding-left: 60px; } @@ -427,8 +438,13 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { border-left: $e2e-unverified-color 5px solid; } +.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { + border-left: $e2e-unknown-color 5px solid; +} + .mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line, -.mx_EventTile:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line { +.mx_EventTile:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line, +.mx_EventTile:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line { padding-left: 78px; } @@ -439,14 +455,16 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp, -.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp { +.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp, +.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp { left: 3px; width: auto; } // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > .mx_EventTile_e2eIcon, -.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon { +.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon, +.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > .mx_EventTile_e2eIcon { display: block; left: 41px; } diff --git a/res/css/views/rooms/_InviteOnlyIcon.scss b/res/css/views/rooms/_InviteOnlyIcon.scss new file mode 100644 index 0000000000..b71fd6348d --- /dev/null +++ b/res/css/views/rooms/_InviteOnlyIcon.scss @@ -0,0 +1,58 @@ +/* +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. +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. +*/ + +@define-mixin mx_InviteOnlyIcon { + width: 12px; + height: 12px; + position: relative; + display: block !important; +} + +@define-mixin mx_InviteOnlyIcon_padlock { + 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; +} + +.mx_InviteOnlyIcon_large { + @mixin mx_InviteOnlyIcon; + margin: 0 4px; + + &::before { + @mixin mx_InviteOnlyIcon_padlock; + width: 12px; + height: 12px; + } +} + +.mx_InviteOnlyIcon_small { + @mixin mx_InviteOnlyIcon; + left: -2px; + + &::before { + @mixin mx_InviteOnlyIcon_padlock; + width: 10px; + height: 10px; + } +} diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index 5efca51844..a05b4c0c0e 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -76,6 +76,8 @@ limitations under the License. left: 60px; margin-right: 0; // Counteract the E2EIcon class margin-left: 3px; // Counteract the E2EIcon class + width: 15px; + height: 15px; } .mx_MessageComposer_noperm_error { diff --git a/res/css/views/rooms/_RoomBreadcrumbs.scss b/res/css/views/rooms/_RoomBreadcrumbs.scss index 67350aac34..3858d836e6 100644 --- a/res/css/views/rooms/_RoomBreadcrumbs.scss +++ b/res/css/views/rooms/_RoomBreadcrumbs.scss @@ -41,7 +41,7 @@ limitations under the License. overflow-x: visible; } - .mx_AutoHideScrollbar_offset { + .mx_AutoHideScrollbar { display: flex; flex-direction: row; height: 100%; diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index 45b9733faa..47b8131ef0 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -19,7 +19,12 @@ limitations under the License. border-bottom: 1px solid $primary-hairline-color; .mx_E2EIcon { - margin: 0 5px; + margin: 0; + position: absolute; + bottom: -2px; + right: -6px; + height: 15px; + width: 15px; } } @@ -171,6 +176,7 @@ limitations under the License. width: 28px; height: 28px; margin: 0 7px; + position: relative; } .mx_RoomHeader_avatar .mx_BaseAvatar_image { @@ -263,24 +269,3 @@ 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/_RoomPreviewBar.scss b/res/css/views/rooms/_RoomPreviewBar.scss index c7d03e3523..981cf06c69 100644 --- a/res/css/views/rooms/_RoomPreviewBar.scss +++ b/res/css/views/rooms/_RoomPreviewBar.scss @@ -33,6 +33,13 @@ limitations under the License. } } + h3, + .mx_RoomPreviewBar_message p { + // break-word, with fallback to break-all, which is wider supported + word-break: break-all; + word-break: break-word; + } + .mx_Spinner { width: auto; height: auto; @@ -117,12 +124,17 @@ limitations under the License. .mx_RoomPreviewBar_actions { flex-direction: column-reverse; .mx_AccessibleButton { - padding: 7px 50px;//extra wide + padding: 7px 50px; //extra wide } & > * { margin-top: 12px; } + .mx_AccessibleButton.mx_AccessibleButton_kind_primary { + // to account for the padding of the primary button which causes inconsistent look between + // subsequent secondary (text) buttons + margin-bottom: 7px; + } } } diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index cb1137bb2f..31d887cbbb 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -98,9 +98,22 @@ limitations under the License. z-index: 2; } +// Note we match .mx_E2EIcon to make sure this matches more tightly than just +// .mx_E2EIcon on its own +.mx_RoomTile_e2eIcon.mx_E2EIcon { + height: 14px; + width: 14px; + display: block; + position: absolute; + bottom: -2px; + right: -5px; + z-index: 1; + margin: 0; +} + .mx_RoomTile_name { font-size: 14px; - padding: 0 6px; + padding: 0 4px; color: $roomtile-name-color; white-space: nowrap; overflow-x: hidden; @@ -142,10 +155,11 @@ limitations under the License. } } -// toggle menuButton and badge on hover/menu displayed +// toggle menuButton and badge on menu displayed .mx_RoomTile_menuDisplayed, // or on keyboard focus of room tile -.mx_RoomTile.focus-visible:focus-within, +.mx_LeftPanel_container:not(.collapsed) .mx_RoomTile:focus-within, +// or on pointer hover .mx_LeftPanel_container:not(.collapsed) .mx_RoomTile:hover { .mx_RoomTile_menuButton { display: block; @@ -200,31 +214,3 @@ 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/css/views/rooms/_SearchableEntityList.scss b/res/css/views/rooms/_SearchableEntityList.scss deleted file mode 100644 index 37a663123d..0000000000 --- a/res/css/views/rooms/_SearchableEntityList.scss +++ /dev/null @@ -1,77 +0,0 @@ -/* -Copyright 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_SearchableEntityList { - display: flex; - - flex-direction: column; -} - -.mx_SearchableEntityList_query { - font-family: $font-family; - border-radius: 3px; - border: 1px solid $input-border-color; - padding: 9px; - color: $primary-fg-color; - background-color: $primary-bg-color; - margin-left: 3px; - font-size: 15px; - margin-bottom: 8px; - width: 189px; -} - -.mx_SearchableEntityList_query::-moz-placeholder { - color: $primary-fg-color; - opacity: 0.5; - font-size: 12px; -} - -.mx_SearchableEntityList_query::-webkit-input-placeholder { - color: $primary-fg-color; - opacity: 0.5; - font-size: 12px; -} - -.mx_SearchableEntityList_listWrapper { - flex: 1; - - overflow-y: auto; -} - -.mx_SearchableEntityList_list { - display: table; - table-layout: fixed; - width: 100%; -} - -.mx_SearchableEntityList_list .mx_EntityTile_chevron { - display: none; -} - -.mx_SearchableEntityList_hrWrapper { - width: 100%; - flex: 0 0 auto; -} - -.mx_SearchableEntityList hr { - height: 1px; - border: 0px; - color: $primary-fg-color; - background-color: $primary-fg-color; - margin-right: 15px; - margin-top: 11px; - margin-bottom: 11px; -} diff --git a/res/css/views/rooms/_TopUnreadMessagesBar.scss b/res/css/views/rooms/_TopUnreadMessagesBar.scss index 77f19dac1c..28eddf1fa2 100644 --- a/res/css/views/rooms/_TopUnreadMessagesBar.scss +++ b/res/css/views/rooms/_TopUnreadMessagesBar.scss @@ -25,19 +25,16 @@ limitations under the License. } .mx_TopUnreadMessagesBar::after { - content: "·"; + content: ""; position: absolute; top: -8px; left: 11px; - width: 16px; - height: 16px; + width: 4px; + height: 4px; border-radius: 16px; - font-weight: 600; - font-size: 30px; - line-height: 14px; - text-align: center; - color: $secondary-accent-color; - background-color: $accent-color; + background-color: $secondary-accent-color; + border: 6px solid $accent-color; + pointer-events: none; } .mx_TopUnreadMessagesBar_scrollUp { @@ -54,8 +51,30 @@ limitations under the License. position: absolute; width: 38px; height: 38px; - mask: url('$(res)/img/icon-jump-to-first-unread.svg'); + mask-image: url('$(res)/img/icon-jump-to-first-unread.svg'); mask-repeat: no-repeat; mask-position: 9px 13px; background: $roomtile-name-color; } + +.mx_TopUnreadMessagesBar_markAsRead { + display: block; + width: 18px; + height: 18px; + background: $primary-bg-color; + border: 1.3px solid $roomtile-name-color; + border-radius: 10px; + margin: 5px auto; +} + +.mx_TopUnreadMessagesBar_markAsRead::before { + content: ""; + position: absolute; + width: 18px; + height: 18px; + mask-image: url('$(res)/img/cancel.svg'); + mask-repeat: no-repeat; + mask-size: 10px; + mask-position: 4px 4px; + background: $roomtile-name-color; +} diff --git a/res/css/views/rooms/_WhoIsTypingTile.scss b/res/css/views/rooms/_WhoIsTypingTile.scss index ef20c24c84..579ea7e73e 100644 --- a/res/css/views/rooms/_WhoIsTypingTile.scss +++ b/res/css/views/rooms/_WhoIsTypingTile.scss @@ -31,14 +31,15 @@ limitations under the License. margin-left: -12px; } -.mx_WhoIsTypingTile_avatars .mx_BaseAvatar_image { - border: 1px solid $primary-bg-color; -} - .mx_WhoIsTypingTile_avatars .mx_BaseAvatar_initial { padding-top: 1px; } +.mx_WhoIsTypingTile_avatars .mx_BaseAvatar { + border: 1px solid $primary-bg-color; + border-radius: 40px; +} + .mx_WhoIsTypingTile_remainingAvatarPlaceholder { position: relative; display: inline-block; diff --git a/res/css/views/settings/_DevicesPanel.scss b/res/css/views/settings/_DevicesPanel.scss index 581ff47fc1..49debe842f 100644 --- a/res/css/views/settings/_DevicesPanel.scss +++ b/res/css/views/settings/_DevicesPanel.scss @@ -18,7 +18,7 @@ limitations under the License. display: table; table-layout: fixed; width: 880px; - border-spacing: 2px; + border-spacing: 10px; } .mx_DevicesPanel_header { @@ -32,7 +32,11 @@ limitations under the License. .mx_DevicesPanel_header > div { display: table-cell; - vertical-align: bottom; + vertical-align: middle; +} + +.mx_DevicesPanel_header .mx_DevicesPanel_deviceName { + width: 50%; } .mx_DevicesPanel_header .mx_DevicesPanel_deviceLastSeen { diff --git a/res/css/views/settings/_E2eAdvancedPanel.scss b/res/css/views/settings/_E2eAdvancedPanel.scss new file mode 100644 index 0000000000..9e32685d12 --- /dev/null +++ b/res/css/views/settings/_E2eAdvancedPanel.scss @@ -0,0 +1,20 @@ +/* +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. +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_E2eAdvancedPanel_settingLongDescription { + margin-right: 150px; +} + diff --git a/res/css/views/settings/tabs/_SettingsTab.scss b/res/css/views/settings/tabs/_SettingsTab.scss index 794c8106be..01a1d94956 100644 --- a/res/css/views/settings/tabs/_SettingsTab.scss +++ b/res/css/views/settings/tabs/_SettingsTab.scss @@ -45,9 +45,17 @@ limitations under the License. margin: 10px 100px 10px 0; // Align with the rest of the view } -.mx_SettingsTab_section .mx_SettingsFlag { - margin-right: 100px; - margin-bottom: 10px; +.mx_SettingsTab_section { + margin-bottom: 24px; + + .mx_SettingsFlag { + margin-right: 100px; + margin-bottom: 10px; + } + + &.mx_SettingsTab_subsectionText .mx_SettingsFlag { + margin-right: 0px !important; + } } .mx_SettingsTab_section .mx_SettingsFlag .mx_SettingsFlag_label { diff --git a/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss index d003e175d9..be0af9123b 100644 --- a/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss @@ -14,6 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_PreferencesUserSettingsTab .mx_Field { - @mixin mx_Settings_fullWidthField; +.mx_PreferencesUserSettingsTab { + .mx_Field { + @mixin mx_Settings_fullWidthField; + } + + .mx_SettingsTab_section { + margin-bottom: 30px; + } } diff --git a/res/css/views/verification/_VerificationShowSas.scss b/res/css/views/verification/_VerificationShowSas.scss index a0da7e2539..5038d40b73 100644 --- a/res/css/views/verification/_VerificationShowSas.scss +++ b/res/css/views/verification/_VerificationShowSas.scss @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd. +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -28,21 +29,35 @@ limitations under the License. .mx_VerificationShowSas_emojiSas { text-align: center; + display: flex; + flex-wrap: wrap; + justify-content: center; + margin: 25px 0; } .mx_VerificationShowSas_emojiSas_block { display: inline-block; - margin-left: 15px; - margin-right: 15px; - text-align: center; - margin-bottom: 20px; + margin-bottom: 16px; + position: relative; + width: 52px; +} + +.mx_Dialog .mx_VerificationShowSas_emojiSas_block, +.mx_AuthPage_modal .mx_VerificationShowSas_emojiSas_block { + width: 60px; } .mx_VerificationShowSas_emojiSas_emoji { - font-size: 48px; + font-size: 32px; } .mx_VerificationShowSas_emojiSas_label { - text-align: center; - font-weight: bold; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + font-size: 12px; +} + +.mx_VerificationShowSas_emojiSas_break { + flex-basis: 100%; } diff --git a/res/img/admin.svg b/res/img/admin.svg deleted file mode 100644 index 7ea7459304..0000000000 --- a/res/img/admin.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - icons_owner - Created with sketchtool. - - - - - - - - - - - - diff --git a/res/img/feather-customised/chevron-down.svg b/res/img/feather-customised/chevron-down.svg new file mode 100644 index 0000000000..bcb185ede7 --- /dev/null +++ b/res/img/feather-customised/chevron-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/icons-settings-room.svg b/res/img/icons-settings-room.svg deleted file mode 100644 index 421eefdefa..0000000000 --- a/res/img/icons-settings-room.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/res/img/mod.svg b/res/img/mod.svg deleted file mode 100644 index 847baf98f9..0000000000 --- a/res/img/mod.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - icons_admin - Created with sketchtool. - - - - - - - - - - - diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index a3515a9d99..bfa2272283 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -165,6 +165,8 @@ $reaction-row-button-hover-border-color: $header-panel-text-primary-color; $reaction-row-button-selected-bg-color: #1f6954; $reaction-row-button-selected-border-color: $accent-color; +$kbd-border-color: #000000; + $tooltip-timeline-bg-color: $tagpanel-bg-color; $tooltip-timeline-fg-color: #ffffff; @@ -219,10 +221,6 @@ $user-tile-hover-bg-color: $header-panel-bg-color; filter: invert(1); } -.gm-scrollbar .thumb { - filter: invert(1); -} - // markdown overrides: .mx_EventTile_content .markdown-body pre:hover { border-color: #808080 !important; // inverted due to rules below diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 288fb3cadc..9bdd712e07 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -5,9 +5,12 @@ Arial empirically gets it right, hence prioritising Arial here. */ /* We fall through to Twemoji for emoji rather than falling through to native Emoji fonts (if any) to ensure cross-browser consistency */ -$font-family: Nunito, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', Arial, Helvetica, Sans-Serif; +/* Noto Color Emoji contains digits, in fixed-width, therefore causing + digits in flowed text to stand out. + TODO: Consider putting all emoji fonts to the end rather than the front. */ +$font-family: Nunito, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Arial, Helvetica, Sans-Serif, 'Noto Color Emoji'; -$monospace-font-family: Inconsolata, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', Courier, monospace; +$monospace-font-family: Inconsolata, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Courier, monospace, 'Noto Color Emoji'; // unified palette // try to use these colors when possible @@ -224,6 +227,7 @@ $copy-button-url: "$(res)/img/icon_copy_message.svg"; // e2e $e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color +$e2e-unknown-color: #e8bf37; $e2e-unverified-color: #e8bf37; $e2e-warning-color: #ba6363; @@ -286,6 +290,8 @@ $reaction-row-button-hover-border-color: $focus-bg-color; $reaction-row-button-selected-bg-color: #e9fff9; $reaction-row-button-selected-border-color: $accent-color; +$kbd-border-color: $reaction-row-button-border-color; + $tooltip-timeline-bg-color: $tagpanel-bg-color; $tooltip-timeline-fg-color: #ffffff; diff --git a/scripts/ci/build.sh b/scripts/ci/build.sh deleted file mode 100755 index 0b1fa23093..0000000000 --- a/scripts/ci/build.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -# -# script which is run by the CI build (after `yarn test`). -# -# clones riot-web develop and runs the tests against our version of react-sdk. - -set -ev - -RIOT_WEB_DIR=riot-web -REACT_SDK_DIR=`pwd` - -yarn link - -scripts/fetchdep.sh vector-im riot-web - -pushd "$RIOT_WEB_DIR" - -yarn link matrix-js-sdk -yarn link matrix-react-sdk - -yarn install - -yarn build - -popd diff --git a/scripts/ci/end-to-end-tests.sh b/scripts/ci/end-to-end-tests.sh index a592888292..2f907dffa2 100755 --- a/scripts/ci/end-to-end-tests.sh +++ b/scripts/ci/end-to-end-tests.sh @@ -6,30 +6,23 @@ set -ev -upload_logs() { - echo "--- Uploading logs" - buildkite-agent artifact upload "logs/**/*;synapse/installations/consent/homeserver.log" -} - handle_error() { EXIT_CODE=$? - if [ $TESTS_STARTED -eq 1 ]; then - upload_logs - fi exit $EXIT_CODE } trap 'handle_error' ERR -RIOT_WEB_DIR=riot-web -REACT_SDK_DIR=`pwd` - echo "--- Building Riot" -scripts/ci/build.sh +scripts/ci/layered-riot-web.sh +cd ../riot-web +riot_web_dir=`pwd` +CI_PACKAGE=true yarn build +cd ../matrix-react-sdk # run end to end tests pushd test/end-to-end-tests -ln -s $REACT_SDK_DIR/$RIOT_WEB_DIR riot/riot-web +ln -s $riot_web_dir riot/riot-web # PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh # CHROME_PATH=$(which google-chrome-stable) ./run.sh echo "--- Install synapse & other dependencies" diff --git a/scripts/ci/install-deps.sh b/scripts/ci/install-deps.sh index a2e2e59a45..14b5fc5393 100755 --- a/scripts/ci/install-deps.sh +++ b/scripts/ci/install-deps.sh @@ -6,9 +6,9 @@ scripts/fetchdep.sh matrix-org matrix-js-sdk pushd matrix-js-sdk yarn link -yarn install +yarn install $@ yarn build popd yarn link matrix-js-sdk -yarn install +yarn install $@ diff --git a/scripts/ci/layered-riot-web.sh b/scripts/ci/layered-riot-web.sh new file mode 100755 index 0000000000..f58794b451 --- /dev/null +++ b/scripts/ci/layered-riot-web.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# Creates an environment similar to one that riot-web would expect for +# development. This means going one directory up (and assuming we're in +# a directory like /workdir/matrix-react-sdk) and putting riot-web and +# the js-sdk there. + +cd ../ # Assume we're at something like /workdir/matrix-react-sdk + +# Set up the js-sdk first +matrix-react-sdk/scripts/fetchdep.sh matrix-org matrix-js-sdk +pushd matrix-js-sdk +yarn link +yarn install +popd + +# Now set up the react-sdk +pushd matrix-react-sdk +yarn link matrix-js-sdk +yarn link +yarn install +popd + +# Finally, set up riot-web +matrix-react-sdk/scripts/fetchdep.sh vector-im riot-web +pushd riot-web +yarn link matrix-js-sdk +yarn link matrix-react-sdk +yarn install +yarn build:res +popd diff --git a/scripts/ci/riot-unit-tests.sh b/scripts/ci/riot-unit-tests.sh index 215af13030..337c0fe6c3 100755 --- a/scripts/ci/riot-unit-tests.sh +++ b/scripts/ci/riot-unit-tests.sh @@ -6,9 +6,7 @@ set -ev -RIOT_WEB_DIR=riot-web - -scripts/ci/build.sh -pushd "$RIOT_WEB_DIR" +scripts/ci/layered-riot-web.sh +cd ../riot-web +yarn build:genfiles # so the tests can run. Faster version of `build` yarn test -popd diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh index f82752bfc5..0142305797 100755 --- a/scripts/fetchdep.sh +++ b/scripts/fetchdep.sh @@ -17,7 +17,7 @@ clone() { if [ -n "$branch" ] then echo "Trying to use $org/$repo#$branch" - git clone git://github.com/$org/$repo.git $repo --branch "$branch" && exit 0 + git clone git://github.com/$org/$repo.git $repo --branch "$branch" --depth 1 && exit 0 fi } diff --git a/scripts/gen-i18n.js b/scripts/gen-i18n.js index 3d3d5af116..a4d53aea2f 100755 --- a/scripts/gen-i18n.js +++ b/scripts/gen-i18n.js @@ -237,7 +237,7 @@ const walkOpts = { const fullPath = path.join(root, fileStats.name); let trs; - if (fileStats.name.endsWith('.js')) { + if (fileStats.name.endsWith('.js') || fileStats.name.endsWith('.tsx')) { trs = getTranslationsJs(fullPath); } else if (fileStats.name.endsWith('.html')) { trs = getTranslationsOther(fullPath); diff --git a/scripts/generate-eslint-error-ignore-file b/scripts/generate-eslint-error-ignore-file index 3a635f5a7d..54aacfc9fa 100755 --- a/scripts/generate-eslint-error-ignore-file +++ b/scripts/generate-eslint-error-ignore-file @@ -14,8 +14,10 @@ echo "generating $out" # autogenerated file: run scripts/generate-eslint-error-ignore-file to update. EOF - - ./node_modules/.bin/eslint --no-ignore -f json src test | + + ./node_modules/.bin/eslint -f json src test | jq -r '.[] | select((.errorCount + .warningCount) > 0) | .filePath' | sed -e 's/.*matrix-react-sdk\///'; } > "$out" +# also append rules from eslintignore file +cat .eslintignore >> $out diff --git a/scripts/reskindex.js b/scripts/reskindex.js index 3919295078..9fb0e1a7c0 100755 --- a/scripts/reskindex.js +++ b/scripts/reskindex.js @@ -2,17 +2,20 @@ var fs = require('fs'); var path = require('path'); var glob = require('glob'); -var args = require('optimist').argv; +var args = require('minimist')(process.argv); var chokidar = require('chokidar'); var componentIndex = path.join('src', 'component-index.js'); var componentIndexTmp = componentIndex+".tmp"; var componentsDir = path.join('src', 'components'); -var componentGlob = '**/*.js'; +var componentJsGlob = '**/*.js'; +var componentTsGlob = '**/*.tsx'; var prevFiles = []; function reskindex() { - var files = glob.sync(componentGlob, {cwd: componentsDir}).sort(); + var jsFiles = glob.sync(componentJsGlob, {cwd: componentsDir}).sort(); + var tsFiles = glob.sync(componentTsGlob, {cwd: componentsDir}).sort(); + var files = [...tsFiles, ...jsFiles]; if (!filesHaveChanged(files, prevFiles)) { return; } @@ -36,7 +39,7 @@ function reskindex() { strm.write("let components = {};\n"); for (var i = 0; i < files.length; ++i) { - var file = files[i].replace('.js', ''); + var file = files[i].replace('.js', '').replace('.tsx', ''); var moduleName = (file.replace(/\//g, '.')); var importName = moduleName.replace(/\./g, "$"); @@ -79,7 +82,7 @@ if (!args.w) { } var watchDebouncer = null; -chokidar.watch(path.join(componentsDir, componentGlob)).on('all', (event, path) => { +chokidar.watch(path.join(componentsDir, componentJsGlob)).on('all', (event, path) => { if (path === componentIndex) return; if (watchDebouncer) clearTimeout(watchDebouncer); watchDebouncer = setTimeout(reskindex, 1000); diff --git a/src/Analytics.js b/src/Analytics.js index d0c7a52814..c96cfdefee 100644 --- a/src/Analytics.js +++ b/src/Analytics.js @@ -1,18 +1,21 @@ /* - Copyright 2017 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2017 Michael Telatynski <7t3chguy@gmail.com> +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. - You may obtain a copy of the License at +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 + 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. - */ +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 { getCurrentLanguage, _t, _td } from './languageHandler'; import PlatformPeg from './PlatformPeg'; @@ -54,6 +57,8 @@ function getRedactedUrl() { } const customVariables = { + // The Matomo installation at https://matomo.riot.im is currently configured + // with a limit of 10 custom variables. 'App Platform': { id: 1, expl: _td('The platform you\'re on'), @@ -61,7 +66,7 @@ const customVariables = { }, 'App Version': { id: 2, - expl: _td('The version of Riot.im'), + expl: _td('The version of Riot'), example: '15.0.0', }, 'User Type': { @@ -84,20 +89,25 @@ const customVariables = { expl: _td('Whether or not you\'re using the Richtext mode of the Rich Text Editor'), example: 'off', }, - 'Breadcrumbs': { - id: 9, - expl: _td("Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)"), - example: 'disabled', - }, 'Homeserver URL': { id: 7, expl: _td('Your homeserver\'s URL'), example: 'https://matrix.org', }, - 'Identity Server URL': { + 'Touch Input': { id: 8, - expl: _td('Your identity server\'s URL'), - example: 'https://vector.im', + expl: _td("Whether you're using Riot on a device where touch is the primary input mechanism"), + example: 'false', + }, + 'Breadcrumbs': { + id: 9, + expl: _td("Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)"), + example: 'disabled', + }, + 'Installed PWA': { + id: 10, + expl: _td("Whether you're using Riot as an installed Progressive Web App"), + example: 'false', }, }; @@ -106,61 +116,80 @@ function whitelistRedact(whitelist, str) { return ''; } +const UID_KEY = "mx_Riot_Analytics_uid"; +const CREATION_TS_KEY = "mx_Riot_Analytics_cts"; +const VISIT_COUNT_KEY = "mx_Riot_Analytics_vc"; +const LAST_VISIT_TS_KEY = "mx_Riot_Analytics_lvts"; + +function getUid() { + try { + let data = localStorage.getItem(UID_KEY); + if (!data) { + localStorage.setItem(UID_KEY, data = [...Array(16)].map(() => Math.random().toString(16)[2]).join('')); + } + return data; + } catch (e) { + console.error("Analytics error: ", e); + return ""; + } +} + +const HEARTBEAT_INTERVAL = 30 * 1000; // seconds + class Analytics { constructor() { - this._paq = null; - this.disabled = true; + this.baseUrl = null; + this.siteId = null; + this.visitVariables = {}; + this.firstPage = true; + this._heartbeatIntervalID = null; + + this.creationTs = localStorage.getItem(CREATION_TS_KEY); + if (!this.creationTs) { + localStorage.setItem(CREATION_TS_KEY, this.creationTs = new Date().getTime()); + } + + this.lastVisitTs = localStorage.getItem(LAST_VISIT_TS_KEY); + this.visitCount = localStorage.getItem(VISIT_COUNT_KEY) || 0; + localStorage.setItem(VISIT_COUNT_KEY, parseInt(this.visitCount, 10) + 1); + } + + get disabled() { + return !this.baseUrl; } /** * Enable Analytics if initialized but disabled * otherwise try and initalize, no-op if piwik config missing */ - enable() { - if (this._paq || this._init()) { - this.disabled = false; - } - } + async enable() { + if (!this.disabled) return; - /** - * Disable Analytics calls, will not fully unload Piwik until a refresh, - * but this is second best, Piwik should not pull anything implicitly. - */ - disable() { - this.trackEvent('Analytics', 'opt-out'); - // disableHeartBeatTimer is undocumented but exists in the piwik code - // the _paq.push method will result in an error being printed in the console - // if an unknown method signature is passed - this._paq.push(['disableHeartBeatTimer']); - this.disabled = true; - } - - _init() { const config = SdkConfig.get(); if (!config || !config.piwik || !config.piwik.url || !config.piwik.siteId) return; - const url = config.piwik.url; - const siteId = config.piwik.siteId; - const self = this; - - window._paq = this._paq = window._paq || []; - - this._paq.push(['setTrackerUrl', url+'piwik.php']); - this._paq.push(['setSiteId', siteId]); - - this._paq.push(['trackAllContentImpressions']); - this._paq.push(['discardHashTag', false]); - this._paq.push(['enableHeartBeatTimer']); - // this._paq.push(['enableLinkTracking', true]); + this.baseUrl = new URL("piwik.php", config.piwik.url); + // set constants + this.baseUrl.searchParams.set("rec", 1); // rec is required for tracking + this.baseUrl.searchParams.set("idsite", config.piwik.siteId); // rec is required for tracking + this.baseUrl.searchParams.set("apiv", 1); // API version to use + this.baseUrl.searchParams.set("send_image", 0); // we want a 204, not a tiny GIF + // set user parameters + this.baseUrl.searchParams.set("_id", getUid()); // uuid + this.baseUrl.searchParams.set("_idts", this.creationTs); // first ts + this.baseUrl.searchParams.set("_idvc", parseInt(this.visitCount, 10)+ 1); // visit count + if (this.lastVisitTs) { + this.baseUrl.searchParams.set("_viewts", this.lastVisitTs); // last visit ts + } const platform = PlatformPeg.get(); this._setVisitVariable('App Platform', platform.getHumanReadableName()); - platform.getAppVersion().then((version) => { - this._setVisitVariable('App Version', version); - }).catch(() => { + try { + this._setVisitVariable('App Version', await platform.getAppVersion()); + } catch (e) { this._setVisitVariable('App Version', 'unknown'); - }); + } this._setVisitVariable('Chosen Language', getCurrentLanguage()); @@ -168,20 +197,77 @@ class Analytics { this._setVisitVariable('Instance', window.location.pathname); } - (function() { - const g = document.createElement('script'); - const s = document.getElementsByTagName('script')[0]; - g.type='text/javascript'; g.async=true; g.defer=true; g.src=url+'piwik.js'; + let installedPWA = "unknown"; + try { + // Known to work at least for desktop Chrome + installedPWA = window.matchMedia('(display-mode: standalone)').matches; + } catch (e) { } + this._setVisitVariable('Installed PWA', installedPWA); - g.onload = function() { - console.log('Initialised anonymous analytics'); - self._paq = window._paq; - }; + let touchInput = "unknown"; + try { + // MDN claims broad support across browsers + touchInput = window.matchMedia('(pointer: coarse)').matches; + } catch (e) { } + this._setVisitVariable('Touch Input', touchInput); - s.parentNode.insertBefore(g, s); - })(); + // start heartbeat + this._heartbeatIntervalID = window.setInterval(this.ping.bind(this), HEARTBEAT_INTERVAL); + } - return true; + /** + * Disable Analytics, stop the heartbeat and clear identifiers from localStorage + */ + disable() { + if (this.disabled) return; + this.trackEvent('Analytics', 'opt-out'); + window.clearInterval(this._heartbeatIntervalID); + this.baseUrl = null; + this.visitVariables = {}; + localStorage.removeItem(UID_KEY); + localStorage.removeItem(CREATION_TS_KEY); + localStorage.removeItem(VISIT_COUNT_KEY); + localStorage.removeItem(LAST_VISIT_TS_KEY); + } + + async _track(data) { + if (this.disabled) return; + + const now = new Date(); + const params = { + ...data, + url: getRedactedUrl(), + + _cvar: JSON.stringify(this.visitVariables), // user custom vars + res: `${window.screen.width}x${window.screen.height}`, // resolution as WWWWxHHHH + rand: String(Math.random()).slice(2, 8), // random nonce to cache-bust + h: now.getHours(), + m: now.getMinutes(), + s: now.getSeconds(), + }; + + const url = new URL(this.baseUrl); + for (const key in params) { + url.searchParams.set(key, params[key]); + } + + try { + await window.fetch(url, { + method: "GET", + mode: "no-cors", + cache: "no-cache", + redirect: "follow", + }); + } catch (e) { + console.error("Analytics error: ", e); + } + } + + ping() { + this._track({ + ping: 1, + }); + localStorage.setItem(LAST_VISIT_TS_KEY, new Date().getTime()); // update last visit ts } trackPageChange(generationTimeMs) { @@ -193,31 +279,29 @@ class Analytics { return; } - if (typeof generationTimeMs === 'number') { - this._paq.push(['setGenerationTimeMs', generationTimeMs]); - } else { + if (typeof generationTimeMs !== 'number') { console.warn('Analytics.trackPageChange: expected generationTimeMs to be a number'); // But continue anyway because we still want to track the change } - this._paq.push(['setCustomUrl', getRedactedUrl()]); - this._paq.push(['trackPageView']); + this._track({ + gt_ms: generationTimeMs, + }); } trackEvent(category, action, name, value) { if (this.disabled) return; - this._paq.push(['setCustomUrl', getRedactedUrl()]); - this._paq.push(['trackEvent', category, action, name, value]); - } - - logout() { - if (this.disabled) return; - this._paq.push(['deleteCookies']); + this._track({ + e_c: category, + e_a: action, + e_n: name, + e_v: value, + }); } _setVisitVariable(key, value) { if (this.disabled) return; - this._paq.push(['setCustomVariable', customVariables[key].id, key, value, 'visit']); + this.visitVariables[customVariables[key].id] = [key, value]; } setLoggedIn(isGuest, homeserverUrl, identityServerUrl) { @@ -227,16 +311,9 @@ class Analytics { if (!config.piwik) return; const whitelistedHSUrls = config.piwik.whitelistedHSUrls || []; - const whitelistedISUrls = config.piwik.whitelistedISUrls || []; this._setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In'); this._setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl)); - this._setVisitVariable('Identity Server URL', whitelistRedact(whitelistedISUrls, identityServerUrl)); - } - - setRichtextMode(state) { - if (this.disabled) return; - this._setVisitVariable('RTE: Uses Richtext Mode', state ? 'on' : 'off'); } setBreadcrumbs(state) { @@ -244,13 +321,11 @@ class Analytics { this._setVisitVariable('Breadcrumbs', state ? 'enabled' : 'disabled'); } - showDetailsModal() { + showDetailsModal = () => { let rows = []; - if (window.Piwik) { - const Tracker = window.Piwik.getAsyncTracker(); - rows = Object.values(customVariables).map((v) => Tracker.getCustomVariable(v.id)).filter(Boolean); + if (!this.disabled) { + rows = Object.values(this.visitVariables); } else { - // Piwik may not have been enabled, so show example values rows = Object.keys(customVariables).map( (k) => [ k, @@ -271,7 +346,7 @@ class Analytics { }, ), }, - { expl: _td('Your User Agent'), value: navigator.userAgent }, + { expl: _td('Your user agent'), value: navigator.userAgent }, { expl: _td('Your device resolution'), value: resolution }, ]; @@ -280,7 +355,7 @@ class Analytics { title: _t('Analytics'), description:
- { _t('The information being sent to us to help make Riot.im better includes:') } + { _t('The information being sent to us to help make Riot better includes:') }
{ rows.map((row) => @@ -300,7 +375,7 @@ class Analytics { , }); - } + }; } if (!global.mxAnalytics) { diff --git a/src/AsyncWrapper.js b/src/AsyncWrapper.js new file mode 100644 index 0000000000..b7b81688e1 --- /dev/null +++ b/src/AsyncWrapper.js @@ -0,0 +1,92 @@ +/* +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. +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 createReactClass from 'create-react-class'; +import * as sdk from './index'; +import PropTypes from 'prop-types'; +import { _t } from './languageHandler'; + +/** + * Wrap an asynchronous loader function with a react component which shows a + * spinner until the real component loads. + */ +export default createReactClass({ + propTypes: { + /** A promise which resolves with the real component + */ + prom: PropTypes.object.isRequired, + }, + + getInitialState: function() { + return { + component: null, + error: null, + }; + }, + + componentWillMount: function() { + this._unmounted = false; + // XXX: temporary logging to try to diagnose + // https://github.com/vector-im/riot-web/issues/3148 + console.log('Starting load of AsyncWrapper for modal'); + this.props.prom.then((result) => { + if (this._unmounted) { + return; + } + // Take the 'default' member if it's there, then we support + // passing in just an import()ed module, since ES6 async import + // always returns a module *namespace*. + const component = result.default ? result.default : result; + this.setState({component}); + }).catch((e) => { + console.warn('AsyncWrapper promise failed', e); + this.setState({error: e}); + }); + }, + + componentWillUnmount: function() { + this._unmounted = true; + }, + + _onWrapperCancelClick: function() { + this.props.onFinished(false); + }, + + render: function() { + if (this.state.component) { + const Component = this.state.component; + return ; + } else if (this.state.error) { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return + {_t("Unable to load! Check your network connectivity and try again.")} + + ; + } else { + // show a spinner until the component is loaded. + const Spinner = sdk.getComponent("elements.Spinner"); + return ; + } + }, +}); + diff --git a/src/Avatar.js b/src/Avatar.js index 5a330c31e9..217b196348 100644 --- a/src/Avatar.js +++ b/src/Avatar.js @@ -102,6 +102,8 @@ export function getInitialLetter(name) { } export function avatarUrlForRoom(room, width, height, resizeMethod) { + if (!room) return null; // null-guard + const explicitRoomAvatar = room.getAvatarUrl( MatrixClientPeg.get().getHomeserverUrl(), width, diff --git a/src/BasePlatform.js b/src/BasePlatform.js index 14e34a1f40..5d809eb28f 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -4,6 +4,7 @@ Copyright 2016 Aviral Dasgupta Copyright 2016 OpenMarket Ltd Copyright 2018 New Vector Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,6 +19,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import {MatrixClient} from "matrix-js-sdk"; import dis from './dispatcher'; import BaseEventIndexManager from './indexing/BaseEventIndexManager'; @@ -162,4 +164,28 @@ export default class BasePlatform { getEventIndexingManager(): BaseEventIndexManager | null { return null; } + + setLanguage(preferredLangs: string[]) {} + + getSSOCallbackUrl(hsUrl: string, isUrl: string): URL { + const url = new URL(window.location.href); + // XXX: at this point, the fragment will always be #/login, which is no + // use to anyone. Ideally, we would get the intended fragment from + // MatrixChat.screenAfterLogin so that you could follow #/room links etc + // through an SSO login. + url.hash = ""; + url.searchParams.set("homeserver", hsUrl); + url.searchParams.set("identityServer", isUrl); + return url; + } + + /** + * Begin Single Sign On flows. + * @param {MatrixClient} mxClient the matrix client using which we should start the flow + * @param {"sso"|"cas"} loginType the type of SSO it is, CAS/SSO. + */ + startSingleSignOn(mxClient: MatrixClient, loginType: "sso"|"cas") { + const callbackUrl = this.getSSOCallbackUrl(mxClient.getHomeserverUrl(), mxClient.getIdentityServerUrl()); + window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType); // redirect to SSO + } } diff --git a/src/CallHandler.js b/src/CallHandler.js index 33e15d3cc9..362db939a3 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -64,8 +64,8 @@ import SdkConfig from './SdkConfig'; import { showUnknownDeviceDialogForCalls } from './cryptodevices'; import WidgetUtils from './utils/WidgetUtils'; import WidgetEchoStore from './stores/WidgetEchoStore'; -import {IntegrationManagers} from "./integrations/IntegrationManagers"; import SettingsStore, { SettingLevel } from './settings/SettingsStore'; +import {generateHumanReadableId} from "./utils/NamingUtils"; global.mxCalls = { //room_id: MatrixCall @@ -139,11 +139,11 @@ function _setCallListeners(call) { Modal.createTrackedDialog('Call Failed', '', QuestionDialog, { title: _t('Call Failed'), description: _t( - "There are unknown devices in this room: "+ + "There are unknown sessions in this room: "+ "if you proceed without verifying them, it will be "+ "possible for someone to eavesdrop on your call.", ), - button: _t('Review Devices'), + button: _t('Review Sessions'), onFinished: function(confirmed) { if (confirmed) { const room = MatrixClientPeg.get().getRoom(call.roomId); @@ -395,32 +395,6 @@ function _onAction(payload) { } async function _startCallApp(roomId, type) { - // check for a working integration manager. Technically we could put - // the state event in anyway, but the resulting widget would then not - // work for us. Better that the user knows before everyone else in the - // room sees it. - const managers = IntegrationManagers.sharedInstance(); - let haveScalar = false; - if (managers.hasManager()) { - try { - const scalarClient = managers.getPrimaryManager().getScalarClient(); - await scalarClient.connect(); - haveScalar = scalarClient.hasCredentials(); - } catch (e) { - // ignore - } - } - - if (!haveScalar) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - - Modal.createTrackedDialog('Could not connect to the integration server', '', ErrorDialog, { - title: _t('Could not connect to the integration server'), - description: _t('A conference call could not be started because the integrations server is not available'), - }); - return; - } - dis.dispatch({ action: 'appsDrawer', show: true, @@ -456,31 +430,22 @@ async function _startCallApp(roomId, type) { return; } - // This inherits its poor naming from the field of the same name that goes into - // the event. It's just a random string to make the Jitsi URLs unique. - const widgetSessionId = Math.random().toString(36).substring(2); - const confId = room.roomId.replace(/[^A-Za-z0-9]/g, '') + widgetSessionId; - // NB. we can't just encodeURICompoent all of these because the $ signs need to be there - // (but currently the only thing that needs encoding is the confId) - const queryString = [ - 'confId='+encodeURIComponent(confId), - 'isAudioConf='+(type === 'voice' ? 'true' : 'false'), - 'displayName=$matrix_display_name', - 'avatarUrl=$matrix_avatar_url', - 'email=$matrix_user_id', - ].join('&'); + const confId = `JitsiConference_${generateHumanReadableId()}`; + const jitsiDomain = SdkConfig.get()['jitsi']['preferredDomain']; - let widgetUrl; - if (SdkConfig.get().integrations_jitsi_widget_url) { - // Try this config key. This probably isn't ideal as a way of discovering this - // URL, but this will at least allow the integration manager to not be hardcoded. - widgetUrl = SdkConfig.get().integrations_jitsi_widget_url + '?' + queryString; - } else { - const apiUrl = IntegrationManagers.sharedInstance().getPrimaryManager().apiUrl; - widgetUrl = apiUrl + '/widgets/jitsi.html?' + queryString; - } + let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl(); - const widgetData = { widgetSessionId }; + // TODO: Remove URL hacks when the mobile clients eventually support v2 widgets + const parsedUrl = new URL(widgetUrl); + parsedUrl.search = ''; // set to empty string to make the URL class use searchParams instead + parsedUrl.searchParams.set('confId', confId); + widgetUrl = parsedUrl.toString(); + + const widgetData = { + conferenceId: confId, + isAudioOnly: type === 'voice', + domain: jitsiDomain, + }; const widgetId = ( 'jitsi_' + diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index 0773a8d32d..da7c0673d2 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -20,6 +20,8 @@ import {MatrixClientPeg} from './MatrixClientPeg'; import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase'; import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; import { _t } from './languageHandler'; +import SettingsStore from './settings/SettingsStore'; +import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib"; // This stores the secret storage private keys in memory for the JS SDK. This is // only meant to act as a cache to avoid prompting the user multiple times @@ -27,9 +29,43 @@ import { _t } from './languageHandler'; // single secret storage operation, as it will clear the cached keys once the // operation ends. let secretStorageKeys = {}; -let cachingAllowed = false; +let secretStorageBeingAccessed = false; -async function getSecretStorageKey({ keys: keyInfos }) { +function isCachingAllowed() { + return ( + secretStorageBeingAccessed || + SettingsStore.getValue("keepSecretStoragePassphraseForSession") + ); +} + +export class AccessCancelledError extends Error { + constructor() { + super("Secret storage access canceled"); + } +} + +async function confirmToDismiss(name) { + let description; + if (name === "m.cross_signing.user_signing") { + description = _t("If you cancel now, you won't complete verifying the other user."); + } else if (name === "m.cross_signing.self_signing") { + description = _t("If you cancel now, you won't complete verifying your other session."); + } else { + description = _t("If you cancel now, you won't complete your secret storage operation."); + } + + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + const [sure] = await Modal.createDialog(QuestionDialog, { + title: _t("Cancel entering passphrase?"), + description, + danger: true, + cancelButton: _t("Enter passphrase"), + button: _t("Cancel"), + }).finished; + return sure; +} + +async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { const keyInfoEntries = Object.entries(keyInfos); if (keyInfoEntries.length > 1) { throw new Error("Multiple storage key requests not implemented"); @@ -37,7 +73,7 @@ async function getSecretStorageKey({ keys: keyInfos }) { const [name, info] = keyInfoEntries[0]; // Check the in-memory cache - if (cachingAllowed && secretStorageKeys[name]) { + if (isCachingAllowed() && secretStorageKeys[name]) { return [name, secretStorageKeys[name]]; } @@ -56,32 +92,111 @@ async function getSecretStorageKey({ keys: keyInfos }) { sdk.getComponent("dialogs.secretstorage.AccessSecretStorageDialog"); const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "", AccessSecretStorageDialog, + /* props= */ { keyInfo: info, checkPrivateKey: async (input) => { + if (!info.pubkey) { + return true; + } const key = await inputToKey(input); return MatrixClientPeg.get().checkSecretStoragePrivateKey(key, info.pubkey); }, }, + /* className= */ null, + /* isPriorityModal= */ false, + /* isStaticModal= */ false, + /* options= */ { + onBeforeClose: async (reason) => { + if (reason === "backgroundClick") { + return confirmToDismiss(ssssItemName); + } + return true; + }, + }, ); const [input] = await finished; if (!input) { - throw new Error("Secret storage access canceled"); + throw new AccessCancelledError(); } const key = await inputToKey(input); // Save to cache to avoid future prompts in the current session - if (cachingAllowed) { + if (isCachingAllowed()) { secretStorageKeys[name] = key; } return [name, key]; } +const onSecretRequested = async function({ + user_id: userId, + device_id: deviceId, + request_id: requestId, + name, + device_trust: deviceTrust, +}) { + console.log("onSecretRequested", userId, deviceId, requestId, name, deviceTrust); + const client = MatrixClientPeg.get(); + if (userId !== client.getUserId()) { + return; + } + if (!deviceTrust || !deviceTrust.isVerified()) { + console.log(`CrossSigningManager: Ignoring request from untrusted device ${deviceId}`); + return; + } + if (name.startsWith("m.cross_signing")) { + const callbacks = client.getCrossSigningCacheCallbacks(); + if (!callbacks.getCrossSigningKeyCache) return; + /* Explicit enumeration here is deliberate – never share the master key! */ + if (name === "m.cross_signing.self_signing") { + const key = await callbacks.getCrossSigningKeyCache("self_signing"); + if (!key) { + console.log( + `self_signing requested by ${deviceId}, but not found in cache`, + ); + } + return key && encodeBase64(key); + } else if (name === "m.cross_signing.user_signing") { + const key = await callbacks.getCrossSigningKeyCache("user_signing"); + if (!key) { + console.log( + `user_signing requested by ${deviceId}, but not found in cache`, + ); + } + return key && encodeBase64(key); + } + } else if (name === "m.megolm_backup.v1") { + const key = await client._crypto.getSessionBackupPrivateKey(); + if (!key) { + console.log( + `session backup key requested by ${deviceId}, but not found in cache`, + ); + } + return key && encodeBase64(key); + } + console.warn("onSecretRequested didn't recognise the secret named ", name); +}; + export const crossSigningCallbacks = { getSecretStorageKey, + onSecretRequested, }; +export async function promptForBackupPassphrase() { + let key; + + const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); + const { finished } = Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, { + showSummary: false, keyCallback: k => key = k, + }, null, /* priority = */ false, /* static = */ true); + + const success = await finished; + if (!success) throw new Error("Key backup prompt cancelled"); + + return key; +} + /** * This helper should be used whenever you need to access secret storage. It * ensures that secret storage (and also cross-signing since they each depend on @@ -101,18 +216,21 @@ export const crossSigningCallbacks = { * * @param {Function} [func] An operation to perform once secret storage has been * bootstrapped. Optional. + * @param {bool} [forceReset] Reset secret storage even if it's already set up */ -export async function accessSecretStorage(func = async () => { }) { +export async function accessSecretStorage(func = async () => { }, forceReset = false) { const cli = MatrixClientPeg.get(); - cachingAllowed = true; - + secretStorageBeingAccessed = true; try { - if (!cli.hasSecretStorageKey()) { + if (!await cli.hasSecretStorageKey() || forceReset) { // This dialog calls bootstrap itself after guiding the user through // passphrase creation. const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '', import("./async-components/views/dialogs/secretstorage/CreateSecretStorageDialog"), - null, null, /* priority = */ false, /* static = */ true, + { + force: forceReset, + }, + null, /* priority = */ false, /* static = */ true, ); const [confirmed] = await finished; if (!confirmed) { @@ -125,7 +243,7 @@ export async function accessSecretStorage(func = async () => { }) { const { finished } = Modal.createTrackedDialog( 'Cross-signing keys dialog', '', InteractiveAuthDialog, { - title: _t("Send cross-signing keys to homeserver"), + title: _t("Setting up keys"), matrixClient: MatrixClientPeg.get(), makeRequest, }, @@ -135,6 +253,7 @@ export async function accessSecretStorage(func = async () => { }) { throw new Error("Cross-signing key upload auth canceled"); } }, + getBackupPassphrase: promptForBackupPassphrase, }); } @@ -143,7 +262,9 @@ export async function accessSecretStorage(func = async () => { }) { return await func(); } finally { // Clear secret storage key cache now that work is complete - cachingAllowed = false; - secretStorageKeys = {}; + secretStorageBeingAccessed = false; + if (!isCachingAllowed()) { + secretStorageKeys = {}; + } } } diff --git a/src/DeviceListener.js b/src/DeviceListener.js new file mode 100644 index 0000000000..f8555c7602 --- /dev/null +++ b/src/DeviceListener.js @@ -0,0 +1,203 @@ +/* +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. +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 { MatrixClientPeg } from './MatrixClientPeg'; +import SettingsStore from './settings/SettingsStore'; +import * as sdk from './index'; +import { _t } from './languageHandler'; +import ToastStore from './stores/ToastStore'; + +function toastKey(deviceId) { + return 'unverified_session_' + deviceId; +} + +const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000; +const THIS_DEVICE_TOAST_KEY = 'setupencryption'; + +export default class DeviceListener { + static sharedInstance() { + if (!global.mx_DeviceListener) global.mx_DeviceListener = new DeviceListener(); + return global.mx_DeviceListener; + } + + constructor() { + // set of device IDs we're currently showing toasts for + this._activeNagToasts = new Set(); + // device IDs for which the user has dismissed the verify toast ('Later') + this._dismissed = new Set(); + // has the user dismissed any of the various nag toasts to setup encryption on this device? + this._dismissedThisDeviceToast = false; + + // cache of the key backup info + this._keyBackupInfo = null; + this._keyBackupFetchedAt = null; + } + + start() { + MatrixClientPeg.get().on('crypto.devicesUpdated', this._onDevicesUpdated); + MatrixClientPeg.get().on('deviceVerificationChanged', this._onDeviceVerificationChanged); + MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged); + MatrixClientPeg.get().on('accountData', this._onAccountData); + this._recheck(); + } + + stop() { + if (MatrixClientPeg.get()) { + MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this._onDevicesUpdated); + MatrixClientPeg.get().removeListener('deviceVerificationChanged', this._onDeviceVerificationChanged); + MatrixClientPeg.get().removeListener('userTrustStatusChanged', this._onUserTrustStatusChanged); + MatrixClientPeg.get().removeListener('accountData', this._onAccountData); + } + this._dismissed.clear(); + } + + dismissVerification(deviceId) { + this._dismissed.add(deviceId); + this._recheck(); + } + + dismissEncryptionSetup() { + this._dismissedThisDeviceToast = true; + this._recheck(); + } + + _onDevicesUpdated = (users) => { + if (!users.includes(MatrixClientPeg.get().getUserId())) return; + this._recheck(); + } + + _onDeviceVerificationChanged = (userId) => { + if (userId !== MatrixClientPeg.get().getUserId()) return; + this._recheck(); + } + + _onUserTrustStatusChanged = (userId, trustLevel) => { + if (userId !== MatrixClientPeg.get().getUserId()) return; + this._recheck(); + } + + _onAccountData = (ev) => { + // User may have migrated SSSS to symmetric, in which case we can dismiss that toast + if (ev.getType().startsWith('m.secret_storage.key.')) { + this._recheck(); + } + } + + // The server doesn't tell us when key backup is set up, so we poll + // & cache the result + async _getKeyBackupInfo() { + const now = (new Date()).getTime(); + if (!this._keyBackupInfo || this._keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) { + this._keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); + this._keyBackupFetchedAt = now; + } + return this._keyBackupInfo; + } + + async _recheck() { + const cli = MatrixClientPeg.get(); + + if ( + !SettingsStore.isFeatureEnabled("feature_cross_signing") || + !await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing") + ) return; + + if (!cli.isCryptoEnabled()) return; + + const crossSigningReady = await cli.isCrossSigningReady(); + + if (this._dismissedThisDeviceToast) { + ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY); + } else { + if (!crossSigningReady) { + // cross signing isn't enabled - nag to enable it + // There are 3 different toasts for: + if (cli.getStoredCrossSigningForUser(cli.getUserId())) { + // Cross-signing on account but this device doesn't trust the master key (verify this session) + ToastStore.sharedInstance().addOrReplaceToast({ + key: THIS_DEVICE_TOAST_KEY, + title: _t("Verify this session"), + icon: "verification_warning", + props: {kind: 'verify_this_session'}, + component: sdk.getComponent("toasts.SetupEncryptionToast"), + }); + } else { + const backupInfo = await this._getKeyBackupInfo(); + if (backupInfo) { + // No cross-signing on account but key backup available (upgrade encryption) + ToastStore.sharedInstance().addOrReplaceToast({ + key: THIS_DEVICE_TOAST_KEY, + title: _t("Encryption upgrade available"), + icon: "verification_warning", + props: {kind: 'upgrade_encryption'}, + component: sdk.getComponent("toasts.SetupEncryptionToast"), + }); + } else { + // No cross-signing or key backup on account (set up encryption) + ToastStore.sharedInstance().addOrReplaceToast({ + key: THIS_DEVICE_TOAST_KEY, + title: _t("Set up encryption"), + icon: "verification_warning", + props: {kind: 'set_up_encryption'}, + component: sdk.getComponent("toasts.SetupEncryptionToast"), + }); + } + } + return; + } else if (await cli.secretStorageKeyNeedsUpgrade()) { + ToastStore.sharedInstance().addOrReplaceToast({ + key: THIS_DEVICE_TOAST_KEY, + title: _t("Encryption upgrade available"), + icon: "verification_warning", + props: {kind: 'upgrade_ssss'}, + component: sdk.getComponent("toasts.SetupEncryptionToast"), + }); + } + } + + // as long as cross-signing isn't ready, + // you can't see or dismiss any device toasts + if (crossSigningReady) { + const newActiveToasts = new Set(); + + const devices = await cli.getStoredDevicesForUser(cli.getUserId()); + for (const device of devices) { + if (device.deviceId == cli.deviceId) continue; + + const deviceTrust = await cli.checkDeviceTrust(cli.getUserId(), device.deviceId); + if (deviceTrust.isCrossSigningVerified() || this._dismissed.has(device.deviceId)) { + ToastStore.sharedInstance().dismissToast(toastKey(device.deviceId)); + } else { + this._activeNagToasts.add(device.deviceId); + ToastStore.sharedInstance().addOrReplaceToast({ + key: toastKey(device.deviceId), + title: _t("Unverified login. Was this you?"), + icon: "verification_warning", + props: { device }, + component: sdk.getComponent("toasts.UnverifiedSessionToast"), + }); + newActiveToasts.add(device.deviceId); + } + } + + // clear any other outstanding toasts (eg. logged out devices) + for (const deviceId of this._activeNagToasts) { + if (!newActiveToasts.has(deviceId)) ToastStore.sharedInstance().dismissToast(toastKey(deviceId)); + } + this._activeNagToasts = newActiveToasts; + } + } +} diff --git a/src/Entities.js b/src/Entities.js deleted file mode 100644 index 872a837f3a..0000000000 --- a/src/Entities.js +++ /dev/null @@ -1,137 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import * as sdk from './index'; - -function isMatch(query, name, uid) { - query = query.toLowerCase(); - name = name.toLowerCase(); - uid = uid.toLowerCase(); - - // direct prefix matches - if (name.indexOf(query) === 0 || uid.indexOf(query) === 0) { - return true; - } - - // strip @ on uid and try matching again - if (uid.length > 1 && uid[0] === "@" && uid.substring(1).indexOf(query) === 0) { - return true; - } - - // split spaces in name and try matching constituent parts - const parts = name.split(" "); - for (let i = 0; i < parts.length; i++) { - if (parts[i].indexOf(query) === 0) { - return true; - } - } - return false; -} - -/* - * Converts various data models to Entity objects. - * - * Entity objects provide an interface for UI components to use to display - * members in a data-agnostic way. This means they don't need to care if the - * underlying data model is a RoomMember, User or 3PID data structure, it just - * cares about rendering. - */ - -class Entity { - constructor(model) { - this.model = model; - } - - getJsx() { - return null; - } - - matches(queryString) { - return false; - } -} - -class MemberEntity extends Entity { - getJsx() { - const MemberTile = sdk.getComponent("rooms.MemberTile"); - return ( - - ); - } - - matches(queryString) { - return isMatch(queryString, this.model.name, this.model.userId); - } -} - -class UserEntity extends Entity { - constructor(model, showInviteButton, inviteFn) { - super(model); - this.showInviteButton = Boolean(showInviteButton); - this.inviteFn = inviteFn; - this.onClick = this.onClick.bind(this); - } - - onClick() { - if (this.inviteFn) { - this.inviteFn(this.model.userId); - } - } - - getJsx() { - const UserTile = sdk.getComponent("rooms.UserTile"); - return ( - - ); - } - - matches(queryString) { - const name = this.model.displayName || this.model.userId; - return isMatch(queryString, name, this.model.userId); - } -} - -export function newEntity(jsx, matchFn) { - const entity = new Entity(); - entity.getJsx = function() { - return jsx; - }; - entity.matches = matchFn; - return entity; -} - -/** - * @param {RoomMember[]} members - * @return {Entity[]} - */ -export function fromRoomMembers(members) { - return members.map(function(m) { - return new MemberEntity(m); - }); -} - -/** - * @param {User[]} users - * @param {boolean} showInviteButton - * @param {Function} inviteFn Called with the user ID. - * @return {Entity[]} - */ -export function fromUsers(users, showInviteButton, inviteFn) { - return users.map(function(u) { - return new UserEntity(u, showInviteButton, inviteFn); - }); -} diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js index 64caba0fdf..ea76c85643 100644 --- a/src/FromWidgetPostMessageApi.js +++ b/src/FromWidgetPostMessageApi.js @@ -24,6 +24,8 @@ import {MatrixClientPeg} from "./MatrixClientPeg"; import RoomViewStore from "./stores/RoomViewStore"; import {IntegrationManagers} from "./integrations/IntegrationManagers"; import SettingsStore from "./settings/SettingsStore"; +import {Capability, KnownWidgetActions} from "./widgets/WidgetApi"; +import SdkConfig from "./SdkConfig"; const WIDGET_API_VERSION = '0.0.2'; // Current API version const SUPPORTED_WIDGET_API_VERSIONS = [ @@ -213,11 +215,18 @@ export default class FromWidgetPostMessageApi { const data = event.data.data; const val = data.value; - if (ActiveWidgetStore.widgetHasCapability(widgetId, 'm.always_on_screen')) { + if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.AlwaysOnScreen)) { ActiveWidgetStore.setWidgetPersistence(widgetId, val); } } else if (action === 'get_openid') { // Handled by caller + } else if (action === KnownWidgetActions.GetRiotWebConfig) { + if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.GetRiotWebConfig)) { + this.sendResponse(event, { + api: INBOUND_API_NAME, + config: SdkConfig.get(), + }); + } } else { console.warn('Widget postMessage event unhandled'); this.sendError(event, {message: 'The postMessage was unhandled'}); diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 236aa0157e..a58ea25c8a 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -23,7 +23,6 @@ import ReplyThread from "./components/views/elements/ReplyThread"; import React from 'react'; import sanitizeHtml from 'sanitize-html'; -import highlight from 'highlight.js'; import * as linkify from 'linkifyjs'; import linkifyMatrix from './linkify-matrix'; import _linkifyElement from 'linkifyjs/element'; @@ -160,7 +159,7 @@ const transformTags = { // custom to matrix delete attribs.target; } } - attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/ + attribs.rel = 'noreferrer noopener'; // https://mathiasbynens.github.io/rel-noopener/ return { tagName, attribs }; }, 'img': function(tagName, attribs) { @@ -467,11 +466,12 @@ export function bodyToHtml(content, highlights, opts={}) { /** * Linkifies the given string. This is a wrapper around 'linkifyjs/string'. * - * @param {string} str - * @returns {string} + * @param {string} str string to linkify + * @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options + * @returns {string} Linkified string */ -export function linkifyString(str) { - return _linkifyString(str); +export function linkifyString(str, options = linkifyMatrix.options) { + return _linkifyString(str, options); } /** @@ -489,10 +489,11 @@ export function linkifyElement(element, options = linkifyMatrix.options) { * Linkify the given string and sanitize the HTML afterwards. * * @param {string} dirtyHtml The HTML string to sanitize and linkify + * @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options * @returns {string} */ -export function linkifyAndSanitizeHtml(dirtyHtml) { - return sanitizeHtml(linkifyString(dirtyHtml), sanitizeHtmlParams); +export function linkifyAndSanitizeHtml(dirtyHtml, options = linkifyMatrix.options) { + return sanitizeHtml(linkifyString(dirtyHtml, options), sanitizeHtmlParams); } /** diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js index 72432b9a44..4a830d6506 100644 --- a/src/IdentityAuthClient.js +++ b/src/IdentityAuthClient.js @@ -181,24 +181,12 @@ export default class IdentityAuthClient { } async registerForToken(check=true) { - try { - const hsOpenIdToken = await MatrixClientPeg.get().getOpenIdToken(); - // XXX: The spec is `token`, but we used `access_token` for a Sydent release. - const { access_token: accessToken, token } = - await this._matrixClient.registerWithIdentityServer(hsOpenIdToken); - const identityAccessToken = token ? token : accessToken; - if (check) await this._checkToken(identityAccessToken); - return identityAccessToken; - } catch (e) { - if (e.cors === "rejected" || e.httpStatus === 404) { - // Assume IS only supports deprecated v1 API for now - // TODO: Remove this path once v2 is only supported version - // See https://github.com/vector-im/riot-web/issues/10443 - console.warn("IS doesn't support v2 auth"); - this.authEnabled = false; - return; - } - throw e; - } + const hsOpenIdToken = await MatrixClientPeg.get().getOpenIdToken(); + // XXX: The spec is `token`, but we used `access_token` for a Sydent release. + const { access_token: accessToken, token } = + await this._matrixClient.registerWithIdentityServer(hsOpenIdToken); + const identityAccessToken = token ? token : accessToken; + if (check) await this._checkToken(identityAccessToken); + return identityAccessToken; } } diff --git a/src/KeyRequestHandler.js b/src/KeyRequestHandler.js index 65dc7fdb0f..30f3b7d50e 100644 --- a/src/KeyRequestHandler.js +++ b/src/KeyRequestHandler.js @@ -1,5 +1,6 @@ /* Copyright 2017 Vector Creations 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. @@ -16,7 +17,10 @@ limitations under the License. import * as sdk from './index'; import Modal from './Modal'; +import SettingsStore from './settings/SettingsStore'; +// TODO: We can remove this once cross-signing is the only way. +// https://github.com/vector-im/riot-web/issues/11908 export default class KeyRequestHandler { constructor(matrixClient) { this._matrixClient = matrixClient; @@ -30,6 +34,11 @@ export default class KeyRequestHandler { } handleKeyRequest(keyRequest) { + // Ignore own device key requests if cross-signing lab enabled + if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { + return; + } + const userId = keyRequest.userId; const deviceId = keyRequest.deviceId; const requestId = keyRequest.requestId; @@ -60,6 +69,11 @@ export default class KeyRequestHandler { } handleKeyRequestCancellation(cancellation) { + // Ignore own device key requests if cross-signing lab enabled + if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { + return; + } + // see if we can find the request in the queue const userId = cancellation.userId; const deviceId = cancellation.deviceId; diff --git a/src/Keyboard.js b/src/Keyboard.ts similarity index 92% rename from src/Keyboard.js rename to src/Keyboard.ts index 478d75acc1..23e2bbf0d6 100644 --- a/src/Keyboard.js +++ b/src/Keyboard.ts @@ -22,6 +22,7 @@ export const Key = { PAGE_UP: "PageUp", PAGE_DOWN: "PageDown", BACKSPACE: "Backspace", + DELETE: "Delete", ARROW_UP: "ArrowUp", ARROW_DOWN: "ArrowDown", ARROW_LEFT: "ArrowLeft", @@ -36,10 +37,12 @@ export const Key = { CONTEXT_MENU: "ContextMenu", COMMA: ",", + PERIOD: ".", LESS_THAN: "<", GREATER_THAN: ">", BACKTICK: "`", SPACE: " ", + SLASH: "/", A: "a", B: "b", C: "c", @@ -68,8 +71,9 @@ export const Key = { Z: "z", }; +export const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; + export function isOnlyCtrlOrCmdKeyEvent(ev) { - const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; if (isMac) { return ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey; } else { @@ -78,7 +82,6 @@ export function isOnlyCtrlOrCmdKeyEvent(ev) { } export function isOnlyCtrlOrCmdIgnoreShiftKeyEvent(ev) { - const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; if (isMac) { return ev.metaKey && !ev.altKey && !ev.ctrlKey; } else { diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 0796e326a0..b9fbf4f1bc 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -2,6 +2,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd Copyright 2018 New Vector 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. @@ -35,8 +36,10 @@ import { sendLoginRequest } from "./Login"; import * as StorageManager from './utils/StorageManager'; import SettingsStore from "./settings/SettingsStore"; import TypingStore from "./stores/TypingStore"; +import ToastStore from "./stores/ToastStore"; import {IntegrationManagers} from "./integrations/IntegrationManagers"; import {Mjolnir} from "./mjolnir/Mjolnir"; +import DeviceListener from "./DeviceListener"; /** * Called at startup, to attempt to build a logged-in Matrix session. It tries @@ -310,7 +313,7 @@ async function _restoreFromLocalStorage(opts) { } } -function _handleLoadSessionFailure(e) { +async function _handleLoadSessionFailure(e) { console.error("Unable to load session", e); const SessionRestoreErrorDialog = @@ -320,16 +323,15 @@ function _handleLoadSessionFailure(e) { error: e.message, }); - return modal.finished.then(([success]) => { - if (success) { - // user clicked continue. - _clearStorage(); - return false; - } + const [success] = await modal.finished; + if (success) { + // user clicked continue. + await _clearStorage(); + return false; + } - // try, try again - return loadSession(); - }); + // try, try again + return loadSession(); } /** @@ -375,7 +377,7 @@ export function hydrateSession(credentials) { const overwrite = credentials.userId !== oldUserId || credentials.deviceId !== oldDeviceId; if (overwrite) { - console.warn("Clearing all data: Old session belongs to a different user/device"); + console.warn("Clearing all data: Old session belongs to a different user/session"); } return _doSetLoggedIn(credentials, overwrite); @@ -432,7 +434,7 @@ async function _doSetLoggedIn(credentials, clearStorage) { } } - Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl, credentials.identityServerUrl); + Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl); if (localStorage) { try { @@ -575,6 +577,7 @@ async function startMatrixClient(startSyncing=true) { Notifier.start(); UserActivity.sharedInstance().start(); TypingStore.sharedInstance().reset(); // just in case + ToastStore.sharedInstance().reset(); if (!SettingsStore.getValue("lowBandwidth")) { Presence.start(); } @@ -588,13 +591,19 @@ async function startMatrixClient(startSyncing=true) { Mjolnir.sharedInstance().start(); if (startSyncing) { - await MatrixClientPeg.start(); + // The client might want to populate some views with events from the + // index (e.g. the FilePanel), therefore initialize the event index + // before the client. await EventIndexPeg.init(); + await MatrixClientPeg.start(); } else { console.warn("Caller requested only auxiliary services be started"); await MatrixClientPeg.assign(); } + // This needs to be started after crypto is set up + DeviceListener.sharedInstance().start(); + // dispatch that we finished starting up to wire up any other bits // of the matrix client that cannot be set prior to starting up. dis.dispatch({action: 'client_started'}); @@ -622,7 +631,7 @@ export async function onLoggedOut() { * @returns {Promise} promise which resolves once the stores have been cleared */ async function _clearStorage() { - Analytics.logout(); + Analytics.disable(); if (window.localStorage) { window.localStorage.clear(); @@ -651,6 +660,7 @@ export function stopMatrixClient(unsetClient=true) { ActiveWidgetStore.stop(); IntegrationManagers.sharedInstance().stopWatching(); Mjolnir.sharedInstance().stop(); + DeviceListener.sharedInstance().stop(); if (DMRoomMap.shared()) DMRoomMap.shared().stop(); EventIndexPeg.stop(); const cli = MatrixClientPeg.get(); diff --git a/src/Login.js b/src/Login.js index d9ce8adaaa..1590e5ac28 100644 --- a/src/Login.js +++ b/src/Login.js @@ -3,6 +3,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd Copyright 2018 New Vector Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,8 +20,6 @@ limitations under the License. import Matrix from "matrix-js-sdk"; -import url from 'url'; - export default class Login { constructor(hsUrl, isUrl, fallbackHsUrl, opts) { this._hsUrl = hsUrl; @@ -29,6 +28,7 @@ export default class Login { this._currentFlowIndex = 0; this._flows = []; this._defaultDeviceDisplayName = opts.defaultDeviceDisplayName; + this._tempClient = null; // memoize } getHomeserverUrl() { @@ -40,10 +40,12 @@ export default class Login { } setHomeserverUrl(hsUrl) { + this._tempClient = null; // clear memoization this._hsUrl = hsUrl; } setIdentityServerUrl(isUrl) { + this._tempClient = null; // clear memoization this._isUrl = isUrl; } @@ -52,8 +54,9 @@ export default class Login { * requests. * @returns {MatrixClient} */ - _createTemporaryClient() { - return Matrix.createClient({ + createTemporaryClient() { + if (this._tempClient) return this._tempClient; // use memoization + return this._tempClient = Matrix.createClient({ baseUrl: this._hsUrl, idBaseUrl: this._isUrl, }); @@ -61,7 +64,7 @@ export default class Login { getFlows() { const self = this; - const client = this._createTemporaryClient(); + const client = this.createTemporaryClient(); return client.loginFlows().then(function(result) { self._flows = result.flows; self._currentFlowIndex = 0; @@ -139,21 +142,6 @@ export default class Login { throw error; }); } - - getSsoLoginUrl(loginType) { - const client = this._createTemporaryClient(); - const parsedUrl = url.parse(window.location.href, true); - - // XXX: at this point, the fragment will always be #/login, which is no - // use to anyone. Ideally, we would get the intended fragment from - // MatrixChat.screenAfterLogin so that you could follow #/room links etc - // through an SSO login. - parsedUrl.hash = ""; - - parsedUrl.query["homeserver"] = client.getHomeserverUrl(); - parsedUrl.query["identityServer"] = client.getIdentityServerUrl(); - return client.getSsoLoginUrl(url.format(parsedUrl), loginType); - } } diff --git a/src/Markdown.js b/src/Markdown.js index acfea52100..fb1f8bf0ea 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -91,7 +91,7 @@ export default class Markdown { return true; } - toHTML() { + toHTML({ externalLinks = false } = {}) { const renderer = new commonmark.HtmlRenderer({ safe: false, @@ -125,6 +125,24 @@ export default class Markdown { } }; + renderer.link = function(node, entering) { + const attrs = this.attrs(node); + if (entering) { + attrs.push(['href', this.esc(node.destination)]); + if (node.title) { + attrs.push(['title', this.esc(node.title)]); + } + // Modified link behaviour to treat them all as external and + // thus opening in a new tab. + if (externalLinks) { + attrs.push(['target', '_blank']); + attrs.push(['rel', 'noreferrer noopener']); + } + this.tag('a', attrs); + } else { + this.tag('/a'); + } + }; renderer.html_inline = html_if_tag_allowed; diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 9c939f2fd3..21f05b9759 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -2,7 +2,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd. Copyright 2017, 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. @@ -32,6 +32,7 @@ import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientB import * as StorageManager from './utils/StorageManager'; import IdentityAuthClient from './IdentityAuthClient'; import { crossSigningCallbacks } from './CrossSigningManager'; +import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode"; interface MatrixClientCreds { homeserverUrl: string, @@ -147,6 +148,9 @@ class _MatrixClientPeg { // check that we have a version of the js-sdk which includes initCrypto if (!SettingsStore.getValue("lowBandwidth") && this.matrixClient.initCrypto) { await this.matrixClient.initCrypto(); + this.matrixClient.setCryptoTrustCrossSignedDevices( + !SettingsStore.getValue('e2ee.manuallyVerifyAllSessions'), + ); StorageManager.setCryptoInitialised(true); } } catch (e) { @@ -217,15 +221,20 @@ class _MatrixClientPeg { timelineSupport: true, forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false), fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'), - verificationMethods: [verificationMethods.SAS], + verificationMethods: [ + verificationMethods.SAS, + SHOW_QR_CODE_METHOD, + verificationMethods.RECIPROCATE_QR_CODE, + ], unstableClientRelationAggregation: true, identityServer: new IdentityAuthClient(), }; opts.cryptoCallbacks = {}; - if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { - Object.assign(opts.cryptoCallbacks, crossSigningCallbacks); - } + // These are always installed regardless of the labs flag so that + // cross-signing features can toggle on without reloading and also be + // accessed immediately after login. + Object.assign(opts.cryptoCallbacks, crossSigningCallbacks); this.matrixClient = createMatrixClient(opts); diff --git a/src/Modal.js b/src/Modal.js index 29d3af2e74..de441740f1 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -17,87 +17,14 @@ limitations under the License. import React from 'react'; import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; import Analytics from './Analytics'; -import * as sdk from './index'; import dis from './dispatcher'; -import { _t } from './languageHandler'; -import {defer} from "./utils/promise"; +import {defer} from './utils/promise'; +import AsyncWrapper from './AsyncWrapper'; const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer"; -/** - * Wrap an asynchronous loader function with a react component which shows a - * spinner until the real component loads. - */ -const AsyncWrapper = createReactClass({ - propTypes: { - /** A promise which resolves with the real component - */ - prom: PropTypes.object.isRequired, - }, - - getInitialState: function() { - return { - component: null, - error: null, - }; - }, - - componentWillMount: function() { - this._unmounted = false; - // XXX: temporary logging to try to diagnose - // https://github.com/vector-im/riot-web/issues/3148 - console.log('Starting load of AsyncWrapper for modal'); - this.props.prom.then((result) => { - if (this._unmounted) { - return; - } - // Take the 'default' member if it's there, then we support - // passing in just an import()ed module, since ES6 async import - // always returns a module *namespace*. - const component = result.default ? result.default : result; - this.setState({component}); - }).catch((e) => { - console.warn('AsyncWrapper promise failed', e); - this.setState({error: e}); - }); - }, - - componentWillUnmount: function() { - this._unmounted = true; - }, - - _onWrapperCancelClick: function() { - this.props.onFinished(false); - }, - - render: function() { - if (this.state.component) { - const Component = this.state.component; - return ; - } else if (this.state.error) { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - return - {_t("Unable to load! Check your network connectivity and try again.")} - - ; - } else { - // show a spinner until the component is loaded. - const Spinner = sdk.getComponent("elements.Spinner"); - return ; - } - }, -}); - class ModalManager { constructor() { this._counter = 0; @@ -120,7 +47,7 @@ class ModalManager { } */ ]; - this.closeAll = this.closeAll.bind(this); + this.onBackgroundClick = this.onBackgroundClick.bind(this); } hasDialogs() { @@ -179,7 +106,7 @@ class ModalManager { return this.appendDialogAsync(...rest); } - _buildModal(prom, props, className) { + _buildModal(prom, props, className, options) { const modal = {}; // never call this from onFinished() otherwise it will loop @@ -197,13 +124,27 @@ class ModalManager { ); modal.onFinished = props ? props.onFinished : null; modal.className = className; + modal.onBeforeClose = options.onBeforeClose; + modal.beforeClosePromise = null; + modal.close = closeDialog; + modal.closeReason = null; return {modal, closeDialog, onFinishedProm}; } _getCloseFn(modal, props) { const deferred = defer(); - return [(...args) => { + return [async (...args) => { + if (modal.beforeClosePromise) { + await modal.beforeClosePromise; + } else if (modal.onBeforeClose) { + modal.beforeClosePromise = modal.onBeforeClose(modal.closeReason); + const shouldClose = await modal.beforeClosePromise; + modal.beforeClosePromise = null; + if (!shouldClose) { + return; + } + } deferred.resolve(args); if (props && props.onFinished) props.onFinished.apply(null, args); const i = this._modals.indexOf(modal); @@ -229,6 +170,12 @@ class ModalManager { }, deferred.promise]; } + /** + * @callback onBeforeClose + * @param {string?} reason either "backgroundClick" or null + * @return {Promise} whether the dialog should close + */ + /** * Open a modal view. * @@ -256,11 +203,12 @@ class ModalManager { * also be removed from the stack. This is not compatible * with being a priority modal. Only one modal can be * static at a time. + * @param {Object} options? extra options for the dialog + * @param {onBeforeClose} options.onBeforeClose a callback to decide whether to close the dialog * @returns {object} Object with 'close' parameter being a function that will close the dialog */ - createDialogAsync(prom, props, className, isPriorityModal, isStaticModal) { - const {modal, closeDialog, onFinishedProm} = this._buildModal(prom, props, className); - + createDialogAsync(prom, props, className, isPriorityModal, isStaticModal, options = {}) { + const {modal, closeDialog, onFinishedProm} = this._buildModal(prom, props, className, options); if (isPriorityModal) { // XXX: This is destructive this._priorityModal = modal; @@ -279,7 +227,7 @@ class ModalManager { } appendDialogAsync(prom, props, className) { - const {modal, closeDialog, onFinishedProm} = this._buildModal(prom, props, className); + const {modal, closeDialog, onFinishedProm} = this._buildModal(prom, props, className, {}); this._modals.push(modal); this._reRender(); @@ -289,24 +237,22 @@ class ModalManager { }; } - closeAll() { - const modalsToClose = [...this._modals, this._priorityModal]; - this._modals = []; - this._priorityModal = null; - - if (this._staticModal && modalsToClose.length === 0) { - modalsToClose.push(this._staticModal); - this._staticModal = null; + onBackgroundClick() { + const modal = this._getCurrentModal(); + if (!modal) { + return; } + // we want to pass a reason to the onBeforeClose + // callback, but close is currently defined to + // pass all number of arguments to the onFinished callback + // so, pass the reason to close through a member variable + modal.closeReason = "backgroundClick"; + modal.close(); + modal.closeReason = null; + } - for (let i = 0; i < modalsToClose.length; i++) { - const m = modalsToClose[i]; - if (m && m.onFinished) { - m.onFinished(false); - } - } - - this._reRender(); + _getCurrentModal() { + return this._priorityModal ? this._priorityModal : (this._modals[0] || this._staticModal); } _reRender() { @@ -337,7 +283,7 @@ class ModalManager {
{ this._staticModal.elem }
-
+
); @@ -347,8 +293,8 @@ class ModalManager { ReactDOM.unmountComponentAtNode(this.getOrCreateStaticContainer()); } - const modal = this._priorityModal ? this._priorityModal : this._modals[0]; - if (modal) { + const modal = this._getCurrentModal(); + if (modal !== this._staticModal) { const classes = "mx_Dialog_wrapper " + (this._staticModal ? "mx_Dialog_wrapperWithStaticUnder " : '') + (modal.className ? modal.className : ''); @@ -358,7 +304,7 @@ class ModalManager {
{modal.elem}
-
+
); diff --git a/src/Notifier.js b/src/Notifier.js index b030f1b6f9..36a6f13bb6 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -153,10 +153,12 @@ const Notifier = { }, start: function() { - this.boundOnEvent = this.onEvent.bind(this); - this.boundOnSyncStateChange = this.onSyncStateChange.bind(this); - this.boundOnRoomReceipt = this.onRoomReceipt.bind(this); - this.boundOnEventDecrypted = this.onEventDecrypted.bind(this); + // do not re-bind in the case of repeated call + this.boundOnEvent = this.boundOnEvent || this.onEvent.bind(this); + this.boundOnSyncStateChange = this.boundOnSyncStateChange || this.onSyncStateChange.bind(this); + this.boundOnRoomReceipt = this.boundOnRoomReceipt || this.onRoomReceipt.bind(this); + this.boundOnEventDecrypted = this.boundOnEventDecrypted || this.onEventDecrypted.bind(this); + MatrixClientPeg.get().on('event', this.boundOnEvent); MatrixClientPeg.get().on('Room.receipt', this.boundOnRoomReceipt); MatrixClientPeg.get().on('Event.decrypted', this.boundOnEventDecrypted); @@ -166,7 +168,7 @@ const Notifier = { }, stop: function() { - if (MatrixClientPeg.get() && this.boundOnRoomTimeline) { + if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener('Event', this.boundOnEvent); MatrixClientPeg.get().removeListener('Room.receipt', this.boundOnRoomReceipt); MatrixClientPeg.get().removeListener('Event.decrypted', this.boundOnEventDecrypted); diff --git a/src/Registration.js b/src/Registration.js index ac8baa3cca..ca162bac03 100644 --- a/src/Registration.js +++ b/src/Registration.js @@ -39,6 +39,8 @@ export const SAFE_LOCALPART_REGEX = /^[a-z0-9=_\-./]+$/; * If true, goes to the home page if the user cancels the action * @param {bool} options.go_welcome_on_cancel * If true, goes to the welcome page if the user cancels the action + * @param {bool} options.screen_after + * If present the screen to redirect to after a successful login or register. */ export async function startAnyRegistrationFlow(options) { if (options === undefined) options = {}; @@ -66,13 +68,21 @@ export async function startAnyRegistrationFlow(options) { // }); //} else { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createTrackedDialog('Registration required', '', QuestionDialog, { - title: _t("Registration Required"), - description: _t("You need to register to do this. Would you like to register now?"), - button: _t("Register"), + const modal = Modal.createTrackedDialog('Registration required', '', QuestionDialog, { + hasCancelButton: true, + quitOnly: true, + title: _t("Sign In or Create Account"), + description: _t("Use your account or create a new one to continue."), + button: _t("Create Account"), + extraButtons: [ + , + ], onFinished: (proceed) => { if (proceed) { - dis.dispatch({action: 'start_registration'}); + dis.dispatch({action: 'start_registration', screenAfterLogin: options.screen_after}); } else if (options.go_home_on_cancel) { dis.dispatch({action: 'view_home_page'}); } else if (options.go_welcome_on_cancel) { @@ -101,4 +111,3 @@ export async function startAnyRegistrationFlow(options) { // } // throw new Error("Register request succeeded when it should have returned 401!"); // } - diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 2fe64c994f..839d677069 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -1,6 +1,7 @@ /* Copyright 2016 OpenMarket Ltd Copyright 2017, 2018 New Vector Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,13 +20,9 @@ import React from 'react'; import {MatrixClientPeg} from './MatrixClientPeg'; import MultiInviter from './utils/MultiInviter'; import Modal from './Modal'; -import { getAddressType } from './UserAddress'; -import createRoom from './createRoom'; import * as sdk from './'; -import dis from './dispatcher'; -import DMRoomMap from './utils/DMRoomMap'; import { _t } from './languageHandler'; -import SettingsStore from "./settings/SettingsStore"; +import {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog"; /** * Invites multiple addresses to a room @@ -36,62 +33,27 @@ import SettingsStore from "./settings/SettingsStore"; * @param {string[]} addrs Array of strings of addresses to invite. May be matrix IDs or 3pids. * @returns {Promise} Promise */ -function inviteMultipleToRoom(roomId, addrs) { +export function inviteMultipleToRoom(roomId, addrs) { const inviter = new MultiInviter(roomId); return inviter.invite(addrs).then(states => Promise.resolve({states, inviter})); } 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, { - title: _t('Start a chat'), - description: _t("Who would you like to communicate with?"), - placeholder: (validAddressTypes) => { - // The set of valid address type can be mutated inside the dialog - // when you first have no IS but agree to use one in the dialog. - if (validAddressTypes.includes('email')) { - return _t("Email, name or Matrix ID"); - } - return _t("Name or Matrix ID"); - }, - validAddressTypes: ['mx-user-id', 'email'], - button: _t("Start Chat"), - onFinished: _onStartDmFinished, - }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); + // This dialog handles the room creation internally - we don't need to worry about it. + const InviteDialog = sdk.getComponent("dialogs.InviteDialog"); + Modal.createTrackedDialog( + 'Start DM', '', InviteDialog, {kind: KIND_DM}, + /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, + ); } export function showRoomInviteDialog(roomId) { - const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog"); - - Modal.createTrackedDialog('Chat Invite', '', AddressPickerDialog, { - title: _t('Invite new room members'), - button: _t('Send Invites'), - placeholder: (validAddressTypes) => { - // The set of valid address type can be mutated inside the dialog - // when you first have no IS but agree to use one in the dialog. - if (validAddressTypes.includes('email')) { - return _t("Email, name or Matrix ID"); - } - return _t("Name or Matrix ID"); - }, - validAddressTypes: ['mx-user-id', 'email'], - onFinished: (shouldInvite, addrs) => { - _onRoomInviteFinished(roomId, shouldInvite, addrs); - }, - }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); + // This dialog handles the room creation internally - we don't need to worry about it. + const InviteDialog = sdk.getComponent("dialogs.InviteDialog"); + Modal.createTrackedDialog( + 'Invite Users', '', InviteDialog, {kind: KIND_INVITE, roomId}, + /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, + ); } /** @@ -112,60 +74,6 @@ export function isValid3pidInvite(event) { return true; } -// TODO: Canonical DMs replaces this -function _onStartDmFinished(shouldInvite, addrs) { - if (!shouldInvite) return; - - const addrTexts = addrs.map((addr) => addr.address); - - if (_isDmChat(addrTexts)) { - const rooms = _getDirectMessageRooms(addrTexts[0]); - if (rooms.length > 0) { - // A Direct Message room already exists for this user, so reuse it - dis.dispatch({ - action: 'view_room', - room_id: rooms[0], - should_peek: false, - joining: false, - }); - } else { - // Start a new DM chat - createRoom({dmUserId: addrTexts[0]}).catch((err) => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to start chat', '', ErrorDialog, { - title: _t("Failed to start chat"), - description: ((err && err.message) ? err.message : _t("Operation failed")), - }); - }); - } - } else if (addrTexts.length === 1) { - // Start a new DM chat - createRoom({dmUserId: addrTexts[0]}).catch((err) => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to start chat', '', ErrorDialog, { - title: _t("Failed to start chat"), - description: ((err && err.message) ? err.message : _t("Operation failed")), - }); - }); - } else { - // Start multi user chat - let room; - createRoom().then((roomId) => { - room = MatrixClientPeg.get().getRoom(roomId); - return inviteMultipleToRoom(roomId, addrTexts); - }).then((result) => { - return _showAnyInviteErrors(result.states, room, result.inviter); - }).catch((err) => { - console.error(err.stack); - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, { - title: _t("Failed to invite"), - description: ((err && err.message) ? err.message : _t("Operation failed")), - }); - }); - } -} - export function inviteUsersToRoom(roomId, userIds) { return inviteMultipleToRoom(roomId, userIds).then((result) => { const room = MatrixClientPeg.get().getRoom(roomId); @@ -180,24 +88,6 @@ export function inviteUsersToRoom(roomId, userIds) { }); } -function _onRoomInviteFinished(roomId, shouldInvite, addrs) { - if (!shouldInvite) return; - - const addrTexts = addrs.map((addr) => addr.address); - - // Invite new users to a room - inviteUsersToRoom(roomId, addrTexts); -} - -// TODO: Immutable DMs replaces this -function _isDmChat(addrTexts) { - if (addrTexts.length === 1 && getAddressType(addrTexts[0]) === 'mx-user-id') { - return true; - } else { - return false; - } -} - function _showAnyInviteErrors(addrs, room, inviter) { // Show user any errors const failedUsers = Object.keys(addrs).filter(a => addrs[a] === 'error'); @@ -233,15 +123,3 @@ function _showAnyInviteErrors(addrs, room, inviter) { return addrs; } - -function _getDirectMessageRooms(addr) { - const dmRoomMap = new DMRoomMap(MatrixClientPeg.get()); - const dmRooms = dmRoomMap.getDMRoomsForUserId(addr); - const rooms = dmRooms.filter((dmRoom) => { - const room = MatrixClientPeg.get().getRoom(dmRoom); - if (room) { - return room.getMyMembership() === 'join'; - } - }); - return rooms; -} diff --git a/src/Rooms.js b/src/Rooms.js index f65e0ff218..218e970f35 100644 --- a/src/Rooms.js +++ b/src/Rooms.js @@ -23,7 +23,7 @@ import {MatrixClientPeg} from './MatrixClientPeg'; * of aliases. Otherwise return null; */ export function getDisplayAliasForRoom(room) { - return room.getCanonicalAlias() || room.getAliases()[0]; + return room.getCanonicalAlias() || room.getAltAliases()[0]; } /** diff --git a/src/SdkConfig.ts b/src/SdkConfig.ts index 8177a6c5b8..34f3402334 100644 --- a/src/SdkConfig.ts +++ b/src/SdkConfig.ts @@ -26,6 +26,13 @@ export const DEFAULTS: ConfigOptions = { integrations_rest_url: "https://scalar.vector.im/api", // Where to send bug reports. If not specified, bugs cannot be sent. bug_report_endpoint_url: null, + // Jitsi conference options + jitsi: { + // Default conference domain + preferredDomain: "jitsi.riot.im", + // Default Jitsi Meet API location + externalApiUrl: "https://jitsi.riot.im/libs/external_api.min.js", + }, }; export default class SdkConfig { diff --git a/src/Searching.js b/src/Searching.js index a5d945f64b..663328fe41 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -87,6 +87,13 @@ async function localSearch(searchTerm, roomId = undefined) { searchArgs.room_id = roomId; } + const emptyResult = { + results: [], + highlights: [], + }; + + if (searchTerm === "") return emptyResult; + const eventIndex = EventIndexPeg.get(); const localResult = await eventIndex.search(searchArgs); @@ -97,11 +104,6 @@ async function localSearch(searchTerm, roomId = undefined) { }, }; - const emptyResult = { - results: [], - highlights: [], - }; - const result = MatrixClientPeg.get()._processRoomEventsSearch( emptyResult, response); diff --git a/src/Skinner.js b/src/Skinner.js index 3baecc9fb3..87c5a7be7f 100644 --- a/src/Skinner.js +++ b/src/Skinner.js @@ -20,6 +20,7 @@ class Skinner { } getComponent(name) { + if (!name) throw new Error(`Invalid component name: ${name}`); if (this.components === null) { throw new Error( "Attempted to get a component before a skin has been loaded."+ @@ -41,13 +42,7 @@ class Skinner { }; // Check the skin first - let comp = doLookup(this.components); - - // If that failed, check against our own components - if (!comp) { - // Lazily load our own components because they might end up calling .getComponent() - comp = doLookup(require("./component-index").components); - } + const comp = doLookup(this.components); // Just return nothing instead of erroring - the consumer should be smart enough to // handle this at this point. @@ -75,6 +70,13 @@ class Skinner { const comp = skinObject.components[compKeys[i]]; this.addComponent(compKeys[i], comp); } + + // Now that we have a skin, load our components too + const idx = require("./component-index"); + if (!idx || !idx.components) throw new Error("Invalid react-sdk component index"); + for (const c in idx.components) { + if (!this.components[c]) this.components[c] = idx.components[c]; + } } addComponent(name, comp) { diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 20b8ba76da..d306978f78 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -81,6 +81,8 @@ class Command { } run(roomId, args) { + // if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me` + if (!this.runFn) return; return this.runFn.bind(this)(roomId, args); } @@ -769,7 +771,7 @@ export const CommandMap = { verify: new Command({ name: 'verify', args: ' ', - description: _td('Verifies a user, device, and pubkey tuple'), + description: _td('Verifies a user, session, and pubkey tuple'), runFn: function(roomId, args) { if (args) { const matches = args.match(/^(\S+) +(\S+) +(\S+)$/); @@ -783,22 +785,22 @@ export const CommandMap = { return success((async () => { const device = await cli.getStoredDevice(userId, deviceId); if (!device) { - throw new Error(_t('Unknown (user, device) pair:') + ` (${userId}, ${deviceId})`); + throw new Error(_t('Unknown (user, session) pair:') + ` (${userId}, ${deviceId})`); } const deviceTrust = await cli.checkDeviceTrust(userId, deviceId); if (deviceTrust.isVerified()) { if (device.getFingerprint() === fingerprint) { - throw new Error(_t('Device already verified!')); + throw new Error(_t('Session already verified!')); } else { - throw new Error(_t('WARNING: Device already verified, but keys do NOT MATCH!')); + throw new Error(_t('WARNING: Session already verified, but keys do NOT MATCH!')); } } if (device.getFingerprint() !== fingerprint) { const fprint = device.getFingerprint(); throw new Error( - _t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' + + _t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session' + ' %(deviceId)s is "%(fprint)s" which does not match the provided key ' + '"%(fingerprint)s". This could mean your communications are being intercepted!', { @@ -819,7 +821,7 @@ export const CommandMap = {

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

@@ -891,6 +893,26 @@ export const CommandMap = { }, category: CommandCategories.advanced, }), + + whois: new Command({ + name: "whois", + description: _td("Displays information about a user"), + args: '', + runFn: function(roomId, userId) { + if (!userId || !userId.startsWith("@") || !userId.includes(":")) { + return reject(this.getUsage()); + } + + const member = MatrixClientPeg.get().getRoom(roomId).getMember(userId); + + dis.dispatch({ + action: 'view_user', + member: member || {userId}, + }); + return success(); + }, + category: CommandCategories.advanced, + }), }; /* eslint-enable babel/no-invalid-this */ @@ -905,25 +927,25 @@ const aliases = { /** - * Process the given text for /commands and perform them. + * Process the given text for /commands and return a bound method to perform them. * @param {string} roomId The room in which the command was performed. * @param {string} input The raw text input by the user. - * @return {Object|null} An object with the property 'error' if there was an error + * @return {null|function(): Object} Function returning an object with the property 'error' if there was an error * processing the command, or 'promise' if a request was sent out. * Returns null if the input didn't match a command. */ -export function processCommandInput(roomId, input) { +export function getCommand(roomId, input) { // trim any trailing whitespace, as it can confuse the parser for // IRC-style commands input = input.replace(/\s+$/, ''); if (input[0] !== '/') return null; // not a command - const bits = input.match(/^(\S+?)( +((.|\n)*))?$/); + const bits = input.match(/^(\S+?)(?: +((.|\n)*))?$/); let cmd; let args; if (bits) { cmd = bits[1].substring(1).toLowerCase(); - args = bits[3]; + args = bits[2]; } else { cmd = input; } @@ -932,11 +954,6 @@ export function processCommandInput(roomId, input) { cmd = aliases[cmd]; } if (CommandMap[cmd]) { - // if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me` - if (!CommandMap[cmd].runFn) return null; - - return CommandMap[cmd].run(roomId, args); - } else { - return reject(_t('Unrecognised command:') + ' ' + input); + return () => CommandMap[cmd].run(roomId, args); } } diff --git a/src/TextForEvent.js b/src/TextForEvent.js index a0d088affb..6b1c1dcd2d 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -127,6 +127,13 @@ function textForRoomNameEvent(ev) { if (!ev.getContent().name || ev.getContent().name.trim().length === 0) { return _t('%(senderDisplayName)s removed the room name.', {senderDisplayName}); } + if (ev.getPrevContent().name) { + return _t('%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.', { + senderDisplayName, + oldRoomName: ev.getPrevContent().name, + newRoomName: ev.getContent().name, + }); + } return _t('%(senderDisplayName)s changed the room name to %(roomName)s.', { senderDisplayName, roomName: ev.getContent().name, @@ -269,61 +276,55 @@ function textForMessageEvent(ev) { return message; } -function textForRoomAliasesEvent(ev) { - // An alternative implementation of this as a first-class event can be found at - // https://github.com/matrix-org/matrix-react-sdk/blob/dc7212ec2bd12e1917233ed7153b3e0ef529a135/src/components/views/messages/RoomAliasesEvent.js - // This feels a bit overkill though, and it's not clear the i18n really needs it - // so instead it's landing as a simple textual event. - - const senderName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); - const oldAliases = ev.getPrevContent().aliases || []; - const newAliases = ev.getContent().aliases || []; - - const addedAliases = newAliases.filter((x) => !oldAliases.includes(x)); - const removedAliases = oldAliases.filter((x) => !newAliases.includes(x)); - - if (!addedAliases.length && !removedAliases.length) { - return ''; - } - - if (addedAliases.length && !removedAliases.length) { - return _t('%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.', { - senderName: senderName, - count: addedAliases.length, - addedAddresses: addedAliases.join(', '), - }); - } else if (!addedAliases.length && removedAliases.length) { - return _t('%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.', { - senderName: senderName, - count: removedAliases.length, - removedAddresses: removedAliases.join(', '), - }); - } else { - return _t( - '%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.', { - senderName: senderName, - addedAddresses: addedAliases.join(', '), - removedAddresses: removedAliases.join(', '), - }, - ); - } -} - function textForCanonicalAliasEvent(ev) { const senderName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const oldAlias = ev.getPrevContent().alias; + const oldAltAliases = ev.getPrevContent().alt_aliases || []; const newAlias = ev.getContent().alias; + const newAltAliases = ev.getContent().alt_aliases || []; + const removedAltAliases = oldAltAliases.filter(alias => !newAltAliases.includes(alias)); + const addedAltAliases = newAltAliases.filter(alias => !oldAltAliases.includes(alias)); - if (newAlias) { - return _t('%(senderName)s set the main address for this room to %(address)s.', { - senderName: senderName, - address: ev.getContent().alias, - }); - } else if (oldAlias) { - return _t('%(senderName)s removed the main address for this room.', { + if (!removedAltAliases.length && !addedAltAliases.length) { + if (newAlias) { + return _t('%(senderName)s set the main address for this room to %(address)s.', { + senderName: senderName, + address: ev.getContent().alias, + }); + } else if (oldAlias) { + return _t('%(senderName)s removed the main address for this room.', { + senderName: senderName, + }); + } + } else if (newAlias === oldAlias) { + if (addedAltAliases.length && !removedAltAliases.length) { + return _t('%(senderName)s added the alternative addresses %(addresses)s for this room.', { + senderName: senderName, + addresses: addedAltAliases.join(", "), + count: addedAltAliases.length, + }); + } if (removedAltAliases.length && !addedAltAliases.length) { + return _t('%(senderName)s removed the alternative addresses %(addresses)s for this room.', { + senderName: senderName, + addresses: removedAltAliases.join(", "), + count: removedAltAliases.length, + }); + } if (removedAltAliases.length && addedAltAliases.length) { + return _t('%(senderName)s changed the alternative addresses for this room.', { + senderName: senderName, + }); + } + } else { + // both alias and alt_aliases where modified + return _t('%(senderName)s changed the main and alternative addresses for this room.', { senderName: senderName, }); } + // in case there is no difference between the two events, + // say something as we can't simply hide the tile from here + return _t('%(senderName)s changed the addresses for this room.', { + senderName: senderName, + }); } function textForCallAnswerEvent(event) { @@ -418,14 +419,6 @@ function textForHistoryVisibilityEvent(event) { } } -function textForEncryptionEvent(event) { - const senderName = event.sender ? event.sender.name : event.getSender(); - return _t('%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).', { - senderName, - algorithm: event.getContent().algorithm, - }); -} - // Currently will only display a change if a user's power level is changed function textForPowerEvent(event) { const senderName = event.sender ? event.sender.name : event.getSender(); @@ -596,14 +589,12 @@ const handlers = { }; const stateHandlers = { - 'm.room.aliases': textForRoomAliasesEvent, 'm.room.canonical_alias': textForCanonicalAliasEvent, 'm.room.name': textForRoomNameEvent, 'm.room.topic': textForTopicEvent, 'm.room.member': textForMemberEvent, 'm.room.third_party_invite': textForThreePidInviteEvent, 'm.room.history_visibility': textForHistoryVisibilityEvent, - 'm.room.encryption': textForEncryptionEvent, 'm.room.power_levels': textForPowerEvent, 'm.room.pinned_events': textForPinnedEvent, 'm.room.server_acl': textForServerACLEvent, diff --git a/src/Tinter.js b/src/Tinter.js index de9ae94097..24a4d25a00 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -143,10 +143,14 @@ class Tinter { * over time then the best bet is to register a single callback for the * entire set. * + * To ensure the tintable work happens at least once, it is also called as + * part of registration. + * * @param {Function} tintable Function to call when the tint changes. */ registerTintable(tintable) { this.tintables.push(tintable); + tintable(); } getKeyRgb() { diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index d40a8ab637..30c2389b1e 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -27,6 +27,7 @@ import {MatrixClientPeg} from "./MatrixClientPeg"; import SettingsStore from "./settings/SettingsStore"; import WidgetOpenIDPermissionsDialog from "./components/views/dialogs/WidgetOpenIDPermissionsDialog"; import WidgetUtils from "./utils/WidgetUtils"; +import {KnownWidgetActions} from "./widgets/WidgetApi"; if (!global.mxFromWidgetMessaging) { global.mxFromWidgetMessaging = new FromWidgetPostMessageApi(); @@ -75,6 +76,17 @@ export default class WidgetMessaging { }); } + /** + * Tells the widget that the client is ready to handle further widget requests. + * @returns {Promise<*>} Resolves after the widget has acknowledged the ready message. + */ + flagReadyToContinue() { + return this.messageToWidget({ + api: OUTBOUND_API_NAME, + action: KnownWidgetActions.ClientReady, + }); + } + /** * Request a screenshot from a widget * @return {Promise} To be resolved with screenshot data when it has been generated diff --git a/src/accessibility/KeyboardShortcuts.tsx b/src/accessibility/KeyboardShortcuts.tsx new file mode 100644 index 0000000000..bcbf3d6810 --- /dev/null +++ b/src/accessibility/KeyboardShortcuts.tsx @@ -0,0 +1,360 @@ +/* +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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import * as React from "react"; +import classNames from "classnames"; + +import * as sdk from "../index"; +import Modal from "../Modal"; +import { _t, _td } from "../languageHandler"; +import {isMac, Key} from "../Keyboard"; + +// TS: once languageHandler is TS we can probably inline this into the enum +_td("Navigation"); +_td("Calls"); +_td("Composer"); +_td("Room List"); +_td("Autocomplete"); + +export enum Categories { + NAVIGATION = "Navigation", + CALLS = "Calls", + COMPOSER = "Composer", + ROOM_LIST = "Room List", + AUTOCOMPLETE = "Autocomplete", +} + +// TS: once languageHandler is TS we can probably inline this into the enum +_td("Alt"); +_td("Alt Gr"); +_td("Shift"); +_td("Super"); +_td("Ctrl"); + +export enum Modifiers { + ALT = "Alt", // Option on Mac and displayed as an Icon + ALT_GR = "Alt Gr", + SHIFT = "Shift", + SUPER = "Super", // should this be "Windows"? + // Instead of using below, consider CMD_OR_CTRL + COMMAND = "Command", // This gets displayed as an Icon + CONTROL = "Ctrl", +} + +// Meta-modifier: isMac ? CMD : CONTROL +export const CMD_OR_CTRL = isMac ? Modifiers.COMMAND : Modifiers.CONTROL; + +interface IKeybind { + modifiers?: Modifiers[]; + key: string; // TS: fix this once Key is an enum +} + +interface IShortcut { + keybinds: IKeybind[]; + description: string; +} + +const shortcuts: Record = { + [Categories.COMPOSER]: [ + { + keybinds: [{ + modifiers: [CMD_OR_CTRL], + key: Key.B, + }], + description: _td("Toggle Bold"), + }, { + keybinds: [{ + modifiers: [CMD_OR_CTRL], + key: Key.I, + }], + description: _td("Toggle Italics"), + }, { + keybinds: [{ + modifiers: [CMD_OR_CTRL], + key: Key.GREATER_THAN, + }], + description: _td("Toggle Quote"), + }, { + keybinds: [{ + modifiers: [Modifiers.SHIFT], + key: Key.ENTER, + }], + description: _td("New line"), + }, { + keybinds: [{ + key: Key.ARROW_UP, + }, { + key: Key.ARROW_DOWN, + }], + description: _td("Navigate recent messages to edit"), + }, { + keybinds: [{ + modifiers: [CMD_OR_CTRL], + key: Key.HOME, + }, { + modifiers: [CMD_OR_CTRL], + key: Key.END, + }], + description: _td("Jump to start/end of the composer"), + }, { + keybinds: [{ + modifiers: [Modifiers.CONTROL, Modifiers.ALT], + key: Key.ARROW_UP, + }, { + modifiers: [Modifiers.CONTROL, Modifiers.ALT], + key: Key.ARROW_DOWN, + }], + description: _td("Navigate composer history"), + }, { + keybinds: [{ + key: Key.ESCAPE, + }], + description: _td("Cancel replying to a message"), + }, + ], + + [Categories.CALLS]: [ + { + keybinds: [{ + modifiers: [CMD_OR_CTRL], + key: Key.D, + }], + description: _td("Toggle microphone mute"), + }, { + keybinds: [{ + modifiers: [CMD_OR_CTRL], + key: Key.E, + }], + description: _td("Toggle video on/off"), + }, + ], + + [Categories.ROOM_LIST]: [ + { + keybinds: [{ + modifiers: [CMD_OR_CTRL], + key: Key.K, + }], + description: _td("Jump to room search"), + }, { + keybinds: [{ + key: Key.ARROW_UP, + }, { + key: Key.ARROW_DOWN, + }], + description: _td("Navigate up/down in the room list"), + }, { + keybinds: [{ + key: Key.ENTER, + }], + description: _td("Select room from the room list"), + }, { + keybinds: [{ + key: Key.ARROW_LEFT, + }], + description: _td("Collapse room list section"), + }, { + keybinds: [{ + key: Key.ARROW_RIGHT, + }], + description: _td("Expand room list section"), + }, { + keybinds: [{ + key: Key.ESCAPE, + }], + description: _td("Clear room list filter field"), + }, + ], + + [Categories.NAVIGATION]: [ + { + keybinds: [{ + key: Key.PAGE_UP, + }, { + key: Key.PAGE_DOWN, + }], + description: _td("Scroll up/down in the timeline"), + }, { + keybinds: [{ + modifiers: [Modifiers.ALT, Modifiers.SHIFT], + key: Key.ARROW_UP, + }, { + modifiers: [Modifiers.ALT, Modifiers.SHIFT], + key: Key.ARROW_DOWN, + }], + description: _td("Previous/next unread room or DM"), + }, { + keybinds: [{ + modifiers: [Modifiers.ALT], + key: Key.ARROW_UP, + }, { + modifiers: [Modifiers.ALT], + key: Key.ARROW_DOWN, + }], + description: _td("Previous/next room or DM"), + }, { + keybinds: [{ + modifiers: [CMD_OR_CTRL], + key: Key.BACKTICK, + }], + description: _td("Toggle the top left menu"), + }, { + keybinds: [{ + key: Key.ESCAPE, + }], + description: _td("Close dialog or context menu"), + }, { + keybinds: [{ + key: Key.ENTER, + }, { + key: Key.SPACE, + }], + description: _td("Activate selected button"), + }, { + keybinds: [{ + modifiers: [CMD_OR_CTRL], + key: Key.PERIOD, + }], + description: _td("Toggle right panel"), + }, { + keybinds: [{ + modifiers: [CMD_OR_CTRL], + key: Key.SLASH, + }], + description: _td("Toggle this dialog"), + }, + ], + + [Categories.AUTOCOMPLETE]: [ + { + keybinds: [{ + key: Key.ARROW_UP, + }, { + key: Key.ARROW_DOWN, + }], + description: _td("Move autocomplete selection up/down"), + }, { + keybinds: [{ + key: Key.ESCAPE, + }], + description: _td("Cancel autocomplete"), + }, + ], +}; + +const categoryOrder = [ + Categories.COMPOSER, + Categories.CALLS, + Categories.ROOM_LIST, + Categories.AUTOCOMPLETE, + Categories.NAVIGATION, +]; + +interface IModal { + close: () => void; + finished: Promise; +} + +const modifierIcon: Record = { + [Modifiers.COMMAND]: "⌘", +}; + +if (isMac) { + modifierIcon[Modifiers.ALT] = "⌥"; +} + +const alternateKeyName: Record = { + [Key.PAGE_UP]: _td("Page Up"), + [Key.PAGE_DOWN]: _td("Page Down"), + [Key.ESCAPE]: _td("Esc"), + [Key.ENTER]: _td("Enter"), + [Key.SPACE]: _td("Space"), + [Key.HOME]: _td("Home"), + [Key.END]: _td("End"), +}; +const keyIcon: Record = { + [Key.ARROW_UP]: "↑", + [Key.ARROW_DOWN]: "↓", + [Key.ARROW_LEFT]: "←", + [Key.ARROW_RIGHT]: "→", +}; + +const Shortcut: React.FC<{ + shortcut: IShortcut; +}> = ({shortcut}) => { + const classes = classNames({ + "mx_KeyboardShortcutsDialog_inline": shortcut.keybinds.every(k => !k.modifiers || k.modifiers.length === 0), + }); + + return
+
{ _t(shortcut.description) }
+ { shortcut.keybinds.map(s => { + let text = s.key; + if (alternateKeyName[s.key]) { + text = _t(alternateKeyName[s.key]); + } else if (keyIcon[s.key]) { + text = keyIcon[s.key]; + } + + return
+ { s.modifiers && s.modifiers.map(m => { + return + { modifierIcon[m] || _t(m) }+ + ; + }) } + { text } +
; + }) } +
; +}; + +let activeModal: IModal = null; +export const toggleDialog = () => { + if (activeModal) { + activeModal.close(); + activeModal = null; + return; + } + + const sections = categoryOrder.map(category => { + const list = shortcuts[category]; + return
+

{_t(category)}

+
{list.map(shortcut => )}
+
; + }); + + const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); + activeModal = Modal.createTrackedDialog("Keyboard Shortcuts", "", InfoDialog, { + className: "mx_KeyboardShortcutsDialog", + title: _t("Keyboard Shortcuts"), + description: sections, + hasCloseButton: true, + onKeyDown: (ev) => { + if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey && ev.key === Key.SLASH) { // Ctrl + / + ev.stopPropagation(); + activeModal.close(); + } + }, + onFinished: () => { + activeModal = null; + }, + }); +}; + +export const registerShortcut = (category: Categories, defn: IShortcut) => { + shortcuts[category].push(defn); +}; diff --git a/src/accessibility/RovingTabIndex.js b/src/accessibility/RovingTabIndex.js new file mode 100644 index 0000000000..b481f08fe2 --- /dev/null +++ b/src/accessibility/RovingTabIndex.js @@ -0,0 +1,224 @@ +/* +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. +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, { + createContext, + useCallback, + useContext, + useLayoutEffect, + useMemo, + useRef, + useReducer, +} from "react"; +import PropTypes from "prop-types"; +import {Key} from "../Keyboard"; + +/** + * Module to simplify implementing the Roving TabIndex accessibility technique + * + * Wrap the Widget in an RovingTabIndexContextProvider + * and then for all buttons make use of useRovingTabIndex or RovingTabIndexWrapper. + * The code will keep track of which tabIndex was most recently focused and expose that information as `isActive` which + * can then be used to only set the tabIndex to 0 as expected by the roving tabindex technique. + * When the active button gets unmounted the closest button will be chosen as expected. + * Initially the first button to mount will be given active state. + * + * https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets#Technique_1_Roving_tabindex + */ + +const DOCUMENT_POSITION_PRECEDING = 2; + +const RovingTabIndexContext = createContext({ + state: { + activeRef: null, + refs: [], // list of refs in DOM order + }, + dispatch: () => {}, +}); +RovingTabIndexContext.displayName = "RovingTabIndexContext"; + +// TODO use a TypeScript type here +const types = { + REGISTER: "REGISTER", + UNREGISTER: "UNREGISTER", + SET_FOCUS: "SET_FOCUS", +}; + +const reducer = (state, action) => { + switch (action.type) { + case types.REGISTER: { + if (state.refs.length === 0) { + // Our list of refs was empty, set activeRef to this first item + return { + ...state, + activeRef: action.payload.ref, + refs: [action.payload.ref], + }; + } + + if (state.refs.includes(action.payload.ref)) { + return state; // already in refs, this should not happen + } + + // find the index of the first ref which is not preceding this one in DOM order + let newIndex = state.refs.findIndex(ref => { + return ref.current.compareDocumentPosition(action.payload.ref.current) & DOCUMENT_POSITION_PRECEDING; + }); + + if (newIndex < 0) { + newIndex = state.refs.length; // append to the end + } + + // update the refs list + return { + ...state, + refs: [ + ...state.refs.slice(0, newIndex), + action.payload.ref, + ...state.refs.slice(newIndex), + ], + }; + } + case types.UNREGISTER: { + // filter out the ref which we are removing + const refs = state.refs.filter(r => r !== action.payload.ref); + + if (refs.length === state.refs.length) { + return state; // already removed, this should not happen + } + + if (state.activeRef === action.payload.ref) { + // we just removed the active ref, need to replace it + // pick the ref which is now in the index the old ref was in + const oldIndex = state.refs.findIndex(r => r === action.payload.ref); + return { + ...state, + activeRef: oldIndex >= refs.length ? refs[refs.length - 1] : refs[oldIndex], + refs, + }; + } + + // update the refs list + return { + ...state, + refs, + }; + } + case types.SET_FOCUS: { + // update active ref + return { + ...state, + activeRef: action.payload.ref, + }; + } + default: + return state; + } +}; + +export const RovingTabIndexProvider = ({children, handleHomeEnd, onKeyDown}) => { + const [state, dispatch] = useReducer(reducer, { + activeRef: null, + refs: [], + }); + + const context = useMemo(() => ({state, dispatch}), [state]); + + const onKeyDownHandler = useCallback((ev) => { + let handled = false; + if (handleHomeEnd) { + // check if we actually have any items + switch (ev.key) { + case Key.HOME: + handled = true; + // move focus to first item + if (context.state.refs.length > 0) { + context.state.refs[0].current.focus(); + } + break; + case Key.END: + handled = true; + // move focus to last item + if (context.state.refs.length > 0) { + context.state.refs[context.state.refs.length - 1].current.focus(); + } + break; + } + } + + if (handled) { + ev.preventDefault(); + ev.stopPropagation(); + } else if (onKeyDown) { + return onKeyDown(ev); + } + }, [context.state, onKeyDown, handleHomeEnd]); + + return + { children({onKeyDownHandler}) } + ; +}; +RovingTabIndexProvider.propTypes = { + handleHomeEnd: PropTypes.bool, + onKeyDown: PropTypes.func, +}; + +// Hook to register a roving tab index +// inputRef parameter specifies the ref to use +// onFocus should be called when the index gained focus in any manner +// isActive should be used to set tabIndex in a manner such as `tabIndex={isActive ? 0 : -1}` +// ref should be passed to a DOM node which will be used for DOM compareDocumentPosition +export const useRovingTabIndex = (inputRef) => { + const context = useContext(RovingTabIndexContext); + let ref = useRef(null); + + if (inputRef) { + // if we are given a ref, use it instead of ours + ref = inputRef; + } + + // setup (after refs) + useLayoutEffect(() => { + context.dispatch({ + type: types.REGISTER, + payload: {ref}, + }); + // teardown + return () => { + context.dispatch({ + type: types.UNREGISTER, + payload: {ref}, + }); + }; + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const onFocus = useCallback(() => { + context.dispatch({ + type: types.SET_FOCUS, + payload: {ref}, + }); + }, [ref, context]); + + const isActive = context.state.activeRef === ref; + return [onFocus, isActive, ref]; +}; + +// Wrapper to allow use of useRovingTabIndex outside of React Functional Components. +export const RovingTabIndexWrapper = ({children, inputRef}) => { + const [onFocus, isActive, ref] = useRovingTabIndex(inputRef); + return children({onFocus, isActive, ref}); +}; + diff --git a/src/actions/RoomListActions.js b/src/actions/RoomListActions.js index d534fe5d1d..10a3848dda 100644 --- a/src/actions/RoomListActions.js +++ b/src/actions/RoomListActions.js @@ -15,7 +15,7 @@ limitations under the License. */ import { asyncAction } from './actionCreators'; -import RoomListStore from '../stores/RoomListStore'; +import RoomListStore, {TAG_DM} from '../stores/RoomListStore'; import Modal from '../Modal'; import * as Rooms from '../Rooms'; import { _t } from '../languageHandler'; @@ -73,11 +73,11 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex, const roomId = room.roomId; // Evil hack to get DMs behaving - if ((oldTag === undefined && newTag === 'im.vector.fake.direct') || - (oldTag === 'im.vector.fake.direct' && newTag === undefined) + if ((oldTag === undefined && newTag === TAG_DM) || + (oldTag === TAG_DM && newTag === undefined) ) { return Rooms.guessAndSetDMRoom( - room, newTag === 'im.vector.fake.direct', + room, newTag === TAG_DM, ).catch((err) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to set direct chat tag " + err); @@ -91,10 +91,10 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex, const hasChangedSubLists = oldTag !== newTag; // More evilness: We will still be dealing with moving to favourites/low prio, - // but we avoid ever doing a request with 'im.vector.fake.direct`. + // but we avoid ever doing a request with TAG_DM. // // if we moved lists, remove the old tag - if (oldTag && oldTag !== 'im.vector.fake.direct' && + if (oldTag && oldTag !== TAG_DM && hasChangedSubLists ) { const promiseToDelete = matrixClient.deleteRoomTag( @@ -112,7 +112,7 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex, } // if we moved lists or the ordering changed, add the new tag - if (newTag && newTag !== 'im.vector.fake.direct' && + if (newTag && newTag !== TAG_DM && (hasChangedSubLists || metaData) ) { // metaData is the body of the PUT to set the tag, so it must diff --git a/src/async-components/views/dialogs/EncryptedEventDialog.js b/src/async-components/views/dialogs/EncryptedEventDialog.js index f6e17b1c84..b602cf60fe 100644 --- a/src/async-components/views/dialogs/EncryptedEventDialog.js +++ b/src/async-components/views/dialogs/EncryptedEventDialog.js @@ -191,7 +191,7 @@ export default createReactClass({

{ _t('Event information') }

{ this._renderEventInfo() } -

{ _t('Sender device information') }

+

{ _t('Sender session information') }

{ this._renderDeviceInfo() }
diff --git a/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js new file mode 100644 index 0000000000..120b086ef6 --- /dev/null +++ b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js @@ -0,0 +1,73 @@ +/* +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. +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 * as sdk from '../../../../index'; +import PropTypes from 'prop-types'; +import dis from "../../../../dispatcher"; +import { _t } from '../../../../languageHandler'; + +import SettingsStore, {SettingLevel} from "../../../../settings/SettingsStore"; +import EventIndexPeg from "../../../../indexing/EventIndexPeg"; + +/* + * Allows the user to disable the Event Index. + */ +export default class DisableEventIndexDialog extends React.Component { + static propTypes = { + onFinished: PropTypes.func.isRequired, + } + + constructor(props) { + super(props); + + this.state = { + disabling: false, + }; + } + + _onDisable = async () => { + this.setState({ + disabling: true, + }); + + await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false); + await EventIndexPeg.deleteEventIndex(); + this.props.onFinished(); + dis.dispatch({ action: 'view_user_settings' }); + } + + render() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const Spinner = sdk.getComponent('elements.Spinner'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + + return ( + + {_t("If disabled, messages from encrypted rooms won't appear in search results.")} + {this.state.disabling ? :
} + + + ); + } +} diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js new file mode 100644 index 0000000000..3d7249b5a1 --- /dev/null +++ b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js @@ -0,0 +1,199 @@ +/* +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. +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 * as sdk from '../../../../index'; +import PropTypes from 'prop-types'; +import { _t } from '../../../../languageHandler'; +import SettingsStore, {SettingLevel} from "../../../../settings/SettingsStore"; + +import Modal from '../../../../Modal'; +import {formatBytes, formatCountLong} from "../../../../utils/FormattingUtils"; +import EventIndexPeg from "../../../../indexing/EventIndexPeg"; + +/* + * Allows the user to introspect the event index state and disable it. + */ +export default class ManageEventIndexDialog extends React.Component { + static propTypes = { + onFinished: PropTypes.func.isRequired, + } + + constructor(props) { + super(props); + + this.state = { + eventIndexSize: 0, + eventCount: 0, + crawlingRoomsCount: 0, + roomCount: 0, + currentRoom: null, + crawlerSleepTime: + SettingsStore.getValueAt(SettingLevel.DEVICE, 'crawlerSleepTime'), + }; + } + + updateCurrentRoom = async (room) => { + const eventIndex = EventIndexPeg.get(); + let stats; + + try { + stats = await eventIndex.getStats(); + } catch { + // This call may fail if sporadically, not a huge issue as we will + // try later again and probably succeed. + return; + } + + let currentRoom = null; + + if (room) currentRoom = room.name; + const roomStats = eventIndex.crawlingRooms(); + const crawlingRoomsCount = roomStats.crawlingRooms.size; + const roomCount = roomStats.totalRooms.size; + + this.setState({ + eventIndexSize: stats.size, + eventCount: stats.eventCount, + crawlingRoomsCount: crawlingRoomsCount, + roomCount: roomCount, + currentRoom: currentRoom, + }); + }; + + componentWillUnmount(): void { + const eventIndex = EventIndexPeg.get(); + + if (eventIndex !== null) { + eventIndex.removeListener("changedCheckpoint", this.updateCurrentRoom); + } + } + + async componentWillMount(): void { + let eventIndexSize = 0; + let crawlingRoomsCount = 0; + let roomCount = 0; + let eventCount = 0; + let currentRoom = null; + + const eventIndex = EventIndexPeg.get(); + + if (eventIndex !== null) { + eventIndex.on("changedCheckpoint", this.updateCurrentRoom); + + try { + const stats = await eventIndex.getStats(); + eventIndexSize = stats.size; + eventCount = stats.eventCount; + } catch { + // This call may fail if sporadically, not a huge issue as we + // will try later again in the updateCurrentRoom call and + // probably succeed. + } + + const roomStats = eventIndex.crawlingRooms(); + crawlingRoomsCount = roomStats.crawlingRooms.size; + roomCount = roomStats.totalRooms.size; + + const room = eventIndex.currentRoom(); + if (room) currentRoom = room.name; + } + + this.setState({ + eventIndexSize, + eventCount, + crawlingRoomsCount, + roomCount, + currentRoom, + }); + } + + _onDisable = async () => { + Modal.createTrackedDialogAsync("Disable message search", "Disable message search", + import("./DisableEventIndexDialog"), + null, null, /* priority = */ false, /* static = */ true, + ); + } + + _onDone = () => { + this.props.onFinished(true); + } + + _onCrawlerSleepTimeChange = (e) => { + this.setState({crawlerSleepTime: e.target.value}); + SettingsStore.setValue("crawlerSleepTime", null, SettingLevel.DEVICE, e.target.value); + } + + render() { + let crawlerState; + + if (this.state.currentRoom === null) { + crawlerState = _t("Not currently indexing messages for any room."); + } else { + crawlerState = ( + _t("Currently indexing: %(currentRoom)s.", { currentRoom: this.state.currentRoom }) + ); + } + + const Field = sdk.getComponent('views.elements.Field'); + + const doneRooms = Math.max(0, (this.state.roomCount - this.state.crawlingRoomsCount)); + + const eventIndexingSettings = ( +
+ { + _t( "Riot is securely caching encrypted messages locally for them " + + "to appear in search results:", + ) + } +
+ {crawlerState}
+ {_t("Space used:")} {formatBytes(this.state.eventIndexSize, 0)}
+ {_t("Indexed messages:")} {formatCountLong(this.state.eventCount)}
+ {_t("Indexed rooms:")} {_t("%(doneRooms)s out of %(totalRooms)s", { + doneRooms: formatCountLong(doneRooms), + totalRooms: formatCountLong(this.state.roomCount), + })}
+ +
+
+ ); + + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + + return ( + + {eventIndexingSettings} + + + ); + } +} diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 8940239cfd..3a480a2579 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -24,6 +24,7 @@ import { scorePassword } from '../../../../utils/PasswordScorer'; import { _t } from '../../../../languageHandler'; import { accessSecretStorage } from '../../../../CrossSigningManager'; import SettingsStore from '../../../../settings/SettingsStore'; +import AccessibleButton from "../../../../components/views/elements/AccessibleButton"; const PHASE_PASSPHRASE = 0; const PHASE_PASSPHRASE_CONFIRM = 1; @@ -52,7 +53,6 @@ function selectText(target) { */ export default class CreateKeyBackupDialog extends React.PureComponent { static propTypes = { - secureSecretStorage: PropTypes.bool, onFinished: PropTypes.func.isRequired, } @@ -64,32 +64,28 @@ export default class CreateKeyBackupDialog extends React.PureComponent { this._setZxcvbnResultTimeout = null; this.state = { - secureSecretStorage: props.secureSecretStorage, + secureSecretStorage: null, phase: PHASE_PASSPHRASE, passPhrase: '', passPhraseConfirm: '', copied: false, downloaded: false, 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; - } } - componentDidMount() { + async componentDidMount() { + const cli = MatrixClientPeg.get(); + const secureSecretStorage = ( + SettingsStore.isFeatureEnabled("feature_cross_signing") && + await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing") + ); + this.setState({ secureSecretStorage }); + // If we're using secret storage, skip ahead to the backing up step, as // `accessSecretStorage` will handle passphrases as needed. - if (this.state.secureSecretStorage) { + if (secureSecretStorage) { + this.setState({ phase: PHASE_BACKINGUP }); this._createBackup(); } } @@ -192,45 +188,38 @@ export default class CreateKeyBackupDialog extends React.PureComponent { }); } - _onPassPhraseNextClick = () => { - this.setState({phase: PHASE_PASSPHRASE_CONFIRM}); - } + _onPassPhraseNextClick = async (e) => { + e.preventDefault(); - _onPassPhraseKeyPress = async (e) => { - if (e.key === 'Enter') { - // If we're waiting for the timeout before updating the result at this point, - // skip ahead and do it now, otherwise we'll deny the attempt to proceed - // even if the user entered a valid passphrase - if (this._setZxcvbnResultTimeout !== null) { - clearTimeout(this._setZxcvbnResultTimeout); - this._setZxcvbnResultTimeout = null; - await new Promise((resolve) => { - this.setState({ - zxcvbnResult: scorePassword(this.state.passPhrase), - }, resolve); - }); - } - if (this._passPhraseIsValid()) { - this._onPassPhraseNextClick(); - } + // If we're waiting for the timeout before updating the result at this point, + // skip ahead and do it now, otherwise we'll deny the attempt to proceed + // even if the user entered a valid passphrase + if (this._setZxcvbnResultTimeout !== null) { + clearTimeout(this._setZxcvbnResultTimeout); + this._setZxcvbnResultTimeout = null; + await new Promise((resolve) => { + this.setState({ + zxcvbnResult: scorePassword(this.state.passPhrase), + }, resolve); + }); } - } + if (this._passPhraseIsValid()) { + this.setState({phase: PHASE_PASSPHRASE_CONFIRM}); + } + }; + + _onPassPhraseConfirmNextClick = async (e) => { + e.preventDefault(); + + if (this.state.passPhrase !== this.state.passPhraseConfirm) return; - _onPassPhraseConfirmNextClick = async () => { this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(this.state.passPhrase); this.setState({ - setPassPhrase: true, copied: false, downloaded: false, phase: PHASE_SHOWKEY, }); - } - - _onPassPhraseConfirmKeyPress = (e) => { - if (e.key === 'Enter' && this.state.passPhrase === this.state.passPhraseConfirm) { - this._onPassPhraseConfirmNextClick(); - } - } + }; _onSetAgainClick = () => { this.setState({ @@ -301,7 +290,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
; } - return
+ return

{_t( "Warning: You should only set up key backup from a trusted computer.", {}, { b: sub => {sub} }, @@ -316,7 +305,6 @@ export default class CreateKeyBackupDialog extends React.PureComponent {

- {_t("Advanced")} -

+ + {_t("Set up with a recovery key")} + -
; + ; } _renderPhasePassPhraseConfirm() { @@ -373,7 +362,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
; } const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - return
+ return

{_t( "Please enter your passphrase a second time to confirm.", )}

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

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

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

-

{bodyText}

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

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

{hackWarning}

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

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

{_t( diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 628214a2bb..78e750b817 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.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. @@ -16,23 +16,24 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import * as sdk from '../../../../index'; import {MatrixClientPeg} from '../../../../MatrixClientPeg'; import { scorePassword } from '../../../../utils/PasswordScorer'; import FileSaver from 'file-saver'; import { _t } from '../../../../languageHandler'; import Modal from '../../../../Modal'; +import { promptForBackupPassphrase } from '../../../../CrossSigningManager'; const PHASE_LOADING = 0; -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 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_CONFIRM_SKIP = 8; 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. @@ -52,6 +53,17 @@ function selectText(target) { * Secret Storage in account data. */ export default class CreateSecretStorageDialog extends React.PureComponent { + static propTypes = { + hasCancel: PropTypes.bool, + accountPassword: PropTypes.string, + force: PropTypes.bool, + }; + + static defaultProps = { + hasCancel: true, + force: false, + }; + constructor(props) { super(props); @@ -67,15 +79,25 @@ export default class CreateSecretStorageDialog extends React.PureComponent { copied: false, downloaded: false, zxcvbnResult: null, - setPassPhrase: false, backupInfo: null, backupSigStatus: null, + // does the server offer a UI auth flow with just m.login.password + // for /keys/device_signing/upload? + canUploadKeysWithPasswordOnly: null, + accountPassword: props.accountPassword || "", + accountPasswordCorrect: null, + // status of the key backup toggle switch + useKeyBackup: true, }; this._fetchBackupInfo(); + this._queryKeyUploadAuth(); + + MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatusChange); } componentWillUnmount() { + MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatusChange); if (this._setZxcvbnResultTimeout !== null) { clearTimeout(this._setZxcvbnResultTimeout); } @@ -83,25 +105,67 @@ export default class CreateSecretStorageDialog extends React.PureComponent { async _fetchBackupInfo() { const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); - const backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo); + const backupSigStatus = ( + // we may not have started crypto yet, in which case we definitely don't trust the backup + MatrixClientPeg.get().isCryptoEnabled() && await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo) + ); - const phase = backupInfo ? - (backupSigStatus.usable ? PHASE_MIGRATE : PHASE_RESTORE_KEY_BACKUP) : - PHASE_PASSPHRASE; + const { force } = this.props; + const phase = (backupInfo && !force) ? PHASE_MIGRATE : PHASE_PASSPHRASE; this.setState({ phase, backupInfo, backupSigStatus, }); + + return { + backupInfo, + backupSigStatus, + }; + } + + async _queryKeyUploadAuth() { + try { + await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {}); + // We should never get here: the server should always require + // UI auth to upload device signing keys. If we do, we upload + // no keys which would be a no-op. + console.log("uploadDeviceSigningKeys unexpectedly succeeded without UI auth!"); + } catch (error) { + if (!error.data.flows) { + console.log("uploadDeviceSigningKeys advertised no flows!"); + } + const canUploadKeysWithPasswordOnly = error.data.flows.some(f => { + return f.stages.length === 1 && f.stages[0] === 'm.login.password'; + }); + this.setState({ + canUploadKeysWithPasswordOnly, + }); + } + } + + _onKeyBackupStatusChange = () => { + if (this.state.phase === PHASE_MIGRATE) this._fetchBackupInfo(); } _collectRecoveryKeyNode = (n) => { this._recoveryKeyNode = n; } - _onMigrateNextClick = () => { - this._bootstrapSecretStorage(); + _onUseKeyBackupChange = (enabled) => { + this.setState({ + useKeyBackup: enabled, + }); + } + + _onMigrateFormSubmit = (e) => { + e.preventDefault(); + if (this.state.backupSigStatus.usable) { + this._bootstrapSecretStorage(); + } else { + this._restoreBackup(); + } } _onCopyClick = () => { @@ -127,37 +191,75 @@ export default class CreateSecretStorageDialog extends React.PureComponent { }); } + _doBootstrapUIAuth = async (makeRequest) => { + if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) { + await makeRequest({ + type: 'm.login.password', + identifier: { + type: 'm.id.user', + user: MatrixClientPeg.get().getUserId(), + }, + // https://github.com/matrix-org/synapse/issues/5665 + user: MatrixClientPeg.get().getUserId(), + password: this.state.accountPassword, + }); + } else { + const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); + const { finished } = Modal.createTrackedDialog( + 'Cross-signing keys dialog', '', InteractiveAuthDialog, + { + title: _t("Setting up keys"), + matrixClient: MatrixClientPeg.get(), + makeRequest, + }, + ); + const [confirmed] = await finished; + if (!confirmed) { + throw new Error("Cross-signing key upload auth canceled"); + } + } + } + _bootstrapSecretStorage = async () => { this.setState({ phase: PHASE_STORING, error: null, }); + const cli = MatrixClientPeg.get(); + + const { force } = this.props; + try { - const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); - await cli.bootstrapSecretStorage({ - authUploadDeviceSigningKeys: async (makeRequest) => { - const { finished } = Modal.createTrackedDialog( - 'Cross-signing keys dialog', '', InteractiveAuthDialog, - { - title: _t("Send cross-signing keys to homeserver"), - matrixClient: MatrixClientPeg.get(), - makeRequest, - }, - ); - const [confirmed] = await finished; - if (!confirmed) { - throw new Error("Cross-signing key upload auth canceled"); - } - }, - createSecretStorageKey: async () => this._keyInfo, - keyBackupInfo: this.state.backupInfo, - }); + if (force) { + await cli.bootstrapSecretStorage({ + authUploadDeviceSigningKeys: this._doBootstrapUIAuth, + createSecretStorageKey: async () => this._keyInfo, + setupNewKeyBackup: true, + setupNewSecretStorage: true, + }); + } else { + await cli.bootstrapSecretStorage({ + authUploadDeviceSigningKeys: this._doBootstrapUIAuth, + createSecretStorageKey: async () => this._keyInfo, + keyBackupInfo: this.state.backupInfo, + setupNewKeyBackup: !this.state.backupInfo && this.state.useKeyBackup, + getKeyBackupPassphrase: promptForBackupPassphrase, + }); + } this.setState({ phase: PHASE_DONE, }); } catch (e) { - this.setState({ error: e }); + if (this.state.canUploadKeysWithPasswordOnly && e.httpStatus === 401 && e.data.flows) { + this.setState({ + accountPassword: '', + accountPasswordCorrect: false, + phase: PHASE_MIGRATE, + }); + } else { + this.setState({ error: e }); + } console.error("Error bootstrapping secret storage", e); } } @@ -170,16 +272,26 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this.props.onFinished(true); } - _onRestoreKeyBackupClick = () => { + _restoreBackup = async () => { const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); - Modal.createTrackedDialog( - 'Restore Backup', '', RestoreKeyBackupDialog, null, null, - /* priority = */ false, /* static = */ true, + const { finished } = Modal.createTrackedDialog( + 'Restore Backup', '', RestoreKeyBackupDialog, {showSummary: false}, null, + /* priority = */ false, /* static = */ false, ); + + await finished; + const { backupSigStatus } = await this._fetchBackupInfo(); + if ( + backupSigStatus.usable && + this.state.canUploadKeysWithPasswordOnly && + this.state.accountPassword + ) { + this._bootstrapSecretStorage(); + } } - _onOptOutClick = () => { - this.setState({phase: PHASE_OPTOUT_CONFIRM}); + _onSkipSetupClick = () => { + this.setState({phase: PHASE_CONFIRM_SKIP}); } _onSetUpClick = () => { @@ -198,49 +310,42 @@ export default class CreateSecretStorageDialog extends React.PureComponent { }); } - _onPassPhraseNextClick = () => { - this.setState({phase: PHASE_PASSPHRASE_CONFIRM}); - } + _onPassPhraseNextClick = async (e) => { + e.preventDefault(); - _onPassPhraseKeyPress = async (e) => { - if (e.key === 'Enter') { - // If we're waiting for the timeout before updating the result at this point, - // skip ahead and do it now, otherwise we'll deny the attempt to proceed - // even if the user entered a valid passphrase - if (this._setZxcvbnResultTimeout !== null) { - clearTimeout(this._setZxcvbnResultTimeout); - this._setZxcvbnResultTimeout = null; - await new Promise((resolve) => { - this.setState({ - zxcvbnResult: scorePassword(this.state.passPhrase), - }, resolve); - }); - } - if (this._passPhraseIsValid()) { - this._onPassPhraseNextClick(); - } + // If we're waiting for the timeout before updating the result at this point, + // skip ahead and do it now, otherwise we'll deny the attempt to proceed + // even if the user entered a valid passphrase + if (this._setZxcvbnResultTimeout !== null) { + clearTimeout(this._setZxcvbnResultTimeout); + this._setZxcvbnResultTimeout = null; + await new Promise((resolve) => { + this.setState({ + zxcvbnResult: scorePassword(this.state.passPhrase), + }, resolve); + }); } - } + if (this._passPhraseIsValid()) { + this.setState({phase: PHASE_PASSPHRASE_CONFIRM}); + } + }; + + _onPassPhraseConfirmNextClick = async (e) => { + e.preventDefault(); + + if (this.state.passPhrase !== this.state.passPhraseConfirm) return; - _onPassPhraseConfirmNextClick = async () => { const [keyInfo, encodedRecoveryKey] = await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(this.state.passPhrase); this._keyInfo = keyInfo; this._encodedRecoveryKey = encodedRecoveryKey; this.setState({ - setPassPhrase: true, copied: false, downloaded: false, phase: PHASE_SHOWKEY, }); } - _onPassPhraseConfirmKeyPress = (e) => { - if (e.key === 'Enter' && this.state.passPhrase === this.state.passPhraseConfirm) { - this._onPassPhraseConfirmNextClick(); - } - } - _onSetAgainClick = () => { this.setState({ passPhrase: '', @@ -285,21 +390,10 @@ 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.", - )}

- - -
; + _onAccountPasswordChange = (e) => { + this.setState({ + accountPassword: e.target.value, + }); } _renderPhaseMigrate() { @@ -309,22 +403,58 @@ export default class CreateSecretStorageDialog extends React.PureComponent { // it automatically. // https://github.com/vector-im/riot-web/issues/11696 const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - return
+ const Field = sdk.getComponent('views.elements.Field'); + + let authPrompt; + let nextCaption = _t("Next"); + if (this.state.canUploadKeysWithPasswordOnly) { + authPrompt =
+
{_t("Enter your account password to confirm the upgrade:")}
+
+
; + } else if (!this.state.backupSigStatus.usable) { + authPrompt =
+
{_t("Restore your key backup to upgrade your encryption")}
+
; + nextCaption = _t("Restore"); + } else { + authPrompt =

+ {_t("You'll need to authenticate with the server to confirm the upgrade.")} +

; + } + + return

{_t( - "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", + "Upgrade this session to allow it to verify other sessions, " + + "granting them access to encrypted messages and marking them " + + "as trusted for other users.", )}

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

{_t( - "Warning: You should only set up secret storage from a trusted computer.", {}, - { b: sub => {sub} }, + "Set up encryption on this session to allow it to verify other sessions, " + + "granting them access to encrypted messages and marking them as trusted for other users.", )}

{_t( - "We'll use secret storage to optionally store an encrypted copy of " + - "your cross-signing identity for verifying other devices and message " + - "keys on our server. Protect your access to encrypted messages with a " + - "passphrase to keep it secure.", + "Secure your encryption keys with a passphrase. For maximum security " + + "this should be different to your account password:", )}

-

{_t("For maximum security, this should be different from your account password.")}

-
-
- -
- {strengthMeter} - {helpText} -
+
+ +
+ {strengthMeter} + {helpText}
- + + + > + +
{_t("Advanced")} -

+
-
; + ; } _renderPhasePassPhraseConfirm() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + const Field = sdk.getComponent('views.elements.Field'); let matchText; if (this.state.passPhraseConfirm === this.state.passPhrase) { @@ -412,7 +555,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { let passPhraseMatch = null; if (matchText) { - passPhraseMatch =
+ passPhraseMatch =
{matchText}
@@ -422,71 +565,63 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
; } const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - return
+ return

{_t( - "Please enter your passphrase a second time to confirm.", + "Enter your passphrase a second time to confirm it.", )}

-
-
-
- -
+
+ +
{passPhraseMatch}
- -
; + > + + + ; } _renderPhaseShowKey() { - let bodyText; - if (this.state.setPassPhrase) { - bodyText = _t( - "As a safety net, you can use it to restore your access to encrypted " + - "messages if you forget your passphrase.", - ); - } else { - bodyText = _t( - "As a safety net, you can use it to restore your access to encrypted " + - "messages.", - ); - } - + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); return

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

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

-

{bodyText}

- {_t("Your Recovery Key")} + {_t("Your recovery key")}
{this._encodedRecoveryKey}
- - +
@@ -514,7 +649,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
  • {_t("Save it on a USB key or backup drive", {}, {b: s => {s}})}
  • {_t("Copy it to your personal cloud storage", {}, {b: s => {s}})}
  • - @@ -533,7 +668,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return

    {_t( - "Your access to encrypted messages is now protected.", + "You can now verify your other devices, " + + "and other users to keep your chats safe.", )}

    ; } - _renderPhaseOptOutConfirm() { + _renderPhaseSkipConfirm() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return
    {_t( - "Without setting up secret storage, you won't be able to restore your " + - "access to encrypted messages or your cross-signing identity for " + - "verifying other devices if you log out or use another device.", + "Without completing security on this session, it won’t have " + + "access to encrypted messages.", )} - - +
    ; } _titleForPhase(phase) { switch (phase) { - case PHASE_RESTORE_KEY_BACKUP: - return _t('Restore your Key Backup'); case PHASE_MIGRATE: - return _t('Migrate from Key Backup'); + return _t('Upgrade your encryption'); case PHASE_PASSPHRASE: - return _t('Secure your encrypted messages with a passphrase'); + return _t('Set up encryption'); case PHASE_PASSPHRASE_CONFIRM: - return _t('Confirm your passphrase'); - case PHASE_OPTOUT_CONFIRM: - return _t('Warning!'); + return _t('Confirm passphrase'); + case PHASE_CONFIRM_SKIP: + return _t('Are you sure?'); case PHASE_SHOWKEY: - return _t('Recovery key'); case PHASE_KEEPITSAFE: - return _t('Keep it safe'); + return _t('Make a copy of your recovery key'); case PHASE_STORING: - return _t('Storing secrets...'); + return _t('Setting up keys'); case PHASE_DONE: - return _t('Success!'); + return _t("You're done!"); default: - return null; + return ''; } } @@ -605,9 +737,6 @@ 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; @@ -629,17 +758,24 @@ export default class CreateSecretStorageDialog extends React.PureComponent { case PHASE_DONE: content = this._renderPhaseDone(); break; - case PHASE_OPTOUT_CONFIRM: - content = this._renderPhaseOptOutConfirm(); + case PHASE_CONFIRM_SKIP: + content = this._renderPhaseSkipConfirm(); break; } } + let headerImage; + if (this._titleForPhase(this.state.phase)) { + headerImage = require("../../../../../res/img/e2e/normal.svg"); + } + return (
    {content} diff --git a/src/autocomplete/DuckDuckGoProvider.js b/src/autocomplete/DuckDuckGoProvider.js index ca1b1478cc..8cff83554a 100644 --- a/src/autocomplete/DuckDuckGoProvider.js +++ b/src/autocomplete/DuckDuckGoProvider.js @@ -19,7 +19,6 @@ limitations under the License. import React from 'react'; import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; -import 'whatwg-fetch'; import {TextualCompletion} from './Components'; import type {SelectionRange} from "./Autocompleter"; diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index b28c79ac54..a0f670e769 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -23,7 +23,6 @@ import AutocompleteProvider from './AutocompleteProvider'; import {MatrixClientPeg} from '../MatrixClientPeg'; import QueryMatcher from './QueryMatcher'; import {PillCompletion} from './Components'; -import {getDisplayAliasForRoom} from '../Rooms'; import * as sdk from '../index'; import _sortBy from 'lodash/sortBy'; import {makeRoomPermalink} from "../utils/permalinks/Permalinks"; @@ -40,11 +39,19 @@ function score(query, space) { } } +function matcherObject(room, displayedAlias, matchName = "") { + return { + room, + matchName, + displayedAlias, + }; +} + export default class RoomProvider extends AutocompleteProvider { constructor() { super(ROOM_REGEX); this.matcher = new QueryMatcher([], { - keys: ['displayedAlias', 'name'], + keys: ['displayedAlias', 'matchName'], }); } @@ -56,16 +63,16 @@ export default class RoomProvider extends AutocompleteProvider { const {command, range} = this.getCurrentCommand(query, selection, force); if (command) { // the only reason we need to do this is because Fuse only matches on properties - let matcherObjects = client.getRooms().filter( - (room) => !!room && !!getDisplayAliasForRoom(room), - ).map((room) => { - return { - room: room, - name: room.name, - displayedAlias: getDisplayAliasForRoom(room), - }; - }); - + let matcherObjects = client.getVisibleRooms().reduce((aliases, room) => { + if (room.getCanonicalAlias()) { + aliases = aliases.concat(matcherObject(room, room.getCanonicalAlias(), room.name)); + } + if (room.getAltAliases().length) { + const altAliases = room.getAltAliases().map(alias => matcherObject(room, alias)); + aliases = aliases.concat(altAliases); + } + return aliases; + }, []); // Filter out any matches where the user will have also autocompleted new rooms matcherObjects = matcherObjects.filter((r) => { const tombstone = r.room.currentState.getStateEvents("m.room.tombstone", ""); @@ -84,16 +91,16 @@ export default class RoomProvider extends AutocompleteProvider { completions = _sortBy(completions, [ (c) => score(matchedString, c.displayedAlias), (c) => c.displayedAlias.length, - ]).map((room) => { - const displayAlias = getDisplayAliasForRoom(room.room) || room.roomId; + ]); + completions = completions.map((room) => { return { - completion: displayAlias, - completionId: displayAlias, + completion: room.displayedAlias, + completionId: room.room.roomId, type: "room", suffix: ' ', - href: makeRoomPermalink(displayAlias), + href: makeRoomPermalink(room.displayedAlias), component: ( - } title={room.name} description={displayAlias} /> + } title={room.room.name} description={room.displayedAlias} /> ), range, }; diff --git a/src/components/structures/AutoHideScrollbar.js b/src/components/structures/AutoHideScrollbar.js index 3f27f51f18..04323bb548 100644 --- a/src/components/structures/AutoHideScrollbar.js +++ b/src/components/structures/AutoHideScrollbar.js @@ -1,5 +1,6 @@ /* Copyright 2018 New Vector Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,93 +17,10 @@ limitations under the License. import React from "react"; -// derived from code from github.com/noeldelgado/gemini-scrollbar -// Copyright (c) Noel Delgado (pixelia.me) -function getScrollbarWidth(alternativeOverflow) { - const div = document.createElement('div'); - div.className = 'mx_AutoHideScrollbar'; //to get width of css scrollbar - div.style.position = 'absolute'; - div.style.top = '-9999px'; - div.style.width = '100px'; - div.style.height = '100px'; - div.style.overflow = "scroll"; - if (alternativeOverflow) { - div.style.overflow = alternativeOverflow; - } - div.style.msOverflowStyle = '-ms-autohiding-scrollbar'; - document.body.appendChild(div); - const scrollbarWidth = (div.offsetWidth - div.clientWidth); - document.body.removeChild(div); - return scrollbarWidth; -} - -function install() { - const scrollbarWidth = getScrollbarWidth(); - if (scrollbarWidth !== 0) { - const hasForcedOverlayScrollbar = getScrollbarWidth('overlay') === 0; - // overflow: overlay on webkit doesn't auto hide the scrollbar - if (hasForcedOverlayScrollbar) { - document.body.classList.add("mx_scrollbar_overlay_noautohide"); - } else { - document.body.classList.add("mx_scrollbar_nooverlay"); - const style = document.createElement('style'); - style.type = 'text/css'; - style.innerText = - `body.mx_scrollbar_nooverlay { --scrollbar-width: ${scrollbarWidth}px; }`; - document.head.appendChild(style); - } - } -} - -const installBodyClassesIfNeeded = (function() { - let installed = false; - return function() { - if (!installed) { - install(); - installed = true; - } - }; -})(); - export default class AutoHideScrollbar extends React.Component { constructor(props) { super(props); - this.onOverflow = this.onOverflow.bind(this); - this.onUnderflow = this.onUnderflow.bind(this); this._collectContainerRef = this._collectContainerRef.bind(this); - this._needsOverflowListener = null; - } - - onOverflow() { - this.containerRef.classList.add("mx_AutoHideScrollbar_overflow"); - this.containerRef.classList.remove("mx_AutoHideScrollbar_underflow"); - } - - onUnderflow() { - this.containerRef.classList.remove("mx_AutoHideScrollbar_overflow"); - this.containerRef.classList.add("mx_AutoHideScrollbar_underflow"); - } - - checkOverflow() { - if (!this._needsOverflowListener) { - return; - } - if (this.containerRef.scrollHeight > this.containerRef.clientHeight) { - this.onOverflow(); - } else { - this.onUnderflow(); - } - } - - componentDidUpdate() { - this.checkOverflow(); - } - - componentDidMount() { - installBodyClassesIfNeeded(); - this._needsOverflowListener = - document.body.classList.contains("mx_scrollbar_nooverlay"); - this.checkOverflow(); } _collectContainerRef(ref) { @@ -126,9 +44,7 @@ export default class AutoHideScrollbar extends React.Component { onScroll={this.props.onScroll} onWheel={this.props.onWheel} > -
    - { this.props.children } -
    + { this.props.children }
    ); } } diff --git a/src/components/structures/ContextMenu.js b/src/components/structures/ContextMenu.js index b4b1b80163..b4647a6c30 100644 --- a/src/components/structures/ContextMenu.js +++ b/src/components/structures/ContextMenu.js @@ -255,7 +255,7 @@ export class ContextMenu extends React.Component { if (chevronFace === 'top' || chevronFace === 'bottom') { chevronOffset.left = props.chevronOffset; - } else { + } else if (position.top !== undefined) { const target = position.top; // By default, no adjustment is made @@ -350,7 +350,7 @@ export const ContextMenuButton = ({ label, isExpanded, children, ...props }) => }; ContextMenuButton.propTypes = { ...AccessibleButton.propTypes, - label: PropTypes.string.isRequired, + label: PropTypes.string, isExpanded: PropTypes.bool.isRequired, // whether or not the context menu is currently open }; @@ -377,7 +377,6 @@ export const MenuGroup = ({children, label, ...props}) => {
    ; }; MenuGroup.propTypes = { - ...AccessibleButton.propTypes, label: PropTypes.string.isRequired, className: PropTypes.string, // optional }; diff --git a/src/components/structures/EmbeddedPage.js b/src/components/structures/EmbeddedPage.js index 6d734c3838..f854dc955f 100644 --- a/src/components/structures/EmbeddedPage.js +++ b/src/components/structures/EmbeddedPage.js @@ -23,11 +23,11 @@ import PropTypes from 'prop-types'; import request from 'browser-request'; import { _t } from '../../languageHandler'; import sanitizeHtml from 'sanitize-html'; -import * as sdk from '../../index'; import dis from '../../dispatcher'; import {MatrixClientPeg} from '../../MatrixClientPeg'; import classnames from 'classnames'; import MatrixClientContext from "../../contexts/MatrixClientContext"; +import AutoHideScrollbar from "./AutoHideScrollbar"; export default class EmbeddedPage extends React.PureComponent { static propTypes = { @@ -117,10 +117,9 @@ export default class EmbeddedPage extends React.PureComponent {
    ; if (this.props.scrollbar) { - const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); - return + return {content} - ; + ; } else { return
    {content} diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index 61b3d2d4b9..f8c03be864 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -19,9 +19,10 @@ import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; -import Matrix from 'matrix-js-sdk'; +import {Filter} from 'matrix-js-sdk'; import * as sdk from '../../index'; import {MatrixClientPeg} from '../../MatrixClientPeg'; +import EventIndexPeg from "../../indexing/EventIndexPeg"; import { _t } from '../../languageHandler'; /* @@ -29,6 +30,9 @@ import { _t } from '../../languageHandler'; */ const FilePanel = createReactClass({ displayName: 'FilePanel', + // This is used to track if a decrypted event was a live event and should be + // added to the timeline. + decryptingEvents: new Set(), propTypes: { roomId: PropTypes.string.isRequired, @@ -40,42 +44,147 @@ const FilePanel = createReactClass({ }; }, - componentDidMount: function() { - this.updateTimelineSet(this.props.roomId); + onRoomTimeline(ev, room, toStartOfTimeline, removed, data) { + if (room.roomId !== this.props.roomId) return; + if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return; + + if (ev.isBeingDecrypted()) { + this.decryptingEvents.add(ev.getId()); + } else { + this.addEncryptedLiveEvent(ev); + } }, - updateTimelineSet: function(roomId) { + onEventDecrypted(ev, err) { + if (ev.getRoomId() !== this.props.roomId) return; + const eventId = ev.getId(); + + if (!this.decryptingEvents.delete(eventId)) return; + if (err) return; + + this.addEncryptedLiveEvent(ev); + }, + + addEncryptedLiveEvent(ev, toStartOfTimeline) { + if (!this.state.timelineSet) return; + + const timeline = this.state.timelineSet.getLiveTimeline(); + if (ev.getType() !== "m.room.message") return; + if (["m.file", "m.image", "m.video", "m.audio"].indexOf(ev.getContent().msgtype) == -1) { + return; + } + + if (!this.state.timelineSet.eventIdToTimeline(ev.getId())) { + this.state.timelineSet.addEventToTimeline(ev, timeline, false); + } + }, + + async componentDidMount() { + const client = MatrixClientPeg.get(); + + await this.updateTimelineSet(this.props.roomId); + + if (!MatrixClientPeg.get().isRoomEncrypted(this.props.roomId)) return; + + // The timelineSets filter makes sure that encrypted events that contain + // URLs never get added to the timeline, even if they are live events. + // These methods are here to manually listen for such events and add + // them despite the filter's best efforts. + // + // We do this only for encrypted rooms and if an event index exists, + // this could be made more general in the future or the filter logic + // could be fixed. + if (EventIndexPeg.get() !== null) { + client.on('Room.timeline', this.onRoomTimeline); + client.on('Event.decrypted', this.onEventDecrypted); + } + }, + + componentWillUnmount() { + const client = MatrixClientPeg.get(); + if (client === null) return; + + if (!MatrixClientPeg.get().isRoomEncrypted(this.props.roomId)) return; + + if (EventIndexPeg.get() !== null) { + client.removeListener('Room.timeline', this.onRoomTimeline); + client.removeListener('Event.decrypted', this.onEventDecrypted); + } + }, + + async fetchFileEventsServer(room) { + const client = MatrixClientPeg.get(); + + const filter = new Filter(client.credentials.userId); + filter.setDefinition( + { + "room": { + "timeline": { + "contains_url": true, + "types": [ + "m.room.message", + ], + }, + }, + }, + ); + + const filterId = await client.getOrCreateFilter("FILTER_FILES_" + client.credentials.userId, filter); + filter.filterId = filterId; + const timelineSet = room.getOrCreateFilteredTimelineSet(filter); + + return timelineSet; + }, + + onPaginationRequest(timelineWindow, direction, limit) { + const client = MatrixClientPeg.get(); + const eventIndex = EventIndexPeg.get(); + const roomId = this.props.roomId; + + const room = client.getRoom(roomId); + + // We override the pagination request for encrypted rooms so that we ask + // the event index to fulfill the pagination request. Asking the server + // to paginate won't ever work since the server can't correctly filter + // out events containing URLs + if (client.isRoomEncrypted(roomId) && eventIndex !== null) { + return eventIndex.paginateTimelineWindow(room, timelineWindow, direction, limit); + } else { + return timelineWindow.paginate(direction, limit); + } + }, + + async updateTimelineSet(roomId: string) { const client = MatrixClientPeg.get(); const room = client.getRoom(roomId); + const eventIndex = EventIndexPeg.get(); this.noRoom = !room; if (room) { - const filter = new Matrix.Filter(client.credentials.userId); - filter.setDefinition( - { - "room": { - "timeline": { - "contains_url": true, - "types": [ - "m.room.message", - ], - }, - }, - }, - ); + let timelineSet; - // FIXME: we shouldn't be doing this every time we change room - see comment above. - client.getOrCreateFilter("FILTER_FILES_" + client.credentials.userId, filter).then( - (filterId)=>{ - filter.filterId = filterId; - const timelineSet = room.getOrCreateFilteredTimelineSet(filter); - this.setState({ timelineSet: timelineSet }); - }, - (error)=>{ - console.error("Failed to get or create file panel filter", error); - }, - ); + try { + timelineSet = await this.fetchFileEventsServer(room); + + // If this room is encrypted the file panel won't be populated + // correctly since the defined filter doesn't support encrypted + // events and the server can't check if encrypted events contain + // URLs. + // + // This is where our event index comes into place, we ask the + // event index to populate the timelineSet for us. This call + // will add 10 events to the live timeline of the set. More can + // be requested using pagination. + if (client.isRoomEncrypted(roomId) && eventIndex !== null) { + const timeline = timelineSet.getLiveTimeline(); + await eventIndex.populateFileTimeline(timelineSet, timeline, room, 10); + } + + this.setState({ timelineSet: timelineSet }); + } catch (error) { + console.error("Failed to get or create file panel filter", error); + } } else { console.error("Failed to add filtered timelineSet for FilePanel as no room!"); } @@ -111,6 +220,7 @@ const FilePanel = createReactClass({ manageReadMarkers={false} timelineSet={this.state.timelineSet} showUrlPreview = {false} + onPaginationRequest={this.onPaginationRequest} tileShape="file_grid" resizeNotifier={this.props.resizeNotifier} empty={_t('There are no visible files in this room')} diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 5ae0699a2f..524694fe95 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -39,6 +39,7 @@ import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Perm import {Group} from "matrix-js-sdk"; import {allSettled, sleep} from "../../utils/promise"; import RightPanelStore from "../../stores/RightPanelStore"; +import AutoHideScrollbar from "./AutoHideScrollbar"; const LONG_DESC_PLACEHOLDER = _td( `

    HTML for your community's page

    @@ -423,6 +424,7 @@ export default createReactClass({ membershipBusy: false, publicityBusy: false, inviterProfile: null, + showRightPanel: RightPanelStore.getSharedInstance().isOpenForGroup, }; }, @@ -435,12 +437,18 @@ export default createReactClass({ this._initGroupStore(this.props.groupId, true); this._dispatcherRef = dis.register(this._onAction); + this._rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this._onRightPanelStoreUpdate); }, componentWillUnmount: function() { this._unmounted = true; this._matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership); dis.unregister(this._dispatcherRef); + + // Remove RightPanelStore listener + if (this._rightPanelStoreToken) { + this._rightPanelStoreToken.remove(); + } }, componentWillReceiveProps: function(newProps) { @@ -454,6 +462,12 @@ export default createReactClass({ } }, + _onRightPanelStoreUpdate: function() { + this.setState({ + showRightPanel: RightPanelStore.getSharedInstance().isOpenForGroup, + }); + }, + _onGroupMyMembership: function(group) { if (this._unmounted || group.groupId !== this.props.groupId) return; if (group.myMembership === 'leave') { @@ -481,7 +495,7 @@ export default createReactClass({ group_id: groupId, }, }); - dis.dispatch({action: 'require_registration'}); + dis.dispatch({action: 'require_registration', screen_after: {screen: `group/${groupId}`}}); willDoOnboarding = true; } if (stateKey === GroupStore.STATE_KEY.Summary) { @@ -554,10 +568,6 @@ export default createReactClass({ GROUP_JOINPOLICY_INVITE, }, }); - dis.dispatch({ - action: 'panel_disable', - sideDisabled: true, - }); }, _onShareClick: function() { @@ -580,10 +590,6 @@ export default createReactClass({ profileForm: null, }); break; - case 'after_right_panel_phase_change': - // We don't keep state on the right panel, so just re-render to update - this.forceUpdate(); - break; default: break; } @@ -726,7 +732,7 @@ export default createReactClass({ _onJoinClick: async function() { if (this._matrixClient.isGuest()) { - dis.dispatch({action: 'require_registration'}); + dis.dispatch({action: 'require_registration', screen_after: {screen: `group/${this.props.groupId}`}}); return; } @@ -821,10 +827,10 @@ export default createReactClass({ {_t( "Want more than a community? Get your own server", {}, { - a: sub => {sub}, + a: sub => {sub}, }, )} - +
    ; @@ -1173,7 +1179,6 @@ export default createReactClass({ render: function() { const GroupAvatar = sdk.getComponent("avatars.GroupAvatar"); const Spinner = sdk.getComponent("elements.Spinner"); - const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); if (this.state.summaryLoading && this.state.error === null || this.state.saving) { return ; @@ -1299,9 +1304,7 @@ export default createReactClass({ ); } - const rightPanel = RightPanelStore.getSharedInstance().isOpenForGroup - ? - : undefined; + const rightPanel = this.state.showRightPanel ? : undefined; const headerClasses = { "mx_GroupView_header": true, @@ -1332,10 +1335,10 @@ export default createReactClass({
    - + { this._getMembershipSection() } { this._getGroupSection() } - + ); diff --git a/src/components/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.js index f14d99f730..05ad4f7561 100644 --- a/src/components/structures/IndicatorScrollbar.js +++ b/src/components/structures/IndicatorScrollbar.js @@ -66,6 +66,22 @@ export default class IndicatorScrollbar extends React.Component { this._autoHideScrollbar = autoHideScrollbar; } + + componentDidUpdate(prevProps) { + const prevLen = prevProps && prevProps.children && prevProps.children.length || 0; + const curLen = this.props.children && this.props.children.length || 0; + // check overflow only if amount of children changes. + // if we don't guard here, we end up with an infinite + // render > componentDidUpdate > checkOverflow > setState > render loop + if (prevLen !== curLen) { + this.checkOverflow(); + } + } + + componentDidMount() { + this.checkOverflow(); + } + checkOverflow() { const hasTopOverflow = this._scrollElement.scrollTop > 0; const hasBottomOverflow = this._scrollElement.scrollHeight > @@ -95,10 +111,6 @@ export default class IndicatorScrollbar extends React.Component { this._scrollElement.classList.remove("mx_IndicatorScrollbar_rightOverflow"); } - if (this._autoHideScrollbar) { - this._autoHideScrollbar.checkOverflow(); - } - if (this.props.trackHorizontalOverflow) { this.setState({ // Offset from absolute position of the container diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index 53bb990e26..f4adb5751f 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -20,7 +20,7 @@ import React, {createRef} from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; -import {getEntryComponentForLoginType} from '../views/auth/InteractiveAuthEntryComponents'; +import getEntryComponentForLoginType from '../views/auth/InteractiveAuthEntryComponents'; import * as sdk from '../../index'; @@ -161,6 +161,7 @@ export default createReactClass({ _authStateUpdated: function(stageType, stageState) { const oldStage = this.state.authStage; this.setState({ + busy: false, authStage: stageType, stageState: stageState, errorText: stageState.error, @@ -184,11 +185,13 @@ export default createReactClass({ errorText: null, stageErrorText: null, }); - } else { - this.setState({ - busy: false, - }); } + // The JS SDK eagerly reports itself as "not busy" right after any + // immediate work has completed, but that's not really what we want at + // the UI layer, so we ignore this signal and show a spinner until + // there's a new screen to show the user. This is implemented by setting + // `busy: false` in `_authStateUpdated`. + // See also https://github.com/vector-im/riot-web/issues/12546 }, _setFocus: function() { diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index 8a7d10e5b5..f5e0bca67e 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -129,9 +129,6 @@ const LeftPanel = createReactClass({ if (!this.focusedElement) return; switch (ev.key) { - case Key.TAB: - this._onMoveFocus(ev, ev.shiftKey); - break; case Key.ARROW_UP: this._onMoveFocus(ev, true, true); break; diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 9597f99cd2..e7a6f4c1a9 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -39,6 +39,7 @@ import RoomListActions from '../../actions/RoomListActions'; import ResizeHandle from '../views/elements/ResizeHandle'; import {Resizer, CollapseDistributor} from '../../resizer'; import MatrixClientContext from "../../contexts/MatrixClientContext"; +import * as KeyboardShortcuts from "../../accessibility/KeyboardShortcuts"; // We need to fetch each pinned message individually (if we don't already have it) // so each pinned message may trigger a request. Limit the number per room for sanity. // NB. this is just for server notices rather than pinned messages in general. @@ -337,13 +338,13 @@ const LoggedInView = createReactClass({ let handled = false; const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev); - const hasModifier = ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey || - ev.key === Key.ALT || ev.key === Key.CONTROL || ev.key === Key.META || ev.key === Key.SHIFT; + const hasModifier = ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey; + const isModifier = ev.key === Key.ALT || ev.key === Key.CONTROL || ev.key === Key.META || ev.key === Key.SHIFT; switch (ev.key) { case Key.PAGE_UP: case Key.PAGE_DOWN: - if (!hasModifier) { + if (!hasModifier && !isModifier) { this._onScrollKeyPressed(ev); handled = true; } @@ -365,8 +366,6 @@ const LoggedInView = createReactClass({ } break; case Key.BACKTICK: - if (ev.key !== "`") break; - // Ideally this would be CTRL+P for "Profile", but that's // taken by the print dialog. CTRL+I for "Information" // was previously chosen but conflicted with italics in @@ -379,12 +378,43 @@ const LoggedInView = createReactClass({ handled = true; } break; + + case Key.SLASH: + if (ctrlCmdOnly) { + KeyboardShortcuts.toggleDialog(); + handled = true; + } + break; + + case Key.ARROW_UP: + case Key.ARROW_DOWN: + if (ev.altKey && !ev.ctrlKey && !ev.metaKey) { + dis.dispatch({ + action: 'view_room_delta', + delta: ev.key === Key.ARROW_UP ? -1 : 1, + unread: ev.shiftKey, + }); + handled = true; + } + break; + + case Key.PERIOD: + if (ctrlCmdOnly && (this.props.page_type === "room_view" || this.props.page_type === "group_view")) { + dis.dispatch({ + action: 'toggle_right_panel', + type: this.props.page_type === "room_view" ? "room" : "group", + }); + handled = true; + } } if (handled) { ev.stopPropagation(); ev.preventDefault(); - } else if (!hasModifier) { + } else if (!isModifier && !ev.altKey && !ev.ctrlKey && !ev.metaKey) { + // The above condition is crafted to _allow_ characters with Shift + // already pressed (but not the Shift key down itself). + const isClickShortcut = ev.target !== document.body && (ev.key === Key.SPACE || ev.key === Key.ENTER); @@ -585,7 +615,8 @@ const LoggedInView = createReactClass({ limitType={usageLimitEvent.getContent().limit_type} />; } else if (this.props.showCookieBar && - this.props.config.piwik + this.props.config.piwik && + navigator.doNotTrack !== "1" ) { const policyUrl = this.props.config.piwik.policyUrl || null; topBar = ; diff --git a/src/components/structures/MainSplit.js b/src/components/structures/MainSplit.js index 772be358cf..7c66f21a04 100644 --- a/src/components/structures/MainSplit.js +++ b/src/components/structures/MainSplit.js @@ -93,14 +93,19 @@ export default class MainSplit extends React.Component { const bodyView = React.Children.only(this.props.children); const panelView = this.props.panel; - if (this.props.collapsedRhs || !panelView) { - return bodyView; - } else { - return
    - { bodyView } + const hasResizer = !this.props.collapsedRhs && panelView; + + let children; + if (hasResizer) { + children = { panelView } -
    ; + ; } + + return
    + { bodyView } + { children } +
    ; } } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index ed6b7746bc..9d17cd8647 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -2,7 +2,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd Copyright 2017-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. @@ -21,6 +21,7 @@ import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import * as Matrix from "matrix-js-sdk"; +import { isCryptoAvailable } from 'matrix-js-sdk/src/crypto'; // focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss import 'focus-visible'; @@ -52,6 +53,7 @@ import createRoom from "../../createRoom"; import KeyRequestHandler from '../../KeyRequestHandler'; import { _t, getCurrentLanguage } from '../../languageHandler'; import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; +import ThemeController from "../../settings/controllers/ThemeController"; import { startAnyRegistrationFlow } from "../../Registration.js"; import { messageForSyncError } from '../../utils/ErrorUtils'; import ResizeNotifier from "../../utils/ResizeNotifier"; @@ -62,7 +64,8 @@ import { countRoomsWithNotif } from '../../RoomNotifs'; import { ThemeWatcher } from "../../theme"; import { storeRoomAliasInCache } from '../../RoomAliasCache'; import { defer } from "../../utils/promise"; -import KeyVerificationStateObserver from '../../utils/KeyVerificationStateObserver'; +import ToastStore from "../../stores/ToastStore"; +import * as StorageManager from "../../utils/StorageManager"; /** constants for MatrixChat.state.view */ export const VIEWS = { @@ -79,25 +82,24 @@ export const VIEWS = { // we are showing the registration view REGISTER: 3, - // completeing the registration flow + // completing the registration flow POST_REGISTRATION: 4, // showing the 'forgot password' view FORGOT_PASSWORD: 5, - // we have valid matrix credentials (either via an explicit login, via the - // initial re-animation/guest registration, or via a registration), and are - // now setting up a matrixclient to talk to it. This isn't an instant - // process because we need to clear out indexeddb. While it is going on we - // show a big spinner. - LOGGING_IN: 6, + // showing flow to trust this new device with cross-signing + COMPLETE_SECURITY: 6, + + // flow to setup SSSS / cross-signing on this account + E2E_SETUP: 7, // we are logged in with an active matrix client. - LOGGED_IN: 7, + LOGGED_IN: 8, // We are logged out (invalid token) but have our local state again. The user // should log back in to rehydrate the client. - SOFT_LOGOUT: 8, + SOFT_LOGOUT: 9, }; // Actions that are redirected through the onboarding process prior to being @@ -256,6 +258,9 @@ export default createReactClass({ // logout page. Lifecycle.loadSession({}); } + + this._accountPassword = null; + this._accountPasswordTimer = null; }, componentDidMount: function() { @@ -352,6 +357,8 @@ export default createReactClass({ window.removeEventListener("focus", this.onFocus); window.removeEventListener('resize', this.handleResize); this.state.resizeNotifier.removeListener("middlePanelResized", this._dispatchTimelineResize); + + if (this._accountPasswordTimer !== null) clearTimeout(this._accountPasswordTimer); }, componentWillUpdate: function(props, state) { @@ -501,6 +508,8 @@ export default createReactClass({ view: VIEWS.LOGIN, }); this.notifyNewScreen('login'); + ThemeController.isLogin = true; + this._themeWatcher.recheck(); break; case 'start_post_registration': this.setState({ @@ -550,13 +559,19 @@ export default createReactClass({ case 'view_user_info': this._viewUser(payload.userId, payload.subAction); break; - case 'view_room': + case 'view_room': { // Takes either a room ID or room alias: if switching to a room the client is already // known to be in (eg. user clicks on a room in the recents panel), supply the ID // If the user is clicking on a room in the context of the alias being presented // to them, supply the room alias. If both are supplied, the room ID will be ignored. - this._viewRoom(payload); + const promise = this._viewRoom(payload); + if (payload.deferred_action) { + promise.then(() => { + dis.dispatch(payload.deferred_action); + }); + } break; + } case 'view_prev_room': this._viewNextRoom(-1); break; @@ -585,9 +600,8 @@ export default createReactClass({ break; case 'view_room_directory': { const RoomDirectory = sdk.getComponent("structures.RoomDirectory"); - Modal.createTrackedDialog('Room directory', '', RoomDirectory, { - config: this.props.config, - }, 'mx_RoomDirectory_dialogWrapper'); + Modal.createTrackedDialog('Room directory', '', RoomDirectory, {}, + 'mx_RoomDirectory_dialogWrapper', false, true); // View the welcome or home page if we need something to look at this._viewSomethingBehindModal(); @@ -643,6 +657,7 @@ export default createReactClass({ collapseLhs: true, }); break; + case 'focus_room_filter': // for CtrlOrCmd+K to work by expanding the left panel first case 'show_left_panel': this.setState({ collapseLhs: false, @@ -656,16 +671,14 @@ export default createReactClass({ }); break; } - case 'on_logging_in': - // We are now logging in, so set the state to reflect that - // NB. This does not touch 'ready' since if our dispatches - // are delayed, the sync could already have completed - this.setStateForNewView({ - view: VIEWS.LOGGING_IN, - }); - break; case 'on_logged_in': - if (!Lifecycle.isSoftLogout()) { + if ( + !Lifecycle.isSoftLogout() && + this.state.view !== VIEWS.LOGIN && + this.state.view !== VIEWS.REGISTER && + this.state.view !== VIEWS.COMPLETE_SECURITY && + this.state.view !== VIEWS.E2E_SETUP + ) { this._onLoggedIn(); } break; @@ -757,6 +770,8 @@ export default createReactClass({ } this.setStateForNewView(newState); + ThemeController.isLogin = true; + this._themeWatcher.recheck(); this.notifyNewScreen('register'); }, @@ -853,7 +868,7 @@ export default createReactClass({ waitFor = this.firstSyncPromise.promise; } - waitFor.then(() => { + return waitFor.then(() => { let presentedId = roomInfo.room_alias || roomInfo.room_id; const room = MatrixClientPeg.get().getRoom(roomInfo.room_id); if (room) { @@ -876,7 +891,7 @@ export default createReactClass({ presentedId += "/" + roomInfo.event_id; } newState.ready = true; - this.setState(newState, ()=>{ + this.setState(newState, () => { this.notifyNewScreen('room/' + presentedId); }); }); @@ -907,6 +922,8 @@ export default createReactClass({ view: VIEWS.WELCOME, }); this.notifyNewScreen('welcome'); + ThemeController.isLogin = true; + this._themeWatcher.recheck(); }, _viewHome: function() { @@ -916,6 +933,8 @@ export default createReactClass({ }); this._setPage(PageTypes.HomePage); this.notifyNewScreen('home'); + ThemeController.isLogin = false; + this._themeWatcher.recheck(); }, _viewUser: function(userId, subAction) { @@ -968,9 +987,9 @@ export default createReactClass({ const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog'); const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog); - const [shouldCreate, createOpts] = await modal.finished; + const [shouldCreate, opts] = await modal.finished; if (shouldCreate) { - createRoom({createOpts}); + createRoom(opts); } }, @@ -995,6 +1014,10 @@ export default createReactClass({ // needs to be reset so that they can revisit /user/.. // (and trigger // `_chatCreateOrReuse` again) go_welcome_on_cancel: true, + screen_after: { + screen: `user/${this.props.config.welcomeUserId}`, + params: { action: 'chat' }, + }, }); return; } @@ -1162,14 +1185,23 @@ export default createReactClass({ * Called when a new logged in session has started */ _onLoggedIn: async function() { + ThemeController.isLogin = false; this.setStateForNewView({ view: VIEWS.LOGGED_IN }); - if (MatrixClientPeg.currentUserIsJustRegistered()) { + // If a specific screen is set to be shown after login, show that above + // all else, as it probably means the user clicked on something already. + if (this._screenAfterLogin && this._screenAfterLogin.screen) { + this.showScreen( + this._screenAfterLogin.screen, + this._screenAfterLogin.params, + ); + this._screenAfterLogin = null; + } else if (MatrixClientPeg.currentUserIsJustRegistered()) { MatrixClientPeg.setJustRegisteredUserId(null); if (this.props.config.welcomeUserId && getCurrentLanguage().startsWith("en")) { const welcomeUserRoom = await this._startWelcomeUserChat(); if (welcomeUserRoom === null) { - // We didn't rediret to the welcome user room, so show + // We didn't redirect to the welcome user room, so show // the homepage. dis.dispatch({action: 'view_home_page'}); } @@ -1181,6 +1213,8 @@ export default createReactClass({ } else { this._showScreenAfterLogin(); } + + StorageManager.tryPersistStorage(); }, _showScreenAfterLogin: function() { @@ -1228,6 +1262,8 @@ export default createReactClass({ }); this.subTitleStatus = ''; this._setPageSubtitle(); + ThemeController.isLogin = true; + this._themeWatcher.recheck(); }, /** @@ -1357,7 +1393,8 @@ export default createReactClass({ cancelButton: _t('Dismiss'), onFinished: (confirmed) => { if (confirmed) { - window.open(consentUri, '_blank'); + const wnd = window.open(consentUri, '_blank'); + wnd.opener = null; } }, }, null, true); @@ -1389,6 +1426,8 @@ export default createReactClass({ cli.on("Session.logged_out", () => dft.stop()); cli.on("Event.decrypted", (e, err) => dft.eventDecrypted(e, err)); + // TODO: We can remove this once cross-signing is the only way. + // https://github.com/vector-im/riot-web/issues/11908 const krh = new KeyRequestHandler(cli); cli.on("crypto.roomKeyRequest", (req) => { krh.handleKeyRequest(req); @@ -1456,35 +1495,40 @@ export default createReactClass({ } }); - if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { - cli.on("crypto.verification.request", request => { - let requestObserver; - if (request.event.getRoomId()) { - requestObserver = new KeyVerificationStateObserver( - request.event, MatrixClientPeg.get()); - } + cli.on("crypto.keySignatureUploadFailure", (failures, source, continuation) => { + const KeySignatureUploadFailedDialog = + sdk.getComponent('views.dialogs.KeySignatureUploadFailedDialog'); + Modal.createTrackedDialog( + 'Failed to upload key signatures', + 'Failed to upload key signatures', + KeySignatureUploadFailedDialog, + { failures, source, continuation }); + }); - if (!requestObserver || requestObserver.pending) { - dis.dispatch({ - action: "show_toast", - toast: { - key: request.event.getId(), - title: _t("Verification Request"), - icon: "verification", - props: {request, requestObserver}, - component: sdk.getComponent("toasts.VerificationRequestToast"), - }, - }); - } - }); - } else { - cli.on("crypto.verification.start", (verifier) => { + cli.on("crypto.verification.request", request => { + const isFlagOn = SettingsStore.isFeatureEnabled("feature_cross_signing"); + + if (!isFlagOn && !request.channel.deviceId) { + request.cancel({code: "m.invalid_message", reason: "This client has cross-signing disabled"}); + return; + } + + if (request.verifier) { const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog"); Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, { - verifier, + verifier: request.verifier, }, null, /* priority = */ false, /* static = */ true); - }); - } + } else if (request.pending) { + ToastStore.sharedInstance().addOrReplaceToast({ + key: 'verifreq_' + request.channel.transactionId, + title: _t("Verification Request"), + icon: "verification", + props: {request}, + component: sdk.getComponent("toasts.VerificationRequestToast"), + priority: ToastStore.PRIORITY_REALTIME, + }); + } + }); // Fire the tinter right on startup to ensure the default theme is applied // A later sync can/will correct the tint to be the right value for the user const colorScheme = SettingsStore.getValue("roomColor"); @@ -1573,6 +1617,10 @@ export default createReactClass({ dis.dispatch({ action: 'view_my_groups', }); + } else if (screen === 'complete_security') { + dis.dispatch({ + action: 'start_complete_security', + }); } else if (screen == 'post_registration') { dis.dispatch({ action: 'start_post_registration', @@ -1734,6 +1782,10 @@ export default createReactClass({ this.showScreen("forgot_password"); }, + onRegisterFlowComplete: function(credentials, password) { + return this.onUserCompletedLoginFlow(credentials, password); + }, + // returns a promise which resolves to the new MatrixClient onRegistered: function(credentials) { return Lifecycle.setLoggedIn(credentials); @@ -1822,21 +1874,101 @@ export default createReactClass({ this._loggedInView = ref; }, + async onUserCompletedLoginFlow(credentials, password) { + this._accountPassword = password; + // self-destruct the password after 5mins + if (this._accountPasswordTimer !== null) clearTimeout(this._accountPasswordTimer); + this._accountPasswordTimer = setTimeout(() => { + this._accountPassword = null; + this._accountPasswordTimer = null; + }, 60 * 5 * 1000); + + // Wait for the client to be logged in (but not started) + // which is enough to ask the server about account data. + const loggedIn = new Promise(resolve => { + const actionHandlerRef = dis.register(payload => { + if (payload.action !== "on_logged_in") { + return; + } + dis.unregister(actionHandlerRef); + resolve(); + }); + }); + + // Create and start the client in the background + const setLoggedInPromise = Lifecycle.setLoggedIn(credentials); + await loggedIn; + + const cli = MatrixClientPeg.get(); + // We're checking `isCryptoAvailable` here instead of `isCryptoEnabled` + // because the client hasn't been started yet. + if (!isCryptoAvailable()) { + this._onLoggedIn(); + } + + // Test for the master cross-signing key in SSSS as a quick proxy for + // whether cross-signing has been set up on the account. + let masterKeyInStorage = false; + try { + masterKeyInStorage = !!await cli.getAccountDataFromServer("m.cross_signing.master"); + } catch (e) { + if (e.errcode !== "M_NOT_FOUND") { + console.warn("Secret storage account data check failed", e); + } + } + + if (masterKeyInStorage) { + // Auto-enable cross-signing for the new session when key found in + // secret storage. + SettingsStore.setFeatureEnabled("feature_cross_signing", true); + this.setStateForNewView({ view: VIEWS.COMPLETE_SECURITY }); + } else if ( + SettingsStore.isFeatureEnabled("feature_cross_signing") && + await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing") + ) { + // This will only work if the feature is set to 'enable' in the config, + // since it's too early in the lifecycle for users to have turned the + // labs flag on. + this.setStateForNewView({ view: VIEWS.E2E_SETUP }); + } else { + this._onLoggedIn(); + } + + return setLoggedInPromise; + }, + + // complete security / e2e setup has finished + onCompleteSecurityE2eSetupFinished() { + this._onLoggedIn(); + }, + render: function() { // console.log(`Rendering MatrixChat with view ${this.state.view}`); let view; - if ( - this.state.view === VIEWS.LOADING || - this.state.view === VIEWS.LOGGING_IN - ) { + if (this.state.view === VIEWS.LOADING) { const Spinner = sdk.getComponent('elements.Spinner'); view = (
    ); + } else if (this.state.view === VIEWS.COMPLETE_SECURITY) { + const CompleteSecurity = sdk.getComponent('structures.auth.CompleteSecurity'); + view = ( + + ); + } else if (this.state.view === VIEWS.E2E_SETUP) { + const E2eSetup = sdk.getComponent('structures.auth.E2eSetup'); + view = ( + + ); } else if (this.state.view === VIEWS.POST_REGISTRATION) { // needs to be before normal PageTypes as you are logged in technically const PostRegistration = sdk.getComponent('structures.auth.PostRegistration'); @@ -1901,9 +2033,10 @@ export default createReactClass({ email={this.props.startingFragmentQueryParams.email} brand={this.props.config.brand} makeRegistrationUrl={this._makeRegistrationUrl} - onLoggedIn={this.onRegistered} + onLoggedIn={this.onRegisterFlowComplete} onLoginClick={this.onLoginClick} onServerConfigChange={this.onServerConfigChange} + defaultDeviceDisplayName={this.props.defaultDeviceDisplayName} {...this.getServerProperties()} /> ); @@ -1921,7 +2054,7 @@ export default createReactClass({ const Login = sdk.getComponent('structures.auth.Login'); view = ( { + this.setState({ + showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), + }); + }; + /* get the DOM node representing the given event */ getNodeForEventId(eventId) { if (!this.eventNodes) { @@ -402,10 +414,6 @@ export default class MessagePanel extends React.Component { }; _getEventTiles() { - const DateSeparator = sdk.getComponent('messages.DateSeparator'); - const EventListSummary = sdk.getComponent('views.elements.EventListSummary'); - const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary'); - this.eventNodes = {}; let i; @@ -447,184 +455,48 @@ export default class MessagePanel extends React.Component { this._readReceiptsByEvent = this._getReadReceiptsByShownEvent(); } + let grouper = null; + for (i = 0; i < this.props.events.length; i++) { const mxEv = this.props.events[i]; const eventId = mxEv.getId(); const last = (mxEv === lastShownEvent); - // Wrap initial room creation events into an EventListSummary - // Grouping only events sent by the same user that sent the `m.room.create` and only until - // the first non-state event or membership event which is not regarding the sender of the `m.room.create` event - const shouldGroup = (ev) => { - if (ev.getType() === "m.room.member" - && (ev.getStateKey() !== mxEv.getSender() || ev.getContent()["membership"] !== "join")) { - return false; - } - if (ev.isState() && ev.getSender() === mxEv.getSender()) { - return true; - } - return false; - }; - if (mxEv.getType() === "m.room.create") { - let summaryReadMarker = null; - const ts1 = mxEv.getTs(); - - if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) { - const dateSeparator =
  • ; - ret.push(dateSeparator); + if (grouper) { + if (grouper.shouldGroup(mxEv)) { + grouper.add(mxEv); + continue; + } else { + // not part of group, so get the group tiles, close the + // group, and continue like a normal event + ret.push(...grouper.getTiles()); + prevEvent = grouper.getNewPrevEvent(); + grouper = null; } - - // If RM event is the first in the summary, append the RM after the summary - summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(mxEv.getId()); - - // If this m.room.create event should be shown (room upgrade) then show it before the summary - if (this._shouldShowEvent(mxEv)) { - // pass in the mxEv as prevEvent as well so no extra DateSeparator is rendered - ret.push(...this._getTilesForEvent(mxEv, mxEv, false)); - } - - const summarisedEvents = []; // Don't add m.room.create here as we don't want it inside the summary - for (;i + 1 < this.props.events.length; i++) { - const collapsedMxEv = this.props.events[i + 1]; - - // Ignore redacted/hidden member events - if (!this._shouldShowEvent(collapsedMxEv)) { - // If this hidden event is the RM and in or at end of a summary put RM after the summary. - summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId()); - continue; - } - - if (!shouldGroup(collapsedMxEv) || this._wantsDateSeparator(mxEv, collapsedMxEv.getDate())) { - break; - } - - // If RM event is in the summary, mark it as such and the RM will be appended after the summary. - summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId()); - - summarisedEvents.push(collapsedMxEv); - } - - // At this point, i = the index of the last event in the summary sequence - const eventTiles = summarisedEvents.map((e) => { - // In order to prevent DateSeparators from appearing in the expanded form - // of EventListSummary, render each member event as if the previous - // one was itself. This way, the timestamp of the previous event === the - // timestamp of the current event, and no DateSeparator is inserted. - return this._getTilesForEvent(e, e, e === lastShownEvent); - }).reduce((a, b) => a.concat(b), []); - - // Get sender profile from the latest event in the summary as the m.room.create doesn't contain one - const ev = this.props.events[i]; - ret.push( - { eventTiles } - ); - - if (summaryReadMarker) { - ret.push(summaryReadMarker); - } - - prevEvent = mxEv; - continue; } - const wantTile = this._shouldShowEvent(mxEv); - - // Wrap consecutive member events in a ListSummary, ignore if redacted - if (isMembershipChange(mxEv) && wantTile) { - let summaryReadMarker = null; - const ts1 = mxEv.getTs(); - // Ensure that the key of the MemberEventListSummary does not change with new - // member events. This will prevent it from being re-created unnecessarily, and - // instead will allow new props to be provided. In turn, the shouldComponentUpdate - // method on MELS can be used to prevent unnecessary renderings. - // - // Whilst back-paginating with a MELS at the top of the panel, prevEvent will be null, - // so use the key "membereventlistsummary-initial". Otherwise, use the ID of the first - // membership event, which will not change during forward pagination. - const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial"); - - if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) { - const dateSeparator =
  • ; - ret.push(dateSeparator); + for (const Grouper of groupers) { + if (Grouper.canStartGroup(this, mxEv)) { + grouper = new Grouper(this, mxEv, prevEvent, lastShownEvent); } - - // If RM event is the first in the MELS, append the RM after MELS - summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(mxEv.getId()); - - const summarisedEvents = [mxEv]; - for (;i + 1 < this.props.events.length; i++) { - const collapsedMxEv = this.props.events[i + 1]; - - // Ignore redacted/hidden member events - if (!this._shouldShowEvent(collapsedMxEv)) { - // If this hidden event is the RM and in or at end of a MELS put RM after MELS. - summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId()); - continue; - } - - if (!isMembershipChange(collapsedMxEv) || - this._wantsDateSeparator(mxEv, collapsedMxEv.getDate())) { - break; - } - - // If RM event is in MELS mark it as such and the RM will be appended after MELS. - summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId()); - - summarisedEvents.push(collapsedMxEv); - } - - let highlightInMels = false; - - // At this point, i = the index of the last event in the summary sequence - let eventTiles = summarisedEvents.map((e) => { - if (e.getId() === this.props.highlightedEventId) { - highlightInMels = true; - } - // In order to prevent DateSeparators from appearing in the expanded form - // of MemberEventListSummary, render each member event as if the previous - // one was itself. This way, the timestamp of the previous event === the - // timestamp of the current event, and no DateSeparator is inserted. - return this._getTilesForEvent(e, e, e === lastShownEvent); - }).reduce((a, b) => a.concat(b), []); - - if (eventTiles.length === 0) { - eventTiles = null; - } - - ret.push( - { eventTiles } - ); - - if (summaryReadMarker) { - ret.push(summaryReadMarker); - } - - prevEvent = mxEv; - continue; } + if (!grouper) { + const wantTile = this._shouldShowEvent(mxEv); + if (wantTile) { + // make sure we unpack the array returned by _getTilesForEvent, + // otherwise react will auto-generate keys and we will end up + // replacing all of the DOM elements every time we paginate. + ret.push(...this._getTilesForEvent(prevEvent, mxEv, last)); + prevEvent = mxEv; + } - if (wantTile) { - // make sure we unpack the array returned by _getTilesForEvent, - // otherwise react will auto-generate keys and we will end up - // replacing all of the DOM elements every time we paginate. - ret.push(...this._getTilesForEvent(prevEvent, mxEv, last)); - prevEvent = mxEv; + const readMarker = this._readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex); + if (readMarker) ret.push(readMarker); } + } - const readMarker = this._readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex); - if (readMarker) ret.push(readMarker); + if (grouper) { + ret.push(...grouper.getTiles()); } return ret; @@ -651,7 +523,8 @@ export default class MessagePanel extends React.Component { // if there is a previous event and it has the same sender as this event // and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL if (prevEvent !== null && prevEvent.sender && mxEv.sender && mxEv.sender.userId === prevEvent.sender.userId && - (mxEv.getType() === prevEvent.getType() || eventTypeContinues) && + // if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile + haveTileForEvent(prevEvent) && (mxEv.getType() === prevEvent.getType() || eventTypeContinues) && (mxEv.getTs() - prevEvent.getTs() <= CONTINUATION_MAX_INTERVAL)) { continuation = true; } @@ -906,7 +779,7 @@ export default class MessagePanel extends React.Component { ); let whoIsTyping; - if (this.props.room && !this.props.tileShape) { + if (this.props.room && !this.props.tileShape && this.state.showTypingNotifications) { whoIsTyping = (, + ); + } + + // If this m.room.create event should be shown (room upgrade) then show it before the summary + if (panel._shouldShowEvent(createEvent)) { + // pass in the createEvent as prevEvent as well so no extra DateSeparator is rendered + ret.push(...panel._getTilesForEvent(createEvent, createEvent, false)); + } + + for (const ejected of this.ejectedEvents) { + ret.push(...panel._getTilesForEvent( + createEvent, ejected, createEvent === lastShownEvent, + )); + } + + const eventTiles = this.events.map((e) => { + // In order to prevent DateSeparators from appearing in the expanded form + // of EventListSummary, render each member event as if the previous + // one was itself. This way, the timestamp of the previous event === the + // timestamp of the current event, and no DateSeparator is inserted. + return panel._getTilesForEvent(e, e, e === lastShownEvent); + }).reduce((a, b) => a.concat(b), []); + // Get sender profile from the latest event in the summary as the m.room.create doesn't contain one + const ev = this.events[this.events.length - 1]; + ret.push( + + { eventTiles } + , + ); + + if (this.readMarker) { + ret.push(this.readMarker); + } + + return ret; + } + + getNewPrevEvent() { + return this.createEvent; + } +} + +// Wrap consecutive member events in a ListSummary, ignore if redacted +class MemberGrouper { + static canStartGroup = function(panel, ev) { + return panel._shouldShowEvent(ev) && isMembershipChange(ev); + } + + constructor(panel, ev, prevEvent, lastShownEvent) { + this.panel = panel; + this.readMarker = panel._readMarkerForEvent(ev.getId()); + this.events = [ev]; + this.prevEvent = prevEvent; + this.lastShownEvent = lastShownEvent; + } + + shouldGroup(ev) { + if (this.panel._wantsDateSeparator(this.events[0], ev.getDate())) { + return false; + } + return isMembershipChange(ev); + } + + add(ev) { + if (ev.getType() === 'm.room.member') { + // We'll just double check that it's worth our time to do so, through an + // ugly hack. If textForEvent returns something, we should group it for + // rendering but if it doesn't then we'll exclude it. + const renderText = textForEvent(ev); + if (!renderText || renderText.trim().length === 0) return; // quietly ignore + } + this.readMarker = this.readMarker || this.panel._readMarkerForEvent(ev.getId()); + this.events.push(ev); + } + + getTiles() { + // If we don't have any events to group, don't even try to group them. The logic + // below assumes that we have a group of events to deal with, but we might not if + // the events we were supposed to group were redacted. + if (!this.events || !this.events.length) return []; + + const DateSeparator = sdk.getComponent('messages.DateSeparator'); + const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary'); + + const panel = this.panel; + const lastShownEvent = this.lastShownEvent; + const ret = []; + + if (panel._wantsDateSeparator(this.prevEvent, this.events[0].getDate())) { + const ts = this.events[0].getTs(); + ret.push( +
  • , + ); + } + + // Ensure that the key of the MemberEventListSummary does not change with new + // member events. This will prevent it from being re-created unnecessarily, and + // instead will allow new props to be provided. In turn, the shouldComponentUpdate + // method on MELS can be used to prevent unnecessary renderings. + // + // Whilst back-paginating with a MELS at the top of the panel, prevEvent will be null, + // so use the key "membereventlistsummary-initial". Otherwise, use the ID of the first + // membership event, which will not change during forward pagination. + const key = "membereventlistsummary-" + ( + this.prevEvent ? this.events[0].getId() : "initial" + ); + + let highlightInMels; + let eventTiles = this.events.map((e) => { + if (e.getId() === panel.props.highlightedEventId) { + highlightInMels = true; + } + // In order to prevent DateSeparators from appearing in the expanded form + // of MemberEventListSummary, render each member event as if the previous + // one was itself. This way, the timestamp of the previous event === the + // timestamp of the current event, and no DateSeparator is inserted. + return panel._getTilesForEvent(e, e, e === lastShownEvent); + }).reduce((a, b) => a.concat(b), []); + + if (eventTiles.length === 0) { + eventTiles = null; + } + + ret.push( + + { eventTiles } + , + ); + + if (this.readMarker) { + ret.push(this.readMarker); + } + + return ret; + } + + getNewPrevEvent() { + return this.events[0]; + } +} + +// all the grouper classes that we use +const groupers = [CreationGrouper, MemberGrouper]; diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index b26ab5ff70..f1209b7b9e 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -22,6 +22,7 @@ import { _t } from '../../languageHandler'; import dis from '../../dispatcher'; import AccessibleButton from '../views/elements/AccessibleButton'; import MatrixClientContext from "../../contexts/MatrixClientContext"; +import AutoHideScrollbar from "./AutoHideScrollbar"; export default createReactClass({ displayName: 'MyGroups', @@ -62,8 +63,6 @@ export default createReactClass({ const Loader = sdk.getComponent("elements.Spinner"); const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader'); const GroupTile = sdk.getComponent("groups.GroupTile"); - const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); - let content; let contentHeader; @@ -74,7 +73,7 @@ export default createReactClass({ }); contentHeader = groupNodes.length > 0 ?

    { _t('Your Communities') }

    :
    ; content = groupNodes.length > 0 ? - +

    { _t( @@ -93,7 +92,7 @@ export default createReactClass({

    { groupNodes }
    - : + :
    { _t( "You're not currently a member of any communities.", diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index a725b73d9a..8d25116827 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -48,6 +48,7 @@ export default class RightPanel extends React.Component { phase: this._getPhaseFromProps(), isUserPrivilegedInGroup: null, member: this._getUserForPanel(), + verificationRequest: RightPanelStore.getSharedInstance().roomPanelPhaseParams.verificationRequest, }; this.onAction = this.onAction.bind(this); this.onRoomStateMember = this.onRoomStateMember.bind(this); @@ -68,15 +69,35 @@ export default class RightPanel extends React.Component { return this.props.user || lastParams['member']; } + // gets the current phase from the props and also maybe the store _getPhaseFromProps() { const rps = RightPanelStore.getSharedInstance(); + const userForPanel = this._getUserForPanel(); if (this.props.groupId) { if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) { dis.dispatch({action: "set_right_panel_phase", phase: RIGHT_PANEL_PHASES.GroupMemberList}); return RIGHT_PANEL_PHASES.GroupMemberList; } return rps.groupPanelPhase; - } else if (this._getUserForPanel()) { + } else if (userForPanel) { + // XXX FIXME AAAAAARGH: What is going on with this class!? It takes some of its state + // from its props and some from a store, except if the contents of the store changes + // while it's mounted in which case it replaces all of its state with that of the store, + // except it uses a dispatch instead of a normal store listener? + // Unfortunately rewriting this would almost certainly break showing the right panel + // in some of the many cases, and I don't have time to re-architect it and test all + // the flows now, so adding yet another special case so if the store thinks there is + // a verification going on for the member we're displaying, we show that, otherwise + // we race if a verification is started while the panel isn't displayed because we're + // not mounted in time to get the dispatch. + // Until then, let this code serve as a warning from history. + if ( + rps.roomPanelPhaseParams.member && + userForPanel.userId === rps.roomPanelPhaseParams.member.userId && + rps.roomPanelPhaseParams.verificationRequest + ) { + return rps.roomPanelPhase; + } return RIGHT_PANEL_PHASES.RoomMemberInfo; } else { if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.roomPanelPhase)) { @@ -160,6 +181,8 @@ export default class RightPanel extends React.Component { groupId: payload.groupId, member: payload.member, event: payload.event, + verificationRequest: payload.verificationRequest, + verificationRequestPromise: payload.verificationRequestPromise, }); } } @@ -179,62 +202,83 @@ export default class RightPanel extends React.Component { let panel =
    ; - if (this.props.roomId && this.state.phase === RIGHT_PANEL_PHASES.RoomMemberList) { - panel = ; - } else if (this.props.groupId && this.state.phase === RIGHT_PANEL_PHASES.GroupMemberList) { - panel = ; - } else if (this.state.phase === RIGHT_PANEL_PHASES.GroupRoomList) { - panel = ; - } else if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberInfo) { - if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { - const onClose = () => { - dis.dispatch({ - action: "view_user", - member: null, - }); - }; - panel = ; - } else { - panel = ; - } - } else if (this.state.phase === RIGHT_PANEL_PHASES.Room3pidMemberInfo) { - panel = ; - } else if (this.state.phase === RIGHT_PANEL_PHASES.GroupMemberInfo) { - if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { - const onClose = () => { - dis.dispatch({ - action: "view_user", - member: null, - }); - }; - panel = ; - } else { - panel = ( - ; + } + break; + case RIGHT_PANEL_PHASES.GroupMemberList: + if (this.props.groupId) { + panel = ; + } + break; + case RIGHT_PANEL_PHASES.GroupRoomList: + panel = ; + break; + case RIGHT_PANEL_PHASES.RoomMemberInfo: + case RIGHT_PANEL_PHASES.EncryptionPanel: + if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { + const onClose = () => { + dis.dispatch({ + action: "view_user", + member: this.state.phase === RIGHT_PANEL_PHASES.EncryptionPanel ? this.state.member : null, + }); + }; + panel = ; + } else { + panel = ; + } + break; + case RIGHT_PANEL_PHASES.Room3pidMemberInfo: + panel = ; + break; + case RIGHT_PANEL_PHASES.GroupMemberInfo: + if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { + const onClose = () => { + dis.dispatch({ + action: "view_user", + member: null, + }); + }; + panel = - ); - } - } else if (this.state.phase === RIGHT_PANEL_PHASES.GroupRoomInfo) { - panel = ; - } else if (this.state.phase === RIGHT_PANEL_PHASES.NotificationPanel) { - panel = ; - } else if (this.state.phase === RIGHT_PANEL_PHASES.FilePanel) { - panel = ; + key={this.state.member.userId} + onClose={onClose} />; + } else { + panel = ( + + ); + } + break; + case RIGHT_PANEL_PHASES.GroupRoomInfo: + panel = ; + break; + case RIGHT_PANEL_PHASES.NotificationPanel: + panel = ; + break; + case RIGHT_PANEL_PHASES.FilePanel: + panel = ; + break; } const classes = classNames("mx_RightPanel", "mx_fadable", { @@ -244,7 +288,7 @@ export default class RightPanel extends React.Component { }); return ( -