diff --git a/CHANGELOG.md b/CHANGELOG.md index 07e478fa02..b6f25f1858 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,170 @@ +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) diff --git a/README.md b/README.md index d6fd6db1b7..69aafeb724 100644 --- a/README.md +++ b/README.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/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 1ff0fb6f55..7ba69c4272 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "2.2.3", + "version": "2.3.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -64,8 +64,8 @@ "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", @@ -89,7 +89,6 @@ "qrcode-react": "^0.1.16", "qs": "^6.6.0", "react": "^16.9.0", - "react-addons-css-transition-group": "15.6.2", "react-beautiful-dnd": "^4.0.1", "react-dom": "^16.9.0", "react-focus-lock": "^2.2.1", diff --git a/res/css/_common.scss b/res/css/_common.scss index ad64aced50..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; @@ -60,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; } @@ -76,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; } @@ -253,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%; @@ -321,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; } @@ -350,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; } @@ -446,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 6890a1ffd1..0ba2b609e8 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"; @@ -132,6 +133,7 @@ @import "./views/messages/_MNoticeBody.scss"; @import "./views/messages/_MStickerBody.scss"; @import "./views/messages/_MTextBody.scss"; +@import "./views/messages/_MVideoBody.scss"; @import "./views/messages/_MessageActionBar.scss"; @import "./views/messages/_MessageTimestamp.scss"; @import "./views/messages/_MjolnirBody.scss"; @@ -186,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 2575169664..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; @@ -196,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; } @@ -226,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; } @@ -258,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 { @@ -337,7 +337,7 @@ limitations under the License. display: none; } -.mx_GroupView_body .mx_AutoHideScrollbar_offset > * { +.mx_GroupView_body .mx_AutoHideScrollbar > * { margin: 11px 50px 50px 68px; } @@ -366,7 +366,7 @@ limitations under the License. padding: 40px 20px; } -.mx_GroupView .mx_MemberInfo .mx_AutoHideScrollbar_offset > :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/_MyGroups.scss b/res/css/structures/_MyGroups.scss index 36150c33a5..73f1332cd0 100644 --- a/res/css/structures/_MyGroups.scss +++ b/res/css/structures/_MyGroups.scss @@ -105,7 +105,7 @@ limitations under the License. .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; } @@ -149,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; } @@ -161,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 f3a7b0e243..e0814182f5 100644 --- a/res/css/structures/_RoomDirectory.scss +++ b/res/css/structures/_RoomDirectory.scss @@ -64,7 +64,7 @@ limitations under the License. } .mx_RoomDirectory_table { - font-size: 12px; + font-size: $font-12px; color: $primary-fg-color; width: 100%; text-align: left; @@ -112,7 +112,7 @@ limitations under the License. .mx_RoomDirectory_name { display: inline-block; - font-size: 18px; + font-size: $font-18px; font-weight: 600; } @@ -124,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; @@ -136,7 +136,7 @@ limitations under the License. } .mx_RoomDirectory_alias { - font-size: 12px; + font-size: $font-12px; color: $settings-grey-fg-color; } @@ -150,7 +150,7 @@ limitations under the License. } .mx_RoomDirectory > span { - font-size: 15px; + 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 472831c0d9..0065ffa502 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -137,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; @@ -157,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 601492d43c..3050840fe8 100644 --- a/res/css/structures/auth/_CompleteSecurity.scss +++ b/res/css/structures/auth/_CompleteSecurity.scss @@ -34,7 +34,7 @@ limitations under the License. } .mx_CompleteSecurity_body { - font-size: 15px; + font-size: $font-15px; } .mx_CompleteSecurity_waiting { 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/_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 2b0f8dceca..daa6bd2352 100644 --- a/res/css/views/dialogs/_UnknownDeviceDialog.scss +++ b/res/css/views/dialogs/_UnknownDeviceDialog.scss @@ -27,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 { diff --git a/res/css/views/directory/_NetworkDropdown.scss b/res/css/views/directory/_NetworkDropdown.scss index 106392f880..269b507e3c 100644 --- a/res/css/views/directory/_NetworkDropdown.scss +++ b/res/css/views/directory/_NetworkDropdown.scss @@ -47,9 +47,9 @@ limitations under the License. .mx_NetworkDropdown_server_title { padding: 0 10px; - font-size: 15px; + font-size: $font-15px; font-weight: 600; - line-height: 20px; + line-height: $font-20px; margin-bottom: 4px; // remove server button @@ -77,16 +77,16 @@ limitations under the License. .mx_NetworkDropdown_server_subtitle { padding: 0 10px; - font-size: 10px; - line-height: 14px; + font-size: $font-10px; + line-height: $font-14px; margin-top: -4px; margin-bottom: 4px; color: $muted-fg-color; } .mx_NetworkDropdown_server_network { - font-size: 12px; - line-height: 16px; + font-size: $font-12px; + line-height: $font-16px; padding: 4px 10px; cursor: pointer; position: relative; @@ -154,7 +154,7 @@ limitations under the License. .mx_NetworkDropdown_handle_server { color: $muted-fg-color; - font-size: 12px; + font-size: $font-12px; } } 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 75ef3fbabd..e4b1ac5574 100644 --- a/res/css/views/elements/_DirectorySearchBox.scss +++ b/res/css/views/elements/_DirectorySearchBox.scss @@ -32,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/_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/_MVideoBody.scss b/res/css/views/messages/_MVideoBody.scss new file mode 100644 index 0000000000..3b05c53f34 --- /dev/null +++ b/res/css/views/messages/_MVideoBody.scss @@ -0,0 +1,22 @@ +/* +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. +*/ + +span.mx_MVideoBody { + video.mx_MVideoBody { + max-width: 100%; + height: auto; + } +} 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..fe5b081042 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; @@ -34,12 +34,17 @@ limitations under the License. background-color: $reaction-row-button-selected-bg-color; border-color: $reaction-row-button-selected-border-color; } -} -.mx_ReactionsRowButton_content { - max-width: 100px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - padding-right: 4px; + // ignore mouse events for all children, treat it as one entire hoverable entity + * { + pointer-events: none; + } + + .mx_ReactionsRowButton_content { + max-width: 100px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + padding-right: 4px; + } } 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/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..e9013eb7b7 100644 --- a/res/css/views/rooms/_BasicMessageComposer.scss +++ b/res/css/views/rooms/_BasicMessageComposer.scss @@ -44,27 +44,29 @@ limitations under the License. outline: none; overflow-x: hidden; - span.mx_UserPill, span.mx_RoomPill { - padding-left: 21px; - position: relative; + &.mx_BasicMessageComposer_input_shouldShowPillAvatar { + span.mx_UserPill, span.mx_RoomPill { + padding-left: 21px; + position: relative; - // avatar psuedo element - &::before { - position: absolute; - left: 2px; - top: 2px; - content: var(--avatar-letter); - width: 16px; - height: 16px; - background: var(--avatar-background), $avatar-bg-color; - color: $avatar-initial-color; - background-repeat: no-repeat; - background-size: 16px; - border-radius: 8px; - text-align: center; - font-weight: normal; - line-height: 16px; - font-size: 10.4px; + // avatar psuedo element + &::before { + position: absolute; + left: 2px; + top: 2px; + content: var(--avatar-letter); + width: 16px; + height: 16px; + background: var(--avatar-background), $avatar-bg-color; + color: $avatar-initial-color; + background-repeat: no-repeat; + background-size: 16px; + border-radius: 8px; + text-align: center; + font-weight: normal; + 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 2fa9994155..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,16 +111,19 @@ limitations under the License. } .mx_EventTile_line, .mx_EventTile_reply { + clear: both; position: relative; 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 { +.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; @@ -312,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; } @@ -341,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 { @@ -393,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 { @@ -471,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; @@ -489,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; @@ -589,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; @@ -599,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 981cf06c69..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 { @@ -48,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/_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/_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 01a1d94956..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,7 +40,7 @@ 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 } @@ -61,7 +61,7 @@ limitations under the License. .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/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 bfa2272283..5d6ba033c8 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -185,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; 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 9bdd712e07..f5f3013354 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -310,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; @@ -331,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/gen-i18n.js b/scripts/gen-i18n.js index a4d53aea2f..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') || fileStats.name.endsWith('.tsx')) { + 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/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 362db939a3..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 SettingsStore, { SettingLevel } from './settings/SettingsStore'; import {generateHumanReadableId} from "./utils/NamingUtils"; +import {Jitsi} from "./widgets/Jitsi"; global.mxCalls = { //room_id: MatrixCall @@ -430,8 +430,8 @@ async function _startCallApp(roomId, type) { return; } - const confId = `JitsiConference_${generateHumanReadableId()}`; - const jitsiDomain = SdkConfig.get()['jitsi']['preferredDomain']; + const confId = `JitsiConference${generateHumanReadableId()}`; + const jitsiDomain = Jitsi.getInstance().preferredDomain; let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl(); diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index 5c254bbd00..07ec776bd1 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -96,11 +96,8 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { { keyInfo: info, checkPrivateKey: async (input) => { - if (!info.pubkey) { - return true; - } const key = await inputToKey(input); - return MatrixClientPeg.get().checkSecretStoragePrivateKey(key, info.pubkey); + return await MatrixClientPeg.get().checkSecretStorageKey(key, info); }, }, /* className= */ null, @@ -145,13 +142,34 @@ const onSecretRequested = async function({ console.log(`CrossSigningManager: Ignoring request from untrusted device ${deviceId}`); return; } - const callbacks = client.getCrossSigningCacheCallbacks(); - if (!callbacks.getCrossSigningKeyCache) return; - if (name === "m.cross_signing.self_signing") { - const key = await callbacks.getCrossSigningKeyCache("self_signing"); - return key && encodeBase64(key); - } else if (name === "m.cross_signing.user_signing") { - const key = await callbacks.getCrossSigningKeyCache("user_signing"); + 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); @@ -167,7 +185,7 @@ export async function promptForBackupPassphrase() { const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); const { finished } = Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, { - showSummary: false, keyCallback: k => key = k, + showSummary: false, keyCallback: k => key = k, }, null, /* priority = */ false, /* static = */ true); const success = await finished; @@ -195,19 +213,19 @@ export async function promptForBackupPassphrase() { * * @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, ); diff --git a/src/DeviceListener.js b/src/DeviceListener.js index 7878a1a670..21c844e11c 100644 --- a/src/DeviceListener.js +++ b/src/DeviceListener.js @@ -50,6 +50,7 @@ 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(); } @@ -59,6 +60,7 @@ 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(); @@ -89,9 +91,20 @@ export default class DeviceListener { this._recheck(); } + _onCrossSingingKeysChanged = () => { + this._recheck(); + } + _onAccountData = (ev) => { - // User may have migrated SSSS to symmetric, in which case we can dismiss that toast - if (ev.getType().startsWith('m.secret_storage.key.')) { + // 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(); } } @@ -119,89 +132,88 @@ export default class DeviceListener { const crossSigningReady = await cli.isCrossSigningReady(); - if (!crossSigningReady) { - if (this._dismissedThisDeviceToast) { - ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY); - 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 (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 if (await cli.secretStorageKeyNeedsUpgrade()) { - if (this._dismissedThisDeviceToast) { - ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY); - return; + + // clear any other outstanding toasts (eg. logged out devices) + for (const deviceId of this._activeNagToasts) { + if (!newActiveToasts.has(deviceId)) ToastStore.sharedInstance().dismissToast(toastKey(deviceId)); } - - 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 { - ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY); + this._activeNagToasts = newActiveToasts; } - - 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; } } 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/Keyboard.ts b/src/Keyboard.ts index 817d0a0b97..23e2bbf0d6 100644 --- a/src/Keyboard.ts +++ 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", diff --git a/src/Lifecycle.js b/src/Lifecycle.js index b9fbf4f1bc..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 @@ -578,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(); @@ -603,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. @@ -637,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 34f3402334..400d29a20f 100644 --- a/src/SdkConfig.ts +++ b/src/SdkConfig.ts @@ -30,8 +30,6 @@ export const DEFAULTS: ConfigOptions = { jitsi: { // Default conference domain preferredDomain: "jitsi.riot.im", - // Default Jitsi Meet API location - externalApiUrl: "https://jitsi.riot.im/libs/external_api.min.js", }, }; diff --git a/src/SlashCommands.js b/src/SlashCommands.tsx similarity index 88% rename from src/SlashCommands.js rename to src/SlashCommands.tsx index d306978f78..71815dde8c 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) { @@ -321,7 +350,7 @@ export const CommandMap = { return success(cli.setRoomTopic(roomId, args)); } const room = cli.getRoom(roomId); - if (!room) return reject('Bad room ID: ' + roomId); + if (!room) return reject(_t("Failed to set topic")); const topicEvents = room.currentState.getStateEvents('m.room.topic', ''); const topic = topicEvents && topicEvents.getContent().topic; @@ -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,14 +716,15 @@ 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(); const room = cli.getRoom(roomId); - if (!room) return reject('Bad room ID: ' + roomId); + if (!room) return reject(_t("Command failed")); const powerLevelEvent = room.currentState.getStateEvents('m.room.power_levels', ''); + if (!powerLevelEvent.getContent().users[args]) return reject(_t("Could not find user in room")); return success(cli.setPowerLevel(roomId, userId, powerLevel, powerLevelEvent)); } } @@ -712,10 +733,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) { @@ -724,9 +743,10 @@ export const CommandMap = { if (matches) { const cli = MatrixClientPeg.get(); const room = cli.getRoom(roomId); - if (!room) return reject('Bad room ID: ' + roomId); + if (!room) return reject(_t("Command failed")); const powerLevelEvent = room.currentState.getStateEvents('m.room.power_levels', ''); + if (!powerLevelEvent.getContent().users[args]) return reject(_t("Could not find user in room")); return success(cli.setPowerLevel(roomId, args, undefined, powerLevelEvent)); } } @@ -734,9 +754,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 +764,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 +784,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 +850,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 +863,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 +873,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 +883,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 +894,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,28 +912,28 @@ 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); + }); +}); - -/** - * Process the given text for /commands and return a bound method to perform them. - * @param {string} roomId The room in which the command was performed. - * @param {string} input The raw text input by the user. - * @return {null|function(): Object} Function returning an object with the property 'error' if there was an error - * processing the command, or 'promise' if a request was sent out. - * Returns null if the input didn't match a command. - */ -export function getCommand(roomId, input) { +export function parseCommandString(input) { // trim any trailing whitespace, as it can confuse the parser for // IRC-style commands input = input.replace(/\s+$/, ''); @@ -950,10 +949,21 @@ export function getCommand(roomId, input) { cmd = input; } - if (aliases[cmd]) { - cmd = aliases[cmd]; - } - if (CommandMap[cmd]) { - return () => CommandMap[cmd].run(roomId, args); + return {cmd, args}; +} + +/** + * Process the given text for /commands and return a bound method to perform them. + * @param {string} roomId The room in which the command was performed. + * @param {string} input The raw text input by the user. + * @return {null|function(): Object} Function returning an object with the property 'error' if there was an error + * processing the command, or 'promise' if a request was sent out. + * Returns null if the input didn't match a command. + */ +export function getCommand(roomId, input) { + const {cmd, args} = parseCommandString(input); + + if (CommandMap.has(cmd)) { + return () => CommandMap.get(cmd).run(roomId, args, cmd); } } 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 index c2739beefa..bcbf3d6810 100644 --- a/src/accessibility/KeyboardShortcuts.tsx +++ b/src/accessibility/KeyboardShortcuts.tsx @@ -118,6 +118,11 @@ const shortcuts: Record = { key: Key.ARROW_DOWN, }], description: _td("Navigate composer history"), + }, { + keybinds: [{ + key: Key.ESCAPE, + }], + description: _td("Cancel replying to a message"), }, ], 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 371fdcaf64..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,16 +126,12 @@ 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; @@ -168,7 +164,6 @@ export default class ManageEventIndexDialog extends React.Component { totalRooms: formatCountLong(this.state.roomCount), })}
{ - const blob = new Blob([this._encodedRecoveryKey], { + const blob = new Blob([this._recoveryKey.encodedPrivateKey], { type: 'text/plain;charset=us-ascii', }); FileSaver.saveAs(blob, 'recovery-key.txt'); @@ -234,17 +234,25 @@ 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, + getKeyBackupPassphrase: () => { + // We may already have the backup key if we earlier went + // through the restore backup path, so pass it along + // rather than prompting again. + if (this._backupKey) { + return this._backupKey; + } + return promptForBackupPassphrase(); + }, }); } this.setState({ @@ -273,10 +281,18 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } _restoreBackup = async () => { + // It's possible we'll need the backup key later on for bootstrapping, + // so let's stash it here, rather than prompting for it twice. + const keyCallback = k => this._backupKey = k; + const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); const { finished } = Modal.createTrackedDialog( - 'Restore Backup', '', RestoreKeyBackupDialog, {showSummary: false}, null, - /* priority = */ false, /* static = */ false, + 'Restore Backup', '', RestoreKeyBackupDialog, + { + showSummary: false, + keyCallback, + }, + null, /* priority = */ false, /* static = */ false, ); await finished; @@ -299,10 +315,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } _onSkipPassPhraseClick = async () => { - const [keyInfo, encodedRecoveryKey] = + this._recoveryKey = await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(); - this._keyInfo = keyInfo; - this._encodedRecoveryKey = encodedRecoveryKey; this.setState({ copied: false, downloaded: false, @@ -335,10 +349,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, @@ -412,7 +424,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/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index 9373ed662e..670776644e 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -100,6 +100,8 @@ export default class EmojiProvider extends AutocompleteProvider { // then sort by score (Infinity if matchedString not in shortname) sorters.push((c) => score(matchedString, c.shortname)); + // then sort by max score of all shortcodes, trim off the `:` + sorters.push((c) => Math.min(...c.emoji.shortcodes.map(s => score(matchedString.substring(1), s)))); // If the matchedString is not empty, sort by length of shortname. Example: // matchedString = ":bookmark" // completions = [":bookmark:", ":bookmark_tabs:", ...] 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/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 f854dc955f..0aababf030 100644 --- a/src/components/structures/EmbeddedPage.js +++ b/src/components/structures/EmbeddedPage.js @@ -37,6 +37,8 @@ export default class EmbeddedPage extends React.PureComponent { className: PropTypes.string, // Whether to wrap the page in a scrollbar scrollbar: PropTypes.bool, + // Map of keys to replace with values, e.g {$placeholder: "value"} + replaceMap: PropTypes.object, }; static contextType = MatrixClientContext; @@ -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 }); }, ); diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 524694fe95..3b32e5c907 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -428,12 +428,11 @@ export default createReactClass({ }; }, - 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); @@ -451,8 +450,9 @@ export default createReactClass({ } }, - 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, 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.tsx similarity index 86% rename from src/components/structures/LoggedInView.js rename to src/components/structures/LoggedInView.tsx index e7a6f4c1a9..0e2a74faae 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.tsx @@ -1,7 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd -Copyright 2017, 2018 New Vector Ltd +Copyright 2017, 2018, 2020 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,23 +16,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixClient } from 'matrix-js-sdk'; -import React, {createRef} from 'react'; -import createReactClass from 'create-react-class'; -import PropTypes from 'prop-types'; +import * as React from 'react'; +import * as PropTypes from 'prop-types'; +import { MatrixClient } from 'matrix-js-sdk/src/client'; +import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; 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'; import * as sdk from '../../index'; import dis from '../../dispatcher'; import sessionStore from '../../stores/SessionStore'; -import {MatrixClientPeg} from '../../MatrixClientPeg'; +import {MatrixClientPeg, MatrixClientCreds} 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'; @@ -40,6 +39,8 @@ 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"; +import ResizeNotifier from "../../utils/ResizeNotifier"; // 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. @@ -52,6 +53,52 @@ function canElementReceiveInput(el) { !!el.getAttribute("contenteditable"); } +interface IProps { + matrixClient: MatrixClient; + onRegistered: (credentials: MatrixClientCreds) => Promise; + viaServers?: string[]; + hideToSRUsers: boolean; + resizeNotifier: ResizeNotifier; + middleDisabled: boolean; + initialEventPixelOffset: number; + leftDisabled: boolean; + rightDisabled: boolean; + showCookieBar: boolean; + hasNewVersion: boolean; + userHasGeneratedPassword: boolean; + showNotifierToolbar: boolean; + page_type: string; + autoJoin: boolean; + thirdPartyInvite?: object; + roomOobData?: object; + currentRoomId: string; + ConferenceHandler?: object; + collapseLhs: boolean; + checkingForUpdate: boolean; + config: { + piwik: { + policyUrl: string; + }, + [key: string]: any, + }; + currentUserId?: string; + currentGroupId?: string; + currentGroupIsNew?: boolean; + version?: string; + newVersion?: string; + newVersionReleaseNotes?: string; +} +interface IState { + mouseDown?: { + x: number; + y: number; + }; + syncErrorData: any; + useCompactLayout: boolean; + serverNoticeEvents: MatrixEvent[]; + userHasGeneratedPassword: boolean; +} + /** * This is what our MatrixChat shows when we are logged in. The precise view is * determined by the page_type property. @@ -61,10 +108,10 @@ function canElementReceiveInput(el) { * * Components mounted below us can access the matrix client via the react context. */ -const LoggedInView = createReactClass({ - displayName: 'LoggedInView', +class LoggedInView extends React.PureComponent { + static displayName = 'LoggedInView'; - propTypes: { + static propTypes = { matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, page_type: PropTypes.string.isRequired, onRoomCreated: PropTypes.func, @@ -77,24 +124,28 @@ const LoggedInView = createReactClass({ viaServers: PropTypes.arrayOf(PropTypes.string), // and lots and lots of other stuff. - }, + }; - getInitialState: function() { - return { + protected readonly _matrixClient: MatrixClient; + protected readonly _roomView: React.RefObject; + protected readonly _resizeContainer: React.RefObject; + protected readonly _sessionStore: sessionStore; + protected readonly _sessionStoreToken: { remove: () => void }; + protected resizer: Resizer; + + constructor(props, context) { + super(props, context); + + this.state = { + mouseDown: undefined, + syncErrorData: undefined, + userHasGeneratedPassword: false, // use compact timeline view useCompactLayout: SettingsStore.getValue('useCompactLayout'), // any currently active server notice events serverNoticeEvents: [], }; - }, - componentDidMount: function() { - this.resizer = this._createResizer(); - this.resizer.attach(); - this._loadResizerPreferences(); - }, - - componentWillMount: function() { // stash the MatrixClient in case we log out before we are unmounted this._matrixClient = this.props.matrixClient; @@ -116,22 +167,29 @@ const LoggedInView = createReactClass({ fixupColorFonts(); - this._roomView = createRef(); - }, + this._roomView = React.createRef(); + this._resizeContainer = React.createRef(); + } - componentDidUpdate(prevProps) { + componentDidMount() { + this.resizer = this._createResizer(); + this.resizer.attach(); + this._loadResizerPreferences(); + } + + componentDidUpdate(prevProps, prevState) { // attempt to guess when a banner was opened or closed if ( (prevProps.showCookieBar !== this.props.showCookieBar) || (prevProps.hasNewVersion !== this.props.hasNewVersion) || - (prevProps.userHasGeneratedPassword !== this.props.userHasGeneratedPassword) || + (prevState.userHasGeneratedPassword !== this.state.userHasGeneratedPassword) || (prevProps.showNotifierToolbar !== this.props.showNotifierToolbar) ) { this.props.resizeNotifier.notifyBannersChanged(); } - }, + } - componentWillUnmount: function() { + componentWillUnmount() { document.removeEventListener('keydown', this._onNativeKeyDown, false); this._matrixClient.removeListener("accountData", this.onAccountData); this._matrixClient.removeListener("sync", this.onSync); @@ -140,7 +198,7 @@ const LoggedInView = createReactClass({ this._sessionStoreToken.remove(); } this.resizer.detach(); - }, + } // Child components assume that the client peg will not be null, so give them some // sort of assurance here by only allowing a re-render if the client is truthy. @@ -148,22 +206,22 @@ const LoggedInView = createReactClass({ // This is required because `LoggedInView` maintains its own state and if this state // updates after the client peg has been made null (during logout), then it will // attempt to re-render and the children will throw errors. - shouldComponentUpdate: function() { + shouldComponentUpdate() { return Boolean(MatrixClientPeg.get()); - }, + } - canResetTimelineInRoom: function(roomId) { + canResetTimelineInRoom = (roomId) => { if (!this._roomView.current) { return true; } return this._roomView.current.canResetTimeline(); - }, + }; - _setStateFromSessionStore() { + _setStateFromSessionStore = () => { this.setState({ userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()), }); - }, + }; _createResizer() { const classNames = { @@ -187,24 +245,22 @@ const LoggedInView = createReactClass({ }, }; const resizer = new Resizer( - this.resizeContainer, + this._resizeContainer.current, CollapseDistributor, collapseConfig); resizer.setClassNames(classNames); return resizer; - }, + } _loadResizerPreferences() { - let lhsSize = window.localStorage.getItem("mx_lhs_size"); - if (lhsSize !== null) { - lhsSize = parseInt(lhsSize, 10); - } else { + let lhsSize = parseInt(window.localStorage.getItem("mx_lhs_size"), 10); + if (isNaN(lhsSize)) { lhsSize = 350; } this.resizer.forHandleAt(0).resize(lhsSize); - }, + } - onAccountData: function(event) { + onAccountData = (event) => { if (event.getType() === "im.vector.web.settings") { this.setState({ useCompactLayout: event.getContent().useCompactLayout, @@ -213,9 +269,9 @@ const LoggedInView = createReactClass({ if (event.getType() === "m.ignored_user_list") { dis.dispatch({action: "ignore_state_changed"}); } - }, + }; - onSync: function(syncState, oldSyncState, data) { + onSync = (syncState, oldSyncState, data) => { const oldErrCode = ( this.state.syncErrorData && this.state.syncErrorData.error && @@ -237,16 +293,16 @@ const LoggedInView = createReactClass({ if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') { this._updateServerNoticeEvents(); } - }, + }; - onRoomStateEvents: function(ev, state) { + onRoomStateEvents = (ev, state) => { const roomLists = RoomListStore.getRoomLists(); if (roomLists['m.server_notice'] && roomLists['m.server_notice'].some(r => r.roomId === ev.getRoomId())) { this._updateServerNoticeEvents(); } - }, + }; - _updateServerNoticeEvents: async function() { + _updateServerNoticeEvents = async () => { const roomLists = RoomListStore.getRoomLists(); if (!roomLists['m.server_notice']) return []; @@ -259,16 +315,16 @@ const LoggedInView = createReactClass({ const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM); for (const eventId of pinnedEventIds) { const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0); - const ev = timeline.getEvents().find(ev => ev.getId() === eventId); - if (ev) pinnedEvents.push(ev); + const event = timeline.getEvents().find(ev => ev.getId() === eventId); + if (event) pinnedEvents.push(event); } } this.setState({ serverNoticeEvents: pinnedEvents, }); - }, + }; - _onPaste: function(ev) { + _onPaste = (ev) => { let canReceiveInput = false; let element = ev.target; // test for all parents because the target can be a child of a contenteditable element @@ -282,7 +338,7 @@ const LoggedInView = createReactClass({ // so dispatch synchronously before paste happens dis.dispatch({action: 'focus_composer'}, true); } - }, + }; /* SOME HACKERY BELOW: @@ -306,22 +362,22 @@ const LoggedInView = createReactClass({ We also listen with a native listener on the document to get keydown events when no element is focused. Bubbling is irrelevant here as the target is the body element. */ - _onReactKeyDown: function(ev) { + _onReactKeyDown = (ev) => { // events caught while bubbling up on the root element // of this component, so something must be focused. this._onKeyDown(ev); - }, + }; - _onNativeKeyDown: function(ev) { + _onNativeKeyDown = (ev) => { // only pass this if there is no focused element. // if there is, _onKeyDown will be called by the // react keydown handler that respects the react bubbling order. if (ev.target === document.body) { this._onKeyDown(ev); } - }, + }; - _onKeyDown: function(ev) { + _onKeyDown = (ev) => { /* // Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers // Will need to find a better meta key if anyone actually cares about using this. @@ -380,7 +436,7 @@ const LoggedInView = createReactClass({ break; case Key.SLASH: - if (ctrlCmdOnly) { + if (isOnlyCtrlOrCmdIgnoreShiftKeyEvent(ev)) { KeyboardShortcuts.toggleDialog(); handled = true; } @@ -431,19 +487,19 @@ const LoggedInView = createReactClass({ // that would prevent typing in the now-focussed composer } } - }, + }; /** * dispatch a page-up/page-down/etc to the appropriate component * @param {Object} ev The key event */ - _onScrollKeyPressed: function(ev) { + _onScrollKeyPressed = (ev) => { if (this._roomView.current) { this._roomView.current.handleScrollKey(ev); } - }, + }; - _onDragEnd: function(result) { + _onDragEnd = (result) => { // Dragged to an invalid destination, not onto a droppable if (!result.destination) { return; @@ -466,9 +522,9 @@ const LoggedInView = createReactClass({ } else if (dest.startsWith('room-sub-list-droppable_')) { this._onRoomTileEndDrag(result); } - }, + }; - _onRoomTileEndDrag: function(result) { + _onRoomTileEndDrag = (result) => { let newTag = result.destination.droppableId.split('_')[1]; let prevTag = result.source.droppableId.split('_')[1]; if (newTag === 'undefined') newTag = undefined; @@ -485,9 +541,9 @@ const LoggedInView = createReactClass({ prevTag, newTag, oldIndex, newIndex, ), true); - }, + }; - _onMouseDown: function(ev) { + _onMouseDown = (ev) => { // When the panels are disabled, clicking on them results in a mouse event // which bubbles to certain elements in the tree. When this happens, close // any settings page that is currently open (user/room/group). @@ -506,9 +562,9 @@ const LoggedInView = createReactClass({ }); } } - }, + }; - _onMouseUp: function(ev) { + _onMouseUp = (ev) => { if (!this.state.mouseDown) return; const deltaX = ev.pageX - this.state.mouseDown.x; @@ -527,17 +583,12 @@ const LoggedInView = createReactClass({ // Always clear the mouseDown state to ensure we don't accidentally // use stale values due to the mouseDown checks. this.setState({mouseDown: null}); - }, + }; - _setResizeContainerRef(div) { - this.resizeContainer = div; - }, - - render: function() { + render() { 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'); @@ -576,13 +627,7 @@ const LoggedInView = createReactClass({ break; case PageTypes.HomePage: - { - const pageUrl = getHomePageUrl(this.props.config); - pageElement = ; - } + pageElement = ; break; case PageTypes.UserView: @@ -653,7 +698,7 @@ const LoggedInView = createReactClass({ { topBar } -
+
); - }, -}); + } +} export default LoggedInView; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 52002f0591..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'); @@ -657,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, @@ -1520,7 +1521,7 @@ export default createReactClass({ } else if (request.pending) { ToastStore.sharedInstance().addOrReplaceToast({ key: 'verifreq_' + request.channel.transactionId, - title: _t("Verification Request"), + title: request.isSelfVerification ? _t("Self-verification request") : _t("Verification Request"), icon: "verification", props: {request}, component: sdk.getComponent("toasts.VerificationRequestToast"), @@ -1906,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); + } } } @@ -2021,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 4fd57d95e6..c3a2bdbc59 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -838,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())) { @@ -863,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; } @@ -950,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; @@ -971,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 f1209b7b9e..f179cab6ad 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -38,7 +38,7 @@ export default createReactClass({ contextType: MatrixClientContext, }, - componentWillMount: function() { + componentDidMount: function() { this._fetch(); }, diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index 8d25116827..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); diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 664aaaf21f..0b07c10c8a 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -56,7 +56,8 @@ export default createReactClass({ }; }, - componentWillMount: function() { + // TODO: [REACT-WARNING] Move this to constructor + UNSAFE_componentWillMount: function() { this._unmounted = false; this.nextBatch = null; this.filterTimeout = null; @@ -89,9 +90,7 @@ export default createReactClass({ ), }); }); - }, - componentDidMount: function() { this.refreshRoomList(); }, 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 9428de3e22..2ae2d71100 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -126,7 +126,7 @@ export default class RoomSubList extends React.PureComponent { break; case 'view_room': - if (this.state.hidden && !this.props.forceExpand && + if (this.state.hidden && !this.props.forceExpand && payload.show_room_tile && this.props.list.some((r) => r.roomId === payload.room_id) ) { this.toggle(); @@ -193,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 6454136164..ff35f56e9a 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -55,6 +55,7 @@ import RightPanelStore from "../../stores/RightPanelStore"; import {haveTileForEvent} from "../views/rooms/EventTile"; import RoomContext from "../../contexts/RoomContext"; import MatrixClientContext from "../../contexts/MatrixClientContext"; +import { shieldStatusForRoom } from '../../utils/ShieldUtils'; const DEBUG = false; let debuglog = function() {}; @@ -167,7 +168,8 @@ 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); this.context.on("Room", this.onRoom); this.context.on("Room.timeline", this.onRoomTimeline); @@ -180,6 +182,7 @@ export default createReactClass({ 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); @@ -235,6 +238,11 @@ export default createReactClass({ 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:', @@ -466,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(); @@ -493,6 +505,7 @@ export default createReactClass({ 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); @@ -814,6 +827,13 @@ export default createReactClass({ this._updateE2EStatus(room); }, + onCrossSigningKeysChanged: function() { + const room = this.state.room; + if (room) { + this._updateE2EStatus(room); + } + }, + _updateE2EStatus: async function(room) { if (!this.context.isRoomEncrypted(room.roomId)) { return; @@ -837,40 +857,9 @@ export default createReactClass({ return; } - // Duplication between here and _updateE2eStatus in RoomTile /* At this point, the user has encryption on and cross-signing on */ - const e2eMembers = await room.getEncryptionTargetMembers(); - const verified = []; - const unverified = []; - e2eMembers.map(({userId}) => userId) - .filter((userId) => userId !== this.context.getUserId()) - .forEach((userId) => { - (this.context.checkUserTrust(userId).isCrossSigningVerified() ? - verified : unverified).push(userId); - }); - - debuglog("e2e verified", verified, "unverified", unverified); - - /* Check all verified user devices. */ - /* Don't alarm if no other users are verified */ - const targets = (verified.length > 0) ? [...verified, this.context.getUserId()] : verified; - for (const userId of targets) { - const devices = await this.context.getStoredDevicesForUser(userId); - const anyDeviceNotVerified = devices.some(({deviceId}) => { - return !this.context.checkDeviceTrust(userId, deviceId).isVerified(); - }); - if (anyDeviceNotVerified) { - this.setState({ - e2eStatus: "warning", - }); - debuglog("e2e status set to warning as not all users trust all of their sessions." + - " Aborted on user", userId); - return; - } - } - this.setState({ - e2eStatus: unverified.length === 0 ? "verified" : "normal", + e2eStatus: await shieldStatusForRoom(this.context, room), }); }, @@ -1243,7 +1232,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 :(")), diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index c218fee5d6..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) { 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.tsx b/src/components/structures/TabbedView.tsx index ea485acc1a..c0e0e58db8 100644 --- a/src/components/structures/TabbedView.tsx +++ b/src/components/structures/TabbedView.tsx @@ -20,6 +20,7 @@ import * as React from "react"; import {_t} from '../../languageHandler'; import * as PropTypes from "prop-types"; import * as sdk from "../../index"; +import AutoHideScrollbar from './AutoHideScrollbar'; import { ReactNode } from "react"; /** @@ -113,9 +114,9 @@ export default class TabbedView extends React.Component { private _renderTabPanel(tab: Tab): React.ReactNode { return (
-
+ {tab.body} -
+
); } diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index f1a39d6fcf..6642cce098 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -44,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); 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 3154564cd3..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,232 +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); - } - } - - _onUsePassphraseClick = async () => { - this.setState({ - phase: PHASE_BUSY, - }); - const cli = MatrixClientPeg.get(); - try { - const backupInfo = await cli.getKeyBackupVersion(); - this.setState({backupInfo}); - - // The control flow is fairly twisted here... - // For the purposes of completing security, we only wait on getting - // as far as the trust check and then show a green shield. - // We also begin the key backup restore as well, which we're - // awaiting inside `accessSecretStorage` only so that it keeps your - // passphase cached for that work. This dialog itself will only wait - // on the first trust check, and the key backup restore will happen - // in the background. - await new Promise((resolve, reject) => { - try { - accessSecretStorage(async () => { - await cli.checkOwnCrossSigningTrust(); - resolve(); - if (backupInfo) { - // A complete restore can take many minutes for large - // accounts / slow servers, so we allow the dialog - // to advance before this. - await cli.restoreKeyBackupWithSecretStorage(backupInfo); - } - }); - } catch (e) { - console.error(e); - reject(e); - } - }); - - 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) { - const InlineSpinner = sdk.getComponent('elements.InlineSpinner'); + if (phase === PHASE_INTRO) { icon = ; title = _t("Complete security"); - body = ( -
-

{_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) { 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}`); } @@ -271,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/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/ReportEventDialog.js b/src/components/views/dialogs/ReportEventDialog.js index 99853582dd..f5509dec4d 100644 --- a/src/components/views/dialogs/ReportEventDialog.js +++ b/src/components/views/dialogs/ReportEventDialog.js @@ -123,7 +123,6 @@ export default class ReportEventDialog extends PureComponent {

{adminMessage} { @@ -72,7 +72,7 @@ export default class RoomSettingsDialog extends React.Component { )); tabs.push(new Tab( _td("Notifications"), - "mx_RoomSettingsDialog_rolesIcon", + "mx_RoomSettingsDialog_notificationsIcon", , )); diff --git a/src/components/views/dialogs/RoomUpgradeDialog.js b/src/components/views/dialogs/RoomUpgradeDialog.js index dc734718d5..c45d82303b 100644 --- a/src/components/views/dialogs/RoomUpgradeDialog.js +++ b/src/components/views/dialogs/RoomUpgradeDialog.js @@ -30,7 +30,7 @@ export default createReactClass({ onFinished: PropTypes.func.isRequired, }, - componentWillMount: async function() { + componentDidMount: async function() { const recommended = await this.props.room.getRecommendedVersion(); this._targetVersion = recommended.version; this.setState({busy: false}); diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index 611ea64e49..f99d065e7e 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -62,6 +62,7 @@ export default createReactClass({ }; }, + // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs UNSAFE_componentWillMount: function() { this._input_value = createRef(); this._uiAuth = createRef(); diff --git a/src/components/views/dialogs/SetPasswordDialog.js b/src/components/views/dialogs/SetPasswordDialog.js index c48690bb48..fcc6e67656 100644 --- a/src/components/views/dialogs/SetPasswordDialog.js +++ b/src/components/views/dialogs/SetPasswordDialog.js @@ -75,8 +75,8 @@ export default createReactClass({ }; }, - componentWillMount: function() { - console.info('SetPasswordDialog component will mount'); + componentDidMount: function() { + console.info('SetPasswordDialog component did mount'); }, _onPasswordChanged: function(res) { diff --git a/src/components/views/dialogs/SetupEncryptionDialog.js b/src/components/views/dialogs/SetupEncryptionDialog.js new file mode 100644 index 0000000000..f32a289a29 --- /dev/null +++ b/src/components/views/dialogs/SetupEncryptionDialog.js @@ -0,0 +1,29 @@ +/* +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 SetupEncryptionBody from '../../structures/auth/SetupEncryptionBody'; +import BaseDialog from './BaseDialog'; +import { _t } from '../../../languageHandler'; + +export default function SetupEncryptionDialog({onFinished}) { + return + + ; +} diff --git a/src/components/views/dialogs/ShareDialog.js b/src/components/views/dialogs/ShareDialog.js index b42a88ceac..1bc9decd39 100644 --- a/src/components/views/dialogs/ShareDialog.js +++ b/src/components/views/dialogs/ShareDialog.js @@ -70,9 +70,16 @@ export default class ShareDialog extends React.Component { this.onCopyClick = this.onCopyClick.bind(this); this.onLinkSpecificEventCheckboxClick = this.onLinkSpecificEventCheckboxClick.bind(this); + let permalinkCreator: RoomPermalinkCreator = null; + if (props.target instanceof Room) { + permalinkCreator = new RoomPermalinkCreator(props.target); + permalinkCreator.load(); + } + this.state = { // MatrixEvent defaults to share linkSpecificEvent linkSpecificEvent: this.props.target instanceof MatrixEvent, + permalinkCreator, }; this._link = createRef(); @@ -121,14 +128,6 @@ export default class ShareDialog extends React.Component { }); } - componentWillMount() { - if (this.props.target instanceof Room) { - const permalinkCreator = new RoomPermalinkCreator(this.props.target); - permalinkCreator.load(); - this.setState({permalinkCreator}); - } - } - componentWillUnmount() { // if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close // the tooltip otherwise, such as pressing Escape or clicking X really quickly diff --git a/src/components/views/dialogs/SlashCommandHelpDialog.js b/src/components/views/dialogs/SlashCommandHelpDialog.js index 9e48a92ed1..bae5b37993 100644 --- a/src/components/views/dialogs/SlashCommandHelpDialog.js +++ b/src/components/views/dialogs/SlashCommandHelpDialog.js @@ -16,14 +16,14 @@ limitations under the License. import React from 'react'; import {_t} from "../../../languageHandler"; -import {CommandCategories, CommandMap} from "../../../SlashCommands"; +import {CommandCategories, Commands} from "../../../SlashCommands"; import * as sdk from "../../../index"; export default ({onFinished}) => { const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); const categories = {}; - Object.values(CommandMap).forEach(cmd => { + Commands.forEach(cmd => { if (!categories[cmd.category]) { categories[cmd.category] = []; } @@ -41,7 +41,7 @@ export default ({onFinished}) => { categories[category].forEach(cmd => { rows.push( - {cmd.command} + {cmd.getCommand()} {cmd.args} {cmd.description} ); diff --git a/src/components/views/dialogs/TextInputDialog.js b/src/components/views/dialogs/TextInputDialog.js index b9f6f6ebce..d7ca3f144d 100644 --- a/src/components/views/dialogs/TextInputDialog.js +++ b/src/components/views/dialogs/TextInputDialog.js @@ -55,6 +55,7 @@ export default createReactClass({ }; }, + // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs UNSAFE_componentWillMount: function() { this._field = createRef(); }, @@ -116,7 +117,6 @@ export default createReactClass({
{ + this.setState({verificationRequest: r}); + }); + } } render() { const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog"); const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel"); + const request = this.state.verificationRequest; + const otherUserId = request && request.otherUserId; const member = this.props.member || - MatrixClientPeg.get().getUser(this.props.verificationRequest.otherUserId); + otherUserId && MatrixClientPeg.get().getUser(otherUserId); + const title = request && request.isSelfVerification ? + _t("Verify this session") : _t("Verification Request"); + return +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. @@ -41,12 +42,30 @@ import PersistedElement from "./PersistedElement"; const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:']; const ENABLE_REACT_PERF = false; +/** + * Does template substitution on a URL (or any string). Variables will be + * passed through encodeURIComponent. + * @param {string} uriTemplate The path with template variables e.g. '/foo/$bar'. + * @param {Object} variables The key/value pairs to replace the template + * variables with. E.g. { '$bar': 'baz' }. + * @return {string} The result of replacing all template variables e.g. '/foo/baz'. + */ +function uriFromTemplate(uriTemplate, variables) { + let out = uriTemplate; + for (const [key, val] of Object.entries(variables)) { + out = out.replace( + '$' + key, encodeURIComponent(val), + ); + } + return out; +} + export default class AppTile extends React.Component { constructor(props) { super(props); // The key used for PersistedElement - this._persistKey = 'widget_' + this.props.id; + this._persistKey = 'widget_' + this.props.app.id; this.state = this._getNewState(props); @@ -78,7 +97,7 @@ export default class AppTile extends React.Component { // This is a function to make the impact of calling SettingsStore slightly less const hasPermissionToLoad = () => { const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", newProps.room.roomId); - return !!currentlyAllowedWidgets[newProps.eventId]; + return !!currentlyAllowedWidgets[newProps.app.eventId]; }; const PersistedElement = sdk.getComponent("elements.PersistedElement"); @@ -86,7 +105,7 @@ export default class AppTile extends React.Component { initialising: true, // True while we are mangling the widget URL // True while the iframe content is loading loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey), - widgetUrl: this._addWurlParams(newProps.url), + widgetUrl: this._addWurlParams(newProps.app.url), // Assume that widget has permission to load if we are the user who // added it to the room, or if explicitly granted by the user hasPermissionToLoad: newProps.userId === newProps.creatorUserId || hasPermissionToLoad(), @@ -103,7 +122,7 @@ export default class AppTile extends React.Component { * @return {Boolean} True if capability supported */ _hasCapability(capability) { - return ActiveWidgetStore.widgetHasCapability(this.props.id, capability); + return ActiveWidgetStore.widgetHasCapability(this.props.app.id, capability); } /** @@ -117,55 +136,52 @@ export default class AppTile extends React.Component { * If url can not be parsed, it is returned unmodified. */ _addWurlParams(urlString) { - const u = url.parse(urlString); - if (!u) { - console.error("_addWurlParams", "Invalid URL", urlString); - return url; + try { + const parsed = new URL(urlString); + + // TODO: Replace these with proper widget params + // See https://github.com/matrix-org/matrix-doc/pull/1958/files#r405714833 + parsed.searchParams.set('widgetId', this.props.app.id); + parsed.searchParams.set('parentUrl', window.location.href.split('#', 2)[0]); + + // Replace the encoded dollar signs back to dollar signs. They have no special meaning + // in HTTP, but URL parsers encode them anyways. + return parsed.toString().replace(/%24/g, '$'); + } catch (e) { + console.error("Failed to add widget URL params:", e); + return urlString; } - - const params = qs.parse(u.query); - // Append widget ID to query parameters - params.widgetId = this.props.id; - // Append current / parent URL, minus the hash because that will change when - // we view a different room (ie. may change for persistent widgets) - params.parentUrl = window.location.href.split('#', 2)[0]; - u.search = undefined; - u.query = params; - - return u.format(); } isMixedContent() { const parentContentProtocol = window.location.protocol; - const u = url.parse(this.props.url); + const u = url.parse(this.props.app.url); const childContentProtocol = u.protocol; if (parentContentProtocol === 'https:' && childContentProtocol !== 'https:') { console.warn("Refusing to load mixed-content app:", - parentContentProtocol, childContentProtocol, window.location, this.props.url); + parentContentProtocol, childContentProtocol, window.location, this.props.app.url); return true; } return false; } - componentWillMount() { + componentDidMount() { // Only fetch IM token on mount if we're showing and have permission to load if (this.props.show && this.state.hasPermissionToLoad) { this.setScalarToken(); } - } - componentDidMount() { // Widget action listeners this.dispatcherRef = dis.register(this._onAction); } componentWillUnmount() { // Widget action listeners - dis.unregister(this.dispatcherRef); + if (this.dispatcherRef) dis.unregister(this.dispatcherRef); // if it's not remaining on screen, get rid of the PersistedElement container - if (!ActiveWidgetStore.getWidgetPersistence(this.props.id)) { - ActiveWidgetStore.destroyPersistentWidget(this.props.id); + if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) { + ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); const PersistedElement = sdk.getComponent("elements.PersistedElement"); PersistedElement.destroyElement(this._persistKey); } @@ -176,11 +192,11 @@ export default class AppTile extends React.Component { * Component initialisation is only complete when this function has resolved */ setScalarToken() { - if (!WidgetUtils.isScalarUrl(this.props.url)) { + if (!WidgetUtils.isScalarUrl(this.props.app.url)) { console.warn('Non-scalar widget, not setting scalar token!', url); this.setState({ error: null, - widgetUrl: this._addWurlParams(this.props.url), + widgetUrl: this._addWurlParams(this.props.app.url), initialising: false, }); return; @@ -191,7 +207,7 @@ export default class AppTile extends React.Component { console.warn("No integration manager - not setting scalar token", url); this.setState({ error: null, - widgetUrl: this._addWurlParams(this.props.url), + widgetUrl: this._addWurlParams(this.props.app.url), initialising: false, }); return; @@ -204,7 +220,7 @@ export default class AppTile extends React.Component { console.warn('Non-scalar manager, not setting scalar token!', url); this.setState({ error: null, - widgetUrl: this._addWurlParams(this.props.url), + widgetUrl: this._addWurlParams(this.props.app.url), initialising: false, }); return; @@ -217,7 +233,7 @@ export default class AppTile extends React.Component { this._scalarClient.getScalarToken().then((token) => { // Append scalar_token as a query param if not already present this._scalarClient.scalarToken = token; - const u = url.parse(this._addWurlParams(this.props.url)); + const u = url.parse(this._addWurlParams(this.props.app.url)); const params = qs.parse(u.query); if (!params.scalar_token) { params.scalar_token = encodeURIComponent(token); @@ -245,14 +261,17 @@ export default class AppTile extends React.Component { }); } - componentWillReceiveProps(nextProps) { - if (nextProps.url !== this.props.url) { + // TODO: [REACT-WARNING] Replace with appropriate lifecycle event + UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase + if (nextProps.app.url !== this.props.app.url) { this._getNewState(nextProps); // Fetch IM token for new URL if we're showing and have permission to load if (this.props.show && this.state.hasPermissionToLoad) { this.setScalarToken(); } - } else if (nextProps.show && !this.props.show) { + } + + if (nextProps.show && !this.props.show) { // We assume that persisted widgets are loaded and don't need a spinner. if (this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey)) { this.setState({ @@ -263,7 +282,9 @@ export default class AppTile extends React.Component { if (this.state.hasPermissionToLoad) { this.setScalarToken(); } - } else if (nextProps.widgetPageTitle !== this.props.widgetPageTitle) { + } + + if (nextProps.widgetPageTitle !== this.props.widgetPageTitle) { this.setState({ widgetPageTitle: nextProps.widgetPageTitle, }); @@ -280,7 +301,7 @@ export default class AppTile extends React.Component { } _onEditClick() { - console.log("Edit widget ID ", this.props.id); + console.log("Edit widget ID ", this.props.app.id); if (this.props.onEditClick) { this.props.onEditClick(); } else { @@ -289,21 +310,21 @@ export default class AppTile extends React.Component { IntegrationManagers.sharedInstance().openAll( this.props.room, 'type_' + this.props.type, - this.props.id, + this.props.app.id, ); } else { IntegrationManagers.sharedInstance().getPrimaryManager().open( this.props.room, 'type_' + this.props.type, - this.props.id, + this.props.app.id, ); } } } _onSnapshotClick() { - console.warn("Requesting widget snapshot"); - ActiveWidgetStore.getWidgetMessaging(this.props.id).getScreenshot() + console.log("Requesting widget snapshot"); + ActiveWidgetStore.getWidgetMessaging(this.props.app.id).getScreenshot() .catch((err) => { console.error("Failed to get screenshot", err); }) @@ -315,6 +336,28 @@ export default class AppTile extends React.Component { }); } + /** + * Ends all widget interaction, such as cancelling calls and disabling webcams. + * @private + */ + _endWidgetActions() { + // HACK: This is a really dirty way to ensure that Jitsi cleans up + // its hold on the webcam. Without this, the widget holds a media + // stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351 + if (this._appFrame.current) { + // In practice we could just do `+= ''` to trick the browser + // into thinking the URL changed, however I can foresee this + // being optimized out by a browser. Instead, we'll just point + // the iframe at a page that is reasonably safe to use in the + // event the iframe doesn't wink away. + // This is relative to where the Riot instance is located. + this._appFrame.current.src = 'about:blank'; + } + + // Delete the widget from the persisted store for good measure. + PersistedElement.destroyElement(this._persistKey); + } + /* If user has permission to modify widgets, delete the widget, * otherwise revoke access for the widget to load in the user's browser */ @@ -336,22 +379,11 @@ export default class AppTile extends React.Component { } this.setState({deleting: true}); - // HACK: This is a really dirty way to ensure that Jitsi cleans up - // its hold on the webcam. Without this, the widget holds a media - // stream open, even after death. See https://github.com/vector-im/riot-web/issues/7351 - if (this._appFrame.current) { - // In practice we could just do `+= ''` to trick the browser - // into thinking the URL changed, however I can foresee this - // being optimized out by a browser. Instead, we'll just point - // the iframe at a page that is reasonably safe to use in the - // event the iframe doesn't wink away. - // This is relative to where the Riot instance is located. - this._appFrame.current.src = 'about:blank'; - } + this._endWidgetActions(); WidgetUtils.setRoomWidget( this.props.room.roomId, - this.props.id, + this.props.app.id, ).catch((e) => { console.error('Failed to delete widget', e); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -369,7 +401,7 @@ export default class AppTile extends React.Component { } _onRevokeClicked() { - console.info("Revoke widget permissions - %s", this.props.id); + console.info("Revoke widget permissions - %s", this.props.app.id); this._revokeWidgetPermission(); } @@ -380,10 +412,10 @@ export default class AppTile extends React.Component { // Destroy the old widget messaging before starting it back up again. Some widgets // have startup routines that run when they are loaded, so we just need to reinitialize // the messaging for them. - ActiveWidgetStore.delWidgetMessaging(this.props.id); + ActiveWidgetStore.delWidgetMessaging(this.props.app.id); this._setupWidgetMessaging(); - ActiveWidgetStore.setRoomId(this.props.id, this.props.room.roomId); + ActiveWidgetStore.setRoomId(this.props.app.id, this.props.room.roomId); this.setState({loading: false}); } @@ -391,10 +423,10 @@ export default class AppTile extends React.Component { // FIXME: There's probably no reason to do this here: it should probably be done entirely // in ActiveWidgetStore. const widgetMessaging = new WidgetMessaging( - this.props.id, this.props.url, this.props.userWidget, this._appFrame.current.contentWindow); - ActiveWidgetStore.setWidgetMessaging(this.props.id, widgetMessaging); + this.props.app.id, this._getRenderedUrl(), this.props.userWidget, this._appFrame.current.contentWindow); + ActiveWidgetStore.setWidgetMessaging(this.props.app.id, widgetMessaging); widgetMessaging.getCapabilities().then((requestedCapabilities) => { - console.log(`Widget ${this.props.id} requested capabilities: ` + requestedCapabilities); + console.log(`Widget ${this.props.app.id} requested capabilities: ` + requestedCapabilities); requestedCapabilities = requestedCapabilities || []; // Allow whitelisted capabilities @@ -406,7 +438,7 @@ export default class AppTile extends React.Component { }, this.props.whitelistCapabilities); if (requestedWhitelistCapabilies.length > 0 ) { - console.warn(`Widget ${this.props.id} allowing requested, whitelisted properties: ` + + console.log(`Widget ${this.props.app.id} allowing requested, whitelisted properties: ` + requestedWhitelistCapabilies, ); } @@ -414,18 +446,24 @@ export default class AppTile extends React.Component { // TODO -- Add UI to warn about and optionally allow requested capabilities - ActiveWidgetStore.setWidgetCapabilities(this.props.id, requestedWhitelistCapabilies); + ActiveWidgetStore.setWidgetCapabilities(this.props.app.id, requestedWhitelistCapabilies); if (this.props.onCapabilityRequest) { this.props.onCapabilityRequest(requestedCapabilities); } + + // We only tell Jitsi widgets that we're ready because they're realistically the only ones + // using this custom extension to the widget API. + if (this.props.app.type === 'jitsi') { + widgetMessaging.flagReadyToContinue(); + } }).catch((err) => { - console.log(`Failed to get capabilities for widget type ${this.props.type}`, this.props.id, err); + console.log(`Failed to get capabilities for widget type ${this.props.app.type}`, this.props.app.id, err); }); } _onAction(payload) { - if (payload.widgetId === this.props.id) { + if (payload.widgetId === this.props.app.id) { switch (payload.action) { case 'm.sticker': if (this._hasCapability('m.sticker')) { @@ -454,9 +492,9 @@ export default class AppTile extends React.Component { _grantWidgetPermission() { const roomId = this.props.room.roomId; - console.info("Granting permission for widget to load: " + this.props.eventId); + console.info("Granting permission for widget to load: " + this.props.app.eventId); const current = SettingsStore.getValue("allowedWidgets", roomId); - current[this.props.eventId] = true; + current[this.props.app.eventId] = true; SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).then(() => { this.setState({hasPermissionToLoad: true}); @@ -470,14 +508,14 @@ export default class AppTile extends React.Component { _revokeWidgetPermission() { const roomId = this.props.room.roomId; - console.info("Revoking permission for widget to load: " + this.props.eventId); + console.info("Revoking permission for widget to load: " + this.props.app.eventId); const current = SettingsStore.getValue("allowedWidgets", roomId); - current[this.props.eventId] = false; + current[this.props.app.eventId] = false; SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).then(() => { this.setState({hasPermissionToLoad: false}); // Force the widget to be non-persistent (able to be deleted/forgotten) - ActiveWidgetStore.destroyPersistentWidget(this.props.id); + ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); const PersistedElement = sdk.getComponent("elements.PersistedElement"); PersistedElement.destroyElement(this._persistKey); }).catch(err => { @@ -488,8 +526,8 @@ export default class AppTile extends React.Component { formatAppTileName() { let appTileName = "No name"; - if (this.props.name && this.props.name.trim()) { - appTileName = this.props.name.trim(); + if (this.props.app.name && this.props.app.name.trim()) { + appTileName = this.props.app.name.trim(); } return appTileName; } @@ -506,6 +544,10 @@ export default class AppTile extends React.Component { if (this.props.userWidget) { this._onMinimiseClick(); } else { + if (this.props.show) { + // if we were being shown, end the widget as we're about to be minimized. + this._endWidgetActions(); + } dis.dispatch({ action: 'appsDrawer', show: !this.props.show, @@ -513,20 +555,78 @@ export default class AppTile extends React.Component { } } - _getSafeUrl() { - const parsedWidgetUrl = url.parse(this.state.widgetUrl, true); + /** + * Replace the widget template variables in a url with their values + * + * @param {string} u The URL with template variables + * + * @returns {string} url with temlate variables replaced + */ + _templatedUrl(u) { + const myUserId = MatrixClientPeg.get().credentials.userId; + const myUser = MatrixClientPeg.get().getUser(myUserId); + const vars = Object.assign({ + domain: "jitsi.riot.im", // v1 widgets have this hardcoded + }, this.props.app.data, { + 'matrix_user_id': myUserId, + 'matrix_room_id': this.props.room.roomId, + 'matrix_display_name': myUser ? myUser.displayName : myUserId, + 'matrix_avatar_url': myUser ? MatrixClientPeg.get().mxcUrlToHttp(myUser.avatarUrl) : '', + + // TODO: Namespace themes through some standard + 'theme': SettingsStore.getValue("theme"), + }); + + if (vars.conferenceId === undefined) { + // we'll need to parse the conference ID out of the URL for v1 Jitsi widgets + const parsedUrl = new URL(this.props.app.url); + vars.conferenceId = parsedUrl.searchParams.get("confId"); + } + + return uriFromTemplate(u, vars); + } + + /** + * Get the URL used in the iframe + * In cases where we supply our own UI for a widget, this is an internal + * URL different to the one used if the widget is popped out to a separate + * tab / browser + * + * @returns {string} url + */ + _getRenderedUrl() { + let url; + + if (this.props.app.type === 'jitsi') { + console.log("Replacing Jitsi widget URL with local wrapper"); + url = WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: true}); + url = this._addWurlParams(url); + } else { + url = this._getSafeUrl(this.state.widgetUrl); + } + return this._templatedUrl(url); + } + + _getPopoutUrl() { + if (this.props.app.type === 'jitsi') { + return this._templatedUrl( + WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: false}), + ); + } else { + // use app.url, not state.widgetUrl, because we want the one without + // the wURL params for the popped-out version. + return this._templatedUrl(this._getSafeUrl(this.props.app.url)); + } + } + + _getSafeUrl(u) { + const parsedWidgetUrl = url.parse(u, true); if (ENABLE_REACT_PERF) { parsedWidgetUrl.search = null; parsedWidgetUrl.query.react_perf = true; } let safeWidgetUrl = ''; - if (ALLOWED_APP_URL_SCHEMES.includes(parsedWidgetUrl.protocol) || ( - // Check if the widget URL is a Jitsi widget in Electron - parsedWidgetUrl.protocol === 'vector:' - && parsedWidgetUrl.host === 'vector' - && parsedWidgetUrl.pathname === '/webapp/jitsi.html' - && this.props.type === 'jitsi' - )) { + if (ALLOWED_APP_URL_SCHEMES.includes(parsedWidgetUrl.protocol)) { safeWidgetUrl = url.format(parsedWidgetUrl); } return safeWidgetUrl; @@ -556,9 +656,9 @@ export default class AppTile extends React.Component { _onPopoutWidgetClick() { // Using Object.assign workaround as the following opens in a new window instead of a new tab. - // window.open(this._getSafeUrl(), '_blank', 'noopener=yes'); + // window.open(this._getPopoutUrl(), '_blank', 'noopener=yes'); Object.assign(document.createElement('a'), - { target: '_blank', href: this._getSafeUrl(), rel: 'noreferrer noopener'}).click(); + { target: '_blank', href: this._getPopoutUrl(), rel: 'noreferrer noopener'}).click(); } _onReloadWidgetClick() { @@ -635,7 +735,7 @@ export default class AppTile extends React.Component {