diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index ffd398cb14..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,7 +56,6 @@ 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/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d436ca690..b6f25f1858 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,318 @@ +Changes in [2.3.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.3.1) (2020-04-01) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.3.0...v2.3.1) + + * Fix jitsi popout URL + [\#4327](https://github.com/matrix-org/matrix-react-sdk/pull/4327) + * Remove underscore from Jitsi conference names + [\#4324](https://github.com/matrix-org/matrix-react-sdk/pull/4324) + * Fix popout support for jitsi widgets + [\#4322](https://github.com/matrix-org/matrix-react-sdk/pull/4322) + +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) diff --git a/README.md b/README.md index 0fbed22030..69aafeb724 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 @@ -45,7 +45,7 @@ Code should be committed as follows: * In practice, `matrix-react-sdk` is still evolving so fast that the maintenance burden of customising and overriding these components for Riot can seriously impede development. So right now, there should be very few (if any) customisations for Riot. - * CSS: https://github.com/vector-im/riot-web/tree/master/src/skins/vector/css/matrix-react-sdk + * CSS: https://github.com/matrix-org/matrix-react-sdk/tree/master/res/css * Theme specific CSS & resources: https://github.com/matrix-org/matrix-react-sdk/tree/master/res/themes React components in matrix-react-sdk are come in two different flavours: 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/settings.md b/docs/settings.md index 9b780c27c9..46e4a68fdb 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -51,6 +51,17 @@ Settings are the different options a user may set or experience in the applicati } ``` +Settings that support the config level can be set in the config file under the `settingDefaults` key (note that some settings, like the "theme" setting, are special cased in the config file): +```json +{ + ... + "settingDefaults": { + "settingName": true + }, + ... +} +``` + ### Getting values for a setting After importing `SettingsStore`, simply make a call to `SettingsStore.getValue`. The `roomId` parameter should always diff --git a/package.json b/package.json index 6380eabd9e..616f3f541e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "2.2.1", + "version": "2.3.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -40,15 +40,15 @@ "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" @@ -64,15 +64,14 @@ "create-react-class": "^15.6.0", "diff-dom": "^4.1.3", "diff-match-patch": "^1.0.4", - "emojibase-data": "^4.0.2", - "emojibase-regex": "^3.0.0", + "emojibase-data": "^5.0.1", + "emojibase-regex": "^4.0.1", "escape-html": "^1.0.3", "file-saver": "^1.3.3", "filesize": "3.5.6", "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-to-regexp": "^0.4.1", "highlight.js": "^9.15.8", @@ -84,6 +83,7 @@ "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", @@ -93,7 +93,6 @@ "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", "text-encoding-utf-8": "^1.0.1", @@ -118,6 +117,8 @@ "@babel/preset-typescript": "^7.7.4", "@babel/register": "^7.7.4", "@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": "^3.3.1", diff --git a/res/css/_common.scss b/res/css/_common.scss index e062e0bd73..03442ca510 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -16,6 +16,12 @@ See the License for the specific language governing permissions and limitations under the License. */ +@import "./_font-sizes.scss"; + +:root { + font-size: 15px; +} + html { /* hack to stop overscroll bounce on OSX and iOS. N.B. Breaks things when we have legitimate horizontal overscroll */ @@ -25,7 +31,7 @@ html { body { font-family: $font-family; - font-size: 15px; + font-size: $font-15px; background-color: $primary-bg-color; color: $primary-fg-color; border: 0px; @@ -42,10 +48,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. @@ -55,7 +66,7 @@ b { h2 { color: $primary-fg-color; font-weight: 400; - font-size: 18px; + font-size: $font-18px; margin-top: 16px; margin-bottom: 16px; } @@ -71,7 +82,7 @@ input[type=search], input[type=password] { padding: 9px; font-family: $font-family; - font-size: 14px; + font-size: $font-14px; font-weight: 600; min-width: 0; } @@ -202,37 +213,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. @@ -279,7 +259,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { color: $light-fg-color; z-index: 4012; font-weight: 300; - font-size: 15px; + font-size: $font-15px; position: relative; padding: 25px 30px 30px 30px; max-height: 80%; @@ -347,8 +327,8 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { } .mx_Dialog_title { - font-size: 22px; - line-height: 36px; + font-size: $font-22px; + line-height: $font-36px; color: $dialog-title-fg-color; } @@ -376,7 +356,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { .mx_Dialog_content { margin: 24px 0 68px; - font-size: 14px; + font-size: $font-14px; color: $primary-fg-color; word-wrap: break-word; } @@ -472,7 +452,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { } .mx_TextInputDialog_input { - font-size: 15px; + font-size: $font-15px; border-radius: 3px; border: 1px solid $input-border-color; padding: 9px; diff --git a/res/css/_components.scss b/res/css/_components.scss index b3ab7564b1..607257400b 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -1,5 +1,6 @@ // autogenerated by rethemendex.sh @import "./_common.scss"; +@import "./_font-sizes.scss"; @import "./structures/_AutoHideScrollbar.scss"; @import "./structures/_CompatibilityPage.scss"; @import "./structures/_ContextualMenu.scss"; @@ -65,6 +66,7 @@ @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"; @@ -178,7 +180,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"; @@ -187,6 +188,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/_font-sizes.scss b/res/css/_font-sizes.scss new file mode 100644 index 0000000000..ad9e2e7103 --- /dev/null +++ b/res/css/_font-sizes.scss @@ -0,0 +1,63 @@ +/* +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. +*/ + +$font-8px: 0.533rem; +$font-9px: 0.600rem; +$font-10px: 0.667rem; +$font-10-4px: 0.693rem; +$font-11px: 0.733rem; +$font-12px: 0.800rem; +$font-13px: 0.867rem; +$font-14px: 0.933rem; +$font-15px: 1.000rem; +$font-16px: 1.067rem; +$font-17px: 1.133rem; +$font-18px: 1.200rem; +$font-19px: 1.267rem; +$font-20px: 1.333rem; +$font-21px: 1.400rem; +$font-22px: 1.467rem; +$font-23px: 1.533rem; +$font-24px: 1.600rem; +$font-25px: 1.667rem; +$font-26px: 1.733rem; +$font-27px: 1.800rem; +$font-28px: 1.867rem; +$font-29px: 1.933rem; +$font-30px: 2.000rem; +$font-31px: 2.067rem; +$font-32px: 2.133rem; +$font-33px: 2.200rem; +$font-34px: 2.267rem; +$font-35px: 2.333rem; +$font-36px: 2.400rem; +$font-37px: 2.467rem; +$font-38px: 2.533rem; +$font-39px: 2.600rem; +$font-40px: 2.667rem; +$font-41px: 2.733rem; +$font-42px: 2.800rem; +$font-43px: 2.867rem; +$font-44px: 2.933rem; +$font-45px: 3.000rem; +$font-46px: 3.067rem; +$font-47px: 3.133rem; +$font-48px: 3.200rem; +$font-49px: 3.267rem; +$font-50px: 3.333rem; +$font-51px: 3.400rem; +$font-52px: 3.467rem; +$font-400px: 26.667rem; 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/_ContextualMenu.scss b/res/css/structures/_ContextualMenu.scss index fa2d87029d..61070a0541 100644 --- a/res/css/structures/_ContextualMenu.scss +++ b/res/css/structures/_ContextualMenu.scss @@ -36,7 +36,7 @@ limitations under the License. background-color: $menu-bg-color; color: $primary-fg-color; position: absolute; - font-size: 14px; + font-size: $font-14px; z-index: 5001; } diff --git a/res/css/structures/_CreateRoom.scss b/res/css/structures/_CreateRoom.scss index 10f9e23a02..e859beb20e 100644 --- a/res/css/structures/_CreateRoom.scss +++ b/res/css/structures/_CreateRoom.scss @@ -26,7 +26,7 @@ limitations under the License. border-radius: 3px; border: 1px solid $strong-input-border-color; font-weight: 300; - font-size: 13px; + font-size: $font-13px; padding: 9px; margin-top: 6px; } diff --git a/res/css/structures/_FilePanel.scss b/res/css/structures/_FilePanel.scss index 87e885e668..859ee28035 100644 --- a/res/css/structures/_FilePanel.scss +++ b/res/css/structures/_FilePanel.scss @@ -49,7 +49,7 @@ limitations under the License. .mx_FilePanel .mx_EventTile .mx_MFileBody_download { display: flex; - font-size: 14px; + font-size: $font-14px; color: $event-timestamp-color; } @@ -60,7 +60,7 @@ limitations under the License. .mx_FilePanel .mx_EventTile .mx_MImageBody_size { flex: 1 0 0; - font-size: 11px; + font-size: $font-11px; text-align: right; white-space: nowrap; } @@ -80,7 +80,7 @@ limitations under the License. flex: 1 1 auto; line-height: initial; padding: 0px; - font-size: 11px; + font-size: $font-11px; opacity: 1.0; color: $event-timestamp-color; } @@ -90,7 +90,7 @@ limitations under the License. text-align: right; visibility: visible; position: initial; - font-size: 11px; + font-size: $font-11px; opacity: 1.0; color: $event-timestamp-color; } diff --git a/res/css/structures/_GroupView.scss b/res/css/structures/_GroupView.scss index 517b8b1922..ed0cf121a4 100644 --- a/res/css/structures/_GroupView.scss +++ b/res/css/structures/_GroupView.scss @@ -134,7 +134,7 @@ limitations under the License. overflow: hidden; color: $primary-fg-color; font-weight: bold; - font-size: 22px; + font-size: $font-22px; padding-left: 19px; padding-right: 16px; /* why isn't text-overflow working? */ @@ -148,7 +148,7 @@ limitations under the License. max-height: 42px; color: $settings-grey-fg-color; font-weight: 300; - font-size: 13px; + font-size: $font-13px; padding-left: 19px; margin-right: 16px; overflow: hidden; @@ -180,10 +180,6 @@ limitations under the License. line-height: 2em; } -.mx_GroupView > .mx_MainSplit { - flex: 1; -} - .mx_GroupView_body { flex-grow: 1; } @@ -200,7 +196,7 @@ limitations under the License. text-transform: uppercase; color: $h3-color; font-weight: 600; - font-size: 13px; + font-size: $font-13px; margin-bottom: 10px; } @@ -230,7 +226,7 @@ limitations under the License. .mx_GroupView_rooms_header_addRow_label { display: inline-block; vertical-align: top; - line-height: 24px; + line-height: $font-24px; padding-left: 28px; color: $accent-color; } @@ -262,7 +258,7 @@ limitations under the License. .mx_GroupView_membershipSection_description { /* To match textButton */ - line-height: 34px; + line-height: $font-34px; } .mx_GroupView_membershipSection_description .mx_BaseAvatar { @@ -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/_HomePage.scss b/res/css/structures/_HomePage.scss index 3aa80f6f59..0160cf368b 100644 --- a/res/css/structures/_HomePage.scss +++ b/res/css/structures/_HomePage.scss @@ -23,3 +23,84 @@ limitations under the License. margin-left: auto; margin-right: auto; } + +.mx_HomePage_default { + text-align: center; + + .mx_HomePage_default_wrapper { + padding: 25vh 0 12px; + } + + img { + height: 48px; + } + + h1 { + font-weight: 600; + font-size: $font-32px; + line-height: $font-44px; + margin-bottom: 4px; + } + + h4 { + margin-top: 4px; + font-weight: 600; + font-size: $font-18px; + line-height: $font-25px; + color: $muted-fg-color; + } + + .mx_HomePage_default_buttons { + margin: 80px auto 0; + width: fit-content; + + .mx_AccessibleButton { + padding: 73px 8px 15px; // top: 20px top padding + 40px icon + 13px margin + + width: 104px; // 120px - 2* 8px + margin: 0 39px; // 55px - 2* 8px + position: relative; + display: inline-block; + border-radius: 8px; + vertical-align: top; + word-break: break-word; + + font-weight: 600; + font-size: $font-15px; + line-height: $font-20px; + color: $muted-fg-color; + + &:hover { + color: $accent-color; + background: rgba(#03b381, 0.06); + + &::before { + background-color: $accent-color; + } + } + + &::before { + top: 20px; + left: 40px; // (120px-40px)/2 + width: 40px; + height: 40px; + + content: ''; + position: absolute; + background-color: $muted-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + } + + &.mx_HomePage_button_sendDm::before { + mask-image: url('$(res)/img/feather-customised/message-circle.svg'); + } + &.mx_HomePage_button_explore::before { + mask-image: url('$(res)/img/feather-customised/explore.svg'); + } + &.mx_HomePage_button_createGroup::before { + mask-image: url('$(res)/img/feather-customised/group.svg'); + } + } + } +} diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index 85fdfa092d..7d57425f6f 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -147,7 +147,7 @@ limitations under the License. } .mx_AccessibleButton { - font-size: 14px; + font-size: $font-14px; margin: 4px 0 1px 9px; padding: 9px; padding-left: 42px; 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..73f1332cd0 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,12 +95,17 @@ limitations under the License. display: flex; flex-direction: column; + overflow-y: auto; +} + +.mx_MyGroups_scrollable { + overflow-y: inherit; } .mx_MyGroups_placeholder { background-color: $info-plinth-bg-color; color: $info-plinth-fg-color; - line-height: 400px; + line-height: $font-400px; border-radius: 10px; text-align: center; } @@ -147,11 +149,11 @@ limitations under the License. .mx_GroupTile_profile .mx_GroupTile_name { margin: 0px; - font-size: 15px; + font-size: $font-15px; } .mx_GroupTile_profile .mx_GroupTile_groupId { - font-size: 13px; + font-size: $font-13px; opacity: 0.7; } @@ -159,7 +161,7 @@ limitations under the License. display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; - font-size: 13px; + font-size: $font-13px; max-height: 36px; overflow: hidden; } diff --git a/res/css/structures/_NotificationPanel.scss b/res/css/structures/_NotificationPanel.scss index c9e0261ec9..44205b1f01 100644 --- a/res/css/structures/_NotificationPanel.scss +++ b/res/css/structures/_NotificationPanel.scss @@ -39,7 +39,7 @@ limitations under the License. .mx_NotificationPanel .mx_EventTile_roomName { font-weight: bold; - font-size: 14px; + font-size: $font-14px; } .mx_NotificationPanel .mx_EventTile_roomName a { @@ -54,7 +54,7 @@ limitations under the License. .mx_NotificationPanel .mx_EventTile .mx_SenderProfile, .mx_NotificationPanel .mx_EventTile .mx_MessageTimestamp { color: $primary-fg-color; - font-size: 12px; + font-size: $font-12px; display: inline; padding-left: 0px; } diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss index 3c373e8883..10878322e3 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -96,7 +96,7 @@ limitations under the License. } .mx_RightPanel_headerButton_badge { - font-size: 8px; + font-size: $font-8px; border-radius: 8px; color: $accent-fg-color; background-color: $accent-color; diff --git a/res/css/structures/_RoomDirectory.scss b/res/css/structures/_RoomDirectory.scss index 5ae8df7176..e0814182f5 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: $font-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: $font-18px; font-weight: 600; } @@ -123,7 +124,7 @@ limitations under the License. border-radius: 10px; display: inline-block; height: 20px; - line-height: 20px; + line-height: $font-20px; padding: 0 5px; color: $accent-fg-color; background-color: $rte-room-pill-color; @@ -135,7 +136,7 @@ limitations under the License. } .mx_RoomDirectory_alias { - font-size: 12px; + font-size: $font-12px; color: $settings-grey-fg-color; } @@ -148,8 +149,8 @@ limitations under the License. padding: 0; } -.mx_RoomDirectory p { - font-size: 14px; +.mx_RoomDirectory > span { + font-size: $font-15px; margin-top: 0; .mx_AccessibleButton { diff --git a/res/css/structures/_RoomStatusBar.scss b/res/css/structures/_RoomStatusBar.scss index 090a40235f..cd4390ee5c 100644 --- a/res/css/structures/_RoomStatusBar.scss +++ b/res/css/structures/_RoomStatusBar.scss @@ -32,7 +32,7 @@ limitations under the License. .mx_RoomStatusBar_callBar { height: 50px; - line-height: 50px; + line-height: $font-50px; } .mx_RoomStatusBar_placeholderIndicator span { @@ -94,7 +94,7 @@ limitations under the License. border-radius: 40px; width: 24px; height: 24px; - line-height: 24px; + line-height: $font-24px; font-size: 0.8em; vertical-align: top; text-align: center; @@ -132,7 +132,7 @@ limitations under the License. .mx_RoomStatusBar_connectionLostBar_desc { color: $primary-fg-color; - font-size: 13px; + font-size: $font-13px; opacity: 0.5; padding-bottom: 20px; } @@ -145,7 +145,7 @@ limitations under the License. .mx_RoomStatusBar_typingBar { height: 50px; - line-height: 50px; + line-height: $font-50px; color: $primary-fg-color; opacity: 0.5; @@ -155,7 +155,7 @@ limitations under the License. .mx_RoomStatusBar_isAlone { height: 50px; - line-height: 50px; + line-height: $font-50px; color: $primary-fg-color; opacity: 0.5; @@ -174,11 +174,11 @@ limitations under the License. .mx_RoomStatusBar_callBar { height: 40px; - line-height: 40px; + line-height: $font-40px; } .mx_RoomStatusBar_typingBar { height: 40px; - line-height: 40px; + line-height: $font-40px; } } diff --git a/res/css/structures/_RoomSubList.scss b/res/css/structures/_RoomSubList.scss index be44563cfb..2e0c94263e 100644 --- a/res/css/structures/_RoomSubList.scss +++ b/res/css/structures/_RoomSubList.scss @@ -68,7 +68,7 @@ limitations under the License. text-transform: uppercase; color: $roomsublist-label-fg-color; font-weight: 700; - font-size: 12px; + font-size: $font-12px; margin-left: 8px; } @@ -76,7 +76,7 @@ limitations under the License. flex: 0 0 auto; border-radius: 8px; font-weight: 600; - font-size: 12px; + font-size: $font-12px; padding: 0 5px; color: $roomtile-badge-fg-color; background-color: $roomtile-name-color; @@ -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/_RoomView.scss b/res/css/structures/_RoomView.scss index 5e826306c6..f2154ef448 100644 --- a/res/css/structures/_RoomView.scss +++ b/res/css/structures/_RoomView.scss @@ -23,7 +23,7 @@ limitations under the License. .mx_RoomView_fileDropTarget { min-width: 0px; width: 100%; - font-size: 18px; + font-size: $font-18px; text-align: center; pointer-events: none; @@ -186,7 +186,7 @@ limitations under the License. .mx_RoomView_empty { flex: 1 1 auto; - font-size: 13px; + font-size: $font-13px; padding-left: 3em; padding-right: 3em; margin-right: 20px; diff --git a/res/css/structures/_TabbedView.scss b/res/css/structures/_TabbedView.scss index 7904df5a82..4a4bb125a3 100644 --- a/res/css/structures/_TabbedView.scss +++ b/res/css/structures/_TabbedView.scss @@ -39,7 +39,7 @@ limitations under the License. cursor: pointer; display: block; border-radius: 3px; - font-size: 14px; + font-size: $font-14px; min-height: 24px; // use min-height instead of height to allow the label to overflow a bit margin-bottom: 6px; position: relative; diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index dddd2e324c..0065ffa502 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 { @@ -135,9 +137,9 @@ limitations under the License. top: -8px; border-radius: 8px; background-color: $neutral-badge-color; - color: #ffffff; + color: #000; font-weight: 600; - font-size: 10px; + font-size: $font-10px; text-align: center; padding-top: 1px; padding-left: 4px; @@ -155,7 +157,7 @@ limitations under the License. border-radius: 8px; color: $accent-fg-color; font-weight: 600; - font-size: 14px; + font-size: $font-14px; padding: 0 5px; background-color: $roomtile-name-color; } diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss index d1687743d6..af595aaeee 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -77,7 +77,7 @@ limitations under the License. grid-column: 1 / 3; grid-row: 1; margin: 0; - font-size: 15px; + font-size: $font-15px; font-weight: 600; } @@ -96,11 +96,11 @@ limitations under the License. white-space: nowrap; text-overflow: ellipsis; margin: 4px 0 11px 0; - font-size: 12px; + font-size: $font-12px; } .mx_Toast_deviceID { - font-size: 10px; + font-size: $font-10px; } } } diff --git a/res/css/structures/_TopLeftMenuButton.scss b/res/css/structures/_TopLeftMenuButton.scss index ee03978f18..53d44e7c24 100644 --- a/res/css/structures/_TopLeftMenuButton.scss +++ b/res/css/structures/_TopLeftMenuButton.scss @@ -32,7 +32,7 @@ limitations under the License. .mx_TopLeftMenuButton_name { margin: 0 7px; - font-size: 18px; + font-size: $font-18px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; diff --git a/res/css/structures/_ViewSource.scss b/res/css/structures/_ViewSource.scss index b908861c6f..421d1f03cd 100644 --- a/res/css/structures/_ViewSource.scss +++ b/res/css/structures/_ViewSource.scss @@ -29,7 +29,7 @@ limitations under the License. .mx_ViewSource pre { text-align: left; - font-size: 12px; + font-size: $font-12px; padding: 0.5em 1em 0.5em 1em; word-wrap: break-word; white-space: pre-wrap; diff --git a/res/css/structures/auth/_CompleteSecurity.scss b/res/css/structures/auth/_CompleteSecurity.scss index 2bf51d9574..3050840fe8 100644 --- a/res/css/structures/auth/_CompleteSecurity.scss +++ b/res/css/structures/auth/_CompleteSecurity.scss @@ -34,7 +34,11 @@ limitations under the License. } .mx_CompleteSecurity_body { - font-size: 15px; + font-size: $font-15px; +} + +.mx_CompleteSecurity_waiting { + color: $notice-secondary-color; } .mx_CompleteSecurity_actionRow { diff --git a/res/css/views/auth/_AuthBody.scss b/res/css/views/auth/_AuthBody.scss index 7c5b008535..468a4b3d62 100644 --- a/res/css/views/auth/_AuthBody.scss +++ b/res/css/views/auth/_AuthBody.scss @@ -17,7 +17,7 @@ limitations under the License. .mx_AuthBody { width: 500px; - font-size: 12px; + font-size: $font-12px; color: $authpage-secondary-color; background-color: $authpage-body-bg-color; border-radius: 0 4px 4px 0; @@ -25,14 +25,14 @@ limitations under the License. box-sizing: border-box; h2 { - font-size: 24px; + font-size: $font-24px; font-weight: 600; margin-top: 8px; color: $authpage-primary-color; } h3 { - font-size: 14px; + font-size: $font-14px; font-weight: 600; color: $authpage-primary-color; } @@ -98,7 +98,7 @@ limitations under the License. .mx_AuthBody_editServerDetails { padding-left: 1em; - font-size: 12px; + font-size: $font-12px; font-weight: normal; } diff --git a/res/css/views/auth/_AuthButtons.scss b/res/css/views/auth/_AuthButtons.scss index 553adeee14..8deb0f80ac 100644 --- a/res/css/views/auth/_AuthButtons.scss +++ b/res/css/views/auth/_AuthButtons.scss @@ -43,7 +43,7 @@ limitations under the License. cursor: pointer; - font-size: 15px; + font-size: $font-15px; padding: 0 11px; word-break: break-word; } diff --git a/res/css/views/auth/_AuthFooter.scss b/res/css/views/auth/_AuthFooter.scss index ab169a6898..0bc2743d54 100644 --- a/res/css/views/auth/_AuthFooter.scss +++ b/res/css/views/auth/_AuthFooter.scss @@ -17,7 +17,7 @@ limitations under the License. .mx_AuthFooter { text-align: center; width: 100%; - font-size: 14px; + font-size: $font-14px; opacity: 0.72; padding: 20px 0; background: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.8)); diff --git a/res/css/views/auth/_CompleteSecurityBody.scss b/res/css/views/auth/_CompleteSecurityBody.scss index c7860fbe74..46b7abe2cc 100644 --- a/res/css/views/auth/_CompleteSecurityBody.scss +++ b/res/css/views/auth/_CompleteSecurityBody.scss @@ -24,13 +24,13 @@ limitations under the License. box-sizing: border-box; h2 { - font-size: 24px; + font-size: $font-24px; font-weight: 600; margin-top: 0; } h3 { - font-size: 14px; + font-size: $font-14px; font-weight: 600; } diff --git a/res/css/views/auth/_InteractiveAuthEntryComponents.scss b/res/css/views/auth/_InteractiveAuthEntryComponents.scss index 85007aeecb..05cddf2c48 100644 --- a/res/css/views/auth/_InteractiveAuthEntryComponents.scss +++ b/res/css/views/auth/_InteractiveAuthEntryComponents.scss @@ -60,3 +60,14 @@ limitations under the License. .mx_InteractiveAuthEntryComponents_passwordSection { width: 300px; } + +.mx_InteractiveAuthEntryComponents_sso_buttons { + display: flex; + flex-direction: row; + justify-content: flex-end; + margin-top: 20px; + + .mx_AccessibleButton { + margin-left: 5px; + } +} diff --git a/res/css/views/auth/_LanguageSelector.scss b/res/css/views/auth/_LanguageSelector.scss index 6f7eac0cf6..781561f876 100644 --- a/res/css/views/auth/_LanguageSelector.scss +++ b/res/css/views/auth/_LanguageSelector.scss @@ -20,7 +20,7 @@ limitations under the License. .mx_AuthBody_language .mx_Dropdown_input { border: none; - font-size: 14px; + font-size: $font-14px; font-weight: 600; color: $authpage-lang-color; } diff --git a/res/css/views/auth/_ServerTypeSelector.scss b/res/css/views/auth/_ServerTypeSelector.scss index ed781726b7..fbd3d2655d 100644 --- a/res/css/views/auth/_ServerTypeSelector.scss +++ b/res/css/views/auth/_ServerTypeSelector.scss @@ -65,5 +65,5 @@ limitations under the License. } .mx_ServerTypeSelector_description { - font-size: 10px; + font-size: $font-10px; } diff --git a/res/css/views/context_menus/_RoomTileContextMenu.scss b/res/css/views/context_menus/_RoomTileContextMenu.scss index 308cecfe1e..9697ac9bef 100644 --- a/res/css/views/context_menus/_RoomTileContextMenu.scss +++ b/res/css/views/context_menus/_RoomTileContextMenu.scss @@ -38,7 +38,7 @@ limitations under the License. white-space: nowrap; display: flex; align-items: center; - line-height: 16px; + line-height: $font-16px; } .mx_RoomTileContextMenu_tag_field.mx_RoomTileContextMenu_tag_fieldSet { diff --git a/res/css/views/context_menus/_StatusMessageContextMenu.scss b/res/css/views/context_menus/_StatusMessageContextMenu.scss index 2c8d608950..fceb7fba34 100644 --- a/res/css/views/context_menus/_StatusMessageContextMenu.scss +++ b/res/css/views/context_menus/_StatusMessageContextMenu.scss @@ -44,7 +44,7 @@ input.mx_StatusMessageContextMenu_message { .mx_StatusMessageContextMenu_clear { @mixin mx_DialogButton; align-self: start; - font-size: 12px; + font-size: $font-12px; padding: 6px 1em; border: 1px solid transparent; margin-right: 10px; diff --git a/res/css/views/context_menus/_TagTileContextMenu.scss b/res/css/views/context_menus/_TagTileContextMenu.scss index 46b279ce2d..e4ccc030a2 100644 --- a/res/css/views/context_menus/_TagTileContextMenu.scss +++ b/res/css/views/context_menus/_TagTileContextMenu.scss @@ -22,7 +22,7 @@ limitations under the License. white-space: nowrap; display: flex; align-items: center; - line-height: 16px; + line-height: $font-16px; } .mx_TagTileContextMenu_item object { diff --git a/res/css/views/context_menus/_TopLeftMenu.scss b/res/css/views/context_menus/_TopLeftMenu.scss index ed0d0106bc..973c306695 100644 --- a/res/css/views/context_menus/_TopLeftMenu.scss +++ b/res/css/views/context_menus/_TopLeftMenu.scss @@ -19,12 +19,12 @@ limitations under the License. border-radius: 4px; .mx_TopLeftMenu_greyedText { - font-size: 12px; + font-size: $font-12px; opacity: 0.5; } .mx_TopLeftMenu_upgradeLink { - font-size: 12px; + font-size: $font-12px; img { margin-left: 5px; diff --git a/res/css/views/dialogs/_AddressPickerDialog.scss b/res/css/views/dialogs/_AddressPickerDialog.scss index 39a9260ba3..136e497994 100644 --- a/res/css/views/dialogs/_AddressPickerDialog.scss +++ b/res/css/views/dialogs/_AddressPickerDialog.scss @@ -28,7 +28,7 @@ limitations under the License. .mx_AddressPickerDialog_input, .mx_AddressPickerDialog_input:focus { height: 26px; - font-size: 14px; + font-size: $font-14px; font-family: $font-family; padding-left: 12px; padding-right: 12px; @@ -50,7 +50,7 @@ limitations under the License. .mx_AddressPickerDialog_inputContainer { border-radius: 3px; border: solid 1px $input-border-color; - line-height: 36px; + line-height: $font-36px; padding-left: 4px; padding-right: 4px; padding-top: 1px; diff --git a/res/css/views/dialogs/_ConfirmUserActionDialog.scss b/res/css/views/dialogs/_ConfirmUserActionDialog.scss index b859d6bf4d..823f4d1e28 100644 --- a/res/css/views/dialogs/_ConfirmUserActionDialog.scss +++ b/res/css/views/dialogs/_ConfirmUserActionDialog.scss @@ -26,22 +26,22 @@ limitations under the License. } .mx_ConfirmUserActionDialog_name { - font-size: 18px; + font-size: $font-18px; } .mx_ConfirmUserActionDialog_userId { - font-size: 13px; + font-size: $font-13px; } .mx_ConfirmUserActionDialog_reasonField { font-family: $font-family; - font-size: 14px; + font-size: $font-14px; color: $primary-fg-color; background-color: $primary-bg-color; border-radius: 3px; border: solid 1px $input-border-color; - line-height: 36px; + line-height: $font-36px; padding-left: 16px; padding-right: 16px; padding-top: 1px; diff --git a/res/css/views/dialogs/_CreateGroupDialog.scss b/res/css/views/dialogs/_CreateGroupDialog.scss index 128eacc3ce..f7bfc61a98 100644 --- a/res/css/views/dialogs/_CreateGroupDialog.scss +++ b/res/css/views/dialogs/_CreateGroupDialog.scss @@ -25,7 +25,7 @@ limitations under the License. } .mx_CreateGroupDialog_input { - font-size: 15px; + font-size: $font-15px; border-radius: 3px; border: 1px solid $input-border-color; padding: 9px; @@ -44,7 +44,7 @@ limitations under the License. .mx_CreateGroupDialog_prefix, .mx_CreateGroupDialog_suffix { padding: 0px 5px; - line-height: 37px; + line-height: $font-37px; background-color: $input-darker-bg-color; border: 1px solid $input-border-color; text-align: center; diff --git a/res/css/views/dialogs/_CreateRoomDialog.scss b/res/css/views/dialogs/_CreateRoomDialog.scss index 7416ec2ef4..2678f7b4ad 100644 --- a/res/css/views/dialogs/_CreateRoomDialog.scss +++ b/res/css/views/dialogs/_CreateRoomDialog.scss @@ -15,6 +15,8 @@ limitations under the License. */ .mx_CreateRoomDialog_details { + margin-top: 15px; + .mx_CreateRoomDialog_details_summary { outline: none; list-style: none; @@ -49,7 +51,7 @@ limitations under the License. } .mx_CreateRoomDialog_input { - font-size: 15px; + font-size: $font-15px; border-radius: 3px; border: 1px solid $input-border-color; padding: 9px; @@ -71,11 +73,19 @@ limitations under the License. } .mx_CreateRoomDialog { - &.mx_Dialog_fixedWidth { width: 450px; } + .mx_Dialog_content { + margin-bottom: 40px; + } + + p, + .mx_Field_input label { + color: $muted-fg-color; + } + .mx_SettingsFlag { display: flex; } @@ -90,5 +100,18 @@ limitations under the License. flex: 0 0 auto; margin-left: 30px; } + + .mx_CreateRoomDialog_topic { + margin-bottom: 36px; + } + + .mx_Dialog_content > .mx_SettingsFlag { + margin-top: 24px; + } + + p { + margin: 0 85px 0 0; + font-size: $font-12px; + } } diff --git a/res/css/views/dialogs/_DevtoolsDialog.scss b/res/css/views/dialogs/_DevtoolsDialog.scss index 500c46b5fd..35cb6bc7ab 100644 --- a/res/css/views/dialogs/_DevtoolsDialog.scss +++ b/res/css/views/dialogs/_DevtoolsDialog.scss @@ -68,11 +68,11 @@ limitations under the License. width: 240px; color: $input-fg-color; font-family: $font-family; - font-size: 16px; + font-size: $font-16px; } .mx_DevTools_textarea { - font-size: 12px; + font-size: $font-12px; max-width: 684px; min-height: 250px; padding: 10px; diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss index 5e0893b8fd..a77d0bfbba 100644 --- a/res/css/views/dialogs/_InviteDialog.scss +++ b/res/css/views/dialogs/_InviteDialog.scss @@ -40,8 +40,8 @@ limitations under the License. textarea, textarea:focus { height: 34px; - line-height: 34px; - font-size: 14px; + line-height: $font-34px; + font-size: $font-14px; padding-left: 12px; margin: 0 !important; border: 0 !important; @@ -65,7 +65,7 @@ limitations under the License. min-width: 48px; margin-left: 10px; height: 25px; - line-height: 25px; + line-height: $font-25px; } .mx_InviteDialog_buttonAndSpinner { @@ -84,7 +84,7 @@ limitations under the License. padding-bottom: 10px; h3 { - font-size: 12px; + font-size: $font-12px; color: $muted-fg-color; font-weight: bold; text-transform: uppercase; @@ -143,23 +143,23 @@ limitations under the License. .mx_InviteDialog_roomTile_name { font-weight: 600; - font-size: 14px; + font-size: $font-14px; color: $primary-fg-color; margin-left: 7px; } .mx_InviteDialog_roomTile_userId { - font-size: 12px; + font-size: $font-12px; color: $muted-fg-color; margin-left: 7px; } .mx_InviteDialog_roomTile_time { text-align: right; - font-size: 12px; + font-size: $font-12px; color: $muted-fg-color; float: right; - line-height: 36px; // Height of the avatar to keep the time vertically aligned + line-height: $font-36px; // Height of the avatar to keep the time vertically aligned } .mx_InviteDialog_roomTile_highlight { @@ -176,7 +176,7 @@ limitations under the License. border-radius: 12px; display: inline-block; height: 24px; - line-height: 24px; + line-height: $font-24px; padding-left: 8px; padding-right: 8px; color: #ffffff; // this is fine without a var because it's for both themes 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/_MessageEditHistoryDialog.scss b/res/css/views/dialogs/_MessageEditHistoryDialog.scss index 0066faccae..e9d777effd 100644 --- a/res/css/views/dialogs/_MessageEditHistoryDialog.scss +++ b/res/css/views/dialogs/_MessageEditHistoryDialog.scss @@ -35,7 +35,7 @@ limitations under the License. .mx_MessageEditHistoryDialog_edits { list-style-type: none; - font-size: 14px; + font-size: $font-14px; padding: 0; color: $primary-fg-color; @@ -60,7 +60,7 @@ limitations under the License. } .mx_MessageActionBar .mx_AccessibleButton { - font-size: 10px; + font-size: $font-10px; padding: 0 8px; } } diff --git a/res/css/views/dialogs/_NewSessionReviewDialog.scss b/res/css/views/dialogs/_NewSessionReviewDialog.scss index 7e35fe941e..b35c570c80 100644 --- a/res/css/views/dialogs/_NewSessionReviewDialog.scss +++ b/res/css/views/dialogs/_NewSessionReviewDialog.scss @@ -32,6 +32,6 @@ limitations under the License. } .mx_NewSessionReviewDialog_deviceID { - font-size: 12px; + font-size: $font-12px; color: $notice-secondary-color; } diff --git a/res/css/views/dialogs/_RoomSettingsDialog.scss b/res/css/views/dialogs/_RoomSettingsDialog.scss index 2a4e62f9aa..3751c15643 100644 --- a/res/css/views/dialogs/_RoomSettingsDialog.scss +++ b/res/css/views/dialogs/_RoomSettingsDialog.scss @@ -29,6 +29,10 @@ limitations under the License. mask-image: url('$(res)/img/feather-customised/users-sm.svg'); } +.mx_RoomSettingsDialog_notificationsIcon::before { + mask-image: url('$(res)/img/feather-customised/notifications.svg'); +} + .mx_RoomSettingsDialog_bridgesIcon::before { // This icon is pants, please improve :) mask-image: url('$(res)/img/feather-customised/bridge.svg'); diff --git a/res/css/views/dialogs/_SetEmailDialog.scss b/res/css/views/dialogs/_SetEmailDialog.scss index 9d09a208df..37bee7a9ff 100644 --- a/res/css/views/dialogs/_SetEmailDialog.scss +++ b/res/css/views/dialogs/_SetEmailDialog.scss @@ -20,7 +20,7 @@ limitations under the License. padding: 9px; color: $input-fg-color; background-color: $primary-bg-color; - font-size: 15px; + font-size: $font-15px; width: 100%; max-width: 280px; margin-bottom: 10px; diff --git a/res/css/views/dialogs/_SetMxIdDialog.scss b/res/css/views/dialogs/_SetMxIdDialog.scss index f7d8a3d001..1df34f3408 100644 --- a/res/css/views/dialogs/_SetMxIdDialog.scss +++ b/res/css/views/dialogs/_SetMxIdDialog.scss @@ -29,7 +29,7 @@ limitations under the License. padding: 9px; color: $primary-fg-color; background-color: $primary-bg-color; - font-size: 15px; + font-size: $font-15px; width: 100%; max-width: 280px; } diff --git a/res/css/views/dialogs/_SetPasswordDialog.scss b/res/css/views/dialogs/_SetPasswordDialog.scss index 325ff6c6ed..1f99353298 100644 --- a/res/css/views/dialogs/_SetPasswordDialog.scss +++ b/res/css/views/dialogs/_SetPasswordDialog.scss @@ -20,7 +20,7 @@ limitations under the License. padding: 9px; color: $primary-fg-color; background-color: $primary-bg-color; - font-size: 15px; + font-size: $font-15px; max-width: 280px; margin-bottom: 10px; } diff --git a/res/css/views/dialogs/_TermsDialog.scss b/res/css/views/dialogs/_TermsDialog.scss index beb507e778..939a31dee6 100644 --- a/res/css/views/dialogs/_TermsDialog.scss +++ b/res/css/views/dialogs/_TermsDialog.scss @@ -31,7 +31,7 @@ limitations under the License. } .mx_TermsDialog_termsTable { - font-size: 12px; + font-size: $font-12px; width: 100%; } diff --git a/res/css/views/dialogs/_UnknownDeviceDialog.scss b/res/css/views/dialogs/_UnknownDeviceDialog.scss index 02e0fb1fe5..daa6bd2352 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; @@ -35,7 +27,7 @@ limitations under the License. // userid .mx_UnknownDeviceDialog p { font-weight: bold; - font-size: 16px; + font-size: $font-16px; } .mx_UnknownDeviceDialog .mx_DeviceVerifyButtons { @@ -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/directory/_NetworkDropdown.scss b/res/css/views/directory/_NetworkDropdown.scss index d402f6c48f..269b507e3c 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: $font-15px; + font-weight: 600; + line-height: $font-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: $font-10px; + line-height: $font-14px; + margin-top: -4px; + margin-bottom: 4px; + color: $muted-fg-color; + } + + .mx_NetworkDropdown_server_network { + font-size: $font-12px; + line-height: $font-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: $font-12px; + } +} + +.mx_NetworkDropdown_dialog .mx_Dialog { + width: 45vw; +} diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss index b87071745d..96269cea43 100644 --- a/res/css/views/elements/_AccessibleButton.scss +++ b/res/css/views/elements/_AccessibleButton.scss @@ -27,7 +27,7 @@ limitations under the License. text-align: center; border-radius: 4px; display: inline-block; - font-size: 14px; + font-size: $font-14px; } .mx_AccessibleButton_kind_primary { @@ -36,12 +36,20 @@ limitations under the License. font-weight: 600; } +.mx_AccessibleButton_kind_primary_outline { + color: $button-primary-bg-color; + background-color: $button-secondary-bg-color; + border: 1px solid $button-primary-bg-color; + font-weight: 600; +} + .mx_AccessibleButton_kind_secondary { color: $accent-color; font-weight: 600; } -.mx_AccessibleButton_kind_primary.mx_AccessibleButton_disabled { +.mx_AccessibleButton_kind_primary.mx_AccessibleButton_disabled, +.mx_AccessibleButton_kind_primary_outline.mx_AccessibleButton_disabled { opacity: 0.4; } @@ -60,7 +68,14 @@ limitations under the License. background-color: $button-danger-bg-color; } -.mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled { +.mx_AccessibleButton_kind_danger_outline { + color: $button-danger-bg-color; + background-color: $button-secondary-bg-color; + border: 1px solid $button-danger-bg-color; +} + +.mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled, +.mx_AccessibleButton_kind_danger_outline.mx_AccessibleButton_disabled { color: $button-danger-disabled-fg-color; background-color: $button-danger-disabled-bg-color; } diff --git a/res/css/views/elements/_AddressTile.scss b/res/css/views/elements/_AddressTile.scss index 0ecfb17c83..c42f52f8f4 100644 --- a/res/css/views/elements/_AddressTile.scss +++ b/res/css/views/elements/_AddressTile.scss @@ -19,9 +19,9 @@ limitations under the License. border-radius: 3px; background-color: rgba(74, 73, 74, 0.1); border: solid 1px $input-border-color; - line-height: 26px; + line-height: $font-26px; color: $primary-fg-color; - font-size: 14px; + font-size: $font-14px; font-weight: normal; margin-right: 4px; } diff --git a/res/css/views/elements/_DirectorySearchBox.scss b/res/css/views/elements/_DirectorySearchBox.scss index ef944f6fa0..e4b1ac5574 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 { @@ -33,7 +32,7 @@ limitations under the License. background-repeat: no-repeat; text-indent: 18px; font-weight: 600; - font-size: 12px; + font-size: $font-12px; user-select: none; cursor: pointer; } diff --git a/res/css/views/elements/_Dropdown.scss b/res/css/views/elements/_Dropdown.scss index 102ac56bf9..0dd9656c9c 100644 --- a/res/css/views/elements/_Dropdown.scss +++ b/res/css/views/elements/_Dropdown.scss @@ -29,7 +29,7 @@ limitations under the License. position: relative; border-radius: 3px; border: 1px solid $strong-input-border-color; - font-size: 12px; + font-size: $font-12px; user-select: none; } @@ -53,7 +53,7 @@ limitations under the License. .mx_Dropdown_option { height: 35px; - line-height: 35px; + line-height: $font-35px; padding-left: 8px; padding-right: 8px; } 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/_EventListSummary.scss b/res/css/views/elements/_EventListSummary.scss index 99a5c06a5f..f3e9f77aa3 100644 --- a/res/css/views/elements/_EventListSummary.scss +++ b/res/css/views/elements/_EventListSummary.scss @@ -19,7 +19,7 @@ limitations under the License. } .mx_TextualEvent.mx_EventListSummary_summary { - font-size: 14px; + font-size: $font-14px; display: inline-flex; } @@ -27,7 +27,7 @@ limitations under the License. display: inline-block; margin-right: 8px; padding-top: 8px; - line-height: 12px; + line-height: $font-12px; } .mx_EventListSummary_avatars .mx_BaseAvatar { @@ -46,19 +46,19 @@ limitations under the License. .mx_EventListSummary_line { border-bottom: 1px solid $primary-hairline-color; margin-left: 63px; - line-height: 30px; + line-height: $font-30px; } .mx_MatrixChat_useCompactLayout { .mx_EventListSummary { - font-size: 13px; + font-size: $font-13px; .mx_EventTile_line { - line-height: 20px; + line-height: $font-20px; } } .mx_EventListSummary_line { - line-height: 22px; + line-height: $font-22px; } .mx_EventListSummary_toggle { @@ -66,6 +66,6 @@ limitations under the License. } .mx_TextualEvent.mx_EventListSummary_summary { - font-size: 13px; + font-size: $font-13px; } } diff --git a/res/css/views/elements/_Field.scss b/res/css/views/elements/_Field.scss index b260d4b097..cf5bc7ab41 100644 --- a/res/css/views/elements/_Field.scss +++ b/res/css/views/elements/_Field.scss @@ -40,7 +40,7 @@ limitations under the License. .mx_Field textarea { font-weight: normal; font-family: $font-family; - font-size: 14px; + font-size: $font-14px; border: none; // Even without a border here, we still need this avoid overlapping the rounded // corners on the field above. @@ -102,7 +102,7 @@ limitations under the License. background-color 0.25s ease-out 0.1s; color: $primary-fg-color; background-color: transparent; - font-size: 14px; + font-size: $font-14px; position: absolute; left: 0px; top: 0px; @@ -126,7 +126,7 @@ limitations under the License. color 0.25s ease-out 0s, top 0.25s ease-out 0s, background-color 0.25s ease-out 0s; - font-size: 10px; + font-size: $font-10px; top: -13px; padding: 0 2px; background-color: $field-focused-label-bg-color; diff --git a/res/css/views/elements/_FormButton.scss b/res/css/views/elements/_FormButton.scss index 1483fe2091..7ec01f17e6 100644 --- a/res/css/views/elements/_FormButton.scss +++ b/res/css/views/elements/_FormButton.scss @@ -15,9 +15,9 @@ limitations under the License. */ .mx_FormButton { - line-height: 16px; + line-height: $font-16px; padding: 5px 15px; - font-size: 12px; + font-size: $font-12px; height: min-content; &:not(:last-child) { diff --git a/res/css/views/elements/_ImageView.scss b/res/css/views/elements/_ImageView.scss index 67b0d6d7df..0a4ed2a194 100644 --- a/res/css/views/elements/_ImageView.scss +++ b/res/css/views/elements/_ImageView.scss @@ -102,13 +102,13 @@ limitations under the License. } .mx_ImageView_name { - font-size: 18px; + font-size: $font-18px; margin-bottom: 6px; word-wrap: break-word; } .mx_ImageView_metadata { - font-size: 15px; + font-size: $font-15px; opacity: 0.5; } @@ -118,13 +118,13 @@ limitations under the License. margin-bottom: 6px; border-radius: 5px; background-color: $lightbox-bg-color; - font-size: 14px; + font-size: $font-14px; padding: 9px; border: 1px solid $lightbox-border-color; } .mx_ImageView_size { - font-size: 11px; + font-size: $font-11px; } .mx_ImageView_link { @@ -133,7 +133,7 @@ limitations under the License. } .mx_ImageView_button { - font-size: 15px; + font-size: $font-15px; opacity: 0.5; margin-top: 18px; cursor: pointer; diff --git a/res/css/views/elements/_InteractiveTooltip.scss b/res/css/views/elements/_InteractiveTooltip.scss index 17a76436e8..db98d95709 100644 --- a/res/css/views/elements/_InteractiveTooltip.scss +++ b/res/css/views/elements/_InteractiveTooltip.scss @@ -24,7 +24,7 @@ limitations under the License. background-color: $interactive-tooltip-bg-color; color: $interactive-tooltip-fg-color; position: absolute; - font-size: 10px; + font-size: $font-10px; font-weight: 600; padding: 6px; z-index: 5001; diff --git a/res/css/views/elements/_RichText.scss b/res/css/views/elements/_RichText.scss index 5066ee10f3..e3f88cc779 100644 --- a/res/css/views/elements/_RichText.scss +++ b/res/css/views/elements/_RichText.scss @@ -9,7 +9,7 @@ border-radius: 16px; display: inline-block; height: 20px; - line-height: 20px; + line-height: $font-20px; padding-left: 5px; } diff --git a/res/css/views/elements/_Tooltip.scss b/res/css/views/elements/_Tooltip.scss index cc4eb409df..73ac9b3558 100644 --- a/res/css/views/elements/_Tooltip.scss +++ b/res/css/views/elements/_Tooltip.scss @@ -58,8 +58,8 @@ limitations under the License. z-index: 4000; // Higher than dialogs so tooltips can be used in dialogs padding: 10px; pointer-events: none; - line-height: 14px; - font-size: 12px; + line-height: $font-14px; + font-size: $font-12px; font-weight: 600; color: $primary-fg-color; max-width: 200px; @@ -82,7 +82,7 @@ limitations under the License. text-align: center; border: none; border-radius: 3px; - font-size: 14px; + font-size: $font-14px; line-height: 1.2; padding: 6px 8px; diff --git a/res/css/views/elements/_TooltipButton.scss b/res/css/views/elements/_TooltipButton.scss index 6ea36c800e..0c85dac818 100644 --- a/res/css/views/elements/_TooltipButton.scss +++ b/res/css/views/elements/_TooltipButton.scss @@ -28,7 +28,7 @@ limitations under the License. transition: opacity 0.2s ease-in; opacity: 0.6; - line-height: 11px; + line-height: $font-11px; text-align: center; cursor: pointer; diff --git a/res/css/views/emojipicker/_EmojiPicker.scss b/res/css/views/emojipicker/_EmojiPicker.scss index 5d9b3f2687..24561eeeb9 100644 --- a/res/css/views/emojipicker/_EmojiPicker.scss +++ b/res/css/views/emojipicker/_EmojiPicker.scss @@ -163,7 +163,7 @@ limitations under the License. .mx_EmojiPicker_item { display: inline-block; - font-size: 20px; + font-size: $font-20px; padding: 5px; width: 100%; height: 100%; @@ -183,7 +183,7 @@ limitations under the License. } .mx_EmojiPicker_category_label, .mx_EmojiPicker_preview_name { - font-size: 16px; + font-size: $font-16px; font-weight: 600; margin: 0; } @@ -197,7 +197,7 @@ limitations under the License. } .mx_EmojiPicker_preview_emoji { - font-size: 32px; + font-size: $font-32px; padding: 8px 16px; } @@ -212,7 +212,7 @@ limitations under the License. .mx_EmojiPicker_shortcode { color: $light-fg-color; - font-size: 14px; + font-size: $font-14px; &::before, &::after { content: ":"; diff --git a/res/css/views/messages/_DateSeparator.scss b/res/css/views/messages/_DateSeparator.scss index 935ee1aba3..867f58d860 100644 --- a/res/css/views/messages/_DateSeparator.scss +++ b/res/css/views/messages/_DateSeparator.scss @@ -19,7 +19,7 @@ limitations under the License. margin: 4px 0; display: flex; align-items: center; - font-size: 14px; + font-size: $font-14px; color: $roomtopic-color; } diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss index c032051c36..9f3971ecf0 100644 --- a/res/css/views/messages/_MessageActionBar.scss +++ b/res/css/views/messages/_MessageActionBar.scss @@ -21,7 +21,7 @@ limitations under the License. cursor: pointer; display: flex; height: 24px; - line-height: 24px; + line-height: $font-24px; border-radius: 4px; background: $message-action-bar-bg-color; top: -18px; diff --git a/res/css/views/messages/_MessageTimestamp.scss b/res/css/views/messages/_MessageTimestamp.scss index e5c228aa68..f8d91cc083 100644 --- a/res/css/views/messages/_MessageTimestamp.scss +++ b/res/css/views/messages/_MessageTimestamp.scss @@ -16,5 +16,5 @@ limitations under the License. .mx_MessageTimestamp { color: $event-timestamp-color; - font-size: 10px; + font-size: $font-10px; } diff --git a/res/css/views/messages/_ReactionsRow.scss b/res/css/views/messages/_ReactionsRow.scss index 57c02ed3e5..2f5695e1fb 100644 --- a/res/css/views/messages/_ReactionsRow.scss +++ b/res/css/views/messages/_ReactionsRow.scss @@ -21,7 +21,7 @@ limitations under the License. .mx_ReactionsRow_showAll { text-decoration: none; - font-size: 10px; + font-size: $font-10px; font-weight: 600; margin-left: 6px; vertical-align: top; diff --git a/res/css/views/messages/_ReactionsRowButton.scss b/res/css/views/messages/_ReactionsRowButton.scss index e54201d963..941153ca5b 100644 --- a/res/css/views/messages/_ReactionsRowButton.scss +++ b/res/css/views/messages/_ReactionsRowButton.scss @@ -17,7 +17,7 @@ limitations under the License. .mx_ReactionsRowButton { display: inline-flex; height: 20px; - line-height: 21px; + line-height: $font-21px; margin-right: 6px; padding: 0 6px; border: 1px solid $reaction-row-button-border-color; diff --git a/res/css/views/messages/_ViewSourceEvent.scss b/res/css/views/messages/_ViewSourceEvent.scss index a15924e759..076932ee97 100644 --- a/res/css/views/messages/_ViewSourceEvent.scss +++ b/res/css/views/messages/_ViewSourceEvent.scss @@ -17,7 +17,7 @@ limitations under the License. .mx_EventTile_content.mx_ViewSourceEvent { display: flex; opacity: 0.6; - font-size: 12px; + font-size: $font-12px; pre, code { flex: 1; diff --git a/res/css/views/messages/_common_CryptoEvent.scss b/res/css/views/messages/_common_CryptoEvent.scss index 98e1e97e39..637d25d7a1 100644 --- a/res/css/views/messages/_common_CryptoEvent.scss +++ b/res/css/views/messages/_common_CryptoEvent.scss @@ -45,7 +45,7 @@ limitations under the License. .mx_cryptoEvent_title { font-weight: 600; - font-size: 15px; + font-size: $font-15px; grid-column: 2; grid-row: 1; } @@ -56,7 +56,7 @@ limitations under the License. } .mx_cryptoEvent_state, .mx_cryptoEvent_subtitle { - font-size: 12px; + font-size: $font-12px; } .mx_cryptoEvent_state, .mx_cryptoEvent_buttons { diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index 0e4b1bda9e..a4d88f9882 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -20,7 +20,7 @@ limitations under the License. flex-direction: column; flex: 1; overflow-y: auto; - font-size: 12px; + font-size: $font-12px; .mx_UserInfo_cancel { cursor: pointer; @@ -43,7 +43,7 @@ limitations under the License. } h2 { - font-size: 18px; + font-size: $font-18px; font-weight: 600; margin: 18px 0 0 0; } @@ -122,7 +122,7 @@ limitations under the License. text-transform: uppercase; color: $notice-secondary-color; font-weight: bold; - font-size: 12px; + font-size: $font-12px; margin: 4px 0; } @@ -134,24 +134,28 @@ limitations under the License. text-align: center; h2 { - font-size: 18px; - line-height: 25px; + display: flex; + font-size: $font-18px; + line-height: $font-25px; flex: 1; 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; + span { + // 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; + overflow: hidden; + word-break: break-all; + text-overflow: ellipsis; + } .mx_E2EIcon { - margin: 5px; + margin-top: 3px; // visual vertical centering to the top line of text + margin-right: 4px; // margin from displyname + min-width: 18px; // convince flexbox to not collapse it } } @@ -197,7 +201,7 @@ limitations under the License. .mx_UserInfo_field { cursor: pointer; color: $accent-color; - line-height: 16px; + line-height: $font-16px; margin: 8px 0; &.mx_UserInfo_destructive { @@ -206,7 +210,7 @@ limitations under the License. } .mx_UserInfo_statusMessage { - font-size: 11px; + font-size: $font-11px; opacity: 0.5; overflow: hidden; white-space: nowrap; @@ -266,12 +270,31 @@ limitations under the License. } } + .mx_AccessibleButton.mx_AccessibleButton_hasKind { + padding: 8px 18px; + + &.mx_AccessibleButton_kind_primary { + color: $accent-color; + background-color: $accent-bg-color; + } + + &.mx_AccessibleButton_kind_danger { + color: $notice-primary-color; + background-color: $notice-primary-bg-color; + } + } + + .mx_VerificationShowSas .mx_AccessibleButton, .mx_UserInfo_wideButton { display: block; - margin: 16px 0; + margin: 16px 0 8px; } - button.mx_UserInfo_wideButton { - width: 100%; // FIXME get rid of this once we get rid of DialogButtons here + + + .mx_VerificationShowSas { + .mx_AccessibleButton + .mx_AccessibleButton { + margin: 8px 0; // space between buttons + } } } diff --git a/res/css/views/right_panel/_VerificationPanel.scss b/res/css/views/right_panel/_VerificationPanel.scss index 2a733d11a7..a8466a1626 100644 --- a/res/css/views/right_panel/_VerificationPanel.scss +++ b/res/css/views/right_panel/_VerificationPanel.scss @@ -14,10 +14,30 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_UserInfo { - .mx_VerificationPanel_verified_section .mx_E2EIcon { +.mx_VerificationPanel_verified_section, +.mx_VerificationPanel_reciprocate_section { + // center the big shield icon + .mx_E2EIcon { // Override general user info margin - margin: 0 auto !important; + margin: 20px auto !important; + } +} + + +.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_qrCode { @@ -36,6 +56,16 @@ limitations under the License. max-width: 240px; } } + + .mx_VerificationPanel_reciprocate_section { + .mx_FormButton { + width: 100%; + box-sizing: border-box; + padding: 10px; + display: block; + margin: 10px 0; + } + } } // Special case styling for EncryptionPanel in a Modal dialog @@ -45,6 +75,7 @@ limitations under the License. margin-top: 10px; margin-bottom: 10px; align-items: stretch; + justify-content: center; > .mx_VerificationPanel_QRPhase_betweenText { width: 50px; @@ -60,10 +91,12 @@ limitations under the License. border-radius: 10px; flex: 1; display: flex; - padding: 10px; + padding: 20px; align-items: center; flex-direction: column; position: relative; + max-width: 310px; + justify-content: space-between; canvas, .mx_VerificationPanel_QRPhase_noQR { width: 220px !important; @@ -76,31 +109,36 @@ limitations under the License. } > p { + margin-top: 0; font-weight: 700; } .mx_VerificationPanel_QRPhase_helpText { - font-size: 14px; - margin-top: 71px; + font-size: $font-14px; + margin: 30px 0; 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; } } + + .mx_VerificationPanel_reciprocate_section { + .mx_AccessibleButton { + margin-left: 10px; + padding: 7px 40px; + } + + .mx_VerificationPanel_reciprocateButtons { + display: flex; + flex-direction: row; + justify-content: flex-end; + } + } } 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/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss index a3fe573ad0..1b1bab67bc 100644 --- a/res/css/views/rooms/_AppsDrawer.scss +++ b/res/css/views/rooms/_AppsDrawer.scss @@ -46,7 +46,7 @@ $AppsDrawerBodyHeight: 273px; padding: 0; margin: 5px auto 5px auto; color: $accent-color; - font-size: 12px; + font-size: $font-12px; } .mx_AddWidget_button_full_width { @@ -59,7 +59,7 @@ $AppsDrawerBodyHeight: 273px; padding: 9px; color: $primary-hairline-color; background-color: $primary-bg-color; - font-size: 15px; + font-size: $font-15px; } .mx_AppTile { @@ -102,7 +102,7 @@ $AppsDrawerBodyHeight: 273px; .mx_AppTileMenuBar { margin: 0; - font-size: 12px; + font-size: $font-12px; background-color: $widget-menu-bar-bg-color; display: flex; flex-direction: row; @@ -272,7 +272,7 @@ form.mx_Custom_Widget_Form div { flex-direction: column; justify-content: center; align-items: center; - font-size: 16px; + font-size: $font-16px; } .mx_AppPermissionWarning_row { @@ -280,7 +280,7 @@ form.mx_Custom_Widget_Form div { } .mx_AppPermissionWarning_smallText { - font-size: 12px; + font-size: $font-12px; } .mx_AppPermissionWarning_bolder { diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss index ce519b1ea7..cc76623a8c 100644 --- a/res/css/views/rooms/_BasicMessageComposer.scss +++ b/res/css/views/rooms/_BasicMessageComposer.scss @@ -63,8 +63,8 @@ limitations under the License. border-radius: 8px; text-align: center; font-weight: normal; - line-height: 16px; - font-size: 10.4px; + line-height: $font-16px; + font-size: $font-10-4px; } } } diff --git a/res/css/views/rooms/_EntityTile.scss b/res/css/views/rooms/_EntityTile.scss index a2867de3a7..966d2c4e70 100644 --- a/res/css/views/rooms/_EntityTile.scss +++ b/res/css/views/rooms/_EntityTile.scss @@ -78,7 +78,7 @@ limitations under the License. .mx_GroupRoomTile_name { flex: 1 1 0; overflow: hidden; - font-size: 14px; + font-size: $font-14px; text-overflow: ellipsis; white-space: nowrap; } @@ -116,7 +116,7 @@ limitations under the License. } .mx_EntityTile_subtext { - font-size: 11px; + font-size: $font-11px; opacity: 0.5; overflow: hidden; white-space: nowrap; @@ -125,7 +125,7 @@ limitations under the License. .mx_EntityTile_power { padding-inline-start: 6px; - font-size: 10px; + font-size: $font-10px; color: $notice-secondary-color; max-width: 6em; overflow: hidden; diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index d292c729dd..e015f30e48 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -19,7 +19,7 @@ limitations under the License. max-width: 100%; clear: both; padding-top: 18px; - font-size: 14px; + font-size: $font-14px; position: relative; } @@ -64,7 +64,7 @@ limitations under the License. .mx_EventTile .mx_SenderProfile { color: $primary-fg-color; - font-size: 14px; + font-size: $font-14px; display: inline-block; /* anti-zalgo, with overflow hidden */ overflow: hidden; cursor: pointer; @@ -72,7 +72,7 @@ limitations under the License. padding-bottom: 0px; padding-top: 0px; margin: 0px; - line-height: 17px; + line-height: $font-17px; /* the next three lines, along with overflow hidden, truncate long display names */ white-space: nowrap; text-overflow: ellipsis; @@ -111,15 +111,23 @@ limitations under the License. } .mx_EventTile_line, .mx_EventTile_reply { + clear: both; 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; border-radius: 4px; min-height: 24px; - line-height: 22px; + line-height: $font-22px; +} + +.mx_RoomView_timeline_rr_enabled, +// on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter +.mx_EventListSummary { + .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 { @@ -307,7 +315,7 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { .mx_EventTile_readAvatarRemainder { color: $event-timestamp-color; - font-size: 11px; + font-size: $font-11px; position: absolute; } @@ -336,7 +344,7 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { .mx_EventTile_spoiler_reason { color: $event-timestamp-color; - font-size: 11px; + font-size: $font-11px; } .mx_EventTile_spoiler_content { @@ -388,7 +396,7 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { } .mx_EventTile_keyRequestInfo { - font-size: 12px; + font-size: $font-12px; } .mx_EventTile_keyRequestInfo_text { @@ -466,7 +474,7 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { .mx_EventTile_content .mx_EventTile_edited { user-select: none; - font-size: 12px; + font-size: $font-12px; color: $roomtopic-color; display: inline-block; margin-left: 9px; @@ -484,7 +492,7 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { white-space: normal !important; line-height: inherit !important; color: inherit; // inherit the colour from the dark or light theme by default (but not for code blocks) - font-size: 14px; + font-size: $font-14px; pre, code { font-family: $monospace-font-family !important; @@ -584,9 +592,9 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { .mx_EventTile.mx_EventTile_info { // same as the padding for non-compact .mx_EventTile.mx_EventTile_info padding-top: 0px; - font-size: 13px; + font-size: $font-13px; .mx_EventTile_line, .mx_EventTile_reply { - line-height: 20px; + line-height: $font-20px; } .mx_EventTile_avatar { top: 4px; @@ -594,7 +602,7 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { } .mx_EventTile .mx_SenderProfile { - font-size: 13px; + font-size: $font-13px; } .mx_EventTile.mx_EventTile_emote { diff --git a/res/css/views/rooms/_JumpToBottomButton.scss b/res/css/views/rooms/_JumpToBottomButton.scss index 7f458092fb..63cf574596 100644 --- a/res/css/views/rooms/_JumpToBottomButton.scss +++ b/res/css/views/rooms/_JumpToBottomButton.scss @@ -34,8 +34,8 @@ limitations under the License. top: -12px; border-radius: 16px; font-weight: bold; - font-size: 12px; - line-height: 14px; + font-size: $font-12px; + line-height: $font-14px; text-align: center; // to be able to get it centered // with text-align in parent diff --git a/res/css/views/rooms/_MemberDeviceInfo.scss b/res/css/views/rooms/_MemberDeviceInfo.scss index 15b4832dc5..71b05a93fc 100644 --- a/res/css/views/rooms/_MemberDeviceInfo.scss +++ b/res/css/views/rooms/_MemberDeviceInfo.scss @@ -59,7 +59,7 @@ limitations under the License. .mx_MemberDeviceInfo_deviceId { word-break: break-word; - font-size: 13px; + font-size: $font-13px; } .mx_MemberDeviceInfo_deviceInfo { diff --git a/res/css/views/rooms/_MemberInfo.scss b/res/css/views/rooms/_MemberInfo.scss index e3f746e9d3..fb082843f1 100644 --- a/res/css/views/rooms/_MemberInfo.scss +++ b/res/css/views/rooms/_MemberInfo.scss @@ -48,7 +48,7 @@ limitations under the License. } .mx_MemberInfo h2 { - font-size: 18px; + font-size: $font-18px; font-weight: 600; margin: 16px 0 16px 15px; } @@ -94,12 +94,12 @@ limitations under the License. text-transform: uppercase; color: $input-darker-fg-color; font-weight: bold; - font-size: 12px; + font-size: $font-12px; margin: 4px 0; } .mx_MemberInfo_profileField { - font-size: 15px; + font-size: $font-15px; position: relative; } @@ -109,10 +109,10 @@ limitations under the License. .mx_MemberInfo_field { cursor: pointer; - font-size: 15px; + font-size: $font-15px; color: $primary-fg-color; margin-left: 8px; - line-height: 23px; + line-height: $font-23px; } .mx_MemberInfo_createRoom { @@ -128,7 +128,7 @@ limitations under the License. } .mx_MemberInfo label { - font-size: 13px; + font-size: $font-13px; } .mx_MemberInfo label .mx_MemberInfo_label_text { @@ -144,7 +144,7 @@ limitations under the License. } .mx_MemberInfo_statusMessage { - font-size: 11px; + font-size: $font-11px; opacity: 0.5; overflow: hidden; white-space: nowrap; diff --git a/res/css/views/rooms/_MemberList.scss b/res/css/views/rooms/_MemberList.scss index 6e4465583c..99dc2338d4 100644 --- a/res/css/views/rooms/_MemberList.scss +++ b/res/css/views/rooms/_MemberList.scss @@ -30,7 +30,7 @@ limitations under the License. text-transform: uppercase; color: $h3-color; font-weight: 600; - font-size: 13px; + font-size: $font-13px; padding-left: 3px; padding-right: 12px; margin-top: 8px; diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index a05b4c0c0e..7b223be3a4 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -105,7 +105,7 @@ limitations under the License. min-height: 60px; justify-content: flex-start; align-items: flex-start; - font-size: 14px; + font-size: $font-14px; margin-right: 6px; } @@ -161,7 +161,7 @@ limitations under the License. box-shadow: none; color: $primary-fg-color; background-color: $primary-bg-color; - font-size: 14px; + font-size: $font-14px; max-height: 120px; overflow: auto; /* needed for FF */ @@ -242,7 +242,7 @@ limitations under the License. flex-direction: row; align-items: center; - font-size: 10px; + font-size: $font-10px; color: $greyed-fg-color; } diff --git a/res/css/views/rooms/_MessageComposerFormatBar.scss b/res/css/views/rooms/_MessageComposerFormatBar.scss index 1b5a21bed0..27ee7b9795 100644 --- a/res/css/views/rooms/_MessageComposerFormatBar.scss +++ b/res/css/views/rooms/_MessageComposerFormatBar.scss @@ -97,13 +97,13 @@ limitations under the License. .mx_MessageComposerFormatBar_buttonTooltip { white-space: nowrap; - font-size: 13px; + font-size: $font-13px; font-weight: 600; min-width: 54px; text-align: center; .mx_MessageComposerFormatBar_tooltipShortcut { - font-size: 9px; + font-size: $font-9px; opacity: 0.7; } } diff --git a/res/css/views/rooms/_PresenceLabel.scss b/res/css/views/rooms/_PresenceLabel.scss index 26ed1aa6a3..5be83c77d7 100644 --- a/res/css/views/rooms/_PresenceLabel.scss +++ b/res/css/views/rooms/_PresenceLabel.scss @@ -15,6 +15,6 @@ limitations under the License. */ .mx_PresenceLabel { - font-size: 11px; + font-size: $font-11px; opacity: 0.5; } 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/_RoomDropTarget.scss b/res/css/views/rooms/_RoomDropTarget.scss index 1076a0563a..2e8145c2c9 100644 --- a/res/css/views/rooms/_RoomDropTarget.scss +++ b/res/css/views/rooms/_RoomDropTarget.scss @@ -28,7 +28,7 @@ limitations under the License. } .mx_RoomDropTarget { - font-size: 13px; + font-size: $font-13px; padding-top: 5px; padding-bottom: 5px; border: 1px dashed $accent-color; @@ -41,7 +41,7 @@ limitations under the License. .mx_RoomDropTarget_label { position: relative; margin-top: 3px; - line-height: 21px; + line-height: $font-21px; z-index: 1; text-align: center; } diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index 47b8131ef0..969106c9ea 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -77,9 +77,9 @@ limitations under the License. } .mx_RoomHeader_simpleHeader { - line-height: 52px; + line-height: $font-52px; color: $roomheader-color; - font-size: 18px; + font-size: $font-18px; font-weight: 600; overflow: hidden; margin-left: 63px; @@ -102,7 +102,7 @@ limitations under the License. overflow: hidden; color: $roomheader-color; font-weight: 600; - font-size: 18px; + font-size: $font-18px; margin: 0 7px; border-bottom: 1px solid transparent; display: flex; @@ -161,7 +161,7 @@ limitations under the License. flex: 1; color: $roomtopic-color; font-weight: 400; - font-size: 13px; + font-size: $font-13px; margin: 0 7px; margin-top: 4px; // to align baseline of topic with room name overflow: hidden; diff --git a/res/css/views/rooms/_RoomList.scss b/res/css/views/rooms/_RoomList.scss index 5ed22f997d..50a9e7ee1f 100644 --- a/res/css/views/rooms/_RoomList.scss +++ b/res/css/views/rooms/_RoomList.scss @@ -47,13 +47,13 @@ limitations under the License. } .mx_RoomList_emptySubListTip { - font-size: 13px; + font-size: $font-13px; padding: 5px; border: 1px dashed $accent-color; color: $primary-fg-color; background-color: $droptarget-bg-color; border-radius: 4px; - line-height: 16px; + line-height: $font-16px; } .mx_RoomList_emptySubListTip .mx_RoleButton { diff --git a/res/css/views/rooms/_RoomPreviewBar.scss b/res/css/views/rooms/_RoomPreviewBar.scss index 85b6916226..8708f13ada 100644 --- a/res/css/views/rooms/_RoomPreviewBar.scss +++ b/res/css/views/rooms/_RoomPreviewBar.scss @@ -23,7 +23,7 @@ limitations under the License. -webkit-align-items: center; h3 { - font-size: 18px; + font-size: $font-18px; font-weight: 600; &.mx_RoomPreviewBar_spinnerTitle { @@ -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; @@ -41,8 +48,8 @@ limitations under the License. } .mx_RoomPreviewBar_footer { - font-size: 12px; - line-height: 20px; + font-size: $font-12px; + line-height: $font-20px; .mx_Spinner { vertical-align: middle; diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 31d887cbbb..7be2a4e3d4 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -64,7 +64,7 @@ limitations under the License. .mx_RoomTile_subtext { display: inline-block; - font-size: 11px; + font-size: $font-11px; padding: 0 0 0 7px; margin: 0; overflow: hidden; @@ -112,7 +112,7 @@ limitations under the License. } .mx_RoomTile_name { - font-size: 14px; + font-size: $font-14px; padding: 0 4px; color: $roomtile-name-color; white-space: nowrap; @@ -126,7 +126,7 @@ limitations under the License. padding: 0 0.4em; color: $roomtile-badge-fg-color; font-weight: 600; - font-size: 12px; + font-size: $font-12px; } .collapsed { diff --git a/res/css/views/rooms/_SearchBar.scss b/res/css/views/rooms/_SearchBar.scss index b6748e5ad2..fecc8d78d8 100644 --- a/res/css/views/rooms/_SearchBar.scss +++ b/res/css/views/rooms/_SearchBar.scss @@ -22,7 +22,7 @@ limitations under the License. .mx_SearchBar_input { // border: 1px solid $input-border-color; - // font-size: 15px; + // font-size: $font-15px; flex: 1 1 0; margin-left: 22px; } @@ -45,7 +45,7 @@ limitations under the License. border: 0; margin: 0 0 0 22px; padding: 5px; - font-size: 15px; + font-size: $font-15px; cursor: pointer; color: $primary-fg-color; border-bottom: 2px solid $accent-color; 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/_SendMessageComposer.scss b/res/css/views/rooms/_SendMessageComposer.scss index d20f7107b3..0b646666e7 100644 --- a/res/css/views/rooms/_SendMessageComposer.scss +++ b/res/css/views/rooms/_SendMessageComposer.scss @@ -18,7 +18,7 @@ limitations under the License. flex: 1; display: flex; flex-direction: column; - font-size: 14px; + font-size: $font-14px; justify-content: center; margin-right: 6px; // don't grow wider than available space diff --git a/res/css/views/rooms/_TopUnreadMessagesBar.scss b/res/css/views/rooms/_TopUnreadMessagesBar.scss index a3916f321a..28eddf1fa2 100644 --- a/res/css/views/rooms/_TopUnreadMessagesBar.scss +++ b/res/css/views/rooms/_TopUnreadMessagesBar.scss @@ -51,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 579ea7e73e..8b135152d6 100644 --- a/res/css/views/rooms/_WhoIsTypingTile.scss +++ b/res/css/views/rooms/_WhoIsTypingTile.scss @@ -49,7 +49,7 @@ limitations under the License. border-radius: 40px; width: 24px; height: 24px; - line-height: 24px; + line-height: $font-24px; font-size: 0.8em; vertical-align: top; text-align: center; @@ -57,7 +57,7 @@ limitations under the License. .mx_WhoIsTypingTile_label { flex: 1; - font-size: 14px; + font-size: $font-14px; font-weight: 600; color: $eventtile-meta-color; } 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..1fbfb35927 100644 --- a/res/css/views/settings/tabs/_SettingsTab.scss +++ b/res/css/views/settings/tabs/_SettingsTab.scss @@ -19,7 +19,7 @@ limitations under the License. } .mx_SettingsTab_heading { - font-size: 20px; + font-size: $font-20px; font-weight: 600; color: $primary-fg-color; } @@ -29,7 +29,7 @@ limitations under the License. } .mx_SettingsTab_subheading { - font-size: 16px; + font-size: $font-16px; display: block; font-family: $font-family; font-weight: 600; @@ -40,20 +40,28 @@ limitations under the License. .mx_SettingsTab_subsectionText { color: $settings-subsection-fg-color; - font-size: 14px; + font-size: $font-14px; display: block; 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 { vertical-align: middle; display: inline-block; - font-size: 14px; + font-size: $font-14px; color: $primary-fg-color; max-width: calc(100% - 48px); // Force word wrap instead of colliding with the switch box-sizing: border-box; diff --git a/res/css/views/terms/_InlineTermsAgreement.scss b/res/css/views/terms/_InlineTermsAgreement.scss index e00dcf31d1..1d0e3ea8c5 100644 --- a/res/css/views/terms/_InlineTermsAgreement.scss +++ b/res/css/views/terms/_InlineTermsAgreement.scss @@ -16,7 +16,7 @@ limitations under the License. .mx_InlineTermsAgreement_cbContainer { margin-bottom: 10px; - font-size: 14px; + font-size: $font-14px; a { color: $accent-color; diff --git a/res/css/views/verification/_VerificationShowSas.scss b/res/css/views/verification/_VerificationShowSas.scss index 5038d40b73..af003112f7 100644 --- a/res/css/views/verification/_VerificationShowSas.scss +++ b/res/css/views/verification/_VerificationShowSas.scss @@ -48,16 +48,34 @@ limitations under the License. } .mx_VerificationShowSas_emojiSas_emoji { - font-size: 32px; + font-size: $font-32px; } .mx_VerificationShowSas_emojiSas_label { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; - font-size: 12px; + font-size: $font-12px; } .mx_VerificationShowSas_emojiSas_break { flex-basis: 100%; } + +.mx_VerificationShowSas { + .mx_Dialog_buttons { + // this is more specific than the DialogButtons css so gets preference + button.mx_VerificationShowSas_matchButton { + color: $accent-color; + background-color: $accent-bg-color; + border: none; + } + + // this is more specific than the DialogButtons css so gets preference + button.mx_VerificationShowSas_noMatchButton { + color: $notice-primary-color; + background-color: $notice-primary-bg-color; + border: none; + } + } +} diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index b01fbf8c66..4650f30c1d 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -21,5 +21,5 @@ limitations under the License. text-align: center; padding: 6px; font-weight: bold; - font-size: 13px; + font-size: $font-13px; } diff --git a/res/css/views/voip/_IncomingCallbox.scss b/res/css/views/voip/_IncomingCallbox.scss index 64eac25d01..ed33de470d 100644 --- a/res/css/views/voip/_IncomingCallbox.scss +++ b/res/css/views/voip/_IncomingCallbox.scss @@ -54,7 +54,7 @@ limitations under the License. vertical-align: middle; width: 80px; height: 36px; - line-height: 36px; + line-height: $font-36px; border-radius: 36px; color: $accent-fg-color; margin: auto; 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/feather-customised/explore.svg b/res/img/feather-customised/explore.svg new file mode 100644 index 0000000000..45be889bb7 --- /dev/null +++ b/res/img/feather-customised/explore.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/res/img/feather-customised/group.svg b/res/img/feather-customised/group.svg new file mode 100644 index 0000000000..7051860e62 --- /dev/null +++ b/res/img/feather-customised/group.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/res/img/feather-customised/message-circle.svg b/res/img/feather-customised/message-circle.svg new file mode 100644 index 0000000000..acc6d2fb0f --- /dev/null +++ b/res/img/feather-customised/message-circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/themes/dark-custom/css/dark-custom.scss b/res/themes/dark-custom/css/dark-custom.scss index aff647ce26..03ceef45c6 100644 --- a/res/themes/dark-custom/css/dark-custom.scss +++ b/res/themes/dark-custom/css/dark-custom.scss @@ -1,3 +1,4 @@ +@import "../../../../res/css/_font-sizes.scss"; @import "../../light/css/_paths.scss"; @import "../../light/css/_fonts.scss"; @import "../../light/css/_light.scss"; diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index a3515a9d99..5d6ba033c8 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; @@ -183,7 +185,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color; border: 0px; border-radius: 4px; font-family: $font-family; - font-size: 14px; + font-size: $font-14px; color: $button-fg-color; background-color: $button-bg-color; width: auto; @@ -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/dark/css/dark.scss b/res/themes/dark/css/dark.scss index e7ae7c8cf8..d81db4595f 100644 --- a/res/themes/dark/css/dark.scss +++ b/res/themes/dark/css/dark.scss @@ -1,3 +1,4 @@ +@import "../../../../res/css/_font-sizes.scss"; @import "../../light/css/_paths.scss"; @import "../../light/css/_fonts.scss"; @import "../../light/css/_light.scss"; diff --git a/res/themes/light-custom/css/light-custom.scss b/res/themes/light-custom/css/light-custom.scss index 278ca5f0b1..4f80647eba 100644 --- a/res/themes/light-custom/css/light-custom.scss +++ b/res/themes/light-custom/css/light-custom.scss @@ -1,3 +1,4 @@ +@import "../../../../res/css/_font-sizes.scss"; @import "../../light/css/_paths.scss"; @import "../../light/css/_fonts.scss"; @import "../../light/css/_light.scss"; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 626ccb2e13..f5f3013354 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -290,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; @@ -308,7 +310,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color; border: 0px; border-radius: 4px; font-family: $font-family; - font-size: 14px; + font-size: $font-14px; color: $button-fg-color; background-color: $button-bg-color; width: auto; @@ -329,7 +331,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color; @define-mixin mx_DialogButton_small { @mixin mx_DialogButton; - font-size: 15px; + font-size: $font-15px; padding: 0px 1.5em 0px 1.5em; } diff --git a/res/themes/light/css/light.scss b/res/themes/light/css/light.scss index 6acb2d9d94..4f48557648 100644 --- a/res/themes/light/css/light.scss +++ b/res/themes/light/css/light.scss @@ -1,3 +1,4 @@ +@import "../../../../res/css/_font-sizes.scss"; @import "_paths.scss"; @import "_fonts.scss"; @import "_light.scss"; diff --git a/scripts/ci/end-to-end-tests.sh b/scripts/ci/end-to-end-tests.sh index 9bdb512940..2f907dffa2 100755 --- a/scripts/ci/end-to-end-tests.sh +++ b/scripts/ci/end-to-end-tests.sh @@ -6,16 +6,8 @@ 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 } diff --git a/scripts/gen-i18n.js b/scripts/gen-i18n.js index 3d3d5af116..a1823cdf50 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('.ts') || fileStats.name.endsWith('.tsx')) { trs = getTranslationsJs(fullPath); } else if (fileStats.name.endsWith('.html')) { trs = getTranslationsOther(fullPath); diff --git a/scripts/reskindex.js b/scripts/reskindex.js index 81ab111f46..9fb0e1a7c0 100755 --- a/scripts/reskindex.js +++ b/scripts/reskindex.js @@ -8,11 +8,14 @@ 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/AddThreepid.js b/src/AddThreepid.js index 7a3250d0ca..f06f7c187d 100644 --- a/src/AddThreepid.js +++ b/src/AddThreepid.js @@ -21,6 +21,7 @@ import * as sdk from './index'; import Modal from './Modal'; import { _t } from './languageHandler'; import IdentityAuthClient from './IdentityAuthClient'; +import {SSOAuthEntry} from "./components/views/auth/InteractiveAuthEntryComponents"; function getIdServerDomain() { return MatrixClientPeg.get().idBaseUrl.split("://")[1]; @@ -188,11 +189,31 @@ export default class AddThreepid { // pop up an interactive auth dialog const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); + + const dialogAesthetics = { + [SSOAuthEntry.PHASE_PREAUTH]: { + title: _t("Use Single Sign On to continue"), + body: _t("Confirm adding this email address by using " + + "Single Sign On to prove your identity."), + continueText: _t("Single Sign On"), + continueKind: "primary", + }, + [SSOAuthEntry.PHASE_POSTAUTH]: { + title: _t("Confirm adding email"), + body: _t("Click the button below to confirm adding this email address."), + continueText: _t("Confirm"), + continueKind: "primary", + }, + }; const { finished } = Modal.createTrackedDialog('Add Email', '', InteractiveAuthDialog, { title: _t("Add Email Address"), matrixClient: MatrixClientPeg.get(), authData: e.data, makeRequest: this._makeAddThreepidOnlyRequest, + aestheticsForStagePhases: { + [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics, + [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics, + }, }); return finished; } @@ -285,11 +306,30 @@ export default class AddThreepid { // pop up an interactive auth dialog const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); + const dialogAesthetics = { + [SSOAuthEntry.PHASE_PREAUTH]: { + title: _t("Use Single Sign On to continue"), + body: _t("Confirm adding this phone number by using " + + "Single Sign On to prove your identity."), + continueText: _t("Single Sign On"), + continueKind: "primary", + }, + [SSOAuthEntry.PHASE_POSTAUTH]: { + title: _t("Confirm adding phone number"), + body: _t("Click the button below to confirm adding this phone number."), + continueText: _t("Confirm"), + continueKind: "primary", + }, + }; const { finished } = Modal.createTrackedDialog('Add MSISDN', '', InteractiveAuthDialog, { title: _t("Add Phone Number"), matrixClient: MatrixClientPeg.get(), authData: e.data, makeRequest: this._makeAddThreepidOnlyRequest, + aestheticsForStagePhases: { + [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics, + [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics, + }, }); return finished; } diff --git a/src/Analytics.js b/src/Analytics.js index c96cfdefee..e55612c4f1 100644 --- a/src/Analytics.js +++ b/src/Analytics.js @@ -123,8 +123,8 @@ const LAST_VISIT_TS_KEY = "mx_Riot_Analytics_lvts"; function getUid() { try { - let data = localStorage.getItem(UID_KEY); - if (!data) { + let data = localStorage && localStorage.getItem(UID_KEY); + if (!data && localStorage) { localStorage.setItem(UID_KEY, data = [...Array(16)].map(() => Math.random().toString(16)[2]).join('')); } return data; @@ -145,14 +145,16 @@ class Analytics { this.firstPage = true; this._heartbeatIntervalID = null; - this.creationTs = localStorage.getItem(CREATION_TS_KEY); - if (!this.creationTs) { + this.creationTs = localStorage && localStorage.getItem(CREATION_TS_KEY); + if (!this.creationTs && localStorage) { 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); + this.lastVisitTs = localStorage && localStorage.getItem(LAST_VISIT_TS_KEY); + this.visitCount = localStorage && localStorage.getItem(VISIT_COUNT_KEY) || 0; + if (localStorage) { + localStorage.setItem(VISIT_COUNT_KEY, parseInt(this.visitCount, 10) + 1); + } } get disabled() { diff --git a/src/AsyncWrapper.js b/src/AsyncWrapper.js index b7b81688e1..05054cf63a 100644 --- a/src/AsyncWrapper.js +++ b/src/AsyncWrapper.js @@ -38,7 +38,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this._unmounted = false; // XXX: temporary logging to try to diagnose // https://github.com/vector-im/riot-web/issues/3148 diff --git a/src/CallHandler.js b/src/CallHandler.js index 1551b57313..c63bfe309a 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -60,12 +60,12 @@ import * as sdk from './index'; import { _t } from './languageHandler'; import Matrix from 'matrix-js-sdk'; import dis from './dispatcher'; -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"; +import {Jitsi} from "./widgets/Jitsi"; global.mxCalls = { //room_id: MatrixCall @@ -143,7 +143,7 @@ function _setCallListeners(call) { "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 = Jitsi.getInstance().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 f19be03574..1bcf1ba706 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -21,6 +21,7 @@ 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 @@ -96,7 +97,7 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { keyInfo: info, checkPrivateKey: async (input) => { const key = await inputToKey(input); - return MatrixClientPeg.get().checkSecretStoragePrivateKey(key, info.pubkey); + return await MatrixClientPeg.get().checkSecretStorageKey(key, info); }, }, /* className= */ null, @@ -125,10 +126,74 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { 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 @@ -148,19 +213,19 @@ export const crossSigningCallbacks = { * * @param {Function} [func] An operation to perform once secret storage has been * bootstrapped. Optional. - * @param {bool} [force] Reset secret storage even if it's already set up + * @param {bool} [forceReset] Reset secret storage even if it's already set up */ -export async function accessSecretStorage(func = async () => { }, force = false) { +export async function accessSecretStorage(func = async () => { }, forceReset = false) { const cli = MatrixClientPeg.get(); secretStorageBeingAccessed = true; try { - if (!await cli.hasSecretStorageKey() || force) { + 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"), { - force, + force: forceReset, }, null, /* priority = */ false, /* static = */ true, ); @@ -185,6 +250,7 @@ export async function accessSecretStorage(func = async () => { }, force = false) throw new Error("Cross-signing key upload auth canceled"); } }, + getBackupPassphrase: promptForBackupPassphrase, }); } diff --git a/src/DeviceListener.js b/src/DeviceListener.js index 4e7bc8470d..21c844e11c 100644 --- a/src/DeviceListener.js +++ b/src/DeviceListener.js @@ -50,6 +50,8 @@ export default class DeviceListener { MatrixClientPeg.get().on('crypto.devicesUpdated', this._onDevicesUpdated); MatrixClientPeg.get().on('deviceVerificationChanged', this._onDeviceVerificationChanged); MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged); + MatrixClientPeg.get().on('crossSigning.keysChanged', this._onCrossSingingKeysChanged); + MatrixClientPeg.get().on('accountData', this._onAccountData); this._recheck(); } @@ -58,6 +60,8 @@ export default class DeviceListener { MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this._onDevicesUpdated); MatrixClientPeg.get().removeListener('deviceVerificationChanged', this._onDeviceVerificationChanged); MatrixClientPeg.get().removeListener('userTrustStatusChanged', this._onUserTrustStatusChanged); + MatrixClientPeg.get().removeListener('crossSigning.keysChanged', this._onCrossSingingKeysChanged); + MatrixClientPeg.get().removeListener('accountData', this._onAccountData); } this._dismissed.clear(); } @@ -87,6 +91,24 @@ export default class DeviceListener { this._recheck(); } + _onCrossSingingKeysChanged = () => { + this._recheck(); + } + + _onAccountData = (ev) => { + // User may have: + // * migrated SSSS to symmetric + // * uploaded keys to secret storage + // * completed secret storage creation + // which result in account data changes affecting checks below. + if ( + ev.getType().startsWith('m.secret_storage.') || + ev.getType().startsWith('m.cross_signing.') + ) { + this._recheck(); + } + } + // The server doesn't tell us when key backup is set up, so we poll // & cache the result async _getKeyBackupInfo() { @@ -99,80 +121,99 @@ export default class DeviceListener { } async _recheck() { - if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) return; const cli = MatrixClientPeg.get(); - if (!cli.isCryptoEnabled()) return; - if (!cli.getCrossSigningId()) { - if (this._dismissedThisDeviceToast) { - ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY); - return; - } + if ( + !SettingsStore.isFeatureEnabled("feature_cross_signing") || + !await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing") + ) return; - // 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) + 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("Encryption upgrade available"), + title: _t("Verify this session"), icon: "verification_warning", - props: {kind: 'upgrade_encryption'}, + props: {kind: 'verify_this_session'}, component: sdk.getComponent("toasts.SetupEncryptionToast"), }); } else { - // No cross-signing or key backup on account (set up encryption) + 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"), + }); + } else { + // cross-signing is ready, and we don't need to upgrade encryption + ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY); + } + } + + // 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: THIS_DEVICE_TOAST_KEY, - title: _t("Set up encryption"), + key: toastKey(device.deviceId), + title: _t("Unverified login. Was this you?"), icon: "verification_warning", - props: {kind: 'set_up_encryption'}, - component: sdk.getComponent("toasts.SetupEncryptionToast"), + props: { device }, + component: sdk.getComponent("toasts.UnverifiedSessionToast"), }); + newActiveToasts.add(device.deviceId); } } - return; - } else { - ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY); - } - 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 session"), - 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; } - - // 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/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js index 64caba0fdf..c9793d40f7 100644 --- a/src/FromWidgetPostMessageApi.js +++ b/src/FromWidgetPostMessageApi.js @@ -24,6 +24,7 @@ import {MatrixClientPeg} from "./MatrixClientPeg"; import RoomViewStore from "./stores/RoomViewStore"; import {IntegrationManagers} from "./integrations/IntegrationManagers"; import SettingsStore from "./settings/SettingsStore"; +import {Capability} from "./widgets/WidgetApi"; const WIDGET_API_VERSION = '0.0.2'; // Current API version const SUPPORTED_WIDGET_API_VERSIONS = [ @@ -99,7 +100,7 @@ export default class FromWidgetPostMessageApi { console.warn('Add FromWidgetPostMessageApi - Endpoint already registered'); return; } else { - console.warn(`Adding fromWidget messaging endpoint for ${widgetId}`, endpoint); + console.log(`Adding fromWidget messaging endpoint for ${widgetId}`, endpoint); this.widgetMessagingEndpoints.push(endpoint); } } @@ -164,7 +165,7 @@ export default class FromWidgetPostMessageApi { const action = event.data.action; const widgetId = event.data.widgetId; if (action === 'content_loaded') { - console.warn('Widget reported content loaded for', widgetId); + console.log('Widget reported content loaded for', widgetId); dis.dispatch({ action: 'widget_content_loaded', widgetId: widgetId, @@ -213,7 +214,7 @@ 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') { 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/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 72cd84bfd9..1baa6c8e0c 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -40,6 +40,7 @@ import ToastStore from "./stores/ToastStore"; import {IntegrationManagers} from "./integrations/IntegrationManagers"; import {Mjolnir} from "./mjolnir/Mjolnir"; import DeviceListener from "./DeviceListener"; +import {Jitsi} from "./widgets/Jitsi"; /** * Called at startup, to attempt to build a logged-in Matrix session. It tries @@ -313,7 +314,7 @@ async function _restoreFromLocalStorage(opts) { } } -function _handleLoadSessionFailure(e) { +async function _handleLoadSessionFailure(e) { console.error("Unable to load session", e); const SessionRestoreErrorDialog = @@ -323,16 +324,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(); } /** @@ -579,9 +579,6 @@ async function startMatrixClient(startSyncing=true) { UserActivity.sharedInstance().start(); TypingStore.sharedInstance().reset(); // just in case ToastStore.sharedInstance().reset(); - if (!SettingsStore.getValue("lowBandwidth")) { - Presence.start(); - } DMRoomMap.makeShared().start(); IntegrationManagers.sharedInstance().startWatching(); ActiveWidgetStore.start(); @@ -604,6 +601,14 @@ async function startMatrixClient(startSyncing=true) { // This needs to be started after crypto is set up DeviceListener.sharedInstance().start(); + // Similarly, don't start sending presence updates until we've started + // the client + if (!SettingsStore.getValue("lowBandwidth")) { + Presence.start(); + } + + // Now that we have a MatrixClientPeg, update the Jitsi info + await Jitsi.getInstance().update(); // dispatch that we finished starting up to wire up any other bits // of the matrix client that cannot be set prior to starting up. @@ -638,6 +643,10 @@ async function _clearStorage() { window.localStorage.clear(); } + if (window.sessionStorage) { + window.sessionStorage.clear(); + } + // create a temporary client to clear out the persistent stores. const cli = createMatrixClient({ // we'll never make any requests, so can pass a bogus HS URL diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 98fcc85d60..21f05b9759 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -148,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) { diff --git a/src/Notifier.js b/src/Notifier.js index 36a6f13bb6..ec92840998 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -37,6 +37,18 @@ import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; const MAX_PENDING_ENCRYPTED = 20; +/* +Override both the content body and the TextForEvent handler for specific msgtypes, in notifications. +This is useful when the content body contains fallback text that would explain that the client can't handle a particular +type of tile. +*/ +const typehandlers = { + "m.key.verification.request": (event) => { + const name = (event.sender || {}).name; + return _t("%(name)s is requesting verification", { name }); + }, +}; + const Notifier = { notifsByRoom: {}, @@ -46,6 +58,9 @@ const Notifier = { pendingEncryptedEventIds: [], notificationMessageForEvent: function(ev) { + if (typehandlers.hasOwnProperty(ev.getContent().msgtype)) { + return typehandlers[ev.getContent().msgtype](ev); + } return TextForEvent.textForEvent(ev); }, @@ -69,7 +84,9 @@ const Notifier = { title = room.name; // notificationMessageForEvent includes sender, // but we already have the sender here - if (ev.getContent().body) msg = ev.getContent().body; + if (ev.getContent().body && !typehandlers.hasOwnProperty(ev.getContent().msgtype)) { + msg = ev.getContent().body; + } } else if (ev.getType() === 'm.room.member') { // context is all in the message here, we don't need // to display sender info @@ -78,7 +95,9 @@ const Notifier = { title = ev.sender.name + " (" + room.name + ")"; // notificationMessageForEvent includes sender, // but we've just out sender in the title - if (ev.getContent().body) msg = ev.getContent().body; + if (ev.getContent().body && !typehandlers.hasOwnProperty(ev.getContent().msgtype)) { + msg = ev.getContent().body; + } } if (!this.isBodyEnabled()) { diff --git a/src/SdkConfig.ts b/src/SdkConfig.ts index 8177a6c5b8..400d29a20f 100644 --- a/src/SdkConfig.ts +++ b/src/SdkConfig.ts @@ -26,6 +26,11 @@ 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", + }, }; 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/SlashCommands.js b/src/SlashCommands.tsx similarity index 89% rename from src/SlashCommands.js rename to src/SlashCommands.tsx index d306978f78..d60434cf97 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.tsx @@ -2,6 +2,7 @@ Copyright 2015, 2016 OpenMarket 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. @@ -17,7 +18,8 @@ limitations under the License. */ -import React from 'react'; +import * as React from 'react'; + import {MatrixClientPeg} from './MatrixClientPeg'; import dis from './dispatcher'; import * as sdk from './index'; @@ -34,11 +36,16 @@ import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/I import {isPermalinkHost, parsePermalink} from "./utils/permalinks/Permalinks"; import {inviteUsersToRoom} from "./RoomInvite"; -const singleMxcUpload = async () => { +// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816 +interface HTMLInputEvent extends Event { + target: HTMLInputElement & EventTarget; +} + +const singleMxcUpload = async (): Promise => { return new Promise((resolve) => { const fileSelector = document.createElement('input'); fileSelector.setAttribute('type', 'file'); - fileSelector.onchange = (ev) => { + fileSelector.onchange = (ev: HTMLInputEvent) => { const file = ev.target.files[0]; const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog"); @@ -62,28 +69,49 @@ export const CommandCategories = { "other": _td("Other"), }; +type RunFn = ((roomId: string, args: string, cmd: string) => {error: any} | {promise: Promise}); + +interface ICommandOpts { + command: string; + aliases?: string[]; + args?: string; + description: string; + runFn?: RunFn; + category: string; + hideCompletionAfterSpace?: boolean; +} + class Command { - constructor({name, args='', description, runFn, category=CommandCategories.other, hideCompletionAfterSpace=false}) { - this.command = '/' + name; - this.args = args; - this.description = description; - this.runFn = runFn; - this.category = category; - this.hideCompletionAfterSpace = hideCompletionAfterSpace; + command: string; + aliases: string[]; + args: undefined | string; + description: string; + runFn: undefined | RunFn; + category: string; + hideCompletionAfterSpace: boolean; + + constructor(opts: ICommandOpts) { + this.command = opts.command; + this.aliases = opts.aliases || []; + this.args = opts.args || ""; + this.description = opts.description; + this.runFn = opts.runFn; + this.category = opts.category || CommandCategories.other; + this.hideCompletionAfterSpace = opts.hideCompletionAfterSpace || false; } getCommand() { - return this.command; + return `/${this.command}`; } getCommandWithArgs() { return this.getCommand() + " " + this.args; } - run(roomId, args) { + run(roomId: string, args: string, cmd: string) { // 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); + return this.runFn.bind(this)(roomId, args, cmd); } getUsage() { @@ -95,7 +123,7 @@ function reject(error) { return {error}; } -function success(promise) { +function success(promise?: Promise) { return {promise}; } @@ -103,11 +131,9 @@ function success(promise) { * functions are called with `this` bound to the Command instance. */ -/* eslint-disable babel/no-invalid-this */ - -export const CommandMap = { - shrug: new Command({ - name: 'shrug', +export const Commands = [ + new Command({ + command: 'shrug', args: '', description: _td('Prepends ¯\\_(ツ)_/¯ to a plain-text message'), runFn: function(roomId, args) { @@ -119,8 +145,8 @@ export const CommandMap = { }, category: CommandCategories.messages, }), - plain: new Command({ - name: 'plain', + new Command({ + command: 'plain', args: '', description: _td('Sends a message as plain text, without interpreting it as markdown'), runFn: function(roomId, messages) { @@ -128,11 +154,20 @@ export const CommandMap = { }, category: CommandCategories.messages, }), - ddg: new Command({ - name: 'ddg', + new Command({ + command: 'html', + args: '', + description: _td('Sends a message as html, without interpreting it as markdown'), + runFn: function(roomId, messages) { + return success(MatrixClientPeg.get().sendHtmlMessage(roomId, messages, messages)); + }, + category: CommandCategories.messages, + }), + new Command({ + command: 'ddg', args: '', description: _td('Searches DuckDuckGo for results'), - runFn: function(roomId, args) { + runFn: function() { const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); // TODO Don't explain this away, actually show a search UI here. Modal.createTrackedDialog('Slash Commands', '/ddg is not a command', ErrorDialog, { @@ -144,9 +179,8 @@ export const CommandMap = { category: CommandCategories.actions, hideCompletionAfterSpace: true, }), - - upgraderoom: new Command({ - name: 'upgraderoom', + new Command({ + command: 'upgraderoom', args: '', description: _td('Upgrades a room to a new version'), runFn: function(roomId, args) { @@ -215,9 +249,8 @@ export const CommandMap = { }, category: CommandCategories.admin, }), - - nick: new Command({ - name: 'nick', + new Command({ + command: 'nick', args: '', description: _td('Changes your display nickname'), runFn: function(roomId, args) { @@ -228,9 +261,9 @@ export const CommandMap = { }, category: CommandCategories.actions, }), - - myroomnick: new Command({ - name: 'myroomnick', + new Command({ + command: 'myroomnick', + aliases: ['roomnick'], args: '', description: _td('Changes your display nickname in the current room only'), runFn: function(roomId, args) { @@ -247,9 +280,8 @@ export const CommandMap = { }, category: CommandCategories.actions, }), - - roomavatar: new Command({ - name: 'roomavatar', + new Command({ + command: 'roomavatar', args: '[]', description: _td('Changes the avatar of the current room'), runFn: function(roomId, args) { @@ -265,9 +297,8 @@ export const CommandMap = { }, category: CommandCategories.actions, }), - - myroomavatar: new Command({ - name: 'myroomavatar', + new Command({ + command: 'myroomavatar', args: '[]', description: _td('Changes your avatar in this current room only'), runFn: function(roomId, args) { @@ -292,9 +323,8 @@ export const CommandMap = { }, category: CommandCategories.actions, }), - - myavatar: new Command({ - name: 'myavatar', + new Command({ + command: 'myavatar', args: '[]', description: _td('Changes your avatar in all rooms'), runFn: function(roomId, args) { @@ -310,9 +340,8 @@ export const CommandMap = { }, category: CommandCategories.actions, }), - - topic: new Command({ - name: 'topic', + new Command({ + command: 'topic', args: '[]', description: _td('Gets or sets the room topic'), runFn: function(roomId, args) { @@ -336,9 +365,8 @@ export const CommandMap = { }, category: CommandCategories.admin, }), - - roomname: new Command({ - name: 'roomname', + new Command({ + command: 'roomname', args: '', description: _td('Sets the room name'), runFn: function(roomId, args) { @@ -349,9 +377,8 @@ export const CommandMap = { }, category: CommandCategories.admin, }), - - invite: new Command({ - name: 'invite', + new Command({ + command: 'invite', args: '', description: _td('Invites user with given id to current room'), runFn: function(roomId, args) { @@ -385,17 +412,20 @@ export const CommandMap = { button: _t("Continue"), }, )); + + finished = finished.then(([useDefault]: any) => { + if (useDefault) { + useDefaultIdentityServer(); + return; + } + throw new Error(_t("Use an identity server to invite by email. Manage in Settings.")); + }); } else { return reject(_t("Use an identity server to invite by email. Manage in Settings.")); } } const inviter = new MultiInviter(roomId); - return success(finished.then(([useDefault] = []) => { - if (useDefault) { - useDefaultIdentityServer(); - } else if (useDefault === false) { - throw new Error(_t("Use an identity server to invite by email. Manage in Settings.")); - } + return success(finished.then(() => { return inviter.invite([address]); }).then(() => { if (inviter.getCompletionState(address) !== "invited") { @@ -408,12 +438,12 @@ export const CommandMap = { }, category: CommandCategories.actions, }), - - join: new Command({ - name: 'join', + new Command({ + command: 'join', + aliases: ['j', 'goto'], args: '', description: _td('Joins room with given alias'), - runFn: function(roomId, args) { + runFn: function(_, args) { if (args) { // Note: we support 2 versions of this command. The first is // the public-facing one for most users and the other is a @@ -521,9 +551,8 @@ export const CommandMap = { }, category: CommandCategories.actions, }), - - part: new Command({ - name: 'part', + new Command({ + command: 'part', args: '[]', description: _td('Leave room'), runFn: function(roomId, args) { @@ -569,9 +598,8 @@ export const CommandMap = { }, category: CommandCategories.actions, }), - - kick: new Command({ - name: 'kick', + new Command({ + command: 'kick', args: ' [reason]', description: _td('Kicks user with given id'), runFn: function(roomId, args) { @@ -585,10 +613,8 @@ export const CommandMap = { }, category: CommandCategories.admin, }), - - // Ban a user from the room with an optional reason - ban: new Command({ - name: 'ban', + new Command({ + command: 'ban', args: ' [reason]', description: _td('Bans user with given id'), runFn: function(roomId, args) { @@ -602,10 +628,8 @@ export const CommandMap = { }, category: CommandCategories.admin, }), - - // Unban a user from ythe room - unban: new Command({ - name: 'unban', + new Command({ + command: 'unban', args: '', description: _td('Unbans user with given ID'), runFn: function(roomId, args) { @@ -620,9 +644,8 @@ export const CommandMap = { }, category: CommandCategories.admin, }), - - ignore: new Command({ - name: 'ignore', + new Command({ + command: 'ignore', args: '', description: _td('Ignores a user, hiding their messages from you'), runFn: function(roomId, args) { @@ -651,9 +674,8 @@ export const CommandMap = { }, category: CommandCategories.actions, }), - - unignore: new Command({ - name: 'unignore', + new Command({ + command: 'unignore', args: '', description: _td('Stops ignoring a user, showing their messages going forward'), runFn: function(roomId, args) { @@ -683,10 +705,8 @@ export const CommandMap = { }, category: CommandCategories.actions, }), - - // Define the power level of a user - op: new Command({ - name: 'op', + new Command({ + command: 'op', args: ' []', description: _td('Define the power level of a user'), runFn: function(roomId, args) { @@ -696,7 +716,7 @@ export const CommandMap = { if (matches) { const userId = matches[1]; if (matches.length === 4 && undefined !== matches[3]) { - powerLevel = parseInt(matches[3]); + powerLevel = parseInt(matches[3], 10); } if (!isNaN(powerLevel)) { const cli = MatrixClientPeg.get(); @@ -712,10 +732,8 @@ export const CommandMap = { }, category: CommandCategories.admin, }), - - // Reset the power level of a user - deop: new Command({ - name: 'deop', + new Command({ + command: 'deop', args: '', description: _td('Deops user with given id'), runFn: function(roomId, args) { @@ -734,9 +752,8 @@ export const CommandMap = { }, category: CommandCategories.admin, }), - - devtools: new Command({ - name: 'devtools', + new Command({ + command: 'devtools', description: _td('Opens the Developer Tools dialog'), runFn: function(roomId) { const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog'); @@ -745,9 +762,8 @@ export const CommandMap = { }, category: CommandCategories.advanced, }), - - addwidget: new Command({ - name: 'addwidget', + new Command({ + command: 'addwidget', args: '', description: _td('Adds a custom widget by URL to the room'), runFn: function(roomId, args) { @@ -766,10 +782,8 @@ export const CommandMap = { }, category: CommandCategories.admin, }), - - // Verify a user, device, and pubkey tuple - verify: new Command({ - name: 'verify', + new Command({ + command: 'verify', args: ' ', description: _td('Verifies a user, session, and pubkey tuple'), runFn: function(roomId, args) { @@ -834,20 +848,8 @@ export const CommandMap = { }, category: CommandCategories.advanced, }), - - // Command definitions for autocompletion ONLY: - - // /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes - me: new Command({ - name: 'me', - args: '', - description: _td('Displays action'), - category: CommandCategories.messages, - hideCompletionAfterSpace: true, - }), - - discardsession: new Command({ - name: 'discardsession', + new Command({ + command: 'discardsession', description: _td('Forces the current outbound group session in an encrypted room to be discarded'), runFn: function(roomId) { try { @@ -859,9 +861,8 @@ export const CommandMap = { }, category: CommandCategories.advanced, }), - - rainbow: new Command({ - name: "rainbow", + new Command({ + command: "rainbow", description: _td("Sends the given message coloured as a rainbow"), args: '', runFn: function(roomId, args) { @@ -870,9 +871,8 @@ export const CommandMap = { }, category: CommandCategories.messages, }), - - rainbowme: new Command({ - name: "rainbowme", + new Command({ + command: "rainbowme", description: _td("Sends the given emote coloured as a rainbow"), args: '', runFn: function(roomId, args) { @@ -881,9 +881,8 @@ export const CommandMap = { }, category: CommandCategories.messages, }), - - help: new Command({ - name: "help", + new Command({ + command: "help", description: _td("Displays list of commands with usages and descriptions"), runFn: function() { const SlashCommandHelpDialog = sdk.getComponent('dialogs.SlashCommandHelpDialog'); @@ -893,18 +892,16 @@ export const CommandMap = { }, category: CommandCategories.advanced, }), - - whois: new Command({ - name: "whois", + new Command({ + command: "whois", description: _td("Displays information about a user"), - args: '', + 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}, @@ -913,17 +910,26 @@ export const CommandMap = { }, category: CommandCategories.advanced, }), -}; -/* eslint-enable babel/no-invalid-this */ + // Command definitions for autocompletion ONLY: + // /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes + new Command({ + command: 'me', + args: '', + description: _td('Displays action'), + category: CommandCategories.messages, + hideCompletionAfterSpace: true, + }), +]; -// helpful aliases -const aliases = { - j: "join", - newballsplease: "discardsession", - goto: "join", // because it handles event permalinks magically - roomnick: "myroomnick", -}; +// build a map from names and aliases to the Command objects. +export const CommandMap = new Map(); +Commands.forEach(cmd => { + CommandMap.set(cmd.command, cmd); + cmd.aliases.forEach(alias => { + CommandMap.set(alias, cmd); + }); +}); /** @@ -950,10 +956,7 @@ export function getCommand(roomId, input) { cmd = input; } - if (aliases[cmd]) { - cmd = aliases[cmd]; - } - if (CommandMap[cmd]) { - return () => CommandMap[cmd].run(roomId, args); + if (CommandMap.has(cmd)) { + return () => CommandMap.get(cmd).run(roomId, args, cmd); } } diff --git a/src/TextForEvent.js b/src/TextForEvent.js index d4003058c8..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,85 +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 maxShown = 3; - - 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) { - if (addedAliases.length > maxShown) { - return _t("%(senderName)s added %(addedAddresses)s and %(count)s other addresses to this room", { - senderName: senderName, - count: addedAliases.length - maxShown, - addedAddresses: addedAliases.slice(0, maxShown).join(', '), - }); - } - 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) { - if (removedAliases.length > maxShown) { - return _t("%(senderName)s removed %(removedAddresses)s and %(count)s other addresses from this room", { - senderName: senderName, - count: removedAliases.length - maxShown, - removedAddresses: removedAliases.slice(0, maxShown).join(', '), - }); - } - return _t('%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.', { - senderName: senderName, - count: removedAliases.length, - removedAddresses: removedAliases.join(', '), - }); - } else { - const combined = addedAliases.length + removedAliases.length; - if (combined > maxShown) { - return _t("%(senderName)s removed %(countRemoved)s and added %(countAdded)s addresses to this room", { - senderName: senderName, - countAdded: addedAliases.length, - countRemoved: removedAliases.length, - }); - } - 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) { @@ -612,7 +589,6 @@ const handlers = { }; const stateHandlers = { - 'm.room.aliases': textForRoomAliasesEvent, 'm.room.canonical_alias': textForCanonicalAliasEvent, 'm.room.name': textForRoomNameEvent, 'm.room.topic': textForTopicEvent, diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index d40a8ab637..5f877bd48a 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,12 +76,23 @@ 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 */ getScreenshot() { - console.warn('Requesting screenshot for', this.widgetId); + console.log('Requesting screenshot for', this.widgetId); return this.messageToWidget({ api: OUTBOUND_API_NAME, action: "screenshot", @@ -94,12 +106,12 @@ export default class WidgetMessaging { * @return {Promise} To be resolved with an array of requested widget capabilities */ getCapabilities() { - console.warn('Requesting capabilities for', this.widgetId); + console.log('Requesting capabilities for', this.widgetId); return this.messageToWidget({ api: OUTBOUND_API_NAME, action: "capabilities", }).then((response) => { - console.warn('Got capabilities for', this.widgetId, response.capabilities); + console.log('Got capabilities for', this.widgetId, response.capabilities); return response.capabilities; }); } 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/async-components/views/dialogs/EncryptedEventDialog.js b/src/async-components/views/dialogs/EncryptedEventDialog.js index b602cf60fe..9eb4439816 100644 --- a/src/async-components/views/dialogs/EncryptedEventDialog.js +++ b/src/async-components/views/dialogs/EncryptedEventDialog.js @@ -37,7 +37,7 @@ export default createReactClass({ return { device: null }; }, - componentWillMount: function() { + componentDidMount: function() { this._unmounted = false; const client = MatrixClientPeg.get(); @@ -79,7 +79,7 @@ export default createReactClass({ }, onDeviceVerificationChanged: function(userId, device) { - if (userId == this.props.event.getSender()) { + if (userId === this.props.event.getSender()) { this.refreshDevice().then((dev) => { this.setState({ device: dev }); }); diff --git a/src/async-components/views/dialogs/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/ExportE2eKeysDialog.js index 481075d0fa..7ec9da39de 100644 --- a/src/async-components/views/dialogs/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ExportE2eKeysDialog.js @@ -42,7 +42,8 @@ export default createReactClass({ }; }, - componentWillMount: function() { + // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs + UNSAFE_componentWillMount: function() { this._unmounted = false; this._passphrase1 = createRef(); diff --git a/src/async-components/views/dialogs/ImportE2eKeysDialog.js b/src/async-components/views/dialogs/ImportE2eKeysDialog.js index 591c84f5d3..6b9d2c7e45 100644 --- a/src/async-components/views/dialogs/ImportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ImportE2eKeysDialog.js @@ -54,7 +54,8 @@ export default createReactClass({ }; }, - componentWillMount: function() { + // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs + UNSAFE_componentWillMount: function() { this._unmounted = false; this._file = createRef(); diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js index f3ea3beb1c..5f24fb10fa 100644 --- a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js +++ b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js @@ -30,7 +30,7 @@ import EventIndexPeg from "../../../../indexing/EventIndexPeg"; export default class ManageEventIndexDialog extends React.Component { static propTypes = { onFinished: PropTypes.func.isRequired, - } + }; constructor(props) { super(props); @@ -82,7 +82,7 @@ export default class ManageEventIndexDialog extends React.Component { } } - async componentWillMount(): void { + async componentDidMount(): void { let eventIndexSize = 0; let crawlingRoomsCount = 0; let roomCount = 0; @@ -126,30 +126,28 @@ export default class ManageEventIndexDialog extends React.Component { 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 downloading messages for any room."); + crawlerState = _t("Not currently indexing messages for any room."); } else { crawlerState = ( - _t("Downloading mesages for %(currentRoom)s.", { currentRoom: this.state.currentRoom }) + _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 = (
{ @@ -158,15 +156,14 @@ export default class ManageEventIndexDialog extends React.Component { ) }
+ {crawlerState}
{_t("Space used:")} {formatBytes(this.state.eventIndexSize, 0)}
{_t("Indexed messages:")} {formatCountLong(this.state.eventCount)}
- {_t("Indexed rooms:")} {_t("%(crawlingRooms)s out of %(totalRooms)s", { - crawlingRooms: formatCountLong(this.state.crawlingRoomsCount), + {_t("Indexed rooms:")} {_t("%(doneRooms)s out of %(totalRooms)s", { + doneRooms: formatCountLong(doneRooms), totalRooms: formatCountLong(this.state.roomCount), })}
- {crawlerState}
{ - const blob = new Blob([this._encodedRecoveryKey], { + const blob = new Blob([this._recoveryKey.encodedPrivateKey], { type: 'text/plain;charset=us-ascii', }); FileSaver.saveAs(blob, 'recovery-key.txt'); @@ -233,16 +233,17 @@ export default class CreateSecretStorageDialog extends React.PureComponent { if (force) { await cli.bootstrapSecretStorage({ authUploadDeviceSigningKeys: this._doBootstrapUIAuth, - createSecretStorageKey: async () => this._keyInfo, + createSecretStorageKey: async () => this._recoveryKey, setupNewKeyBackup: true, setupNewSecretStorage: true, }); } else { await cli.bootstrapSecretStorage({ authUploadDeviceSigningKeys: this._doBootstrapUIAuth, - createSecretStorageKey: async () => this._keyInfo, + createSecretStorageKey: async () => this._recoveryKey, keyBackupInfo: this.state.backupInfo, setupNewKeyBackup: !this.state.backupInfo && this.state.useKeyBackup, + getKeyBackupPassphrase: promptForBackupPassphrase, }); } this.setState({ @@ -297,10 +298,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } _onSkipPassPhraseClick = async () => { - const [keyInfo, encodedRecoveryKey] = + this._recoveryKey = await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(); - this._keyInfo = keyInfo; - this._encodedRecoveryKey = encodedRecoveryKey; this.setState({ copied: false, downloaded: false, @@ -333,10 +332,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { if (this.state.passPhrase !== this.state.passPhraseConfirm) return; - const [keyInfo, encodedRecoveryKey] = + this._recoveryKey = await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(this.state.passPhrase); - this._keyInfo = keyInfo; - this._encodedRecoveryKey = encodedRecoveryKey; this.setState({ copied: false, downloaded: false, @@ -410,7 +407,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
{_t("Enter your account password to confirm the upgrade:")}
- {this._encodedRecoveryKey} + {this._recoveryKey.encodedPrivateKey}
diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index da8fa3ed3c..0b8af4d6f9 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -23,17 +23,16 @@ import AutocompleteProvider from './AutocompleteProvider'; import QueryMatcher from './QueryMatcher'; import {TextualCompletion} from './Components'; import type {Completion, SelectionRange} from "./Autocompleter"; -import {CommandMap} from '../SlashCommands'; - -const COMMANDS = Object.values(CommandMap); +import {Commands, CommandMap} from '../SlashCommands'; const COMMAND_RE = /(^\/\w*)(?: .*)?/g; export default class CommandProvider extends AutocompleteProvider { constructor() { super(COMMAND_RE); - this.matcher = new QueryMatcher(COMMANDS, { - keys: ['command', 'args', 'description'], + this.matcher = new QueryMatcher(Commands, { + keys: ['command', 'args', 'description'], + funcs: [({aliases}) => aliases.join(" ")], // aliases }); } @@ -46,31 +45,40 @@ export default class CommandProvider extends AutocompleteProvider { if (command[0] !== command[1]) { // The input looks like a command with arguments, perform exact match const name = command[1].substr(1); // strip leading `/` - if (CommandMap[name]) { + if (CommandMap.has(name)) { // some commands, namely `me` and `ddg` don't suit having the usage shown whilst typing their arguments - if (CommandMap[name].hideCompletionAfterSpace) return []; - matches = [CommandMap[name]]; + if (CommandMap.get(name).hideCompletionAfterSpace) return []; + matches = [CommandMap.get(name)]; } } else { if (query === '/') { // If they have just entered `/` show everything - matches = COMMANDS; + matches = Commands; } else { // otherwise fuzzy match against all of the fields matches = this.matcher.match(command[1]); } } - return matches.map((result) => ({ - // If the command is the same as the one they entered, we don't want to discard their arguments - completion: result.command === command[1] ? command[0] : (result.command + ' '), - type: "command", - component: , - range, - })); + + return matches.map((result) => { + let completion = result.getCommand() + ' '; + const usedAlias = result.aliases.find(alias => `/${alias}` === command[1]); + // If the command (or an alias) is the same as the one they entered, we don't want to discard their arguments + if (usedAlias || result.getCommand() === command[1]) { + completion = command[0]; + } + + return { + completion, + type: "command", + component: , + range, + }; + }); } getName() { 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 898991f4f2..b4647a6c30 100644 --- a/src/components/structures/ContextMenu.js +++ b/src/components/structures/ContextMenu.js @@ -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/CustomRoomTagPanel.js b/src/components/structures/CustomRoomTagPanel.js index e8ff6e814e..6e392ea505 100644 --- a/src/components/structures/CustomRoomTagPanel.js +++ b/src/components/structures/CustomRoomTagPanel.js @@ -30,7 +30,7 @@ class CustomRoomTagPanel extends React.Component { }; } - componentWillMount() { + componentDidMount() { this._tagStoreToken = CustomRoomTagStore.addListener(() => { this.setState({tags: CustomRoomTagStore.getSortedTags()}); }); diff --git a/src/components/structures/EmbeddedPage.js b/src/components/structures/EmbeddedPage.js index 6d734c3838..0aababf030 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 = { @@ -37,6 +37,8 @@ export default class EmbeddedPage extends React.PureComponent { className: PropTypes.string, // Whether to wrap the page in a scrollbar scrollbar: PropTypes.bool, + // Map of keys to replace with values, e.g {$placeholder: "value"} + replaceMap: PropTypes.object, }; static contextType = MatrixClientContext; @@ -56,7 +58,7 @@ export default class EmbeddedPage extends React.PureComponent { return sanitizeHtml(_t(s)); } - componentWillMount() { + componentDidMount() { this._unmounted = false; if (!this.props.url) { @@ -81,6 +83,13 @@ export default class EmbeddedPage extends React.PureComponent { } body = body.replace(/_t\(['"]([\s\S]*?)['"]\)/mg, (match, g1)=>this.translate(g1)); + + if (this.props.replaceMap) { + Object.keys(this.props.replaceMap).forEach(key => { + body = body.split(key).join(this.props.replaceMap[key]); + }); + } + this.setState({ page: body }); }, ); @@ -117,10 +126,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/GroupView.js b/src/components/structures/GroupView.js index af90fbbe83..3b32e5c907 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,28 +424,35 @@ export default createReactClass({ membershipBusy: false, publicityBusy: false, inviterProfile: null, + showRightPanel: RightPanelStore.getSharedInstance().isOpenForGroup, }; }, - componentWillMount: function() { + componentDidMount: function() { this._unmounted = false; this._matrixClient = MatrixClientPeg.get(); this._matrixClient.on("Group.myMembership", this._onGroupMyMembership); - this._changeAvatarComponent = null; 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) { - if (this.props.groupId != newProps.groupId) { + // TODO: [REACT-WARNING] Replace with appropriate lifecycle event + UNSAFE_componentWillReceiveProps: function(newProps) { + if (this.props.groupId !== newProps.groupId) { this.setState({ summary: null, error: null, @@ -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') { @@ -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; } @@ -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/HomePage.tsx b/src/components/structures/HomePage.tsx new file mode 100644 index 0000000000..ddf9cd6d00 --- /dev/null +++ b/src/components/structures/HomePage.tsx @@ -0,0 +1,66 @@ +/* +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 AutoHideScrollbar from './AutoHideScrollbar'; +import { getHomePageUrl } from "../../utils/pages"; +import { _t } from "../../languageHandler"; +import SdkConfig from "../../SdkConfig"; +import * as sdk from "../../index"; +import dis from "../../dispatcher"; + +const onClickSendDm = () => dis.dispatch({action: 'view_create_chat'}); +const onClickExplore = () => dis.dispatch({action: 'view_room_directory'}); +const onClickNewRoom = () => dis.dispatch({action: 'view_create_room'}); + +const HomePage = () => { + const config = SdkConfig.get(); + const pageUrl = getHomePageUrl(config); + + if (pageUrl) { + const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage'); + return ; + } + + const brandingConfig = config.branding; + let logoUrl = "themes/riot/img/logos/riot-logo.svg"; + if (brandingConfig && brandingConfig.authHeaderLogoUrl) { + logoUrl = brandingConfig.authHeaderLogoUrl; + } + + const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); + return +
+ Riot +

{ _t("Welcome to %(appName)s", { appName: config.brand || "Riot" }) }

+

{ _t("Liberate your communication") }

+
+ + { _t("Send a Direct Message") } + + + { _t("Explore Public Rooms") } + + + { _t("Create a Group Chat") } + +
+
+
; +}; + +export default HomePage; 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 f4adb5751f..351e3bbad0 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -1,6 +1,6 @@ /* Copyright 2017 Vector Creations 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. @@ -24,6 +24,8 @@ import getEntryComponentForLoginType from '../views/auth/InteractiveAuthEntryCom import * as sdk from '../../index'; +export const ERROR_USER_CANCELLED = new Error("User cancelled auth session"); + export default createReactClass({ displayName: 'InteractiveAuth', @@ -47,7 +49,7 @@ export default createReactClass({ // @param {bool} status True if the operation requiring // auth was completed sucessfully, false if canceled. // @param {object} result The result of the authenticated call - // if successful, otherwise the error object + // if successful, otherwise the error object. // @param {object} extra Additional information about the UI Auth // process: // * emailSid {string} If email auth was performed, the sid of @@ -75,6 +77,15 @@ export default createReactClass({ // is managed by some other party and should not be managed by // the component itself. continueIsManaged: PropTypes.bool, + + // Called when the stage changes, or the stage's phase changes. First + // argument is the stage, second is the phase. Some stages do not have + // phases and will be counted as 0 (numeric). + onStagePhaseChange: PropTypes.func, + + // continueText and continueKind are passed straight through to the AuthEntryComponent. + continueText: PropTypes.string, + continueKind: PropTypes.string, }, getInitialState: function() { @@ -87,7 +98,8 @@ export default createReactClass({ }; }, - componentWillMount: function() { + // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs + UNSAFE_componentWillMount: function() { this._unmounted = false; this._authLogic = new InteractiveAuth({ authData: this.props.authData, @@ -204,6 +216,16 @@ export default createReactClass({ this._authLogic.submitAuthDict(authData); }, + _onPhaseChange: function(newPhase) { + if (this.props.onStagePhaseChange) { + this.props.onStagePhaseChange(this.state.authStage, newPhase || 0); + } + }, + + _onStageCancel: function() { + this.props.onAuthFinished(false, ERROR_USER_CANCELLED); + }, + _renderCurrentStage: function() { const stage = this.state.authStage; if (!stage) { @@ -232,6 +254,10 @@ export default createReactClass({ fail={this._onAuthStageFailed} setEmailSid={this._setEmailSid} showContinue={!this.props.continueIsManaged} + onPhaseChange={this._onPhaseChange} + continueText={this.props.continueText} + continueKind={this.props.continueKind} + onCancel={this._onStageCancel} /> ); }, diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index f5e0bca67e..a9cd12199b 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -44,7 +44,8 @@ const LeftPanel = createReactClass({ }; }, - componentWillMount: function() { + // TODO: [REACT-WARNING] Move this to constructor + UNSAFE_componentWillMount: function() { this.focusedElement = null; this._breadcrumbsWatcherRef = SettingsStore.watchSetting( diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 20217548b7..d9980aeca6 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -22,7 +22,7 @@ import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { DragDropContext } from 'react-beautiful-dnd'; -import { Key, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard'; +import {Key, isOnlyCtrlOrCmdKeyEvent, isOnlyCtrlOrCmdIgnoreShiftKeyEvent} from '../../Keyboard'; import PageTypes from '../../PageTypes'; import CallMediaHandler from '../../CallMediaHandler'; import { fixupColorFonts } from '../../utils/FontManager'; @@ -32,13 +32,14 @@ import sessionStore from '../../stores/SessionStore'; import {MatrixClientPeg} from '../../MatrixClientPeg'; import SettingsStore from "../../settings/SettingsStore"; import RoomListStore from "../../stores/RoomListStore"; -import { getHomePageUrl } from '../../utils/pages'; import TagOrderActions from '../../actions/TagOrderActions'; 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"; +import HomePage from "./HomePage"; // 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. @@ -93,7 +94,8 @@ const LoggedInView = createReactClass({ this._loadResizerPreferences(); }, - componentWillMount: function() { + // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs + UNSAFE_componentWillMount: function() { // stash the MatrixClient in case we log out before we are unmounted this._matrixClient = this.props.matrixClient; @@ -337,13 +339,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 +367,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 +379,43 @@ const LoggedInView = createReactClass({ handled = true; } break; + + case Key.SLASH: + if (isOnlyCtrlOrCmdIgnoreShiftKeyEvent(ev)) { + 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); @@ -507,7 +538,6 @@ const LoggedInView = createReactClass({ const LeftPanel = sdk.getComponent('structures.LeftPanel'); const RoomView = sdk.getComponent('structures.RoomView'); const UserView = sdk.getComponent('structures.UserView'); - const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage'); const GroupView = sdk.getComponent('structures.GroupView'); const MyGroups = sdk.getComponent('structures.MyGroups'); const ToastContainer = sdk.getComponent('structures.ToastContainer'); @@ -546,13 +576,7 @@ const LoggedInView = createReactClass({ break; case PageTypes.HomePage: - { - const pageUrl = getHomePageUrl(this.props.config); - pageElement = ; - } + pageElement = ; break; case PageTypes.UserView: 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 bc11e66d2c..da416142f8 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -221,7 +221,8 @@ export default createReactClass({ return {serverConfig: props}; }, - componentWillMount: function() { + // TODO: [REACT-WARNING] Move this to constructor + UNSAFE_componentWillMount: function() { SdkConfig.put(this.props.config); // Used by _viewRoom before getting state from sync @@ -261,9 +262,7 @@ export default createReactClass({ this._accountPassword = null; this._accountPasswordTimer = null; - }, - componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); this._themeWatcher = new ThemeWatcher(); this._themeWatcher.start(); @@ -361,7 +360,8 @@ export default createReactClass({ if (this._accountPasswordTimer !== null) clearTimeout(this._accountPasswordTimer); }, - componentWillUpdate: function(props, state) { + // TODO: [REACT-WARNING] Replace with appropriate lifecycle stage + UNSAFE_componentWillUpdate: function(props, state) { if (this.shouldTrackPageChange(this.state, state)) { this.startPageChangeTimer(); } @@ -382,7 +382,7 @@ export default createReactClass({ // Tor doesn't support performance if (!performance || !performance.mark) return null; - // This shouldn't happen because componentWillUpdate and componentDidUpdate + // This shouldn't happen because UNSAFE_componentWillUpdate and componentDidUpdate // are used. if (this._pageChanging) { console.warn('MatrixChat.startPageChangeTimer: timer already started'); @@ -600,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(); @@ -658,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, @@ -1495,26 +1495,40 @@ export default createReactClass({ } }); - if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { - cli.on("crypto.verification.request", request => { - if (request.pending) { - ToastStore.sharedInstance().addOrReplaceToast({ - key: 'verifreq_' + request.channel.transactionId, - title: _t("Verification Request"), - icon: "verification", - props: {request}, - component: sdk.getComponent("toasts.VerificationRequestToast"), - }); - } - }); - } else { - cli.on("crypto.verification.start", (verifier) => { + 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 }); + }); + + 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: request.isSelfVerification ? _t("Self-verification request") : _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"); @@ -1893,13 +1907,19 @@ export default createReactClass({ } // 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); + // whether cross-signing has been set up on the account. We can't + // really continue until we know whether it's there or not so retry + // if this fails. + let masterKeyInStorage; + while (masterKeyInStorage === undefined) { + try { + masterKeyInStorage = !!await cli.getAccountDataFromServer("m.cross_signing.master"); + } catch (e) { + if (e.errcode === "M_NOT_FOUND") { + masterKeyInStorage = false; + } else { + console.warn("Secret storage account data check failed: retrying...", e); + } } } @@ -1908,7 +1928,10 @@ export default createReactClass({ // secret storage. SettingsStore.setFeatureEnabled("feature_cross_signing", true); this.setStateForNewView({ view: VIEWS.COMPLETE_SECURITY }); - } else if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { + } 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. @@ -2005,7 +2028,7 @@ export default createReactClass({ } } else if (this.state.view === VIEWS.WELCOME) { const Welcome = sdk.getComponent('auth.Welcome'); - view = ; + view = ; } else if (this.state.view === VIEWS.REGISTER) { const Registration = sdk.getComponent('structures.auth.Registration'); view = ( diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index a2ac93d282..c3a2bdbc59 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -523,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; } @@ -837,14 +838,16 @@ class CreationGrouper { // events that we include in the group but then eject out and place // above the group. this.ejectedEvents = []; - this.readMarker = panel._readMarkerForEvent(createEvent.getId()); + this.readMarker = panel._readMarkerForEvent( + createEvent.getId(), + createEvent === lastShownEvent, + ); } shouldGroup(ev) { const panel = this.panel; const createEvent = this.createEvent; if (!panel._shouldShowEvent(ev)) { - this.readMarker = this.readMarker || panel._readMarkerForEvent(ev.getId()); return true; } if (panel._wantsDateSeparator(this.createEvent, ev.getDate())) { @@ -862,7 +865,10 @@ class CreationGrouper { add(ev) { const panel = this.panel; - this.readMarker = this.readMarker || panel._readMarkerForEvent(ev.getId()); + this.readMarker = this.readMarker || panel._readMarkerForEvent( + ev.getId(), + ev === this.lastShownEvent, + ); if (!panel._shouldShowEvent(ev)) { return; } @@ -949,7 +955,10 @@ class MemberGrouper { constructor(panel, ev, prevEvent, lastShownEvent) { this.panel = panel; - this.readMarker = panel._readMarkerForEvent(ev.getId()); + this.readMarker = panel._readMarkerForEvent( + ev.getId(), + ev === lastShownEvent, + ); this.events = [ev]; this.prevEvent = prevEvent; this.lastShownEvent = lastShownEvent; @@ -970,7 +979,10 @@ class MemberGrouper { const renderText = textForEvent(ev); if (!renderText || renderText.trim().length === 0) return; // quietly ignore } - this.readMarker = this.readMarker || this.panel._readMarkerForEvent(ev.getId()); + this.readMarker = this.readMarker || this.panel._readMarkerForEvent( + ev.getId(), + ev === this.lastShownEvent, + ); this.events.push(ev); } diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index b26ab5ff70..f179cab6ad 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', @@ -37,7 +38,7 @@ export default createReactClass({ contextType: MatrixClientContext, }, - componentWillMount: function() { + componentDidMount: function() { this._fetch(); }, @@ -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 20df323c10..3c97d2f4ae 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -108,7 +108,7 @@ export default class RightPanel extends React.Component { } } - componentWillMount() { + componentDidMount() { this.dispatcherRef = dis.register(this.onAction); const cli = this.context; cli.on("RoomState.members", this.onRoomStateMember); @@ -123,7 +123,8 @@ export default class RightPanel extends React.Component { this._unregisterGroupStore(this.props.groupId); } - componentWillReceiveProps(newProps) { + // TODO: [REACT-WARNING] Replace with appropriate lifecycle event + UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase if (newProps.groupId !== this.props.groupId) { this._unregisterGroupStore(this.props.groupId); this._initGroupStore(newProps.groupId); @@ -182,6 +183,7 @@ export default class RightPanel extends React.Component { member: payload.member, event: payload.event, verificationRequest: payload.verificationRequest, + verificationRequestPromise: payload.verificationRequestPromise, }); } } @@ -231,6 +233,7 @@ export default class RightPanel extends React.Component { onClose={onClose} phase={this.state.phase} verificationRequest={this.state.verificationRequest} + verificationRequestPromise={this.state.verificationRequestPromise} />; } else { panel = + -
; } const explanation = @@ -628,7 +637,7 @@ export default createReactClass({ title={_t("Explore rooms")} >
-

{explanation}

+ {explanation}
{listHeader} {content} diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 13b73ec02b..639f38a119 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -96,7 +96,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { MatrixClientPeg.get().on("sync", this.onSyncStateChange); MatrixClientPeg.get().on("Room.localEchoUpdated", this._onRoomLocalEchoUpdated); diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index fa2231328c..2ae2d71100 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -111,21 +111,30 @@ export default class RoomSubList extends React.PureComponent { } onAction = (payload) => { - // XXX: Previously RoomList would forceUpdate whenever on_room_read is dispatched, - // but this is no longer true, so we must do it here (and can apply the small - // optimisation of checking that we care about the room being read). - // - // Ultimately we need to transition to a state pushing flow where something - // explicitly notifies the components concerned that the notif count for a room - // has change (e.g. a Flux store). - if (payload.action === 'on_room_read' && - this.props.list.some((r) => r.roomId === payload.roomId) - ) { - this.forceUpdate(); + switch (payload.action) { + case 'on_room_read': + // XXX: Previously RoomList would forceUpdate whenever on_room_read is dispatched, + // but this is no longer true, so we must do it here (and can apply the small + // optimisation of checking that we care about the room being read). + // + // Ultimately we need to transition to a state pushing flow where something + // explicitly notifies the components concerned that the notif count for a room + // has change (e.g. a Flux store). + if (this.props.list.some((r) => r.roomId === payload.roomId)) { + this.forceUpdate(); + } + break; + + case 'view_room': + if (this.state.hidden && !this.props.forceExpand && payload.show_room_tile && + this.props.list.some((r) => r.roomId === payload.room_id) + ) { + this.toggle(); + } } }; - onClick = (ev) => { + toggle = () => { if (this.isCollapsibleOnClick()) { // The header isCollapsible, so the click is to be interpreted as collapse and truncation logic const isHidden = !this.state.hidden; @@ -138,6 +147,10 @@ export default class RoomSubList extends React.PureComponent { } }; + onClick = (ev) => { + this.toggle(); + }; + onHeaderKeyDown = (ev) => { switch (ev.key) { case Key.ARROW_LEFT: @@ -180,6 +193,7 @@ export default class RoomSubList extends React.PureComponent { onRoomTileClick = (roomId, ev) => { dis.dispatch({ action: 'view_room', + show_room_tile: true, // to make sure the room gets scrolled into view room_id: roomId, clear_search: (ev && (ev.key === Key.ENTER || ev.key === Key.SPACE)), }); diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 36e30343e4..90bf3a5a99 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -30,7 +30,6 @@ import classNames from 'classnames'; import { _t } from '../../languageHandler'; import {RoomPermalinkCreator} from '../../utils/permalinks/Permalinks'; -import {MatrixClientPeg} from '../../MatrixClientPeg'; import ContentMessages from '../../ContentMessages'; import Modal from '../../Modal'; import * as sdk from '../../index'; @@ -55,6 +54,8 @@ import AccessibleButton from "../views/elements/AccessibleButton"; import RightPanelStore from "../../stores/RightPanelStore"; import {haveTileForEvent} from "../views/rooms/EventTile"; import RoomContext from "../../contexts/RoomContext"; +import MatrixClientContext from "../../contexts/MatrixClientContext"; +import { shieldStatusForRoom } from '../../utils/ShieldUtils'; const DEBUG = false; let debuglog = function() {}; @@ -97,8 +98,12 @@ export default createReactClass({ viaServers: PropTypes.arrayOf(PropTypes.string), }, + statics: { + contextType: MatrixClientContext, + }, + getInitialState: function() { - const llMembers = MatrixClientPeg.get().hasLazyLoadMembersEnabled(); + const llMembers = this.context.hasLazyLoadMembersEnabled(); return { room: null, roomId: null, @@ -131,6 +136,8 @@ export default createReactClass({ isAlone: false, isPeeking: false, showingPinned: false, + showReadReceipts: true, + showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, // error object, as from the matrix client/server API // If we failed to load information about the room, @@ -161,29 +168,40 @@ export default createReactClass({ }; }, - componentWillMount: function() { + // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs + UNSAFE_componentWillMount: function() { this.dispatcherRef = dis.register(this.onAction); - MatrixClientPeg.get().on("Room", this.onRoom); - MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); - MatrixClientPeg.get().on("Room.name", this.onRoomName); - MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData); - MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents); - MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember); - MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership); - MatrixClientPeg.get().on("accountData", this.onAccountData); - MatrixClientPeg.get().on("crypto.keyBackupStatus", this.onKeyBackupStatus); - MatrixClientPeg.get().on("deviceVerificationChanged", this.onDeviceVerificationChanged); - MatrixClientPeg.get().on("userTrustStatusChanged", this.onUserVerificationChanged); + this.context.on("Room", this.onRoom); + this.context.on("Room.timeline", this.onRoomTimeline); + this.context.on("Room.name", this.onRoomName); + this.context.on("Room.accountData", this.onRoomAccountData); + this.context.on("RoomState.events", this.onRoomStateEvents); + this.context.on("RoomState.members", this.onRoomStateMember); + this.context.on("Room.myMembership", this.onMyMembership); + this.context.on("accountData", this.onAccountData); + this.context.on("crypto.keyBackupStatus", this.onKeyBackupStatus); + this.context.on("deviceVerificationChanged", this.onDeviceVerificationChanged); + this.context.on("userTrustStatusChanged", this.onUserVerificationChanged); + this.context.on("crossSigning.keysChanged", this.onCrossSigningKeysChanged); // Start listening for RoomViewStore updates this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); + this._rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this._onRightPanelStoreUpdate); this._onRoomViewStoreUpdate(true); WidgetEchoStore.on('update', this._onWidgetEchoStoreUpdate); + this._showReadReceiptsWatchRef = SettingsStore.watchSetting("showReadReceipts", null, + this._onReadReceiptsChange); this._roomView = createRef(); this._searchResultsPanel = createRef(); }, + _onReadReceiptsChange: function() { + this.setState({ + showReadReceipts: SettingsStore.getValue("showReadReceipts", this.state.roomId), + }); + }, + _onRoomViewStoreUpdate: function(initial) { if (this.unmounted) { return; @@ -204,8 +222,10 @@ export default createReactClass({ return; } + const roomId = RoomViewStore.getRoomId(); + const newState = { - roomId: RoomViewStore.getRoomId(), + roomId, roomAlias: RoomViewStore.getRoomAlias(), roomLoading: RoomViewStore.isRoomLoading(), roomLoadError: RoomViewStore.getRoomLoadError(), @@ -214,9 +234,15 @@ export default createReactClass({ isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(), forwardingEvent: RoomViewStore.getForwardingEvent(), shouldPeek: RoomViewStore.shouldPeek(), - showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", RoomViewStore.getRoomId()), + showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId), + showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId), }; + if (!initial && this.state.shouldPeek && !newState.shouldPeek) { + // Stop peeking because we have joined this room now + this.context.stopPeeking(); + } + // Temporary logging to diagnose https://github.com/vector-im/riot-web/issues/4307 console.log( 'RVS update:', @@ -231,7 +257,7 @@ export default createReactClass({ // NB: This does assume that the roomID will not change for the lifetime of // the RoomView instance if (initial) { - newState.room = MatrixClientPeg.get().getRoom(newState.roomId); + newState.room = this.context.getRoom(newState.roomId); if (newState.room) { newState.showApps = this._shouldShowApps(newState.room); this._onRoomLoaded(newState.room); @@ -333,7 +359,7 @@ export default createReactClass({ peekLoading: true, isPeeking: true, // this will change to false if peeking fails }); - MatrixClientPeg.get().peekInRoom(roomId).then((room) => { + this.context.peekInRoom(roomId).then((room) => { if (this.unmounted) { return; } @@ -366,7 +392,7 @@ export default createReactClass({ }); } else if (room) { // Stop peeking because we have joined this room previously - MatrixClientPeg.get().stopPeeking(); + this.context.stopPeeking(); this.setState({isPeeking: false}); } } @@ -405,21 +431,6 @@ export default createReactClass({ this.onResize(); document.addEventListener("keydown", this.onKeyDown); - - // XXX: EVIL HACK to autofocus inviting on empty rooms. - // We use the setTimeout to avoid racing with focus_composer. - if (this.state.room && - this.state.room.getJoinedMemberCount() == 1 && - this.state.room.getLiveTimeline() && - this.state.room.getLiveTimeline().getEvents() && - this.state.room.getLiveTimeline().getEvents().length <= 6) { - const inviteBox = document.getElementById("mx_SearchableEntityList_query"); - setTimeout(function() { - if (inviteBox) { - inviteBox.focus(); - } - }, 50); - } }, shouldComponentUpdate: function(nextProps, nextState) { @@ -463,6 +474,10 @@ export default createReactClass({ RoomScrollStateStore.setScrollState(this.state.roomId, this._getScrollState()); } + if (this.state.shouldPeek) { + this.context.stopPeeking(); + } + // stop tracking room changes to format permalinks this._stopAllPermalinkCreators(); @@ -478,18 +493,19 @@ export default createReactClass({ roomView.removeEventListener('dragend', this.onDragLeaveOrEnd); } dis.unregister(this.dispatcherRef); - if (MatrixClientPeg.get()) { - MatrixClientPeg.get().removeListener("Room", this.onRoom); - MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); - MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); - MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData); - MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents); - MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership); - MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember); - MatrixClientPeg.get().removeListener("accountData", this.onAccountData); - MatrixClientPeg.get().removeListener("crypto.keyBackupStatus", this.onKeyBackupStatus); - MatrixClientPeg.get().removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged); - MatrixClientPeg.get().removeListener("userTrustStatusChanged", this.onUserVerificationChanged); + if (this.context) { + this.context.removeListener("Room", this.onRoom); + this.context.removeListener("Room.timeline", this.onRoomTimeline); + this.context.removeListener("Room.name", this.onRoomName); + this.context.removeListener("Room.accountData", this.onRoomAccountData); + this.context.removeListener("RoomState.events", this.onRoomStateEvents); + this.context.removeListener("Room.myMembership", this.onMyMembership); + this.context.removeListener("RoomState.members", this.onRoomStateMember); + this.context.removeListener("accountData", this.onAccountData); + this.context.removeListener("crypto.keyBackupStatus", this.onKeyBackupStatus); + this.context.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged); + this.context.removeListener("userTrustStatusChanged", this.onUserVerificationChanged); + this.context.removeListener("crossSigning.keysChanged", this.onCrossSigningKeysChanged); } window.removeEventListener('beforeunload', this.onPageUnload); @@ -503,9 +519,18 @@ export default createReactClass({ if (this._roomStoreToken) { this._roomStoreToken.remove(); } + // Remove RightPanelStore listener + if (this._rightPanelStoreToken) { + this._rightPanelStoreToken.remove(); + } WidgetEchoStore.removeListener('update', this._onWidgetEchoStoreUpdate); + if (this._showReadReceiptsWatchRef) { + SettingsStore.unwatchSetting(this._showReadReceiptsWatchRef); + this._showReadReceiptsWatchRef = null; + } + // cancel any pending calls to the rate_limited_funcs this._updateRoomMembers.cancelPendingCall(); @@ -514,6 +539,12 @@ export default createReactClass({ // Tinter.tint(); // reset colourscheme }, + _onRightPanelStoreUpdate: function() { + this.setState({ + showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, + }); + }, + onPageUnload(event) { if (ContentMessages.sharedInstance().getCurrentUploads().length > 0) { return event.returnValue = @@ -524,7 +555,6 @@ export default createReactClass({ } }, - onKeyDown: function(ev) { let handled = false; const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev); @@ -553,10 +583,6 @@ export default createReactClass({ onAction: function(payload) { switch (payload.action) { - case 'after_right_panel_phase_change': - // We don't keep state on the right panel, so just re-render to update - this.forceUpdate(); - break; case 'message_send_failed': case 'message_sent': this._checkIfAlone(this.state.room); @@ -568,9 +594,7 @@ export default createReactClass({ payload.data.description || payload.data.name); break; case 'picture_snapshot': - ContentMessages.sharedInstance().sendContentListToRoom( - [payload.file], this.state.room.roomId, MatrixClientPeg.get(), - ); + ContentMessages.sharedInstance().sendContentListToRoom([payload.file], this.state.room.roomId, this.context); break; case 'notifier_enabled': case 'upload_started': @@ -659,7 +683,7 @@ export default createReactClass({ // we'll only be showing a spinner. if (this.state.joining) return; - if (ev.getSender() !== MatrixClientPeg.get().credentials.userId) { + if (ev.getSender() !== this.context.credentials.userId) { // update unread count when scrolled up if (!this.state.searchResults && this.state.atEndOfLiveTimeline) { // no change @@ -715,8 +739,7 @@ export default createReactClass({ _loadMembersIfJoined: async function(room) { // lazy load members if enabled - const cli = MatrixClientPeg.get(); - if (cli.hasLazyLoadMembersEnabled()) { + if (this.context.hasLazyLoadMembersEnabled()) { if (room && room.getMyMembership() === 'join') { try { await room.loadMembersIfNeeded(); @@ -751,7 +774,7 @@ export default createReactClass({ _updatePreviewUrlVisibility: function({roomId}) { // URL Previews in E2EE rooms can be a privacy leak so use a different setting which is per-room explicit - const key = MatrixClientPeg.get().isRoomEncrypted(roomId) ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled'; + const key = this.context.isRoomEncrypted(roomId) ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled'; this.setState({ showUrlPreview: SettingsStore.getValue(key, roomId), }); @@ -784,12 +807,18 @@ export default createReactClass({ this._updateE2EStatus(room); }, + onCrossSigningKeysChanged: function() { + const room = this.state.room; + if (room) { + this._updateE2EStatus(room); + } + }, + _updateE2EStatus: async function(room) { - const cli = MatrixClientPeg.get(); - if (!cli.isRoomEncrypted(room.roomId)) { + if (!this.context.isRoomEncrypted(room.roomId)) { return; } - if (!cli.isCryptoEnabled()) { + if (!this.context.isCryptoEnabled()) { // If crypto is not currently enabled, we aren't tracking devices at all, // so we don't know what the answer is. Let's error on the safe side and show // a warning for this case. @@ -808,40 +837,9 @@ export default createReactClass({ return; } - // Duplication between here and _updateE2eStatus in RoomTile /* At this point, the user has encryption on and cross-signing on */ - const e2eMembers = await room.getEncryptionTargetMembers(); - const verified = []; - const unverified = []; - e2eMembers.map(({userId}) => userId) - .filter((userId) => userId !== cli.getUserId()) - .forEach((userId) => { - (cli.checkUserTrust(userId).isCrossSigningVerified() ? - verified : unverified).push(userId) - }); - - debuglog("e2e verified", verified, "unverified", unverified); - - /* Check all verified user devices. */ - /* Don't alarm if no other users are verified */ - const targets = (verified.length > 0) ? [...verified, cli.getUserId()] : verified; - for (const userId of targets) { - const devices = await cli.getStoredDevicesForUser(userId); - const anyDeviceNotVerified = devices.some(({deviceId}) => { - return !cli.checkDeviceTrust(userId, deviceId).isVerified(); - }); - if (anyDeviceNotVerified) { - this.setState({ - e2eStatus: "warning", - }); - debuglog("e2e status set to warning as not all users trust all of their sessions." + - " Aborted on user", userId); - return; - } - } - this.setState({ - e2eStatus: unverified.length === 0 ? "verified" : "normal", + e2eStatus: await shieldStatusForRoom(this.context, room), }); }, @@ -910,7 +908,7 @@ export default createReactClass({ _updatePermissions: function(room) { if (room) { - const me = MatrixClientPeg.get().getUserId(); + const me = this.context.getUserId(); const canReact = room.getMyMembership() === "join" && room.currentState.maySendEvent("m.reaction", me); const canReply = room.maySendMessage(); @@ -994,7 +992,7 @@ export default createReactClass({ if (this.state.searchResults.next_batch) { debuglog("requesting more search results"); - const searchPromise = MatrixClientPeg.get().backPaginateRoomEventsSearch( + const searchPromise = this.context.backPaginateRoomEventsSearch( this.state.searchResults); return this._handleSearchResult(searchPromise); } else { @@ -1020,10 +1018,8 @@ export default createReactClass({ }, onJoinButtonClicked: function(ev) { - const cli = MatrixClientPeg.get(); - // If the user is a ROU, allow them to transition to a PWLU - if (cli && cli.isGuest()) { + if (this.context && this.context.isGuest()) { // Join this room once the user has registered and logged in // (If we failed to peek, we may not have a valid room object.) dis.dispatch({ @@ -1120,7 +1116,7 @@ export default createReactClass({ ev.stopPropagation(); ev.preventDefault(); ContentMessages.sharedInstance().sendContentListToRoom( - ev.dataTransfer.files, this.state.room.roomId, MatrixClientPeg.get(), + ev.dataTransfer.files, this.state.room.roomId, this.context, ); this.setState({ draggingFile: false }); dis.dispatch({action: 'focus_composer'}); @@ -1133,12 +1129,12 @@ export default createReactClass({ }, injectSticker: function(url, info, text) { - if (MatrixClientPeg.get().isGuest()) { + if (this.context.isGuest()) { dis.dispatch({action: 'require_registration'}); return; } - ContentMessages.sharedInstance().sendStickerContentToRoom(url, this.state.room.roomId, info, text, MatrixClientPeg.get()) + ContentMessages.sharedInstance().sendStickerContentToRoom(url, this.state.room.roomId, info, text, this.context) .then(undefined, (error) => { if (error.name === "UnknownDeviceError") { // Let the staus bar handle this @@ -1216,7 +1212,7 @@ export default createReactClass({ }); }, function(error) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Search failed: " + error); + console.error("Search failed", error); Modal.createTrackedDialog('Search failed', '', ErrorDialog, { title: _t("Search failed"), description: ((error && error.message) ? error.message : _t("Server may be unavailable, overloaded, or search timed out :(")), @@ -1229,12 +1225,9 @@ export default createReactClass({ }, getSearchResultTiles: function() { - const EventTile = sdk.getComponent('rooms.EventTile'); const SearchResultTile = sdk.getComponent('rooms.SearchResultTile'); const Spinner = sdk.getComponent("elements.Spinner"); - const cli = MatrixClientPeg.get(); - // XXX: todo: merge overlapping results somehow? // XXX: why doesn't searching on name work? @@ -1242,21 +1235,21 @@ export default createReactClass({ if (this.state.searchInProgress) { ret.push(
  • - -
  • ); + + ); } if (!this.state.searchResults.next_batch) { if (this.state.searchResults.results.length == 0) { ret.push(
  • -

    { _t("No results") }

    -
  • , - ); +

    { _t("No results") }

    + , + ); } else { ret.push(
  • -

    { _t("No more results") }

    -
  • , - ); +

    { _t("No more results") }

    + , + ); } } @@ -1276,7 +1269,7 @@ export default createReactClass({ const mxEv = result.context.getEvent(); const roomId = mxEv.getRoomId(); - const room = cli.getRoom(roomId); + const room = this.context.getRoom(roomId); if (!haveTileForEvent(mxEv)) { // XXX: can this ever happen? It will make the result count @@ -1343,7 +1336,7 @@ export default createReactClass({ }, onForgetClick: function() { - MatrixClientPeg.get().forget(this.state.room.roomId).then(function() { + this.context.forget(this.state.room.roomId).then(function() { dis.dispatch({ action: 'view_next_room' }); }, function(err) { const errCode = err.errcode || _t("unknown error code"); @@ -1360,7 +1353,7 @@ export default createReactClass({ this.setState({ rejecting: true, }); - MatrixClientPeg.get().leave(this.state.roomId).then(function() { + this.context.leave(this.state.roomId).then(function() { dis.dispatch({ action: 'view_next_room' }); self.setState({ rejecting: false, @@ -1387,15 +1380,14 @@ export default createReactClass({ rejecting: true, }); - const cli = MatrixClientPeg.get(); try { - const myMember = this.state.room.getMember(cli.getUserId()); + const myMember = this.state.room.getMember(this.context.getUserId()); const inviteEvent = myMember.events.member; - const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers(); + const ignoredUsers = this.context.getIgnoredUsers(); ignoredUsers.push(inviteEvent.getSender()); // de-duped internally in the js-sdk - await cli.setIgnoredUsers(ignoredUsers); + await this.context.setIgnoredUsers(ignoredUsers); - await cli.leave(this.state.roomId); + await this.context.leave(this.state.roomId); dis.dispatch({ action: 'view_next_room' }); this.setState({ rejecting: false, @@ -1614,7 +1606,7 @@ export default createReactClass({ const createEvent = this.state.room.currentState.getStateEvents("m.room.create", ""); if (!createEvent || !createEvent.getContent()['predecessor']) return null; - return MatrixClientPeg.get().getRoom(createEvent.getContent()['predecessor']['room_id']); + return this.context.getRoom(createEvent.getContent()['predecessor']['room_id']); }, _getHiddenHighlightCount: function() { @@ -1708,7 +1700,7 @@ export default createReactClass({ ); } else { - const myUserId = MatrixClientPeg.get().credentials.userId; + const myUserId = this.context.credentials.userId; const myMember = this.state.room.getMember(myUserId); const inviteEvent = myMember.events.member; var inviterName = inviteEvent.sender ? inviteEvent.sender.name : inviteEvent.getSender(); @@ -1775,13 +1767,13 @@ export default createReactClass({ const showRoomUpgradeBar = ( roomVersionRecommendation && roomVersionRecommendation.needsUpgrade && - this.state.room.userMayUpgradeRoom(MatrixClientPeg.get().credentials.userId) + this.state.room.userMayUpgradeRoom(this.context.credentials.userId) ); const showRoomRecoveryReminder = ( SettingsStore.getValue("showRoomRecoveryReminder") && - MatrixClientPeg.get().isRoomEncrypted(this.state.room.roomId) && - !MatrixClientPeg.get().getKeyBackupEnabled() + this.context.isRoomEncrypted(this.state.room.roomId) && + !this.context.getKeyBackupEnabled() ); const hiddenHighlightCount = this._getHiddenHighlightCount(); @@ -1852,7 +1844,7 @@ export default createReactClass({ const auxPanel = (
    @@ -2041,7 +2036,7 @@ export default createReactClass({ >
    {auxPanel} -
    +
    {topUnreadMessagesBar} {jumpToBottom} {messagePanel} diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index b81b3ebede..4f44c1a169 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -156,9 +156,8 @@ export default createReactClass({ }; }, - componentWillMount: function() { - this._fillRequestWhileRunning = false; - this._isFilling = false; + // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs + UNSAFE_componentWillMount: function() { this._pendingFillRequests = {b: null, f: null}; if (this.props.resizeNotifier) { @@ -782,7 +781,7 @@ export default createReactClass({ if (!this._divScroll) { // Likewise, we should have the ref by this point, but if not // turn the NPE into something meaningful. - throw new Error("ScrollPanel._getScrollNode called before gemini ref collected"); + throw new Error("ScrollPanel._getScrollNode called before AutoHideScrollbar ref collected"); } return this._divScroll; diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js index e169e09752..0f3f8a6be9 100644 --- a/src/components/structures/SearchBox.js +++ b/src/components/structures/SearchBox.js @@ -53,6 +53,7 @@ export default createReactClass({ }; }, + // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs UNSAFE_componentWillMount: function() { this._search = createRef(); }, diff --git a/src/components/structures/TabbedView.js b/src/components/structures/TabbedView.tsx similarity index 76% rename from src/components/structures/TabbedView.js rename to src/components/structures/TabbedView.tsx index 20af183af8..c0e0e58db8 100644 --- a/src/components/structures/TabbedView.js +++ b/src/components/structures/TabbedView.tsx @@ -1,7 +1,7 @@ /* Copyright 2017 Travis Ralston Copyright 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. @@ -18,41 +18,55 @@ limitations under the License. import * as React from "react"; import {_t} from '../../languageHandler'; -import PropTypes from "prop-types"; +import * as PropTypes from "prop-types"; import * as sdk from "../../index"; +import AutoHideScrollbar from './AutoHideScrollbar'; +import { ReactNode } from "react"; /** * Represents a tab for the TabbedView. */ export class Tab { + public label: string; + public icon: string; + public body: React.ReactNode; + /** * Creates a new tab. * @param {string} tabLabel The untranslated tab label. * @param {string} tabIconClass The class for the tab icon. This should be a simple mask. - * @param {string} tabJsx The JSX for the tab container. + * @param {React.ReactNode} tabJsx The JSX for the tab container. */ - constructor(tabLabel, tabIconClass, tabJsx) { + constructor(tabLabel: string, tabIconClass: string, tabJsx: React.ReactNode) { this.label = tabLabel; this.icon = tabIconClass; this.body = tabJsx; } } -export default class TabbedView extends React.Component { +interface IProps { + tabs: Tab[]; +} + +interface IState { + activeTabIndex: number; +} + +export default class TabbedView extends React.Component { static propTypes = { // The tabs to show tabs: PropTypes.arrayOf(PropTypes.instanceOf(Tab)).isRequired, }; - constructor() { - super(); + constructor(props: IProps) { + super(props); this.state = { activeTabIndex: 0, }; } - _getActiveTabIndex() { + private _getActiveTabIndex() { if (!this.state || !this.state.activeTabIndex) return 0; return this.state.activeTabIndex; } @@ -62,7 +76,7 @@ export default class TabbedView extends React.Component { * @param {Tab} tab the tab to show * @private */ - _setActiveTab(tab) { + private _setActiveTab(tab: Tab) { const idx = this.props.tabs.indexOf(tab); if (idx !== -1) { this.setState({activeTabIndex: idx}); @@ -71,7 +85,7 @@ export default class TabbedView extends React.Component { } } - _renderTabLabel(tab) { + private _renderTabLabel(tab: Tab) { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); let classes = "mx_TabbedView_tabLabel "; @@ -97,17 +111,17 @@ export default class TabbedView extends React.Component { ); } - _renderTabPanel(tab) { + private _renderTabPanel(tab: Tab): React.ReactNode { return (
    -
    + {tab.body} -
    +
    ); } - render() { + public render(): React.ReactNode { const labels = this.props.tabs.map(tab => this._renderTabLabel(tab)); const panel = this._renderTabPanel(this.props.tabs[this._getActiveTabIndex()]); diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index 622e63d8ce..6642cce098 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -28,6 +28,7 @@ import { _t } from '../../languageHandler'; import { Droppable } from 'react-beautiful-dnd'; import classNames from 'classnames'; import MatrixClientContext from "../../contexts/MatrixClientContext"; +import AutoHideScrollbar from "./AutoHideScrollbar"; const TagPanel = createReactClass({ displayName: 'TagPanel', @@ -43,7 +44,7 @@ const TagPanel = createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this.unmounted = false; this.context.on("Group.myMembership", this._onGroupMyMembership); this.context.on("sync", this._onClientSync); @@ -106,7 +107,6 @@ const TagPanel = createReactClass({ const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const ActionButton = sdk.getComponent('elements.ActionButton'); const TintableSvg = sdk.getComponent('elements.TintableSvg'); - const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); const tags = this.state.orderedTags.map((tag, index) => { return
    - ) } - +
    ; }, }); diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 25526c3139..6a08cd78eb 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -202,7 +202,8 @@ const TimelinePanel = createReactClass({ }; }, - componentWillMount: function() { + // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs + UNSAFE_componentWillMount: function() { debuglog("TimelinePanel: mounting"); this.lastRRSentEventId = undefined; @@ -234,7 +235,8 @@ const TimelinePanel = createReactClass({ this._initTimeline(this.props); }, - componentWillReceiveProps: function(newProps) { + // TODO: [REACT-WARNING] Replace with appropriate lifecycle event + UNSAFE_componentWillReceiveProps: function(newProps) { if (newProps.timelineSet !== this.props.timelineSet) { // throw new Error("changing timelineSet on a TimelinePanel is not supported"); diff --git a/src/components/structures/UserView.js b/src/components/structures/UserView.js index 94159a1da4..c4fba137cc 100644 --- a/src/components/structures/UserView.js +++ b/src/components/structures/UserView.js @@ -35,7 +35,7 @@ export default class UserView extends React.Component { this.state = {}; } - componentWillMount() { + componentDidMount() { if (this.props.userId) { this._loadProfileInfo(); } diff --git a/src/components/structures/auth/CompleteSecurity.js b/src/components/structures/auth/CompleteSecurity.js index 6bf3e7f07c..06cece0af2 100644 --- a/src/components/structures/auth/CompleteSecurity.js +++ b/src/components/structures/auth/CompleteSecurity.js @@ -18,13 +18,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import * as sdk from '../../../index'; -import { MatrixClientPeg } from '../../../MatrixClientPeg'; -import { accessSecretStorage, AccessCancelledError } from '../../../CrossSigningManager'; - -const PHASE_INTRO = 0; -const PHASE_BUSY = 1; -const PHASE_DONE = 2; -const PHASE_CONFIRM_SKIP = 3; +import { + SetupEncryptionStore, + PHASE_INTRO, + PHASE_BUSY, + PHASE_DONE, + PHASE_CONFIRM_SKIP, +} from '../../../stores/SetupEncryptionStore'; +import SetupEncryptionBody from "./SetupEncryptionBody"; export default class CompleteSecurity extends React.Component { static propTypes = { @@ -33,202 +34,42 @@ export default class CompleteSecurity extends React.Component { constructor() { super(); - - this.state = { - phase: PHASE_INTRO, - // this serves dual purpose as the object for the request logic and - // the presence of it insidicating that we're in 'verify mode'. - // Because of the latter, it lives in the state. - verificationRequest: null, - backupInfo: null, - }; - MatrixClientPeg.get().on("crypto.verification.request", this.onVerificationRequest); + const store = SetupEncryptionStore.sharedInstance(); + store.on("update", this._onStoreUpdate); + store.start(); + this.state = {phase: store.phase}; } + _onStoreUpdate = () => { + const store = SetupEncryptionStore.sharedInstance(); + this.setState({phase: store.phase}); + }; + componentWillUnmount() { - if (this.state.verificationRequest) { - this.state.verificationRequest.off("change", this.onVerificationRequestChange); - } - if (MatrixClientPeg.get()) { - MatrixClientPeg.get().removeListener("crypto.verification.request", this.onVerificationRequest); - } - } - - onStartClick = async () => { - this.setState({ - phase: PHASE_BUSY, - }); - const cli = MatrixClientPeg.get(); - const backupInfo = await cli.getKeyBackupVersion(); - this.setState({backupInfo}); - try { - await accessSecretStorage(async () => { - await cli.checkOwnCrossSigningTrust(); - if (backupInfo) await cli.restoreKeyBackupWithSecretStorage(backupInfo); - }); - - if (cli.getCrossSigningId()) { - this.setState({ - phase: PHASE_DONE, - }); - } - } catch (e) { - if (!(e instanceof AccessCancelledError)) { - console.log(e); - } - // this will throw if the user hits cancel, so ignore - this.setState({ - phase: PHASE_INTRO, - }); - } - } - - onVerificationRequest = async (request) => { - if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return; - - if (this.state.verificationRequest) { - this.state.verificationRequest.off("change", this.onVerificationRequestChange); - } - await request.accept(); - request.on("change", this.onVerificationRequestChange); - this.setState({ - verificationRequest: request, - }); - } - - onVerificationRequestChange = () => { - if (this.state.verificationRequest.cancelled) { - this.state.verificationRequest.off("change", this.onVerificationRequestChange); - this.setState({ - verificationRequest: null, - }); - } - } - - onSkipClick = () => { - this.setState({ - phase: PHASE_CONFIRM_SKIP, - }); - } - - onSkipConfirmClick = () => { - this.props.onFinished(); - } - - onSkipBackClick = () => { - this.setState({ - phase: PHASE_INTRO, - }); - } - - onDoneClick = () => { - this.props.onFinished(); + const store = SetupEncryptionStore.sharedInstance(); + store.off("update", this._onStoreUpdate); + store.stop(); } render() { const AuthPage = sdk.getComponent("auth.AuthPage"); const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody"); - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); - - const { - phase, - } = this.state; - + const {phase} = this.state; let icon; let title; - let body; - if (this.state.verificationRequest) { - const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel"); - body = ; - } else if (phase === PHASE_INTRO) { + if (phase === PHASE_INTRO) { icon = ; title = _t("Complete security"); - body = ( -
    -

    {_t( - "Verify this session to grant it access to encrypted messages.", - )}

    -
    - - {_t("Skip")} - - - {_t("Start")} - -
    -
    - ); } else if (phase === PHASE_DONE) { icon = ; title = _t("Session verified"); - let message; - if (this.state.backupInfo) { - message =

    {_t( - "Your new session is now verified. It has access to your " + - "encrypted messages, and other users will see it as trusted.", - )}

    ; - } else { - message =

    {_t( - "Your new session is now verified. Other users will see it as trusted.", - )}

    ; - } - body = ( -
    -
    - {message} -
    - - {_t("Done")} - -
    -
    - ); } else if (phase === PHASE_CONFIRM_SKIP) { icon = ; title = _t("Are you sure?"); - body = ( -
    -

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

    -
    - - {_t("Skip")} - - - {_t("Go Back")} - -
    -
    - ); } else if (phase === PHASE_BUSY) { - const Spinner = sdk.getComponent('views.elements.Spinner'); icon = ; title = _t("Complete security"); - body = ; } else { throw new Error(`Unknown phase ${phase}`); } @@ -241,7 +82,7 @@ export default class CompleteSecurity extends React.Component { {title}
    - {body} +
    diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index e921951512..9877c53106 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -69,12 +69,13 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this.reset = null; this._checkServerLiveliness(this.props.serverConfig); }, - componentWillReceiveProps: function(newProps) { + // TODO: [REACT-WARNING] Replace with appropriate lifecycle event + UNSAFE_componentWillReceiveProps: function(newProps) { if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl && newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return; @@ -296,7 +297,6 @@ export default createReactClass({
    { + const store = SetupEncryptionStore.sharedInstance(); + if (store.phase === PHASE_FINISHED) { + this.props.onFinished(); + return; + } + this.setState({ + phase: store.phase, + verificationRequest: store.verificationRequest, + backupInfo: store.backupInfo, + }); + }; + + componentWillUnmount() { + const store = SetupEncryptionStore.sharedInstance(); + store.off("update", this._onStoreUpdate); + store.stop(); + } + + _onUsePassphraseClick = async () => { + const store = SetupEncryptionStore.sharedInstance(); + store.usePassPhrase(); + } + + onSkipClick = () => { + const store = SetupEncryptionStore.sharedInstance(); + store.skip(); + } + + onSkipConfirmClick = () => { + const store = SetupEncryptionStore.sharedInstance(); + store.skipConfirm(); + } + + onSkipBackClick = () => { + const store = SetupEncryptionStore.sharedInstance(); + store.returnAfterSkip(); + } + + onDoneClick = () => { + const store = SetupEncryptionStore.sharedInstance(); + store.done(); + } + + render() { + const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); + + const { + phase, + } = this.state; + + if (this.state.verificationRequest) { + const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel"); + return ; + } else if (phase === PHASE_INTRO) { + const InlineSpinner = sdk.getComponent('elements.InlineSpinner'); + return ( +
    +

    {_t( + "Open an existing session & use it to verify this one, " + + "granting it access to encrypted messages.", + )}

    +

    {_t("Waiting…")}

    +

    {_t( + "If you can’t access one, ", + {}, { + button: sub => + {sub} + , + })}

    +
    + + {_t("Skip")} + +
    +
    + ); + } else if (phase === PHASE_DONE) { + let message; + if (this.state.backupInfo) { + message =

    {_t( + "Your new session is now verified. It has access to your " + + "encrypted messages, and other users will see it as trusted.", + )}

    ; + } else { + message =

    {_t( + "Your new session is now verified. Other users will see it as trusted.", + )}

    ; + } + return ( +
    +
    + {message} +
    + + {_t("Done")} + +
    +
    + ); + } else if (phase === PHASE_CONFIRM_SKIP) { + return ( +
    +

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

    +
    + + {_t("Skip")} + + + {_t("Go Back")} + +
    +
    + ); + } else if (phase === PHASE_BUSY) { + const Spinner = sdk.getComponent('views.elements.Spinner'); + return ; + } else { + console.log(`SetupEncryptionBody: Unknown phase ${phase}`); + } + } +} diff --git a/src/components/structures/auth/SoftLogout.js b/src/components/structures/auth/SoftLogout.js index d38fcf3883..08ab7e8a61 100644 --- a/src/components/structures/auth/SoftLogout.js +++ b/src/components/structures/auth/SoftLogout.js @@ -54,7 +54,7 @@ export default class SoftLogout extends React.Component { this.state = { loginView: LOGIN_VIEW.LOADING, - keyBackupNeeded: true, // assume we do while we figure it out (see componentWillMount) + keyBackupNeeded: true, // assume we do while we figure it out (see componentDidMount) busy: false, password: "", @@ -213,7 +213,6 @@ export default class SoftLogout extends React.Component {

    {introText}

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

    { + // Note: We don't use PlatformPeg's startSsoAuth functions because we almost + // certainly will need to open the thing in a new tab to avoid losing application + // context. + + window.open(this._ssoUrl, '_blank'); + this.setState({phase: SSOAuthEntry.PHASE_POSTAUTH}); + this.props.onPhaseChange(SSOAuthEntry.PHASE_POSTAUTH); + }; + + onConfirmClick = () => { + this.props.submitAuthDict({}); + }; + + render() { + let continueButton = null; + const cancelButton = ( + {_t("Cancel")} + ); + if (this.state.phase === SSOAuthEntry.PHASE_PREAUTH) { + continueButton = ( + {this.props.continueText || _t("Single Sign On")} + ); + } else { + continueButton = ( + {this.props.continueText || _t("Confirm")} + ); + } + + return
    + {cancelButton} + {continueButton} +
    ; + } +} + export const FallbackAuthEntry = createReactClass({ displayName: 'FallbackAuthEntry', @@ -574,9 +693,15 @@ export const FallbackAuthEntry = createReactClass({ loginType: PropTypes.string.isRequired, submitAuthDict: PropTypes.func.isRequired, errorText: PropTypes.string, + onPhaseChange: PropTypes.func.isRequired, }, - componentWillMount: function() { + componentDidMount: function() { + this.props.onPhaseChange(DEFAULT_PHASE); + }, + + // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs + UNSAFE_componentWillMount: function() { // we have to make the user click a button, as browsers will block // the popup if we open it immediately. this._popupWindow = null; @@ -598,7 +723,10 @@ export const FallbackAuthEntry = createReactClass({ } }, - _onShowFallbackClick: function() { + _onShowFallbackClick: function(e) { + e.preventDefault(); + e.stopPropagation(); + const url = this.props.matrixClient.getFallbackAuthUrl( this.props.loginType, this.props.authSessionId, @@ -627,7 +755,7 @@ export const FallbackAuthEntry = createReactClass({ } return ( ); @@ -640,11 +768,12 @@ const AuthEntryComponents = [ EmailIdentityAuthEntry, MsisdnAuthEntry, TermsAuthEntry, + SSOAuthEntry, ]; export default function getEntryComponentForLoginType(loginType) { for (const c of AuthEntryComponents) { - if (c.LOGIN_TYPE == loginType) { + if (c.LOGIN_TYPE === loginType || c.UNSTABLE_LOGIN_TYPE === loginType) { return c; } } diff --git a/src/components/views/auth/ModularServerConfig.js b/src/components/views/auth/ModularServerConfig.js index d8ce145e20..1216202a23 100644 --- a/src/components/views/auth/ModularServerConfig.js +++ b/src/components/views/auth/ModularServerConfig.js @@ -106,7 +106,8 @@ export default class ModularServerConfig extends ServerConfig { )}
    - this[FIELD_EMAIL] = field} type="text" label={emailPlaceholder} @@ -524,7 +523,6 @@ export default createReactClass({ onOptionChange={this.onPhoneCountryChange} />; return this[FIELD_PHONE_NUMBER] = field} type="text" label={phoneLabel} diff --git a/src/components/views/auth/ServerConfig.js b/src/components/views/auth/ServerConfig.js index a9e26b8fb7..ee6f57a521 100644 --- a/src/components/views/auth/ServerConfig.js +++ b/src/components/views/auth/ServerConfig.js @@ -72,7 +72,8 @@ export default class ServerConfig extends React.PureComponent { }; } - componentWillReceiveProps(newProps) { + // TODO: [REACT-WARNING] Replace with appropriate lifecycle event + UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase if (newProps.serverConfig.hsUrl === this.state.hsUrl && newProps.serverConfig.isUrl === this.state.isUrl) return; @@ -223,7 +224,8 @@ export default class ServerConfig extends React.PureComponent { {sub} , })} - , })} -
    -
    diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 4c34cee853..3e3a2e6bd9 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -74,7 +74,8 @@ export default createReactClass({ this.context.removeListener('sync', this.onClientSync); }, - componentWillReceiveProps: function(nextProps) { + // TODO: [REACT-WARNING] Replace with appropriate lifecycle event + UNSAFE_componentWillReceiveProps: function(nextProps) { // work out if we need to call setState (if the image URLs array has changed) const newState = this._getState(nextProps); const newImageUrls = newState.imageUrls; diff --git a/src/components/views/avatars/MemberAvatar.js b/src/components/views/avatars/MemberAvatar.js index a07a184aa1..826aa5fddf 100644 --- a/src/components/views/avatars/MemberAvatar.js +++ b/src/components/views/avatars/MemberAvatar.js @@ -51,7 +51,8 @@ export default createReactClass({ return this._getState(this.props); }, - componentWillReceiveProps: function(nextProps) { + // TODO: [REACT-WARNING] Replace with appropriate lifecycle event + UNSAFE_componentWillReceiveProps: function(nextProps) { this.setState(this._getState(nextProps)); }, diff --git a/src/components/views/avatars/MemberStatusMessageAvatar.js b/src/components/views/avatars/MemberStatusMessageAvatar.js index 54f11e8e91..eef3f86d9a 100644 --- a/src/components/views/avatars/MemberStatusMessageAvatar.js +++ b/src/components/views/avatars/MemberStatusMessageAvatar.js @@ -49,7 +49,7 @@ export default class MemberStatusMessageAvatar extends React.Component { this._button = createRef(); } - componentWillMount() { + componentDidMount() { if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) { throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user"); } diff --git a/src/components/views/avatars/RoomAvatar.js b/src/components/views/avatars/RoomAvatar.js index c79e1827da..a72d318b8d 100644 --- a/src/components/views/avatars/RoomAvatar.js +++ b/src/components/views/avatars/RoomAvatar.js @@ -63,7 +63,8 @@ export default createReactClass({ } }, - componentWillReceiveProps: function(newProps) { + // TODO: [REACT-WARNING] Replace with appropriate lifecycle event + UNSAFE_componentWillReceiveProps: function(newProps) { this.setState({ urls: this.getImageUrls(newProps), }); diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index 4fc6dd58cc..452489aa65 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -61,7 +61,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { MatrixClientPeg.get().on('RoomMember.powerLevel', this._checkPermissions); this._checkPermissions(); }, diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js index 2d8dec29c7..d281656bbe 100644 --- a/src/components/views/context_menus/RoomTileContextMenu.js +++ b/src/components/views/context_menus/RoomTileContextMenu.js @@ -82,7 +82,7 @@ export default createReactClass({ }; }, - componentWillMount: function() { + componentDidMount: function() { this._unmounted = false; }, diff --git a/src/components/views/context_menus/StatusMessageContextMenu.js b/src/components/views/context_menus/StatusMessageContextMenu.js index d5cba45956..5e6f06dd5d 100644 --- a/src/components/views/context_menus/StatusMessageContextMenu.js +++ b/src/components/views/context_menus/StatusMessageContextMenu.js @@ -35,7 +35,7 @@ export default class StatusMessageContextMenu extends React.Component { }; } - componentWillMount() { + componentDidMount() { const { user } = this.props; if (!user) { return; diff --git a/src/components/views/context_menus/TopLeftMenu.js b/src/components/views/context_menus/TopLeftMenu.js index f1309cac2d..4448ecd041 100644 --- a/src/components/views/context_menus/TopLeftMenu.js +++ b/src/components/views/context_menus/TopLeftMenu.js @@ -26,6 +26,7 @@ import { getHostingLink } from '../../../utils/HostingLink'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MenuItem} from "../../structures/ContextMenu"; import * as sdk from "../../../index"; +import {getHomePageUrl} from "../../../utils/pages"; export default class TopLeftMenu extends React.Component { static propTypes = { @@ -47,15 +48,7 @@ export default class TopLeftMenu extends React.Component { } hasHomePage() { - const config = SdkConfig.get(); - const pagesConfig = config.embeddedPages; - if (pagesConfig && pagesConfig.homeUrl) { - return true; - } - // This is a deprecated config option for the home page - // (despite the name, given we also now have a welcome - // page, which is not the same). - return !!config.welcomePageUrl; + return !!getHomePageUrl(SdkConfig.get()); } render() { diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index e309c3a0cf..451ec9cfde 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -107,6 +107,7 @@ export default createReactClass({ }; }, + // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs UNSAFE_componentWillMount: function() { this._textinput = createRef(); }, diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index 9238024b60..67d70aabe4 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -86,7 +86,8 @@ export default createReactClass({ }; }, - componentWillMount() { + // TODO: [REACT-WARNING] Move this to constructor + UNSAFE_componentWillMount() { this._matrixClient = MatrixClientPeg.get(); }, diff --git a/src/components/views/dialogs/BugReportDialog.js b/src/components/views/dialogs/BugReportDialog.js index fe95041373..6e337d53dc 100644 --- a/src/components/views/dialogs/BugReportDialog.js +++ b/src/components/views/dialogs/BugReportDialog.js @@ -166,7 +166,6 @@ export default class BugReportDialog extends React.Component { ) }

    {_t("Set a room alias to easily share your room with other people.")}

    ); + publicPrivateLabel = (

    {_t("Set a room alias to easily share your room with other people.")}

    ); const domain = MatrixClientPeg.get().getDomain(); aliasField = (
    - this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} value={this.state.alias} /> + this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} value={this.state.alias} />
    ); } else { - privateLabel = (

    {_t("This room is private, and can only be joined by invitation.")}

    ); + publicPrivateLabel = (

    {_t("This room is private, and can only be joined by invitation.")}

    ); + } + + let e2eeSection; + if (!this.state.isPublic && SettingsStore.isFeatureEnabled("feature_cross_signing")) { + e2eeSection = + +

    { _t("You can’t disable this later. Bridges & most bots won’t work yet.") }

    +
    ; } const title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room'); @@ -188,11 +206,11 @@ export default createReactClass({ >
    - this._nameFieldRef = ref} label={ _t('Name') } onChange={this.onNameChange} onValidate={this.onNameValidate} value={this.state.name} className="mx_CreateRoomDialog_name" /> - + this._nameFieldRef = ref} label={ _t('Name') } onChange={this.onNameChange} onValidate={this.onNameValidate} value={this.state.name} className="mx_CreateRoomDialog_name" /> + - { privateLabel } - { publicLabel } + { publicPrivateLabel } + { e2eeSection } { aliasField }
    { this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') } diff --git a/src/components/views/dialogs/DeactivateAccountDialog.js b/src/components/views/dialogs/DeactivateAccountDialog.js index d7468933df..3889f0989a 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.js +++ b/src/components/views/dialogs/DeactivateAccountDialog.js @@ -1,6 +1,6 @@ /* Copyright 2016 OpenMarket 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. @@ -23,71 +23,109 @@ import Analytics from '../../../Analytics'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import * as Lifecycle from '../../../Lifecycle'; import { _t } from '../../../languageHandler'; +import InteractiveAuth, {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth"; +import {DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry} from "../auth/InteractiveAuthEntryComponents"; + +const dialogAesthetics = { + [SSOAuthEntry.PHASE_PREAUTH]: { + body: _t("Confirm your account deactivation by using Single Sign On to prove your identity."), + continueText: _t("Single Sign On"), + continueKind: "danger", + }, + [SSOAuthEntry.PHASE_POSTAUTH]: { + body: _t("Are you sure you want to deactivate your account? This is irreversible."), + continueText: _t("Confirm account deactivation"), + continueKind: "danger", + }, +}; + +// This is the same as aestheticsForStagePhases in InteractiveAuthDialog minus the `title` +const DEACTIVATE_AESTHETICS = { + [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics, + [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics, + [PasswordAuthEntry.LOGIN_TYPE]: { + [DEFAULT_PHASE]: { + body: _t("To continue, please enter your password:"), + }, + }, +}; export default class DeactivateAccountDialog extends React.Component { constructor(props) { super(props); - this._onOk = this._onOk.bind(this); - this._onCancel = this._onCancel.bind(this); - this._onPasswordFieldChange = this._onPasswordFieldChange.bind(this); - this._onEraseFieldChange = this._onEraseFieldChange.bind(this); - this.state = { - password: "", - busy: false, shouldErase: false, errStr: null, + authData: null, // for UIA + + // A few strings that are passed to InteractiveAuth for design or are displayed + // next to the InteractiveAuth component. + bodyText: null, + continueText: null, + continueKind: null, }; - } - _onPasswordFieldChange(ev) { - this.setState({ - password: ev.target.value, - }); - } - - _onEraseFieldChange(ev) { - this.setState({ - shouldErase: ev.target.checked, - }); - } - - async _onOk() { - this.setState({busy: true}); - - try { - // This assumes that the HS requires password UI auth - // for this endpoint. In reality it could be any UI auth. - const auth = { - type: 'm.login.password', - // TODO: Remove `user` once servers support proper UIA - // See https://github.com/vector-im/riot-web/issues/10312 - user: MatrixClientPeg.get().credentials.userId, - identifier: { - type: "m.id.user", - user: MatrixClientPeg.get().credentials.userId, - }, - password: this.state.password, - }; - await MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase); - } catch (err) { - let errStr = _t('Unknown error'); - // https://matrix.org/jira/browse/SYN-744 - if (err.httpStatus === 401 || err.httpStatus === 403) { - errStr = _t('Incorrect password'); + MatrixClientPeg.get().deactivateAccount(null, false).then(r => { + // If we got here, oops. The server didn't require any auth. + // Our application lifecycle will catch the error and do the logout bits. + // We'll try to log something in an vain attempt to record what happened (storage + // is also obliterated on logout). + console.warn("User's account got deactivated without confirmation: Server had no auth"); + this.setState({errStr: _t("Server did not require any authentication")}); + }).catch(e => { + if (e && e.httpStatus === 401 && e.data) { + // Valid UIA response + this.setState({authData: e.data}); + } else { + this.setState({errStr: _t("Server did not return valid authentication information.")}); } - this.setState({ - busy: false, - errStr: errStr, - }); + }); + } + + _onStagePhaseChange = (stage, phase) => { + const aesthetics = DEACTIVATE_AESTHETICS[stage]; + let bodyText = null; + let continueText = null; + let continueKind = null; + if (aesthetics) { + const phaseAesthetics = aesthetics[phase]; + if (phaseAesthetics && phaseAesthetics.body) bodyText = phaseAesthetics.body; + if (phaseAesthetics && phaseAesthetics.continueText) continueText = phaseAesthetics.continueText; + if (phaseAesthetics && phaseAesthetics.continueKind) continueKind = phaseAesthetics.continueKind; + } + this.setState({bodyText, continueText, continueKind}); + }; + + _onUIAuthFinished = (success, result, extra) => { + if (success) return; // great! makeRequest() will be called too. + + if (result === ERROR_USER_CANCELLED) { + this._onCancel(); return; } - Analytics.trackEvent('Account', 'Deactivate Account'); - Lifecycle.onLoggedOut(); - this.props.onFinished(true); - } + console.error("Error during UI Auth:", {result, extra}); + this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")}); + }; + + _onUIAuthComplete = (auth) => { + MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase).then(r => { + // Deactivation worked - logout & close this dialog + Analytics.trackEvent('Account', 'Deactivate Account'); + Lifecycle.onLoggedOut(); + this.props.onFinished(true); + }).catch(e => { + console.error(e); + this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")}); + }); + }; + + _onEraseFieldChange = (ev) => { + this.setState({ + shouldErase: ev.target.checked, + }); + }; _onCancel() { this.props.onFinished(false); @@ -95,34 +133,36 @@ export default class DeactivateAccountDialog extends React.Component { render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const Loader = sdk.getComponent("elements.Spinner"); - let passwordBoxClass = ''; let error = null; if (this.state.errStr) { error =
    { this.state.errStr }
    ; - passwordBoxClass = 'error'; } - const okLabel = this.state.busy ? : _t('Deactivate Account'); - const okEnabled = this.state.password && !this.state.busy; - - let cancelButton = null; - if (!this.state.busy) { - cancelButton = ; + let auth =
    {_t("Loading...")}
    ; + if (this.state.authData) { + auth = ( +
    + {this.state.bodyText} + +
    + ); } - const Field = sdk.getComponent('elements.Field'); - // this is on purpose not a to prevent Enter triggering submission, to further prevent accidents return ( @@ -172,29 +212,10 @@ export default class DeactivateAccountDialog extends React.Component {

    -

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

    - + {error} + {auth}
    - { error } -
    -
    - - - { cancelButton }
    ); diff --git a/src/components/views/dialogs/DeviceVerifyDialog.js b/src/components/views/dialogs/DeviceVerifyDialog.js index f7826b9c27..39e391269c 100644 --- a/src/components/views/dialogs/DeviceVerifyDialog.js +++ b/src/components/views/dialogs/DeviceVerifyDialog.js @@ -279,6 +279,7 @@ export default class DeviceVerifyDialog extends React.Component { onDone={this._onSasMatchesClick} isSelf={MatrixClientPeg.get().getUserId() === this.props.userId} onStartEmoji={this._onUseSasClick} + inDialog={true} />; } diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js index 348965582b..de0923306f 100644 --- a/src/components/views/dialogs/DevtoolsDialog.js +++ b/src/components/views/dialogs/DevtoolsDialog.js @@ -267,7 +267,8 @@ class FilteredList extends React.PureComponent { }; } - componentWillReceiveProps(nextProps) { + // TODO: [REACT-WARNING] Replace with appropriate lifecycle event + UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase if (this.props.children === nextProps.children && this.props.query === nextProps.query) return; this.setState({ filteredChildren: FilteredList.filterChildren(nextProps.children, nextProps.query), @@ -302,7 +303,7 @@ class FilteredList extends React.PureComponent { render() { const TruncatedList = sdk.getComponent("elements.TruncatedList"); return
    - ; } diff --git a/src/components/views/dialogs/InfoDialog.js b/src/components/views/dialogs/InfoDialog.js index 8a8f51c25a..b63f6ba9c6 100644 --- a/src/components/views/dialogs/InfoDialog.js +++ b/src/components/views/dialogs/InfoDialog.js @@ -32,6 +32,7 @@ export default createReactClass({ button: PropTypes.string, onFinished: PropTypes.func, hasCloseButton: PropTypes.bool, + onKeyDown: PropTypes.func, }, getDefaultProps: function() { @@ -50,10 +51,13 @@ export default createReactClass({ const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return ( -
    { this.props.description } diff --git a/src/components/views/dialogs/InteractiveAuthDialog.js b/src/components/views/dialogs/InteractiveAuthDialog.js index ff9f55cb74..af5dc5108c 100644 --- a/src/components/views/dialogs/InteractiveAuthDialog.js +++ b/src/components/views/dialogs/InteractiveAuthDialog.js @@ -1,6 +1,7 @@ /* Copyright 2016 OpenMarket Ltd 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. @@ -23,6 +24,7 @@ import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import AccessibleButton from '../elements/AccessibleButton'; +import {ERROR_USER_CANCELLED} from "../../structures/InteractiveAuth"; export default createReactClass({ displayName: 'InteractiveAuthDialog', @@ -44,12 +46,36 @@ export default createReactClass({ onFinished: PropTypes.func.isRequired, + // Optional title and body to show when not showing a particular stage title: PropTypes.string, + body: PropTypes.string, + + // Optional title and body pairs for particular stages and phases within + // those stages. Object structure/example is: + // { + // "org.example.stage_type": { + // 1: { + // "body": "This is a body for phase 1" of org.example.stage_type, + // "title": "Title for phase 1 of org.example.stage_type" + // }, + // 2: { + // "body": "This is a body for phase 2 of org.example.stage_type", + // "title": "Title for phase 2 of org.example.stage_type" + // "continueText": "Confirm identity with Example Auth", + // "continueKind": "danger" + // } + // } + // } + aestheticsForStagePhases: PropTypes.object, }, getInitialState: function() { return { authError: null, + + // See _onUpdateStagePhase() + uiaStage: null, + uiaStagePhase: null, }; }, @@ -57,12 +83,21 @@ export default createReactClass({ if (success) { this.props.onFinished(true, result); } else { - this.setState({ - authError: result, - }); + if (result === ERROR_USER_CANCELLED) { + this.props.onFinished(false, null); + } else { + this.setState({ + authError: result, + }); + } } }, + _onUpdateStagePhase: function(newStage, newPhase) { + // We copy the stage and stage phase params into state for title selection in render() + this.setState({uiaStage: newStage, uiaStagePhase: newPhase}); + }, + _onDismissClick: function() { this.props.onFinished(false); }, @@ -71,6 +106,23 @@ export default createReactClass({ const InteractiveAuth = sdk.getComponent("structures.InteractiveAuth"); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + // Let's pick a title, body, and other params text that we'll show to the user. The order + // is most specific first, so stagePhase > our props > defaults. + + let title = this.state.authError ? 'Error' : (this.props.title || _t('Authentication')); + let body = this.state.authError ? null : this.props.body; + let continueText = null; + let continueKind = null; + if (!this.state.authError && this.props.aestheticsForStagePhases) { + if (this.props.aestheticsForStagePhases[this.state.uiaStage]) { + const aesthetics = this.props.aestheticsForStagePhases[this.state.uiaStage][this.state.uiaStagePhase]; + if (aesthetics && aesthetics.title) title = aesthetics.title; + if (aesthetics && aesthetics.body) body = aesthetics.body; + if (aesthetics && aesthetics.continueText) continueText = aesthetics.continueText; + if (aesthetics && aesthetics.continueKind) continueKind = aesthetics.continueKind; + } + } + let content; if (this.state.authError) { content = ( @@ -88,11 +140,16 @@ export default createReactClass({ } else { content = (
    -
    ); @@ -101,7 +158,7 @@ export default createReactClass({ return ( { content } diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index d27a66165e..f0d5443cac 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -35,6 +35,7 @@ import createRoom, {canEncryptToAllUsers} from "../../../createRoom"; import {inviteMultipleToRoom} from "../../../RoomInvite"; import SettingsStore from '../../../settings/SettingsStore'; import RoomListStore, {TAG_DM} from "../../../stores/RoomListStore"; +import {Key} from "../../../Keyboard"; export const KIND_DM = "dm"; export const KIND_INVITE = "invite"; @@ -125,7 +126,7 @@ class ThreepidMember extends Member { class DMUserTile extends React.PureComponent { static propTypes = { member: PropTypes.object.isRequired, // Should be a Member (see interface above) - onRemove: PropTypes.func.isRequired, // takes 1 argument, the member being removed + onRemove: PropTypes.func, // takes 1 argument, the member being removed }; _onRemove = (e) => { @@ -156,18 +157,25 @@ class DMUserTile extends React.PureComponent { width={avatarSize} height={avatarSize} />; - return ( - - - {avatar} - {this.props.member.name} - + let closeButton; + if (this.props.onRemove) { + closeButton = ( {_t('Remove')} + ); + } + + return ( + + + {avatar} + {this.props.member.name} + + { closeButton } ); } @@ -640,11 +648,14 @@ export default class InviteDialog extends React.PureComponent { }); }; - _cancel = () => { - // We do not want the user to close the dialog while an action is in progress - if (this.state.busy) return; - - this.props.onFinished(); + _onKeyDown = (e) => { + // when the field is empty and the user hits backspace remove the right-most target + if (!e.target.value && !this.state.busy && this.state.targets.length > 0 && e.key === Key.BACKSPACE && + !e.ctrlKey && !e.shiftKey && !e.metaKey + ) { + e.preventDefault(); + this._removeMember(this.state.targets[this.state.targets.length - 1]); + } }; _updateFilter = (e) => { @@ -889,7 +900,7 @@ export default class InviteDialog extends React.PureComponent { _onManageSettingsClick = (e) => { e.preventDefault(); dis.dispatch({ action: 'view_user_settings' }); - this._cancel(); + this.props.onFinished(); }; _renderSection(kind: "recents"|"suggestions") { @@ -984,17 +995,18 @@ export default class InviteDialog extends React.PureComponent { _renderEditor() { const targets = this.state.targets.map(t => ( - + )); const input = (