diff --git a/CHANGELOG.md b/CHANGELOG.md index e4a7ddc407..34a3498f9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,110 @@ +Changes in [3.6.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.6.1) (2020-10-20) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.6.0...v3.6.1) + + * [Release] Adjust for new widget messaging APIs + [\#5342](https://github.com/matrix-org/matrix-react-sdk/pull/5342) + * [Release] Fix Jitsi OpenIDC auth + [\#5335](https://github.com/matrix-org/matrix-react-sdk/pull/5335) + +Changes in [3.6.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.6.0) (2020-10-12) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.6.0-rc.1...v3.6.0) + + * Upgrade JS SDK to 8.5.0 + * [Release] Fix templating for v1 jitsi widgets + [\#5306](https://github.com/matrix-org/matrix-react-sdk/pull/5306) + * [Release] Use new preparing event for widget communications + [\#5304](https://github.com/matrix-org/matrix-react-sdk/pull/5304) + +Changes in [3.6.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.6.0-rc.1) (2020-10-07) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.5.0...v3.6.0-rc.1) + + * Upgrade JS SDK to 8.5.0-rc.1 + * Update from Weblate + [\#5297](https://github.com/matrix-org/matrix-react-sdk/pull/5297) + * Fix edited replies being wrongly treated as big emoji + [\#5295](https://github.com/matrix-org/matrix-react-sdk/pull/5295) + * Fix StopGapWidget infinitely recursing + [\#5294](https://github.com/matrix-org/matrix-react-sdk/pull/5294) + * Fix editing and redactions not updating the Reply Thread + [\#5281](https://github.com/matrix-org/matrix-react-sdk/pull/5281) + * Hide Jump to Read Receipt button for users who have not yet sent an RR + [\#5282](https://github.com/matrix-org/matrix-react-sdk/pull/5282) + * fix img tags not always being rendered correctly + [\#5279](https://github.com/matrix-org/matrix-react-sdk/pull/5279) + * Hopefully fix righhtpanel crash + [\#5293](https://github.com/matrix-org/matrix-react-sdk/pull/5293) + * Fix naive pinning limit and app tile widgetMessaging NPE + [\#5283](https://github.com/matrix-org/matrix-react-sdk/pull/5283) + * Show server errors from saving profile settings + [\#5272](https://github.com/matrix-org/matrix-react-sdk/pull/5272) + * Update copy for `redact` permission + [\#5273](https://github.com/matrix-org/matrix-react-sdk/pull/5273) + * Remove width limit on widgets + [\#5265](https://github.com/matrix-org/matrix-react-sdk/pull/5265) + * Fix call container avatar initial centering + [\#5280](https://github.com/matrix-org/matrix-react-sdk/pull/5280) + * Fix right panel for peeking rooms + [\#5268](https://github.com/matrix-org/matrix-react-sdk/pull/5268) + * Add support for dehydrated devices + [\#5239](https://github.com/matrix-org/matrix-react-sdk/pull/5239) + * Use Own Profile Store for the Profile Settings + [\#5277](https://github.com/matrix-org/matrix-react-sdk/pull/5277) + * null-guard defaultAvatarUrlForString + [\#5270](https://github.com/matrix-org/matrix-react-sdk/pull/5270) + * Choose first result on enter in the emoji picker + [\#5257](https://github.com/matrix-org/matrix-react-sdk/pull/5257) + * Fix room directory clipping links in the room's topic + [\#5276](https://github.com/matrix-org/matrix-react-sdk/pull/5276) + * Decorate failed e2ee downgrade attempts better + [\#5278](https://github.com/matrix-org/matrix-react-sdk/pull/5278) + * MELS use latest avatar rather than the first avatar + [\#5262](https://github.com/matrix-org/matrix-react-sdk/pull/5262) + * Fix Encryption Panel close button clashing with Base Card + [\#5261](https://github.com/matrix-org/matrix-react-sdk/pull/5261) + * Wrap canEncryptToAllUsers in a try/catch to handle server errors + [\#5275](https://github.com/matrix-org/matrix-react-sdk/pull/5275) + * Fix conditional on communities prototype room creation dialog + [\#5274](https://github.com/matrix-org/matrix-react-sdk/pull/5274) + * Fix ensureDmExists for encryption detection + [\#5271](https://github.com/matrix-org/matrix-react-sdk/pull/5271) + * Switch to using the Widget API SDK for widget messaging + [\#5171](https://github.com/matrix-org/matrix-react-sdk/pull/5171) + * Ensure package links exist when releasing + [\#5269](https://github.com/matrix-org/matrix-react-sdk/pull/5269) + * Fix the call preview when not in same room as the call + [\#5267](https://github.com/matrix-org/matrix-react-sdk/pull/5267) + * Make the hangup button do things for conference calls + [\#5223](https://github.com/matrix-org/matrix-react-sdk/pull/5223) + * Render Jitsi widget state events in a more obvious way + [\#5222](https://github.com/matrix-org/matrix-react-sdk/pull/5222) + * Make the PIP Jitsi look and feel like the 1:1 PIP + [\#5226](https://github.com/matrix-org/matrix-react-sdk/pull/5226) + * Trim range when formatting so that it excludes leading/trailing spaces + [\#5263](https://github.com/matrix-org/matrix-react-sdk/pull/5263) + * Fix button label on the Set Password Dialog + [\#5264](https://github.com/matrix-org/matrix-react-sdk/pull/5264) + * fix link to classic yarn's `yarn link` + [\#5259](https://github.com/matrix-org/matrix-react-sdk/pull/5259) + * Fix index mismatch between username colors styles and custom theming + [\#5256](https://github.com/matrix-org/matrix-react-sdk/pull/5256) + * Disable autocompletion on security key input during login + [\#5258](https://github.com/matrix-org/matrix-react-sdk/pull/5258) + * fix uninitialised state and eventlistener leak in RoomUpgradeWarningBar + [\#5255](https://github.com/matrix-org/matrix-react-sdk/pull/5255) + * Only set title when it changes + [\#5254](https://github.com/matrix-org/matrix-react-sdk/pull/5254) + * Convert CallHandler to typescript + [\#5248](https://github.com/matrix-org/matrix-react-sdk/pull/5248) + * Retry loading i18n language if it fails + [\#5209](https://github.com/matrix-org/matrix-react-sdk/pull/5209) + * Rework profile area for user and room settings to be more clear + [\#5243](https://github.com/matrix-org/matrix-react-sdk/pull/5243) + * Validation improve pattern for derived data + [\#5241](https://github.com/matrix-org/matrix-react-sdk/pull/5241) + Changes in [3.5.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.5.0) (2020-09-28) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.5.0-rc.1...v3.5.0) diff --git a/README.md b/README.md index 4db02418ba..73afe34df0 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ are currently filed against vector-im/element-web rather than this project). Translation Status ================== -[![Translation status](https://translate.riot.im/widgets/element-web/-/multi-auto.svg)](https://translate.riot.im/engage/element-web/?utm_source=widget) +[![Translation status](https://translate.element.io/widgets/element-web/-/multi-auto.svg)](https://translate.element.io/engage/element-web/?utm_source=widget) Developer Guide =============== diff --git a/package.json b/package.json index ca7d6ee0b7..9a45a5894d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.5.0", + "version": "3.6.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -81,7 +81,7 @@ "linkifyjs": "^2.1.9", "lodash": "^4.17.19", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", - "matrix-widget-api": "^0.1.0-beta.3", + "matrix-widget-api": "^0.1.0-beta.5", "minimist": "^1.2.5", "pako": "^1.0.11", "parse5": "^5.1.1", diff --git a/release.sh b/release.sh index e2cefcbe74..4742f00dea 100755 --- a/release.sh +++ b/release.sh @@ -32,9 +32,7 @@ do echo "Upgrading $i to $latestver..." yarn add -E $i@$latestver git add -u - # The `-e` flag opens the editor and gives you a chance to check - # the upgrade for correctness. - git commit -m "Upgrade $i to $latestver" -e + git commit -m "Upgrade $i to $latestver" fi fi done diff --git a/res/css/_common.scss b/res/css/_common.scss index 3346394edd..666129af34 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -208,12 +208,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { border: 0; } -/* applied to side-panels and messagepanel when in RoomSettings */ -.mx_fadable { - opacity: 1; - transition: opacity 0.2s ease-in-out; -} - // These are magic constants which are excluded from tinting, to let themes // (which only have CSS, unlike skins) tell the app what their non-tinted // colourscheme is by inspecting the stylesheet DOM. diff --git a/res/css/_components.scss b/res/css/_components.scss index 06cdbdcb4b..657d77974f 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -13,6 +13,7 @@ @import "./structures/_HeaderButtons.scss"; @import "./structures/_HomePage.scss"; @import "./structures/_LeftPanel.scss"; +@import "./structures/_LeftPanelWidget.scss"; @import "./structures/_MainSplit.scss"; @import "./structures/_MatrixChat.scss"; @import "./structures/_MyGroups.scss"; @@ -26,7 +27,7 @@ @import "./structures/_ScrollPanel.scss"; @import "./structures/_SearchBox.scss"; @import "./structures/_TabbedView.scss"; -@import "./structures/_TagPanel.scss"; +@import "./structures/_GroupFilterPanel.scss"; @import "./structures/_ToastContainer.scss"; @import "./structures/_UploadBar.scss"; @import "./structures/_UserMenu.scss"; @@ -51,11 +52,11 @@ @import "./views/avatars/_DecoratedRoomAvatar.scss"; @import "./views/avatars/_MemberStatusMessageAvatar.scss"; @import "./views/avatars/_PulsedAvatar.scss"; +@import "./views/avatars/_WidgetAvatar.scss"; @import "./views/context_menus/_IconizedContextMenu.scss"; @import "./views/context_menus/_MessageContextMenu.scss"; @import "./views/context_menus/_StatusMessageContextMenu.scss"; @import "./views/context_menus/_TagTileContextMenu.scss"; -@import "./views/context_menus/_WidgetContextMenu.scss"; @import "./views/dialogs/_AddressPickerDialog.scss"; @import "./views/dialogs/_Analytics.scss"; @import "./views/dialogs/_BugReportDialog.scss"; @@ -74,6 +75,7 @@ @import "./views/dialogs/_InviteDialog.scss"; @import "./views/dialogs/_KeyboardShortcutsDialog.scss"; @import "./views/dialogs/_MessageEditHistoryDialog.scss"; +@import "./views/dialogs/_ModalWidgetDialog.scss"; @import "./views/dialogs/_NewSessionReviewDialog.scss"; @import "./views/dialogs/_RoomSettingsDialog.scss"; @import "./views/dialogs/_RoomSettingsDialogBridges.scss"; diff --git a/res/css/structures/_CustomRoomTagPanel.scss b/res/css/structures/_CustomRoomTagPanel.scss index 3feb2565be..96813cccea 100644 --- a/res/css/structures/_CustomRoomTagPanel.scss +++ b/res/css/structures/_CustomRoomTagPanel.scss @@ -22,7 +22,7 @@ limitations under the License. } .mx_CustomRoomTagPanel { - background-color: $tagpanel-bg-color; + background-color: $groupFilterPanel-bg-color; max-height: 40vh; } diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_GroupFilterPanel.scss similarity index 80% rename from res/css/structures/_TagPanel.scss rename to res/css/structures/_GroupFilterPanel.scss index cdca1f0764..e5a8ef6df2 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_GroupFilterPanel.scss @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_TagPanel { +.mx_GroupFilterPanel { flex: 1; - background-color: $tagpanel-bg-color; + background-color: $groupFilterPanel-bg-color; cursor: pointer; display: flex; @@ -26,49 +26,49 @@ limitations under the License. min-height: 0; } -.mx_TagPanel_items_selected { +.mx_GroupFilterPanel_items_selected { cursor: pointer; } -.mx_TagPanel .mx_TagPanel_divider { +.mx_GroupFilterPanel .mx_GroupFilterPanel_divider { height: 0px; width: 90%; border: none; - border-bottom: 1px solid $tagpanel-divider-color; + border-bottom: 1px solid $groupFilterPanel-divider-color; } -.mx_TagPanel .mx_TagPanel_scroller { +.mx_GroupFilterPanel .mx_GroupFilterPanel_scroller { flex-grow: 1; width: 100%; } -.mx_TagPanel .mx_TagPanel_tagTileContainer { +.mx_GroupFilterPanel .mx_GroupFilterPanel_tagTileContainer { display: flex; flex-direction: column; align-items: center; padding-top: 6px; } -.mx_TagPanel .mx_TagPanel_tagTileContainer > div { +.mx_GroupFilterPanel .mx_GroupFilterPanel_tagTileContainer > div { margin: 6px 0; } -.mx_TagPanel .mx_TagTile { +.mx_GroupFilterPanel .mx_TagTile { // opacity: 0.5; position: relative; } -.mx_TagPanel .mx_TagTile.mx_TagTile_prototype { +.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_prototype { padding: 3px; } -.mx_TagPanel .mx_TagTile:focus, -.mx_TagPanel .mx_TagTile:hover, -.mx_TagPanel .mx_TagTile.mx_TagTile_selected { +.mx_GroupFilterPanel .mx_TagTile:focus, +.mx_GroupFilterPanel .mx_TagTile:hover, +.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_selected { // opacity: 1; } -.mx_TagPanel .mx_TagTile.mx_TagTile_selected_prototype { +.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_selected_prototype { background-color: $primary-bg-color; border-radius: 6px; } @@ -108,7 +108,7 @@ limitations under the License. } } -.mx_TagPanel .mx_TagTile_plus { +.mx_GroupFilterPanel .mx_TagTile_plus { margin-bottom: 12px; height: 32px; width: 32px; @@ -132,7 +132,7 @@ limitations under the License. } } -.mx_TagPanel .mx_TagTile.mx_TagTile_selected::before { +.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_selected::before { content: ''; height: 100%; background-color: $accent-color; @@ -142,7 +142,7 @@ limitations under the License. border-radius: 0 3px 3px 0; } -.mx_TagPanel .mx_TagTile.mx_AccessibleButton:focus { +.mx_GroupFilterPanel .mx_TagTile.mx_AccessibleButton:focus { filter: none; } diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index 5112d07c46..885dd77a84 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -14,29 +14,29 @@ See the License for the specific language governing permissions and limitations under the License. */ -$tagPanelWidth: 56px; // only applies in this file, used for calculations +$groupFilterPanelWidth: 56px; // only applies in this file, used for calculations .mx_LeftPanel { background-color: $roomlist-bg-color; min-width: 260px; max-width: 50%; - // Create a row-based flexbox for the TagPanel and the room list + // Create a row-based flexbox for the GroupFilterPanel and the room list display: flex; - .mx_LeftPanel_tagPanelContainer { + .mx_LeftPanel_GroupFilterPanelContainer { flex-grow: 0; flex-shrink: 0; - flex-basis: $tagPanelWidth; + flex-basis: $groupFilterPanelWidth; height: 100%; - // Create another flexbox so the TagPanel fills the container + // Create another flexbox so the GroupFilterPanel fills the container display: flex; - // TagPanel handles its own CSS + // GroupFilterPanel handles its own CSS } - &:not(.mx_LeftPanel_hasTagPanel) { + &:not(.mx_LeftPanel_hasGroupFilterPanel) { .mx_LeftPanel_roomListContainer { width: 100%; } @@ -45,7 +45,7 @@ $tagPanelWidth: 56px; // only applies in this file, used for calculations // Note: The 'room list' in this context is actually everything that isn't the tag // panel, such as the menu options, breadcrumbs, filtering, etc .mx_LeftPanel_roomListContainer { - width: calc(100% - $tagPanelWidth); + width: calc(100% - $groupFilterPanelWidth); background-color: $roomlist-bg-color; // Create another flexbox (this time a column) for the room list components @@ -169,10 +169,10 @@ $tagPanelWidth: 56px; // only applies in this file, used for calculations min-width: unset; // We have to forcefully set the width to override the resizer's style attribute. - &.mx_LeftPanel_hasTagPanel { - width: calc(68px + $tagPanelWidth) !important; + &.mx_LeftPanel_hasGroupFilterPanel { + width: calc(68px + $groupFilterPanelWidth) !important; } - &:not(.mx_LeftPanel_hasTagPanel) { + &:not(.mx_LeftPanel_hasGroupFilterPanel) { width: 68px !important; } diff --git a/res/css/structures/_LeftPanelWidget.scss b/res/css/structures/_LeftPanelWidget.scss new file mode 100644 index 0000000000..4df651d7b6 --- /dev/null +++ b/res/css/structures/_LeftPanelWidget.scss @@ -0,0 +1,145 @@ +/* +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_LeftPanelWidget { + // largely based on RoomSublist + margin-left: 8px; + margin-bottom: 4px; + + .mx_LeftPanelWidget_headerContainer { + display: flex; + align-items: center; + + height: 24px; + color: $roomlist-header-color; + margin-top: 4px; + + .mx_LeftPanelWidget_stickable { + flex: 1; + max-width: 100%; + + display: flex; + align-items: center; + } + + .mx_LeftPanelWidget_headerText { + flex: 1; + max-width: calc(100% - 16px); + line-height: $font-16px; + font-size: $font-13px; + font-weight: 600; + + // Ellipsize any text overflow + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + + .mx_LeftPanelWidget_collapseBtn { + display: inline-block; + position: relative; + width: 14px; + height: 14px; + margin-right: 6px; + + &::before { + content: ''; + width: 18px; + height: 18px; + position: absolute; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background-color: $roomlist-header-color; + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); + } + + &.mx_LeftPanelWidget_collapseBtn_collapsed::before { + transform: rotate(-90deg); + } + } + } + } + + .mx_LeftPanelWidget_resizeBox { + position: relative; + + display: flex; + flex-direction: column; + overflow: visible; // let the resize handle out + } + + .mx_AppTileFullWidth { + flex: 1 0 0; + overflow: hidden; + // need this to be flex otherwise the overflow hidden from above + // sometimes vertically centers the clipped list ... no idea why it would do this + // as the box model should be top aligned. Happens in both FF and Chromium + display: flex; + flex-direction: column; + box-sizing: border-box; + + mask-image: linear-gradient(0deg, transparent, black 4px); + } + + .mx_LeftPanelWidget_resizerHandle { + cursor: ns-resize; + border-radius: 3px; + + // Override styles from library + width: unset !important; + height: 4px !important; + + position: absolute; + top: -24px !important; // override from library - puts it in the margin-top of the headerContainer + + // Together, these make the bar 64px wide + // These are also overridden from the library + left: calc(50% - 32px) !important; + right: calc(50% - 32px) !important; + } + + &:hover .mx_LeftPanelWidget_resizerHandle { + opacity: 0.8; + background-color: $primary-fg-color; + } + + .mx_LeftPanelWidget_maximizeButton { + margin-left: 8px; + margin-right: 7px; + position: relative; + width: 24px; + height: 24px; + border-radius: 32px; + + &::before { + content: ''; + width: 16px; + height: 16px; + position: absolute; + top: 4px; + left: 4px; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + mask-image: url('$(res)/img/feather-customised/widget/maximise.svg'); + background: $muted-fg-color; + } + } +} + +.mx_LeftPanelWidget_maximizeButtonTooltip { + margin-top: -3px; +} diff --git a/res/css/structures/_MatrixChat.scss b/res/css/structures/_MatrixChat.scss index f4e46a8e94..812a7f8472 100644 --- a/res/css/structures/_MatrixChat.scss +++ b/res/css/structures/_MatrixChat.scss @@ -79,7 +79,6 @@ limitations under the License. height: 100%; } -.mx_MatrixChat > .mx_LeftPanel2:hover + .mx_ResizeHandle_horizontal, .mx_MatrixChat > .mx_ResizeHandle_horizontal:hover { position: relative; diff --git a/res/css/structures/_UserMenu.scss b/res/css/structures/_UserMenu.scss index fecac40e4e..6a352d46a3 100644 --- a/res/css/structures/_UserMenu.scss +++ b/res/css/structures/_UserMenu.scss @@ -230,6 +230,10 @@ limitations under the License. align-items: center; justify-content: center; } + + &.mx_UserMenu_contextMenu_hostingLink { + padding-top: 0; + } } .mx_IconizedContextMenu_icon { diff --git a/src/dispatcher/payloads/AppTileActionPayload.ts b/res/css/views/avatars/_WidgetAvatar.scss similarity index 72% rename from src/dispatcher/payloads/AppTileActionPayload.ts rename to res/css/views/avatars/_WidgetAvatar.scss index 3cdb0f8c1f..8e5cfb54d8 100644 --- a/src/dispatcher/payloads/AppTileActionPayload.ts +++ b/res/css/views/avatars/_WidgetAvatar.scss @@ -14,10 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ActionPayload } from "../payloads"; -import { Action } from "../actions"; - -export interface AppTileActionPayload extends ActionPayload { - action: Action.AppTileDelete | Action.AppTileRevoke; - widgetId: string; +.mx_WidgetAvatar { + border-radius: 4px; } diff --git a/res/css/views/context_menus/_WidgetContextMenu.scss b/res/css/views/context_menus/_WidgetContextMenu.scss deleted file mode 100644 index 60b7b93f99..0000000000 --- a/res/css/views/context_menus/_WidgetContextMenu.scss +++ /dev/null @@ -1,36 +0,0 @@ -/* -Copyright 2019 The Matrix.org Foundaction 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_WidgetContextMenu { - padding: 6px; - - .mx_WidgetContextMenu_option { - padding: 3px 6px 3px 6px; - cursor: pointer; - white-space: nowrap; - } - - .mx_WidgetContextMenu_separator { - margin-top: 0; - margin-bottom: 0; - border-bottom-style: none; - border-left-style: none; - border-right-style: none; - border-top-style: solid; - border-top-width: 1px; - border-color: $menu-border-color; - } -} diff --git a/src/extend.js b/res/css/views/dialogs/_ModalWidgetDialog.scss similarity index 51% rename from src/extend.js rename to res/css/views/dialogs/_ModalWidgetDialog.scss index 263d802ab6..aa2dd0d395 100644 --- a/src/extend.js +++ b/res/css/views/dialogs/_ModalWidgetDialog.scss @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,13 +14,29 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; +.mx_ModalWidgetDialog { + .mx_ModalWidgetDialog_warning { + margin-bottom: 24px; -export default function(dest, src) { - for (const i in src) { - if (src.hasOwnProperty(i)) { - dest[i] = src[i]; + > img { + vertical-align: middle; + margin-right: 8px; } } - return dest; + + .mx_ModalWidgetDialog_buttons { + float: right; + margin-top: 24px; + + .mx_AccessibleButton + .mx_AccessibleButton { + margin-left: 8px; + } + } + + iframe { + width: 100%; + height: 450px; + border: 0; + border-radius: 8px; + } } diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss index 96269cea43..9c26f8f120 100644 --- a/res/css/views/elements/_AccessibleButton.scss +++ b/res/css/views/elements/_AccessibleButton.scss @@ -25,7 +25,7 @@ limitations under the License. .mx_AccessibleButton_hasKind { padding: 7px 18px; text-align: center; - border-radius: 4px; + border-radius: 8px; display: inline-block; font-size: $font-14px; } diff --git a/res/css/views/right_panel/_BaseCard.scss b/res/css/views/right_panel/_BaseCard.scss index 3ff3b52531..9a5a59bda8 100644 --- a/res/css/views/right_panel/_BaseCard.scss +++ b/res/css/views/right_panel/_BaseCard.scss @@ -128,6 +128,13 @@ limitations under the License. mask-size: 20px; mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); } + + &.mx_AccessibleButton_disabled { + padding-right: 12px; + &::after { + content: unset; + } + } } } diff --git a/res/css/views/right_panel/_RoomSummaryCard.scss b/res/css/views/right_panel/_RoomSummaryCard.scss index 0031d3a64c..36882f4e8b 100644 --- a/res/css/views/right_panel/_RoomSummaryCard.scss +++ b/res/css/views/right_panel/_RoomSummaryCard.scss @@ -110,28 +110,107 @@ limitations under the License. .mx_RoomSummaryCard_appsGroup { .mx_RoomSummaryCard_Button { - padding-left: 12px; + // this button is special so we have to override some of the original styling + // as we will be applying it in its children + padding: 0; + height: auto; color: $tertiary-fg-color; - span { - color: $primary-fg-color; + .mx_RoomSummaryCard_icon_app { + padding: 10px 48px 10px 12px; // based on typical mx_RoomSummaryCard_Button padding + text-overflow: ellipsis; + overflow: hidden; + + .mx_BaseAvatar_image { + vertical-align: top; + margin-right: 12px; + } + + span { + color: $primary-fg-color; + } } - img { - vertical-align: top; - margin-right: 12px; - border-radius: 4px; + .mx_RoomSummaryCard_app_pinToggle, + .mx_RoomSummaryCard_app_options { + position: absolute; + top: 0; + height: 100%; // to give bigger interactive zone + width: 24px; + padding: 12px 4px; + box-sizing: border-box; + min-width: 24px; // prevent flexbox crushing + + &:hover { + &::after { + content: ''; + position: absolute; + height: 24px; + width: 24px; + top: 8px; // equal to padding-top of parent + left: 0; + border-radius: 12px; + background-color: rgba(141, 151, 165, 0.1); + } + } + + &::before { + content: ''; + position: absolute; + height: 16px; + width: 16px; + mask-repeat: no-repeat; + mask-position: center; + mask-size: 16px; + background-color: $icon-button-color; + } + } + + .mx_RoomSummaryCard_app_pinToggle { + right: 24px; + + &::before { + mask-image: url('$(res)/img/element-icons/room/pin-upright.svg'); + } + } + + .mx_RoomSummaryCard_app_options { + right: 48px; + display: none; + + &::before { + mask-image: url('$(res)/img/element-icons/room/ellipsis.svg'); + } + } + + &.mx_RoomSummaryCard_Button_pinned { + &::after { + opacity: 0.2; + } + + .mx_RoomSummaryCard_app_pinToggle::before { + background-color: $accent-color; + } + } + + &:hover { + .mx_RoomSummaryCard_icon_app { + padding-right: 72px; + } + + .mx_RoomSummaryCard_app_options { + display: unset; + } } &::before { content: unset; } - } - .mx_RoomSummaryCard_icon_app_pinned::after { - mask-image: url('$(res)/img/element-icons/room/pin-upright.svg'); - background-color: $accent-color; - transform: unset; + &::after { + top: 8px; // re-align based on the height change + pointer-events: none; // pass through to the real button + } } } diff --git a/res/css/views/right_panel/_WidgetCard.scss b/res/css/views/right_panel/_WidgetCard.scss index 315fd5213c..a90e744a5a 100644 --- a/res/css/views/right_panel/_WidgetCard.scss +++ b/res/css/views/right_panel/_WidgetCard.scss @@ -24,34 +24,35 @@ limitations under the License. border: 0; } - &.mx_WidgetCard_noEdit { - .mx_AccessibleButton_kind_secondary { - margin: 0 12px; + .mx_BaseCard_header { + display: inline-flex; - &:first-child { - // expand the Pin to room primary action - flex-grow: 1; - } + & > h2 { + margin-right: 0; + flex-grow: 1; } - } - .mx_WidgetCard_optionsButton { - position: relative; - height: 18px; - width: 26px; - - &::before { - content: ""; - position: absolute; - width: 20px; + .mx_WidgetCard_optionsButton { + position: relative; + margin-right: 44px; height: 20px; - top: 6px; - left: 20px; - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; - mask-image: url('$(res)/img/element-icons/room/ellipsis.svg'); - background-color: $secondary-fg-color; + width: 20px; + min-width: 20px; // prevent crushing by the flexbox + padding: 0; + + &::before { + content: ""; + position: absolute; + width: 20px; + height: 20px; + top: 0; + left: 4px; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/room/ellipsis.svg'); + background-color: $secondary-fg-color; + } } } } diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss index 451704bd88..8731d22660 100644 --- a/res/css/views/rooms/_AppsDrawer.scss +++ b/res/css/views/rooms/_AppsDrawer.scss @@ -47,11 +47,27 @@ $MiniAppTileHeight: 200px; opacity: 0.8; background: $primary-fg-color; } + + .mx_ResizeHandle_horizontal::before { + position: absolute; + left: 3px; + top: 50%; + transform: translate(0, -50%); + + height: 64px; // to match width of the ones on roomlist + width: 4px; + border-radius: 4px; + + content: ''; + + background-color: $primary-fg-color; + opacity: 0.8; + } } } -.mx_AppsDrawer_hidden { - display: none; +.mx_AppsContainer_resizer { + margin-bottom: 8px; } .mx_AppsContainer { @@ -60,53 +76,71 @@ $MiniAppTileHeight: 200px; align-items: stretch; justify-content: center; height: 100%; - margin-bottom: 8px; + width: 100%; + flex: 1; + min-height: 0; + + .mx_AppTile:first-of-type { + border-left-width: 8px; + border-radius: 10px 0 0 10px; + } + .mx_AppTile:last-of-type { + border-right-width: 8px; + border-radius: 0 10px 10px 0; + } + + .mx_ResizeHandle_horizontal { + position: relative; + + > div { + width: 0; + } + } } -.mx_AppsDrawer_minimised .mx_AppsContainer { - // override the re-resizable inline styles - height: inherit !important; - min-height: inherit !important; -} +// TODO this should be 300px but that's too large +$MinWidth: 240px; -.mx_AddWidget_button { - order: 2; - cursor: pointer; - padding: 0; - margin: -3px auto 5px 0; - color: $accent-color; - font-size: $font-12px; -} +.mx_AppsDrawer_2apps .mx_AppTile { + width: 50%; -.mx_SetAppURLDialog_input { - border-radius: 3px; - border: 1px solid $input-border-color; - padding: 9px; - color: $primary-hairline-color; - background-color: $primary-bg-color; - font-size: $font-15px; + &:nth-child(3) { + flex-grow: 1; + width: 0 !important; + min-width: $MinWidth !important; + } +} +.mx_AppsDrawer_3apps .mx_AppTile { + width: 33%; + + &:nth-child(3) { + flex-grow: 1; + width: 0 !important; + min-width: $MinWidth !important; + } } .mx_AppTile { width: 50%; - border: 5px solid $widget-menu-bar-bg-color; - border-radius: 4px; + min-width: $MinWidth; + border: 8px solid $widget-menu-bar-bg-color; + border-left-width: 5px; + border-right-width: 5px; display: flex; flex-direction: column; - - & + .mx_AppTile { - margin-left: 5px; - } + box-sizing: border-box; + background-color: $widget-menu-bar-bg-color; } .mx_AppTileFullWidth { - width: 100%; + width: 100% !important; // to override the inline style set by the resizer margin: 0; padding: 0; border: 5px solid $widget-menu-bar-bg-color; border-radius: 8px; display: flex; flex-direction: column; + background-color: $widget-menu-bar-bg-color; } .mx_AppTile_mini { @@ -118,12 +152,6 @@ $MiniAppTileHeight: 200px; height: $MiniAppTileHeight; } -.mx_AppTile.mx_AppTile_minimised, -.mx_AppTileFullWidth.mx_AppTile_minimised, -.mx_AppTile_mini.mx_AppTile_minimised { - height: 14px; -} - .mx_AppTile .mx_AppTile_persistedWrapper, .mx_AppTileFullWidth .mx_AppTile_persistedWrapper, .mx_AppTile_mini .mx_AppTile_persistedWrapper { @@ -143,19 +171,20 @@ $MiniAppTileHeight: 200px; flex-direction: row; align-items: center; justify-content: space-between; - cursor: pointer; width: 100%; -} - -.mx_AppTileMenuBar_expanded { - padding-bottom: 5px; + padding-top: 2px; + padding-bottom: 8px; } .mx_AppTileMenuBarTitle { - display: flex; - flex-direction: row; - align-items: center; - pointer-events: none; + line-height: 20px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + .mx_WidgetAvatar { + margin-right: 12px; + } } .mx_AppTileMenuBarTitle > :last-child { @@ -179,37 +208,20 @@ $MiniAppTileHeight: 200px; margin: 0 3px; } -.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_minimise { - mask-image: url('$(res)/img/feather-customised/widget/minimise.svg'); - background-color: $accent-color; -} - -.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_maximise { - mask-image: url('$(res)/img/feather-customised/widget/maximise.svg'); - background-color: $accent-color; -} - .mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_popout { mask-image: url('$(res)/img/feather-customised/widget/external-link.svg'); } .mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_menu { - mask-image: url('$(res)/img/icon_context.svg'); -} - -.mx_AppTileMenuBarWidgetDelete { - filter: none; -} - -.mx_AppTileMenuBarWidget:hover { - border: 1px solid $primary-fg-color; - border-radius: 2px; + mask-image: url('$(res)/img/element-icons/room/ellipsis.svg'); } .mx_AppTileBody { height: 100%; width: 100%; overflow: hidden; + border-radius: 8px; + background-color: $widget-body-bg-color; } .mx_AppTileBody_mini { @@ -242,75 +254,8 @@ $MiniAppTileHeight: 200px; display: block; } -.mx_AppTileMenuBarWidgetPadding { - margin-right: 5px; -} - -.mx_AppIconTile { - background-color: $lightbox-bg-color; - border: 1px solid rgba(0, 0, 0, 0); - width: 200px; - box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); - transition: 0.3s; - border-radius: 3px; - margin: 5px; - display: inline-block; -} - -.mx_AppIconTile.mx_AppIconTile_active { - color: $accent-color; - border-color: $accent-color; -} - -.mx_AppIconTile:hover { - border: 1px solid $accent-color; - box-shadow: 0 0 10px 5px rgba(200, 200, 200, 0.5); -} - -.mx_AppIconTile_content { - padding: 2px 16px; - height: 60px; - overflow: hidden; -} - -.mx_AppIconTile_content h4 { - margin-top: 5px; - margin-bottom: 2px; -} - -.mx_AppIconTile_content p { - margin-top: 0; - margin-bottom: 5px; - font-size: smaller; -} - -.mx_AppIconTile_image { - padding: 10px; - max-width: 100px; - max-height: 100px; - width: auto; - height: auto; -} - -.mx_AppIconTile_imageContainer { - text-align: center; - width: 100%; - background-color: white; - border-radius: 3px 3px 0 0; - height: 155px; - display: flex; - justify-content: center; - align-items: center; -} - -form.mx_Custom_Widget_Form div { - margin-top: 10px; - margin-bottom: 10px; -} - .mx_AppPermissionWarning { text-align: center; - background-color: $widget-menu-bar-bg-color; display: flex; height: 100%; flex-direction: column; @@ -375,6 +320,10 @@ form.mx_Custom_Widget_Form div { font-weight: bold; position: relative; height: 100%; + + // match bg of border so that the cut corners have the right fill + background-color: $widget-body-bg-color !important; + border-radius: 8px; } .mx_AppLoading .mx_Spinner { @@ -402,10 +351,6 @@ form.mx_Custom_Widget_Form div { display: none; } -.mx_AppsDrawer_minimised .mx_AppsContainer_resizerHandle { - display: none; -} - /* Avoid apptile iframes capturing mouse event focus when resizing */ .mx_AppsDrawer_resizing iframe { pointer-events: none; diff --git a/res/css/views/rooms/_MemberInfo.scss b/res/css/views/rooms/_MemberInfo.scss index fb082843f1..182c280217 100644 --- a/res/css/views/rooms/_MemberInfo.scss +++ b/res/css/views/rooms/_MemberInfo.scss @@ -70,7 +70,7 @@ limitations under the License. } .mx_MemberInfo_avatar { - background: $tagpanel-bg-color; + background: $groupFilterPanel-bg-color; margin-bottom: 16px; } diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index d240877507..a23a44906f 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -241,6 +241,13 @@ limitations under the License. width: 26px; } +.mx_RoomHeader_appsButton::before { + mask-image: url('$(res)/img/element-icons/room/apps.svg'); +} +.mx_RoomHeader_appsButton_highlight::before { + background-color: $accent-color; +} + .mx_RoomHeader_searchButton::before { mask-image: url('$(res)/img/element-icons/room/search-inset.svg'); } diff --git a/res/css/views/rooms/_RoomSublist.scss b/res/css/views/rooms/_RoomSublist.scss index 543940fb78..82bba40167 100644 --- a/res/css/views/rooms/_RoomSublist.scss +++ b/res/css/views/rooms/_RoomSublist.scss @@ -59,10 +59,6 @@ limitations under the License. width: calc(100% - 22px); } - &.mx_RoomSublist_headerContainer_stickyBottom { - bottom: 0; - } - // We don't have a top style because the top is dependent on the room list header's // height, and is therefore calculated in JS. // The class, mx_RoomSublist_headerContainer_stickyTop, is applied though. diff --git a/res/css/views/rooms/_Stickers.scss b/res/css/views/rooms/_Stickers.scss index d99276b70a..94f42efe83 100644 --- a/res/css/views/rooms/_Stickers.scss +++ b/res/css/views/rooms/_Stickers.scss @@ -16,6 +16,10 @@ border-bottom: none; } + .mx_AppTileMenuBar { + padding: 0; + } + iframe { // Sticker picker depends on the fixed height previously used for all tiles height: 273px; diff --git a/res/css/views/settings/_AvatarSetting.scss b/res/css/views/settings/_AvatarSetting.scss index 4a1f57a00e..a350605ab1 100644 --- a/res/css/views/settings/_AvatarSetting.scss +++ b/res/css/views/settings/_AvatarSetting.scss @@ -85,6 +85,7 @@ limitations under the License. .mx_AvatarSetting_avatarPlaceholder { display: block; height: 90px; + width: inherit; border-radius: 90px; cursor: pointer; } diff --git a/res/img/element-icons/room/apps.svg b/res/img/element-icons/room/apps.svg new file mode 100644 index 0000000000..c90704752c --- /dev/null +++ b/res/img/element-icons/room/apps.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/res/img/element-icons/room/default_app.svg b/res/img/element-icons/room/default_app.svg index 08734170df..baf9bc37fa 100644 --- a/res/img/element-icons/room/default_app.svg +++ b/res/img/element-icons/room/default_app.svg @@ -1,11 +1,21 @@ - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/res/img/element-icons/room/default_cal.svg b/res/img/element-icons/room/default_cal.svg index 5bced115cf..fc440b4553 100644 --- a/res/img/element-icons/room/default_cal.svg +++ b/res/img/element-icons/room/default_cal.svg @@ -1,6 +1,6 @@ - - - - - + + + + + diff --git a/res/img/element-icons/room/default_clock.svg b/res/img/element-icons/room/default_clock.svg index cc21716d15..c7f453aadd 100644 --- a/res/img/element-icons/room/default_clock.svg +++ b/res/img/element-icons/room/default_clock.svg @@ -1,5 +1,5 @@ - - - - + + + + diff --git a/res/img/element-icons/room/default_doc.svg b/res/img/element-icons/room/default_doc.svg index 93e7507be3..aff393ffd5 100644 --- a/res/img/element-icons/room/default_doc.svg +++ b/res/img/element-icons/room/default_doc.svg @@ -1,4 +1,4 @@ - - + + diff --git a/res/img/element-icons/room/default_video.svg b/res/img/element-icons/room/default_video.svg new file mode 100644 index 0000000000..022f1f43b1 --- /dev/null +++ b/res/img/element-icons/room/default_video.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/element-icons/room/integrations.svg b/res/img/element-icons/room/integrations.svg deleted file mode 100644 index 3a39506411..0000000000 --- a/res/img/element-icons/room/integrations.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/element-icons/warning-badge.svg b/res/img/element-icons/warning-badge.svg new file mode 100644 index 0000000000..ac5991f221 --- /dev/null +++ b/res/img/element-icons/warning-badge.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/icon_context.svg b/res/img/icon_context.svg deleted file mode 100644 index 600c5bbd1d..0000000000 --- a/res/img/icon_context.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 331b5f4692..bd709473ef 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -39,7 +39,7 @@ $info-plinth-fg-color: #888; $preview-bar-bg-color: $header-panel-bg-color; -$tagpanel-bg-color: rgba(38, 39, 43, 0.82); +$groupFilterPanel-bg-color: rgba(38, 39, 43, 0.82); $inverted-bg-color: $base-color; // used by AddressSelector @@ -98,7 +98,7 @@ $roomheader-color: $text-primary-color; $roomheader-bg-color: $bg-color; $roomheader-addroom-bg-color: rgba(92, 100, 112, 0.3); $roomheader-addroom-fg-color: $text-primary-color; -$tagpanel-button-color: $header-panel-text-primary-color; +$groupFilterPanel-button-color: $header-panel-text-primary-color; $groupheader-button-color: $header-panel-text-primary-color; $rightpanel-button-color: $header-panel-text-primary-color; $icon-button-color: #8E99A4; @@ -118,7 +118,7 @@ $roomlist-bg-color: rgba(33, 38, 44, 0.90); $roomlist-header-color: $tertiary-fg-color; $roomsublist-divider-color: $primary-fg-color; -$tagpanel-divider-color: $roomlist-header-color; +$groupFilterPanel-divider-color: $roomlist-header-color; $roomtile-preview-color: $secondary-fg-color; $roomtile-default-badge-bg-color: #61708b; @@ -131,6 +131,7 @@ $notice-secondary-color: $roomlist-header-color; $panel-divider-color: transparent; $widget-menu-bar-bg-color: $header-panel-bg-color; +$widget-body-bg-color: rgba(141, 151, 165, 0.2); // event tile lifecycle $event-sending-color: $text-secondary-color; @@ -187,7 +188,7 @@ $reaction-row-button-selected-border-color: $accent-color; $kbd-border-color: #000000; -$tooltip-timeline-bg-color: $tagpanel-bg-color; +$tooltip-timeline-bg-color: $groupFilterPanel-bg-color; $tooltip-timeline-fg-color: #ffffff; $interactive-tooltip-bg-color: $base-color; @@ -202,7 +203,7 @@ $appearance-tab-border-color: $room-highlight-color; // blur amounts for left left panel (only for element theme, used in _mods.scss) $roomlist-background-blur-amount: 60px; -$tagpanel-background-blur-amount: 30px; +$groupFilterPanel-background-blur-amount: 30px; $composer-shadow-color: rgba(0, 0, 0, 0.28); diff --git a/res/themes/dark/css/dark.scss b/res/themes/dark/css/dark.scss index 6d9dc7352c..f9695018e4 100644 --- a/res/themes/dark/css/dark.scss +++ b/res/themes/dark/css/dark.scss @@ -3,7 +3,7 @@ @import "../../light/css/_fonts.scss"; @import "../../light/css/_light.scss"; // important this goes before _mods, -// as $tagpanel-background-blur-amount and +// as $groupFilterPanel-background-blur-amount and // $roomlist-background-blur-amount // are overridden in _dark.scss @import "_dark.scss"; diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 14ce264bc0..60d44b1c31 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -37,8 +37,8 @@ $info-plinth-fg-color: #888; $preview-bar-bg-color: $header-panel-bg-color; -$tagpanel-bg-color: $base-color; -$inverted-bg-color: $tagpanel-bg-color; +$groupFilterPanel-bg-color: $base-color; +$inverted-bg-color: $groupFilterPanel-bg-color; // used by AddressSelector $selected-color: $room-highlight-color; @@ -95,7 +95,7 @@ $topleftmenu-color: $text-primary-color; $roomheader-color: $text-primary-color; $roomheader-addroom-bg-color: #3c4556; // $search-placeholder-color at 0.5 opacity $roomheader-addroom-fg-color: $text-primary-color; -$tagpanel-button-color: $header-panel-text-primary-color; +$groupFilterPanel-button-color: $header-panel-text-primary-color; $groupheader-button-color: $header-panel-text-primary-color; $rightpanel-button-color: $header-panel-text-primary-color; $icon-button-color: $header-panel-text-primary-color; @@ -115,7 +115,7 @@ $roomlist-bg-color: $header-panel-bg-color; $roomsublist-divider-color: $primary-fg-color; -$tagpanel-divider-color: $roomlist-header-color; +$groupFilterPanel-divider-color: $roomlist-header-color; $roomtile-preview-color: #9e9e9e; $roomtile-default-badge-bg-color: #61708b; @@ -126,6 +126,7 @@ $roomtile-selected-bg-color: #1A1D23; $panel-divider-color: $header-panel-border-color; $widget-menu-bar-bg-color: $header-panel-bg-color; +$widget-body-bg-color: #1A1D23; // event tile lifecycle $event-sending-color: $text-secondary-color; @@ -182,7 +183,7 @@ $reaction-row-button-selected-border-color: $accent-color; $kbd-border-color: #000000; -$tooltip-timeline-bg-color: $tagpanel-bg-color; +$tooltip-timeline-bg-color: $groupFilterPanel-bg-color; $tooltip-timeline-fg-color: #ffffff; $interactive-tooltip-bg-color: $base-color; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index b030fb7423..52fb1c8ef2 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -67,8 +67,8 @@ $preview-bar-bg-color: #f7f7f7; $secondary-accent-color: #f2f5f8; $tertiary-accent-color: #d3efe1; -$tagpanel-bg-color: #27303a; -$inverted-bg-color: $tagpanel-bg-color; +$groupFilterPanel-bg-color: #27303a; +$inverted-bg-color: $groupFilterPanel-bg-color; // used by RoomDirectory permissions $plinth-bg-color: $secondary-accent-color; @@ -162,7 +162,7 @@ $roomheader-color: #45474a; $roomheader-bg-color: $primary-bg-color; $roomheader-addroom-bg-color: #91a1c0; $roomheader-addroom-fg-color: $accent-fg-color; -$tagpanel-button-color: #91a1c0; +$groupFilterPanel-button-color: #91a1c0; $groupheader-button-color: #91a1c0; $rightpanel-button-color: #91a1c0; $icon-button-color: #91a1c0; @@ -182,7 +182,7 @@ $roomlist-bg-color: $header-panel-bg-color; $roomlist-header-color: $primary-fg-color; $roomsublist-divider-color: $primary-fg-color; -$tagpanel-divider-color: $roomlist-header-color; +$groupFilterPanel-divider-color: $roomlist-header-color; $roomtile-preview-color: #9e9e9e; $roomtile-default-badge-bg-color: #61708b; @@ -208,6 +208,7 @@ $panel-divider-color: #dee1f3; // ******************** $widget-menu-bar-bg-color: $secondary-accent-color; +$widget-body-bg-color: #fff; // ******************** @@ -305,7 +306,7 @@ $reaction-row-button-selected-border-color: $accent-color; $kbd-border-color: $reaction-row-button-border-color; -$tooltip-timeline-bg-color: $tagpanel-bg-color; +$tooltip-timeline-bg-color: $groupFilterPanel-bg-color; $tooltip-timeline-fg-color: #ffffff; $interactive-tooltip-bg-color: #27303a; diff --git a/res/themes/light-custom/css/_custom.scss b/res/themes/light-custom/css/_custom.scss index 6bb46e8a67..1b9254d100 100644 --- a/res/themes/light-custom/css/_custom.scss +++ b/res/themes/light-custom/css/_custom.scss @@ -49,7 +49,7 @@ $roomtile-selected-bg-color: var(--roomlist-highlights-color); // // --sidebar-color $interactive-tooltip-bg-color: var(--sidebar-color); -$tagpanel-bg-color: var(--sidebar-color); +$groupFilterPanel-bg-color: var(--sidebar-color); $tooltip-timeline-bg-color: var(--sidebar-color); $dialog-backdrop-color: var(--sidebar-color-50pct); $roomlist-button-bg-color: var(--sidebar-color-15pct); diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 140783212d..0c5e271860 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -62,7 +62,7 @@ $preview-bar-bg-color: #f7f7f7; $secondary-accent-color: #f2f5f8; $tertiary-accent-color: #d3efe1; -$tagpanel-bg-color: rgba(232, 232, 232, 0.77); +$groupFilterPanel-bg-color: rgba(232, 232, 232, 0.77); // used by RoomDirectory permissions $plinth-bg-color: $secondary-accent-color; @@ -156,7 +156,7 @@ $roomheader-color: #45474a; $roomheader-bg-color: $primary-bg-color; $roomheader-addroom-bg-color: rgba(92, 100, 112, 0.2); $roomheader-addroom-fg-color: #5c6470; -$tagpanel-button-color: #91A1C0; +$groupFilterPanel-button-color: #91A1C0; $groupheader-button-color: #91A1C0; $rightpanel-button-color: #91A1C0; $icon-button-color: #C1C6CD; @@ -176,7 +176,7 @@ $roomlist-bg-color: rgba(245, 245, 245, 0.90); $roomlist-header-color: $tertiary-fg-color; $roomsublist-divider-color: $primary-fg-color; -$tagpanel-divider-color: $roomlist-header-color; +$groupFilterPanel-divider-color: $roomlist-header-color; $roomtile-preview-color: $secondary-fg-color; $roomtile-default-badge-bg-color: #61708b; @@ -208,6 +208,7 @@ $pinned-color: $notice-secondary-color; // ******************** $widget-menu-bar-bg-color: $secondary-accent-color; +$widget-body-bg-color: #FFF; // ******************** @@ -320,7 +321,7 @@ $appearance-tab-border-color: $input-darker-bg-color; // blur amounts for left left panel (only for element theme, used in _mods.scss) $roomlist-background-blur-amount: 40px; -$tagpanel-background-blur-amount: 20px; +$groupFilterPanel-background-blur-amount: 20px; $composer-shadow-color: rgba(0, 0, 0, 0.04); diff --git a/res/themes/light/css/_mods.scss b/res/themes/light/css/_mods.scss index 9a59acba8e..30aaeedf8f 100644 --- a/res/themes/light/css/_mods.scss +++ b/res/themes/light/css/_mods.scss @@ -6,14 +6,14 @@ @supports (backdrop-filter: none) { .mx_LeftPanel { - background-image: var(--avatar-url); + background-image: var(--avatar-url, unset); background-repeat: no-repeat; background-size: cover; background-position: left top; } - .mx_TagPanel { - backdrop-filter: blur($tagpanel-background-blur-amount); + .mx_GroupFilterPanel { + backdrop-filter: blur($groupFilterPanel-background-blur-amount); } .mx_LeftPanel .mx_LeftPanel_roomListContainer { diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 91b91de90d..161aa3797f 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first import * as ModernizrStatic from "modernizr"; import ContentMessages from "../ContentMessages"; import { IMatrixClientPeg } from "../MatrixClientPeg"; @@ -31,6 +32,9 @@ import type {Renderer} from "react-dom"; import RightPanelStore from "../stores/RightPanelStore"; import WidgetStore from "../stores/WidgetStore"; import CallHandler from "../CallHandler"; +import {Analytics} from "../Analytics"; +import UserActivity from "../UserActivity"; +import {ModalWidgetStore} from "../stores/ModalWidgetStore"; declare global { interface Window { @@ -55,6 +59,9 @@ declare global { mxRightPanelStore: RightPanelStore; mxWidgetStore: WidgetStore; mxCallHandler: CallHandler; + mxAnalytics: Analytics; + mxUserActivity: UserActivity; + mxModalWidgetStore: ModalWidgetStore; } interface Document { diff --git a/src/Analytics.js b/src/Analytics.tsx similarity index 74% rename from src/Analytics.js rename to src/Analytics.tsx index 135cc2eb7a..212bfd3757 100644 --- a/src/Analytics.js +++ b/src/Analytics.tsx @@ -17,7 +17,7 @@ limitations under the License. import React from 'react'; -import { getCurrentLanguage, _t, _td } from './languageHandler'; +import {getCurrentLanguage, _t, _td, IVariables} from './languageHandler'; import PlatformPeg from './PlatformPeg'; import SdkConfig from './SdkConfig'; import Modal from './Modal'; @@ -27,7 +27,7 @@ const hashRegex = /#\/(groups?|room|user|settings|register|login|forgot_password const hashVarRegex = /#\/(group|room|user)\/.*$/; // Remove all but the first item in the hash path. Redact unexpected hashes. -function getRedactedHash(hash) { +function getRedactedHash(hash: string): string { // Don't leak URLs we aren't expecting - they could contain tokens/PII const match = hashRegex.exec(hash); if (!match) { @@ -44,7 +44,7 @@ function getRedactedHash(hash) { // Return the current origin, path and hash separated with a `/`. This does // not include query parameters. -function getRedactedUrl() { +function getRedactedUrl(): string { const { origin, hash } = window.location; let { pathname } = window.location; @@ -56,7 +56,25 @@ function getRedactedUrl() { return origin + pathname + getRedactedHash(hash); } -const customVariables = { +interface IData { + /* eslint-disable camelcase */ + gt_ms?: string; + e_c?: string; + e_a?: string; + e_n?: string; + e_v?: string; + ping?: string; + /* eslint-enable camelcase */ +} + +interface IVariable { + id: number; + expl: string; // explanation + example: string; // example value + getTextVariables?(): IVariables; // object to pass as 2nd argument to `_t` +} + +const customVariables: Record = { // The Matomo installation at https://matomo.riot.im is currently configured // with a limit of 10 custom variables. 'App Platform': { @@ -120,7 +138,7 @@ const customVariables = { }, }; -function whitelistRedact(whitelist, str) { +function whitelistRedact(whitelist: string[], str: string): string { if (whitelist.includes(str)) return str; return ''; } @@ -130,7 +148,7 @@ const CREATION_TS_KEY = "mx_Riot_Analytics_cts"; const VISIT_COUNT_KEY = "mx_Riot_Analytics_vc"; const LAST_VISIT_TS_KEY = "mx_Riot_Analytics_lvts"; -function getUid() { +function getUid(): string { try { let data = localStorage && localStorage.getItem(UID_KEY); if (!data && localStorage) { @@ -145,32 +163,36 @@ function getUid() { const HEARTBEAT_INTERVAL = 30 * 1000; // seconds -class Analytics { +export class Analytics { + private baseUrl: URL = null; + private siteId: string = null; + private visitVariables: Record = {}; // {[id: number]: [name: string, value: string]} + private firstPage = true; + private heartbeatIntervalID: number = null; + + private readonly creationTs: string; + private readonly lastVisitTs: string; + private readonly visitCount: string; + constructor() { - this.baseUrl = null; - this.siteId = null; - this.visitVariables = {}; - - this.firstPage = true; - this._heartbeatIntervalID = null; - this.creationTs = localStorage && localStorage.getItem(CREATION_TS_KEY); if (!this.creationTs && localStorage) { - localStorage.setItem(CREATION_TS_KEY, this.creationTs = new Date().getTime()); + localStorage.setItem(CREATION_TS_KEY, this.creationTs = String(new Date().getTime())); } this.lastVisitTs = localStorage && localStorage.getItem(LAST_VISIT_TS_KEY); - this.visitCount = localStorage && localStorage.getItem(VISIT_COUNT_KEY) || 0; + this.visitCount = localStorage && localStorage.getItem(VISIT_COUNT_KEY) || "0"; + this.visitCount = String(parseInt(this.visitCount, 10) + 1); // increment if (localStorage) { - localStorage.setItem(VISIT_COUNT_KEY, parseInt(this.visitCount, 10) + 1); + localStorage.setItem(VISIT_COUNT_KEY, this.visitCount); } } - get disabled() { + public get disabled() { return !this.baseUrl; } - canEnable() { + public canEnable() { const config = SdkConfig.get(); return navigator.doNotTrack !== "1" && config && config.piwik && config.piwik.url && config.piwik.siteId; } @@ -179,67 +201,67 @@ class Analytics { * Enable Analytics if initialized but disabled * otherwise try and initalize, no-op if piwik config missing */ - async enable() { + public async enable() { if (!this.disabled) return; if (!this.canEnable()) return; const config = SdkConfig.get(); this.baseUrl = new URL("piwik.php", config.piwik.url); // set constants - this.baseUrl.searchParams.set("rec", 1); // rec is required for tracking + this.baseUrl.searchParams.set("rec", "1"); // rec is required for tracking this.baseUrl.searchParams.set("idsite", config.piwik.siteId); // rec is required for tracking - this.baseUrl.searchParams.set("apiv", 1); // API version to use - this.baseUrl.searchParams.set("send_image", 0); // we want a 204, not a tiny GIF + this.baseUrl.searchParams.set("apiv", "1"); // API version to use + this.baseUrl.searchParams.set("send_image", "0"); // we want a 204, not a tiny GIF // set user parameters this.baseUrl.searchParams.set("_id", getUid()); // uuid this.baseUrl.searchParams.set("_idts", this.creationTs); // first ts - this.baseUrl.searchParams.set("_idvc", parseInt(this.visitCount, 10)+ 1); // visit count + this.baseUrl.searchParams.set("_idvc", this.visitCount); // visit count if (this.lastVisitTs) { this.baseUrl.searchParams.set("_viewts", this.lastVisitTs); // last visit ts } const platform = PlatformPeg.get(); - this._setVisitVariable('App Platform', platform.getHumanReadableName()); + this.setVisitVariable('App Platform', platform.getHumanReadableName()); try { - this._setVisitVariable('App Version', await platform.getAppVersion()); + this.setVisitVariable('App Version', await platform.getAppVersion()); } catch (e) { - this._setVisitVariable('App Version', 'unknown'); + this.setVisitVariable('App Version', 'unknown'); } - this._setVisitVariable('Chosen Language', getCurrentLanguage()); + this.setVisitVariable('Chosen Language', getCurrentLanguage()); const hostname = window.location.hostname; if (hostname === 'riot.im') { - this._setVisitVariable('Instance', window.location.pathname); + this.setVisitVariable('Instance', window.location.pathname); } else if (hostname.endsWith('.element.io')) { - this._setVisitVariable('Instance', hostname.replace('.element.io', '')); + this.setVisitVariable('Instance', hostname.replace('.element.io', '')); } let installedPWA = "unknown"; try { // Known to work at least for desktop Chrome - installedPWA = window.matchMedia('(display-mode: standalone)').matches; + installedPWA = String(window.matchMedia('(display-mode: standalone)').matches); } catch (e) { } - this._setVisitVariable('Installed PWA', installedPWA); + this.setVisitVariable('Installed PWA', installedPWA); let touchInput = "unknown"; try { // MDN claims broad support across browsers - touchInput = window.matchMedia('(pointer: coarse)').matches; + touchInput = String(window.matchMedia('(pointer: coarse)').matches); } catch (e) { } - this._setVisitVariable('Touch Input', touchInput); + this.setVisitVariable('Touch Input', touchInput); // start heartbeat - this._heartbeatIntervalID = window.setInterval(this.ping.bind(this), HEARTBEAT_INTERVAL); + this.heartbeatIntervalID = window.setInterval(this.ping.bind(this), HEARTBEAT_INTERVAL); } /** * Disable Analytics, stop the heartbeat and clear identifiers from localStorage */ - disable() { + public disable() { if (this.disabled) return; this.trackEvent('Analytics', 'opt-out'); - window.clearInterval(this._heartbeatIntervalID); + window.clearInterval(this.heartbeatIntervalID); this.baseUrl = null; this.visitVariables = {}; localStorage.removeItem(UID_KEY); @@ -248,7 +270,7 @@ class Analytics { localStorage.removeItem(LAST_VISIT_TS_KEY); } - async _track(data) { + private async _track(data: IData) { if (this.disabled) return; const now = new Date(); @@ -264,13 +286,13 @@ class Analytics { s: now.getSeconds(), }; - const url = new URL(this.baseUrl); + const url = new URL(this.baseUrl.toString()); // copy for (const key in params) { url.searchParams.set(key, params[key]); } try { - await window.fetch(url, { + await window.fetch(url.toString(), { method: "GET", mode: "no-cors", cache: "no-cache", @@ -281,14 +303,14 @@ class Analytics { } } - ping() { + public ping() { this._track({ - ping: 1, + ping: "1", }); - localStorage.setItem(LAST_VISIT_TS_KEY, new Date().getTime()); // update last visit ts + localStorage.setItem(LAST_VISIT_TS_KEY, String(new Date().getTime())); // update last visit ts } - trackPageChange(generationTimeMs) { + public trackPageChange(generationTimeMs?: number) { if (this.disabled) return; if (this.firstPage) { // De-duplicate first page @@ -303,11 +325,11 @@ class Analytics { } this._track({ - gt_ms: generationTimeMs, + gt_ms: String(generationTimeMs), }); } - trackEvent(category, action, name, value) { + public trackEvent(category: string, action: string, name?: string, value?: string) { if (this.disabled) return; this._track({ e_c: category, @@ -317,12 +339,12 @@ class Analytics { }); } - _setVisitVariable(key, value) { + private setVisitVariable(key: keyof typeof customVariables, value: string) { if (this.disabled) return; this.visitVariables[customVariables[key].id] = [key, value]; } - setLoggedIn(isGuest, homeserverUrl, identityServerUrl) { + public setLoggedIn(isGuest: boolean, homeserverUrl: string) { if (this.disabled) return; const config = SdkConfig.get(); @@ -330,16 +352,16 @@ class Analytics { const whitelistedHSUrls = config.piwik.whitelistedHSUrls || []; - this._setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In'); - this._setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl)); + this.setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In'); + this.setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl)); } - setBreadcrumbs(state) { + public setBreadcrumbs(state: boolean) { if (this.disabled) return; - this._setVisitVariable('Breadcrumbs', state ? 'enabled' : 'disabled'); + this.setVisitVariable('Breadcrumbs', state ? 'enabled' : 'disabled'); } - showDetailsModal = () => { + public showDetailsModal = () => { let rows = []; if (!this.disabled) { rows = Object.values(this.visitVariables); @@ -360,7 +382,7 @@ class Analytics { 'e.g. ', {}, { - CurrentPageURL: getRedactedUrl(), + CurrentPageURL: getRedactedUrl, }, ), }, @@ -401,7 +423,7 @@ class Analytics { }; } -if (!global.mxAnalytics) { - global.mxAnalytics = new Analytics(); +if (!window.mxAnalytics) { + window.mxAnalytics = new Analytics(); } -export default global.mxAnalytics; +export default window.mxAnalytics; diff --git a/src/Avatar.js b/src/Avatar.ts similarity index 85% rename from src/Avatar.js rename to src/Avatar.ts index 1c1182b98d..60bdfdcf75 100644 --- a/src/Avatar.js +++ b/src/Avatar.ts @@ -14,14 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; +import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; +import {RoomMember} from "matrix-js-sdk/src/models/room-member"; +import {User} from "matrix-js-sdk/src/models/user"; +import {Room} from "matrix-js-sdk/src/models/room"; + import {MatrixClientPeg} from './MatrixClientPeg'; import DMRoomMap from './utils/DMRoomMap'; -import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; + +export type ResizeMethod = "crop" | "scale"; // Not to be used for BaseAvatar urls as that has similar default avatar fallback already -export function avatarUrlForMember(member, width, height, resizeMethod) { - let url; +export function avatarUrlForMember(member: RoomMember, width: number, height: number, resizeMethod: ResizeMethod) { + let url: string; if (member && member.getAvatarUrl) { url = member.getAvatarUrl( MatrixClientPeg.get().getHomeserverUrl(), @@ -41,7 +46,7 @@ export function avatarUrlForMember(member, width, height, resizeMethod) { return url; } -export function avatarUrlForUser(user, width, height, resizeMethod) { +export function avatarUrlForUser(user: User, width: number, height: number, resizeMethod?: ResizeMethod) { const url = getHttpUriForMxc( MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl, Math.floor(width * window.devicePixelRatio), @@ -54,14 +59,14 @@ export function avatarUrlForUser(user, width, height, resizeMethod) { return url; } -function isValidHexColor(color) { +function isValidHexColor(color: string): boolean { return typeof color === "string" && - (color.length === 7 || color.lengh === 9) && + (color.length === 7 || color.length === 9) && color.charAt(0) === "#" && !color.substr(1).split("").some(c => isNaN(parseInt(c, 16))); } -function urlForColor(color) { +function urlForColor(color: string): string { const size = 40; const canvas = document.createElement("canvas"); canvas.width = size; @@ -79,9 +84,9 @@ function urlForColor(color) { // XXX: Ideally we'd clear this cache when the theme changes // but since this function is at global scope, it's a bit // hard to install a listener here, even if there were a clear event to listen to -const colorToDataURLCache = new Map(); +const colorToDataURLCache = new Map(); -export function defaultAvatarUrlForString(s) { +export function defaultAvatarUrlForString(s: string): string { if (!s) return ""; // XXX: should never happen but empirically does by evidence of a rageshake const defaultColors = ['#0DBD8B', '#368bd6', '#ac3ba8']; let total = 0; @@ -113,7 +118,7 @@ export function defaultAvatarUrlForString(s) { * @param {string} name * @return {string} the first letter */ -export function getInitialLetter(name) { +export function getInitialLetter(name: string): string { if (!name) { // XXX: We should find out what causes the name to sometimes be falsy. console.trace("`name` argument to `getInitialLetter` not supplied"); @@ -146,7 +151,7 @@ export function getInitialLetter(name) { return firstChar.toUpperCase(); } -export function avatarUrlForRoom(room, width, height, resizeMethod) { +export function avatarUrlForRoom(room: Room, width: number, height: number, resizeMethod?: ResizeMethod) { if (!room) return null; // null-guard const explicitRoomAvatar = room.getAvatarUrl( diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 5b368016b6..e303dd3819 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -77,13 +77,29 @@ import ErrorDialog from "./components/views/dialogs/ErrorDialog"; import WidgetStore from "./stores/WidgetStore"; import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore"; import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions"; +import { MatrixCall, CallErrorCode, CallState, CallEvent, CallParty } from "matrix-js-sdk/lib/webrtc/call"; +import Analytics from './Analytics'; -// until we ts-ify the js-sdk voip code -type Call = any; +enum AudioID { + Ring = 'ringAudio', + Ringback = 'ringbackAudio', + CallEnd = 'callendAudio', + Busy = 'busyAudio', +} + +// Unlike 'CallType' in js-sdk, this one includes screen sharing +// (because a screen sharing call is only a screen sharing call to the caller, +// to the callee it's just a video call, at least as far as the current impl +// is concerned). +export enum PlaceCallType { + Voice = 'voice', + Video = 'video', + ScreenSharing = 'screensharing', +} export default class CallHandler { - private calls = new Map(); - private audioPromises = new Map>(); + private calls = new Map(); + private audioPromises = new Map>(); static sharedInstance() { if (!window.mxCallHandler) { @@ -108,20 +124,20 @@ export default class CallHandler { } } - getCallForRoom(roomId: string): Call { + getCallForRoom(roomId: string): MatrixCall { return this.calls.get(roomId) || null; } getAnyActiveCall() { for (const call of this.calls.values()) { - if (call.state !== "ended") { + if (call.state !== CallState.Ended) { return call; } } return null; } - play(audioId: string) { + play(audioId: AudioID) { // TODO: Attach an invisible element for this instead // which listens? const audio = document.getElementById(audioId) as HTMLMediaElement; @@ -150,7 +166,7 @@ export default class CallHandler { } } - pause(audioId: string) { + pause(audioId: AudioID) { // TODO: Attach an invisible element for this instead // which listens? const audio = document.getElementById(audioId) as HTMLMediaElement; @@ -164,8 +180,19 @@ export default class CallHandler { } } - private setCallListeners(call: Call) { - call.on("error", (err) => { + private matchesCallForThisRoom(call: MatrixCall) { + // We don't allow placing more than one call per room, but that doesn't mean there + // can't be more than one, eg. in a glare situation. This checks that the given call + // is the call we consider 'the' call for its room. + const callForThisRoom = this.getCallForRoom(call.roomId); + return callForThisRoom && call.callId === callForThisRoom.callId; + } + + private setCallListeners(call: MatrixCall) { + call.on(CallEvent.Error, (err) => { + if (!this.matchesCallForThisRoom(call)) return; + + Analytics.trackEvent('voip', 'callError', 'error', err); console.error("Call error:", err); if ( MatrixClientPeg.get().getTurnServers().length === 0 && @@ -180,74 +207,103 @@ export default class CallHandler { description: err.message, }); }); - call.on("hangup", () => { + call.on(CallEvent.Hangup, () => { + if (!this.matchesCallForThisRoom(call)) return; + + Analytics.trackEvent('voip', 'callHangup'); + this.removeCallForRoom(call.roomId); }); - // map web rtc states to dummy UI state - // ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing - call.on("state", (newState, oldState) => { - if (newState === "ringing") { - this.setCallState(call, call.roomId, "ringing"); - this.pause("ringbackAudio"); - } else if (newState === "invite_sent") { - this.setCallState(call, call.roomId, "ringback"); - this.play("ringbackAudio"); - } else if (newState === "ended" && oldState === "connected") { - this.removeCallForRoom(call.roomId); - this.pause("ringbackAudio"); - this.play("callendAudio"); - } else if (newState === "ended" && oldState === "invite_sent" && - (call.hangupParty === "remote" || - (call.hangupParty === "local" && call.hangupReason === "invite_timeout") - )) { - this.setCallState(call, call.roomId, "busy"); - this.pause("ringbackAudio"); - this.play("busyAudio"); - Modal.createTrackedDialog('Call Handler', 'Call Timeout', ErrorDialog, { - title: _t('Call Timeout'), - description: _t('The remote side failed to pick up') + '.', - }); - } else if (oldState === "invite_sent") { - this.setCallState(call, call.roomId, "stop_ringback"); - this.pause("ringbackAudio"); - } else if (oldState === "ringing") { - this.setCallState(call, call.roomId, "stop_ringing"); - this.pause("ringbackAudio"); - } else if (newState === "connected") { - this.setCallState(call, call.roomId, "connected"); - this.pause("ringbackAudio"); + call.on(CallEvent.State, (newState: CallState, oldState: CallState) => { + if (!this.matchesCallForThisRoom(call)) return; + + this.setCallState(call, newState); + + switch (oldState) { + case CallState.Ringing: + this.pause(AudioID.Ring); + break; + case CallState.InviteSent: + this.pause(AudioID.Ringback); + break; } + + switch (newState) { + case CallState.Ringing: + this.play(AudioID.Ring); + break; + case CallState.InviteSent: + this.play(AudioID.Ringback); + break; + case CallState.Ended: + Analytics.trackEvent('voip', 'callEnded', 'hangupReason', call.hangupReason); + this.removeCallForRoom(call.roomId); + if (oldState === CallState.InviteSent && ( + call.hangupParty === CallParty.Remote || + (call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout) + )) { + this.play(AudioID.Busy); + let title; + let description; + if (call.hangupReason === CallErrorCode.UserHangup) { + title = _t("Call Declined"); + description = _t("The other party declined the call."); + } else if (call.hangupReason === CallErrorCode.InviteTimeout) { + title = _t("Call Failed"); + // XXX: full stop appended as some relic here, but these + // strings need proper input from design anyway, so let's + // not change this string until we have a proper one. + description = _t('The remote side failed to pick up') + '.'; + } else { + title = _t("Call Failed"); + description = _t("The call could not be established"); + } + + Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, { + title, description, + }); + } else if (call.hangupReason === CallErrorCode.AnsweredElsewhere) { + this.play(AudioID.Busy); + Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, { + title: _t("Answered Elsewhere"), + description: _t("The call was answered on another device."), + }); + } else { + this.play(AudioID.CallEnd); + } + } + }); + call.on(CallEvent.Replaced, (newCall: MatrixCall) => { + if (!this.matchesCallForThisRoom(call)) return; + + console.log(`Call ID ${call.callId} is being replaced by call ID ${newCall.callId}`); + + if (call.state === CallState.Ringing) { + this.pause(AudioID.Ring); + } else if (call.state === CallState.InviteSent) { + this.pause(AudioID.Ringback); + } + + this.calls.set(newCall.roomId, newCall); + this.setCallListeners(newCall); + this.setCallState(newCall, newCall.state); }); } - private setCallState(call: Call, roomId: string, status: string) { + private setCallState(call: MatrixCall, status: CallState) { console.log( - `Call state in ${roomId} changed to ${status} (${call ? call.call_state : "-"})`, + `Call state in ${call.roomId} changed to ${status}`, ); - if (call) { - this.calls.set(roomId, call); - } else { - this.calls.delete(roomId); - } - if (status === "ringing") { - this.play("ringAudio"); - } else if (call && call.call_state === "ringing") { - this.pause("ringAudio"); - } - - if (call) { - call.call_state = status; - } dis.dispatch({ action: 'call_state', - room_id: roomId, + room_id: call.roomId, state: status, }); } private removeCallForRoom(roomId: string) { - this.setCallState(null, roomId, null); + this.calls.delete(roomId); } private showICEFallbackPrompt() { @@ -279,36 +335,40 @@ export default class CallHandler { }, null, true); } - private onAction = (payload: ActionPayload) => { - const placeCall = (newCall) => { - this.setCallListeners(newCall); - if (payload.type === 'voice') { - newCall.placeVoiceCall(); - } else if (payload.type === 'video') { - newCall.placeVideoCall( - payload.remote_element, - payload.local_element, - ); - } else if (payload.type === 'screensharing') { - const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString(); - if (screenCapErrorString) { - this.removeCallForRoom(newCall.roomId); - console.log("Can't capture screen: " + screenCapErrorString); - Modal.createTrackedDialog('Call Handler', 'Unable to capture screen', ErrorDialog, { - title: _t('Unable to capture screen'), - description: screenCapErrorString, - }); - return; - } - newCall.placeScreenSharingCall( - payload.remote_element, - payload.local_element, - ); - } else { - console.error("Unknown conf call type: %s", payload.type); - } - } + private placeCall( + roomId: string, type: PlaceCallType, + localElement: HTMLVideoElement, remoteElement: HTMLVideoElement, + ) { + Analytics.trackEvent('voip', 'placeCall', 'type', type); + const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), roomId); + this.calls.set(roomId, call); + this.setCallListeners(call); + if (type === PlaceCallType.Voice) { + call.placeVoiceCall(); + } else if (type === 'video') { + call.placeVideoCall( + remoteElement, + localElement, + ); + } else if (type === PlaceCallType.ScreenSharing) { + const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString(); + if (screenCapErrorString) { + this.removeCallForRoom(roomId); + console.log("Can't capture screen: " + screenCapErrorString); + Modal.createTrackedDialog('Call Handler', 'Unable to capture screen', ErrorDialog, { + title: _t('Unable to capture screen'), + description: screenCapErrorString, + }); + return; + } + call.placeScreenSharingCall(remoteElement, localElement); + } else { + console.error("Unknown conf call type: %s", type); + } + } + + private onAction = (payload: ActionPayload) => { switch (payload.action) { case 'place_call': { @@ -343,8 +403,8 @@ export default class CallHandler { return; } else if (members.length === 2) { console.info("Place %s call in %s", payload.type, payload.room_id); - const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id); - placeCall(call); + + this.placeCall(payload.room_id, payload.type, payload.local_element, payload.remote_element); } else { // > 2 dis.dispatch({ action: "place_conference_call", @@ -358,6 +418,7 @@ export default class CallHandler { break; case 'place_conference_call': console.info("Place conference call in %s", payload.room_id); + Analytics.trackEvent('voip', 'placeConferenceCall'); this.startCallApp(payload.room_id, payload.type); break; case 'end_conference': @@ -383,24 +444,29 @@ export default class CallHandler { return; } - const call = payload.call; + const call = payload.call as MatrixCall; + Analytics.trackEvent('voip', 'receiveCall', 'type', call.type); + this.calls.set(call.roomId, call) this.setCallListeners(call); - this.setCallState(call, call.roomId, "ringing"); } break; case 'hangup': + case 'reject': if (!this.calls.get(payload.room_id)) { return; // no call to hangup } - this.calls.get(payload.room_id).hangup(); + if (payload.action === 'reject') { + this.calls.get(payload.room_id).reject(); + } else { + this.calls.get(payload.room_id).hangup(CallErrorCode.UserHangup, false); + } this.removeCallForRoom(payload.room_id); break; case 'answer': - if (!this.calls.get(payload.room_id)) { + if (!this.calls.has(payload.room_id)) { return; // no call to answer } this.calls.get(payload.room_id).answer(); - this.setCallState(this.calls.get(payload.room_id), payload.room_id, "connected"); dis.dispatch({ action: "view_room", room_id: payload.room_id, diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index eb8fff0eb1..cba8671143 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -17,7 +17,6 @@ limitations under the License. */ import React from "react"; -import extend from './extend'; import dis from './dispatcher/dispatcher'; import {MatrixClientPeg} from './MatrixClientPeg'; import {MatrixClient} from "matrix-js-sdk/src/client"; @@ -497,7 +496,7 @@ export default class ContentMessages { if (file.type.indexOf('image/') === 0) { content.msgtype = 'm.image'; infoForImageFile(matrixClient, roomId, file).then((imageInfo) => { - extend(content.info, imageInfo); + Object.assign(content.info, imageInfo); resolve(); }, (e) => { console.error(e); @@ -510,7 +509,7 @@ export default class ContentMessages { } else if (file.type.indexOf('video/') === 0) { content.msgtype = 'm.video'; infoForVideoFile(matrixClient, roomId, file).then((videoInfo) => { - extend(content.info, videoInfo); + Object.assign(content.info, videoInfo); resolve(); }, (e) => { content.msgtype = 'm.file'; diff --git a/src/DateUtils.js b/src/DateUtils.ts similarity index 85% rename from src/DateUtils.js rename to src/DateUtils.ts index 108697238c..9b1edf0775 100644 --- a/src/DateUtils.js +++ b/src/DateUtils.ts @@ -17,7 +17,7 @@ limitations under the License. import { _t } from './languageHandler'; -function getDaysArray() { +function getDaysArray(): string[] { return [ _t('Sun'), _t('Mon'), @@ -29,7 +29,7 @@ function getDaysArray() { ]; } -function getMonthsArray() { +function getMonthsArray(): string[] { return [ _t('Jan'), _t('Feb'), @@ -46,11 +46,11 @@ function getMonthsArray() { ]; } -function pad(n) { +function pad(n: number): string { return (n < 10 ? '0' : '') + n; } -function twelveHourTime(date, showSeconds=false) { +function twelveHourTime(date: Date, showSeconds = false): string { let hours = date.getHours() % 12; const minutes = pad(date.getMinutes()); const ampm = date.getHours() >= 12 ? _t('PM') : _t('AM'); @@ -62,7 +62,7 @@ function twelveHourTime(date, showSeconds=false) { return `${hours}:${minutes}${ampm}`; } -export function formatDate(date, showTwelveHour=false) { +export function formatDate(date: Date, showTwelveHour = false): string { const now = new Date(); const days = getDaysArray(); const months = getMonthsArray(); @@ -86,7 +86,7 @@ export function formatDate(date, showTwelveHour=false) { return formatFullDate(date, showTwelveHour); } -export function formatFullDateNoTime(date) { +export function formatFullDateNoTime(date: Date): string { const days = getDaysArray(); const months = getMonthsArray(); return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s', { @@ -97,7 +97,7 @@ export function formatFullDateNoTime(date) { }); } -export function formatFullDate(date, showTwelveHour=false) { +export function formatFullDate(date: Date, showTwelveHour = false): string { const days = getDaysArray(); const months = getMonthsArray(); return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s', { @@ -109,14 +109,14 @@ export function formatFullDate(date, showTwelveHour=false) { }); } -export function formatFullTime(date, showTwelveHour=false) { +export function formatFullTime(date: Date, showTwelveHour = false): string { if (showTwelveHour) { return twelveHourTime(date, true); } return pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds()); } -export function formatTime(date, showTwelveHour=false) { +export function formatTime(date: Date, showTwelveHour = false): string { if (showTwelveHour) { return twelveHourTime(date); } @@ -124,7 +124,7 @@ export function formatTime(date, showTwelveHour=false) { } const MILLIS_IN_DAY = 86400000; -export function wantsDateSeparator(prevEventDate, nextEventDate) { +export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): boolean { if (!nextEventDate || !prevEventDate) { return false; } diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index f2cd1bce9e..6293de063d 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -23,6 +23,7 @@ import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { MatrixClient } from "matrix-js-sdk/src/client"; import {IMatrixClientCreds, MatrixClientPeg} from './MatrixClientPeg'; +import SecurityCustomisations from "./customisations/Security"; import EventIndexPeg from './indexing/EventIndexPeg'; import createMatrixClient from './utils/createMatrixClient'; import Analytics from './Analytics'; @@ -567,6 +568,8 @@ function persistCredentialsToLocalStorage(credentials: IMatrixClientCreds): void localStorage.setItem("mx_device_id", credentials.deviceId); } + SecurityCustomisations.persistCredentials?.(credentials); + console.log(`Session persisted for ${credentials.userId}`); } diff --git a/src/Login.ts b/src/Login.ts index 38d78feab6..ae4aa226ed 100644 --- a/src/Login.ts +++ b/src/Login.ts @@ -22,6 +22,7 @@ limitations under the License. import Matrix from "matrix-js-sdk"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { IMatrixClientCreds } from "./MatrixClientPeg"; +import SecurityCustomisations from "./customisations/Security"; interface ILoginOptions { defaultDeviceDisplayName?: string; @@ -222,11 +223,15 @@ export async function sendLoginRequest( } } - return { + const creds: IMatrixClientCreds = { homeserverUrl: hsUrl, identityServerUrl: isUrl, userId: data.user_id, deviceId: data.device_id, accessToken: data.access_token, }; + + SecurityCustomisations.examineLoginResponse?.(data, creds); + + return creds; } diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 4651a0afe3..5bb10dfa89 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -17,6 +17,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { ICreateClientOpts } from 'matrix-js-sdk/src/matrix'; import {MatrixClient} from 'matrix-js-sdk/src/client'; import {MemoryStore} from 'matrix-js-sdk/src/store/memory'; import * as utils from 'matrix-js-sdk/src/utils'; @@ -249,8 +250,7 @@ class _MatrixClientPeg implements IMatrixClientPeg { } private createClient(creds: IMatrixClientCreds): void { - // TODO: Make these opts typesafe with the js-sdk - const opts = { + const opts: ICreateClientOpts = { baseUrl: creds.homeserverUrl, idBaseUrl: creds.identityServerUrl, accessToken: creds.accessToken, diff --git a/src/Modal.tsx b/src/Modal.tsx index 0a36813961..2f761e7393 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -28,7 +28,7 @@ import AsyncWrapper from './AsyncWrapper'; const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer"; -interface IModal { +export interface IModal { elem: React.ReactNode; className?: string; beforeClosePromise?: Promise; @@ -38,7 +38,7 @@ interface IModal { close(...args: T): void; } -interface IHandle { +export interface IHandle { finished: Promise; close(...args: T): void; } @@ -132,7 +132,7 @@ export class ModalManager { public createTrackedDialogAsync( analyticsAction: string, analyticsInfo: string, - ...rest: Parameters + ...rest: Parameters ) { Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); return this.createDialogAsync(...rest); diff --git a/src/Notifier.ts b/src/Notifier.ts index 2643de1abc..1899896f9b 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -218,7 +218,7 @@ export const Notifier = { // calculated value. It is determined based upon whether or not the master rule is enabled // and other flags. Setting it here would cause a circular reference. - Analytics.trackEvent('Notifier', 'Set Enabled', enable); + Analytics.trackEvent('Notifier', 'Set Enabled', String(enable)); // make sure that we persist the current setting audio_enabled setting // before changing anything @@ -287,7 +287,7 @@ export const Notifier = { setPromptHidden: function(hidden: boolean, persistent = true) { this.toolbarHidden = hidden; - Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', hidden); + Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', String(hidden)); hideNotificationsToast(); diff --git a/src/Presence.js b/src/Presence.ts similarity index 65% rename from src/Presence.js rename to src/Presence.ts index 42bca35f96..660bb0ac94 100644 --- a/src/Presence.js +++ b/src/Presence.ts @@ -19,30 +19,34 @@ limitations under the License. import {MatrixClientPeg} from "./MatrixClientPeg"; import dis from "./dispatcher/dispatcher"; import Timer from './utils/Timer'; +import {ActionPayload} from "./dispatcher/payloads"; - // Time in ms after that a user is considered as unavailable/away +// Time in ms after that a user is considered as unavailable/away const UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins -const PRESENCE_STATES = ["online", "offline", "unavailable"]; + +enum State { + Online = "online", + Offline = "offline", + Unavailable = "unavailable", +} class Presence { - constructor() { - this._activitySignal = null; - this._unavailableTimer = null; - this._onAction = this._onAction.bind(this); - this._dispatcherRef = null; - } + private unavailableTimer: Timer = null; + private dispatcherRef: string = null; + private state: State = null; + /** * Start listening the user activity to evaluate his presence state. * Any state change will be sent to the homeserver. */ - async start() { - this._unavailableTimer = new Timer(UNAVAILABLE_TIME_MS); + public async start() { + this.unavailableTimer = new Timer(UNAVAILABLE_TIME_MS); // the user_activity_start action starts the timer - this._dispatcherRef = dis.register(this._onAction); - while (this._unavailableTimer) { + this.dispatcherRef = dis.register(this.onAction); + while (this.unavailableTimer) { try { - await this._unavailableTimer.finished(); - this.setState("unavailable"); + await this.unavailableTimer.finished(); + this.setState(State.Unavailable); } catch (e) { /* aborted, stop got called */ } } } @@ -50,14 +54,14 @@ class Presence { /** * Stop tracking user activity */ - stop() { - if (this._dispatcherRef) { - dis.unregister(this._dispatcherRef); - this._dispatcherRef = null; + public stop() { + if (this.dispatcherRef) { + dis.unregister(this.dispatcherRef); + this.dispatcherRef = null; } - if (this._unavailableTimer) { - this._unavailableTimer.abort(); - this._unavailableTimer = null; + if (this.unavailableTimer) { + this.unavailableTimer.abort(); + this.unavailableTimer = null; } } @@ -65,14 +69,14 @@ class Presence { * Get the current presence state. * @returns {string} the presence state (see PRESENCE enum) */ - getState() { + public getState() { return this.state; } - _onAction(payload) { + private onAction = (payload: ActionPayload) => { if (payload.action === 'user_activity') { - this.setState("online"); - this._unavailableTimer.restart(); + this.setState(State.Online); + this.unavailableTimer.restart(); } } @@ -81,13 +85,11 @@ class Presence { * If the state has changed, the homeserver will be notified. * @param {string} newState the new presence state (see PRESENCE enum) */ - async setState(newState) { + private async setState(newState: State) { if (newState === this.state) { return; } - if (PRESENCE_STATES.indexOf(newState) === -1) { - throw new Error("Bad presence state: " + newState); - } + const oldState = this.state; this.state = newState; diff --git a/src/Roles.js b/src/Roles.ts similarity index 87% rename from src/Roles.js rename to src/Roles.ts index 7cc3c880d7..b4be97fdce 100644 --- a/src/Roles.js +++ b/src/Roles.ts @@ -13,9 +13,10 @@ 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 { _t } from './languageHandler'; -export function levelRoleMap(usersDefault) { +export function levelRoleMap(usersDefault: number) { return { undefined: _t('Default'), 0: _t('Restricted'), @@ -25,7 +26,7 @@ export function levelRoleMap(usersDefault) { }; } -export function textualPowerLevel(level, usersDefault) { +export function textualPowerLevel(level: number, usersDefault: number): string { const LEVEL_ROLE_MAP = levelRoleMap(usersDefault); if (LEVEL_ROLE_MAP[level]) { return LEVEL_ROLE_MAP[level]; diff --git a/src/Searching.js b/src/Searching.js index b1507e6a49..f65b8920b3 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -360,7 +360,7 @@ function combineEvents(previousSearchResult, localEvents = undefined, serverEven let oldestEventFrom = previousSearchResult.oldestEventFrom; response.highlights = previousSearchResult.highlights; - if (localEvents && serverEvents) { + if (localEvents && serverEvents && serverEvents.results) { // This is a first search call, combine the events from the server and // the local index. Note where our oldest event came from, we shall // fetch the next batch of events from the other source. @@ -379,7 +379,7 @@ function combineEvents(previousSearchResult, localEvents = undefined, serverEven oldestEventFrom = "local"; } combineEventSources(previousSearchResult, response, localEvents.results, cachedEvents); - } else if (serverEvents) { + } else if (serverEvents && serverEvents.results) { // This is a pagination call fetching more events from the server, // meaning that our oldest event was in the local index. // Change the source of the oldest event if our server event is older @@ -454,7 +454,7 @@ function combineResponses(previousSearchResult, localEvents = undefined, serverE return response; } -function restoreEncryptionInfo(searchResultSlice) { +function restoreEncryptionInfo(searchResultSlice = []) { for (let i = 0; i < searchResultSlice.length; i++) { const timeline = searchResultSlice[i].context.getTimeline(); @@ -517,7 +517,7 @@ async function combinedPagination(searchResult) { }, }; - const oldResultCount = searchResult.results.length; + const oldResultCount = searchResult.results ? searchResult.results.length : 0; // Let the client process the combined result. const result = client._processRoomEventsSearch(searchResult, response); diff --git a/src/SecurityManager.js b/src/SecurityManager.ts similarity index 81% rename from src/SecurityManager.js rename to src/SecurityManager.ts index 3272c0f015..220320470a 100644 --- a/src/SecurityManager.js +++ b/src/SecurityManager.ts @@ -14,32 +14,38 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { ICryptoCallbacks, IDeviceTrustLevel, ISecretStorageKeyInfo } from 'matrix-js-sdk/src/matrix'; +import { MatrixClient } from 'matrix-js-sdk/src/client'; import Modal from './Modal'; import * as sdk from './index'; import {MatrixClientPeg} from './MatrixClientPeg'; import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase'; import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; import { _t } from './languageHandler'; -import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib"; +import { encodeBase64 } from "matrix-js-sdk/src/crypto/olmlib"; import { isSecureBackupRequired } from './utils/WellKnownUtils'; import AccessSecretStorageDialog from './components/views/dialogs/security/AccessSecretStorageDialog'; import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog'; import SettingsStore from "./settings/SettingsStore"; +import SecurityCustomisations from "./customisations/Security"; // This stores the secret storage private keys in memory for the JS SDK. This is // only meant to act as a cache to avoid prompting the user multiple times // during the same single operation. Use `accessSecretStorage` below to scope a // single secret storage operation, as it will clear the cached keys once the // operation ends. -let secretStorageKeys = {}; -let secretStorageKeyInfo = {}; +let secretStorageKeys: Record = {}; +let secretStorageKeyInfo: Record = {}; let secretStorageBeingAccessed = false; let nonInteractive = false; -let dehydrationCache = {}; +let dehydrationCache: { + key?: Uint8Array, + keyInfo?: ISecretStorageKeyInfo, +} = {}; -function isCachingAllowed() { +function isCachingAllowed(): boolean { return secretStorageBeingAccessed; } @@ -50,7 +56,7 @@ function isCachingAllowed() { * * @returns {bool} */ -export function isSecretStorageBeingAccessed() { +export function isSecretStorageBeingAccessed(): boolean { return secretStorageBeingAccessed; } @@ -60,7 +66,7 @@ export class AccessCancelledError extends Error { } } -async function confirmToDismiss() { +async function confirmToDismiss(): Promise { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const [sure] = await Modal.createDialog(QuestionDialog, { title: _t("Cancel entering passphrase?"), @@ -72,7 +78,9 @@ async function confirmToDismiss() { return !sure; } -function makeInputToKey(keyInfo) { +function makeInputToKey( + keyInfo: ISecretStorageKeyInfo, +): (keyParams: { passphrase: string, recoveryKey: string }) => Promise { return async ({ passphrase, recoveryKey }) => { if (passphrase) { return deriveKey( @@ -86,7 +94,10 @@ function makeInputToKey(keyInfo) { }; } -async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { +async function getSecretStorageKey( + { keys: keyInfos }: { keys: Record }, + ssssItemName, +): Promise<[string, Uint8Array]> { const keyInfoEntries = Object.entries(keyInfos); if (keyInfoEntries.length > 1) { throw new Error("Multiple storage key requests not implemented"); @@ -100,11 +111,18 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { if (dehydrationCache.key) { if (await MatrixClientPeg.get().checkSecretStorageKey(dehydrationCache.key, keyInfo)) { - cacheSecretStorageKey(keyId, dehydrationCache.key, keyInfo); + cacheSecretStorageKey(keyId, keyInfo, dehydrationCache.key); return [keyId, dehydrationCache.key]; } } + const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.(); + if (keyFromCustomisations) { + console.log("Using key from security customisations (secret storage)") + cacheSecretStorageKey(keyId, keyInfo, keyFromCustomisations); + return [keyId, keyFromCustomisations]; + } + if (nonInteractive) { throw new Error("Could not unlock non-interactively"); } @@ -139,12 +157,21 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { const key = await inputToKey(input); // Save to cache to avoid future prompts in the current session - cacheSecretStorageKey(keyId, key, keyInfo); + cacheSecretStorageKey(keyId, keyInfo, key); return [keyId, key]; } -export async function getDehydrationKey(keyInfo, checkFunc) { +export async function getDehydrationKey( + keyInfo: ISecretStorageKeyInfo, + checkFunc: (Uint8Array) => void, +): Promise { + const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.(); + if (keyFromCustomisations) { + console.log("Using key from security customisations (dehydration)") + return keyFromCustomisations; + } + const inputToKey = makeInputToKey(keyInfo); const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "", AccessSecretStorageDialog, @@ -185,20 +212,24 @@ export async function getDehydrationKey(keyInfo, checkFunc) { return key; } -function cacheSecretStorageKey(keyId, key, keyInfo) { +function cacheSecretStorageKey( + keyId: string, + keyInfo: ISecretStorageKeyInfo, + key: Uint8Array, +): void { if (isCachingAllowed()) { secretStorageKeys[keyId] = key; secretStorageKeyInfo[keyId] = keyInfo; } } -const onSecretRequested = async function({ - user_id: userId, - device_id: deviceId, - request_id: requestId, - name, - device_trust: deviceTrust, -}) { +async function onSecretRequested( + userId: string, + deviceId: string, + requestId: string, + name: string, + deviceTrust: IDeviceTrustLevel, +): Promise { console.log("onSecretRequested", userId, deviceId, requestId, name, deviceTrust); const client = MatrixClientPeg.get(); if (userId !== client.getUserId()) { @@ -233,16 +264,16 @@ const onSecretRequested = async function({ return key && encodeBase64(key); } console.warn("onSecretRequested didn't recognise the secret named ", name); -}; +} -export const crossSigningCallbacks = { +export const crossSigningCallbacks: ICryptoCallbacks = { getSecretStorageKey, cacheSecretStorageKey, onSecretRequested, getDehydrationKey, }; -export async function promptForBackupPassphrase() { +export async function promptForBackupPassphrase(): Promise { let key; const { finished } = Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, { @@ -292,7 +323,7 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f /* priority = */ false, /* static = */ true, /* options = */ { - onBeforeClose(reason) { + onBeforeClose: async (reason) => { // If Secure Backup is required, you cannot leave the modal. if (reason === "backgroundClick") { return !isSecureBackupRequired(); @@ -329,20 +360,25 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f const keyId = Object.keys(secretStorageKeys)[0]; if (keyId && SettingsStore.getValue("feature_dehydration")) { - const dehydrationKeyInfo = - secretStorageKeyInfo[keyId] && secretStorageKeyInfo[keyId].passphrase - ? {passphrase: secretStorageKeyInfo[keyId].passphrase} - : {}; + let dehydrationKeyInfo = {}; + if (secretStorageKeyInfo[keyId] && secretStorageKeyInfo[keyId].passphrase) { + dehydrationKeyInfo = { passphrase: secretStorageKeyInfo[keyId].passphrase }; + } console.log("Setting dehydration key"); await cli.setDehydrationKey(secretStorageKeys[keyId], dehydrationKeyInfo, "Backup device"); + } else if (!keyId) { + console.warn("Not setting dehydration key: no SSSS key found"); } else { - console.log("Not setting dehydration key: no SSSS key found"); + console.log("Not setting dehydration key: feature disabled"); } } // `return await` needed here to ensure `finally` block runs after the // inner operation completes. return await func(); + } catch (e) { + SecurityCustomisations.catchAccessSecretStorageError?.(e); + console.error(e); } finally { // Clear secret storage key cache now that work is complete secretStorageBeingAccessed = false; @@ -354,7 +390,9 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f } // FIXME: this function name is a bit of a mouthful -export async function tryToUnlockSecretStorageWithDehydrationKey(client) { +export async function tryToUnlockSecretStorageWithDehydrationKey( + client: MatrixClient, +): Promise { const key = dehydrationCache.key; let restoringBackup = false; if (key && await client.isSecretStorageReady()) { @@ -366,10 +404,10 @@ export async function tryToUnlockSecretStorageWithDehydrationKey(client) { // we also need to set a new dehydrated device to replace the // device we rehydrated - const dehydrationKeyInfo = - dehydrationCache.keyInfo && dehydrationCache.keyInfo.passphrase - ? {passphrase: dehydrationCache.keyInfo.passphrase} - : {}; + let dehydrationKeyInfo = {}; + if (dehydrationCache.keyInfo && dehydrationCache.keyInfo.passphrase) { + dehydrationKeyInfo = { passphrase: dehydrationCache.keyInfo.passphrase }; + } await client.setDehydrationKey(key, dehydrationKeyInfo, "Backup device"); // and restore from backup diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 34d40bf1fd..d86d88a697 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -198,59 +198,30 @@ function textForRelatedGroupsEvent(ev) { function textForServerACLEvent(ev) { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const prevContent = ev.getPrevContent(); - const changes = []; const current = ev.getContent(); const prev = { deny: Array.isArray(prevContent.deny) ? prevContent.deny : [], allow: Array.isArray(prevContent.allow) ? prevContent.allow : [], allow_ip_literals: !(prevContent.allow_ip_literals === false), }; + let text = ""; if (prev.deny.length === 0 && prev.allow.length === 0) { - text = `${senderDisplayName} set server ACLs for this room: `; + text = _t("%(senderDisplayName)s set the server ACLs for this room.", {senderDisplayName}); } else { - text = `${senderDisplayName} changed the server ACLs for this room: `; + text = _t("%(senderDisplayName)s changed the server ACLs for this room.", {senderDisplayName}); } if (!Array.isArray(current.allow)) { current.allow = []; } - /* If we know for sure everyone is banned, don't bother showing the diff view */ + + // If we know for sure everyone is banned, mark the room as obliterated if (current.allow.length === 0) { - return text + "🎉 All servers are banned from participating! This room can no longer be used."; + return text + " " + _t("🎉 All servers are banned from participating! This room can no longer be used."); } - if (!Array.isArray(current.deny)) { - current.deny = []; - } - - const bannedServers = current.deny.filter((srv) => typeof(srv) === 'string' && !prev.deny.includes(srv)); - const unbannedServers = prev.deny.filter((srv) => typeof(srv) === 'string' && !current.deny.includes(srv)); - const allowedServers = current.allow.filter((srv) => typeof(srv) === 'string' && !prev.allow.includes(srv)); - const unallowedServers = prev.allow.filter((srv) => typeof(srv) === 'string' && !current.allow.includes(srv)); - - if (bannedServers.length > 0) { - changes.push(`Servers matching ${bannedServers.join(", ")} are now banned.`); - } - - if (unbannedServers.length > 0) { - changes.push(`Servers matching ${unbannedServers.join(", ")} were removed from the ban list.`); - } - - if (allowedServers.length > 0) { - changes.push(`Servers matching ${allowedServers.join(", ")} are now allowed.`); - } - - if (unallowedServers.length > 0) { - changes.push(`Servers matching ${unallowedServers.join(", ")} were removed from the allowed list.`); - } - - if (prev.allow_ip_literals !== current.allow_ip_literals) { - const allowban = current.allow_ip_literals ? "allowed" : "banned"; - changes.push(`Participating from a server using an IP literal hostname is now ${allowban}.`); - } - - return text + changes.join(" "); + return text; } function textForMessageEvent(ev) { @@ -329,14 +300,27 @@ function textForCallHangupEvent(event) { reason = _t('(not supported by this browser)'); } else if (eventContent.reason) { if (eventContent.reason === "ice_failed") { + // We couldn't establish a connection at all reason = _t('(could not connect media)'); + } else if (eventContent.reason === "ice_timeout") { + // We established a connection but it died + reason = _t('(connection failed)'); + } else if (eventContent.reason === "user_media_failed") { + // The other side couldn't open capture devices + reason = _t("(their device couldn't start the camera / microphone)"); + } else if (eventContent.reason === "unknown_error") { + // An error code the other side doesn't have a way to express + // (as opposed to an error code they gave but we don't know about, + // in which case we show the error code) + reason = _t("(an error occurred)"); } else if (eventContent.reason === "invite_timeout") { reason = _t('(no answer)'); - } else if (eventContent.reason === "user hangup") { + } else if (eventContent.reason === "user hangup" || eventContent.reason === "user_hangup") { // workaround for https://github.com/vector-im/element-web/issues/5178 // it seems Android randomly sets a reason of "user hangup" which is // interpreted as an error code :( // https://github.com/vector-im/riot-android/issues/2623 + // Also the correct hangup code as of VoIP v1 (with underscore) reason = ''; } else { reason = _t('(unknown failure: %(reason)s)', {reason: eventContent.reason}); @@ -345,6 +329,11 @@ function textForCallHangupEvent(event) { return _t('%(senderName)s ended the call.', {senderName}) + ' ' + reason; } +function textForCallRejectEvent(event) { + const senderName = event.sender ? event.sender.name : _t('Someone'); + return _t('%(senderName)s declined the call.', {senderName}); +} + function textForCallInviteEvent(event) { const senderName = event.sender ? event.sender.name : _t('Someone'); // FIXME: Find a better way to determine this from the event? @@ -574,6 +563,7 @@ const handlers = { 'm.call.invite': textForCallInviteEvent, 'm.call.answer': textForCallAnswerEvent, 'm.call.hangup': textForCallHangupEvent, + 'm.call.reject': textForCallRejectEvent, }; const stateHandlers = { diff --git a/src/UserActivity.js b/src/UserActivity.ts similarity index 61% rename from src/UserActivity.js rename to src/UserActivity.ts index 0174aebaf5..606075ec7c 100644 --- a/src/UserActivity.js +++ b/src/UserActivity.ts @@ -38,26 +38,23 @@ const RECENTLY_ACTIVE_THRESHOLD_MS = 2 * 60 * 1000; * see doc on the userActive* functions for what these mean. */ export default class UserActivity { - constructor(windowObj, documentObj) { - this._window = windowObj; - this._document = documentObj; + private readonly activeNowTimeout: Timer; + private readonly activeRecentlyTimeout: Timer; + private attachedActiveNowTimers: Timer[] = []; + private attachedActiveRecentlyTimers: Timer[] = []; + private lastScreenX = 0; + private lastScreenY = 0; - this._attachedActiveNowTimers = []; - this._attachedActiveRecentlyTimers = []; - this._activeNowTimeout = new Timer(CURRENTLY_ACTIVE_THRESHOLD_MS); - this._activeRecentlyTimeout = new Timer(RECENTLY_ACTIVE_THRESHOLD_MS); - this._onUserActivity = this._onUserActivity.bind(this); - this._onWindowBlurred = this._onWindowBlurred.bind(this); - this._onPageVisibilityChanged = this._onPageVisibilityChanged.bind(this); - this.lastScreenX = 0; - this.lastScreenY = 0; + constructor(private readonly window: Window, private readonly document: Document) { + this.activeNowTimeout = new Timer(CURRENTLY_ACTIVE_THRESHOLD_MS); + this.activeRecentlyTimeout = new Timer(RECENTLY_ACTIVE_THRESHOLD_MS); } static sharedInstance() { - if (global.mxUserActivity === undefined) { - global.mxUserActivity = new UserActivity(window, document); + if (window.mxUserActivity === undefined) { + window.mxUserActivity = new UserActivity(window, document); } - return global.mxUserActivity; + return window.mxUserActivity; } /** @@ -69,8 +66,8 @@ export default class UserActivity { * later on when the user does become active. * @param {Timer} timer the timer to use */ - timeWhileActiveNow(timer) { - this._timeWhile(timer, this._attachedActiveNowTimers); + public timeWhileActiveNow(timer: Timer) { + this.timeWhile(timer, this.attachedActiveNowTimers); if (this.userActiveNow()) { timer.start(); } @@ -85,14 +82,14 @@ export default class UserActivity { * later on when the user does become active. * @param {Timer} timer the timer to use */ - timeWhileActiveRecently(timer) { - this._timeWhile(timer, this._attachedActiveRecentlyTimers); + public timeWhileActiveRecently(timer: Timer) { + this.timeWhile(timer, this.attachedActiveRecentlyTimers); if (this.userActiveRecently()) { timer.start(); } } - _timeWhile(timer, attachedTimers) { + private timeWhile(timer: Timer, attachedTimers: Timer[]) { // important this happens first const index = attachedTimers.indexOf(timer); if (index === -1) { @@ -112,36 +109,36 @@ export default class UserActivity { /** * Start listening to user activity */ - start() { - this._document.addEventListener('mousedown', this._onUserActivity); - this._document.addEventListener('mousemove', this._onUserActivity); - this._document.addEventListener('keydown', this._onUserActivity); - this._document.addEventListener("visibilitychange", this._onPageVisibilityChanged); - this._window.addEventListener("blur", this._onWindowBlurred); - this._window.addEventListener("focus", this._onUserActivity); + public start() { + this.document.addEventListener('mousedown', this.onUserActivity); + this.document.addEventListener('mousemove', this.onUserActivity); + this.document.addEventListener('keydown', this.onUserActivity); + this.document.addEventListener("visibilitychange", this.onPageVisibilityChanged); + this.window.addEventListener("blur", this.onWindowBlurred); + this.window.addEventListener("focus", this.onUserActivity); // can't use document.scroll here because that's only the document // itself being scrolled. Need to use addEventListener's useCapture. // also this needs to be the wheel event, not scroll, as scroll is // fired when the view scrolls down for a new message. - this._window.addEventListener('wheel', this._onUserActivity, { - passive: true, capture: true, + this.window.addEventListener('wheel', this.onUserActivity, { + passive: true, + capture: true, }); } /** * Stop tracking user activity */ - stop() { - this._document.removeEventListener('mousedown', this._onUserActivity); - this._document.removeEventListener('mousemove', this._onUserActivity); - this._document.removeEventListener('keydown', this._onUserActivity); - this._window.removeEventListener('wheel', this._onUserActivity, { - passive: true, capture: true, + public stop() { + this.document.removeEventListener('mousedown', this.onUserActivity); + this.document.removeEventListener('mousemove', this.onUserActivity); + this.document.removeEventListener('keydown', this.onUserActivity); + this.window.removeEventListener('wheel', this.onUserActivity, { + capture: true, }); - - this._document.removeEventListener("visibilitychange", this._onPageVisibilityChanged); - this._window.removeEventListener("blur", this._onWindowBlurred); - this._window.removeEventListener("focus", this._onUserActivity); + this.document.removeEventListener("visibilitychange", this.onPageVisibilityChanged); + this.window.removeEventListener("blur", this.onWindowBlurred); + this.window.removeEventListener("focus", this.onUserActivity); } /** @@ -151,8 +148,8 @@ export default class UserActivity { * user's attention at any given moment. * @returns {boolean} true if user is currently 'active' */ - userActiveNow() { - return this._activeNowTimeout.isRunning(); + public userActiveNow() { + return this.activeNowTimeout.isRunning(); } /** @@ -163,27 +160,27 @@ export default class UserActivity { * (or they may have gone to make tea and left the window focused). * @returns {boolean} true if user has been active recently */ - userActiveRecently() { - return this._activeRecentlyTimeout.isRunning(); + public userActiveRecently() { + return this.activeRecentlyTimeout.isRunning(); } - _onPageVisibilityChanged(e) { - if (this._document.visibilityState === "hidden") { - this._activeNowTimeout.abort(); - this._activeRecentlyTimeout.abort(); + private onPageVisibilityChanged = e => { + if (this.document.visibilityState === "hidden") { + this.activeNowTimeout.abort(); + this.activeRecentlyTimeout.abort(); } else { - this._onUserActivity(e); + this.onUserActivity(e); } - } + }; - _onWindowBlurred() { - this._activeNowTimeout.abort(); - this._activeRecentlyTimeout.abort(); - } + private onWindowBlurred = () => { + this.activeNowTimeout.abort(); + this.activeRecentlyTimeout.abort(); + }; - _onUserActivity(event) { + private onUserActivity = (event: MouseEvent) => { // ignore anything if the window isn't focused - if (!this._document.hasFocus()) return; + if (!this.document.hasFocus()) return; if (event.screenX && event.type === "mousemove") { if (event.screenX === this.lastScreenX && event.screenY === this.lastScreenY) { @@ -195,25 +192,25 @@ export default class UserActivity { } dis.dispatch({action: 'user_activity'}); - if (!this._activeNowTimeout.isRunning()) { - this._activeNowTimeout.start(); + if (!this.activeNowTimeout.isRunning()) { + this.activeNowTimeout.start(); dis.dispatch({action: 'user_activity_start'}); - this._runTimersUntilTimeout(this._attachedActiveNowTimers, this._activeNowTimeout); + UserActivity.runTimersUntilTimeout(this.attachedActiveNowTimers, this.activeNowTimeout); } else { - this._activeNowTimeout.restart(); + this.activeNowTimeout.restart(); } - if (!this._activeRecentlyTimeout.isRunning()) { - this._activeRecentlyTimeout.start(); + if (!this.activeRecentlyTimeout.isRunning()) { + this.activeRecentlyTimeout.start(); - this._runTimersUntilTimeout(this._attachedActiveRecentlyTimers, this._activeRecentlyTimeout); + UserActivity.runTimersUntilTimeout(this.attachedActiveRecentlyTimers, this.activeRecentlyTimeout); } else { - this._activeRecentlyTimeout.restart(); + this.activeRecentlyTimeout.restart(); } - } + }; - async _runTimersUntilTimeout(attachedTimers, timeout) { + private static async runTimersUntilTimeout(attachedTimers: Timer[], timeout: Timer) { attachedTimers.forEach((t) => t.start()); try { await timeout.finished(); diff --git a/src/WhoIsTyping.js b/src/WhoIsTyping.ts similarity index 72% rename from src/WhoIsTyping.js rename to src/WhoIsTyping.ts index d11cddf487..a8ca425ea8 100644 --- a/src/WhoIsTyping.js +++ b/src/WhoIsTyping.ts @@ -14,19 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ +import {Room} from "matrix-js-sdk/src/models/room"; +import {RoomMember} from "matrix-js-sdk/src/models/room-member"; + import {MatrixClientPeg} from "./MatrixClientPeg"; import { _t } from './languageHandler'; -export function usersTypingApartFromMeAndIgnored(room) { - return usersTyping( - room, [MatrixClientPeg.get().credentials.userId].concat(MatrixClientPeg.get().getIgnoredUsers()), - ); +export function usersTypingApartFromMeAndIgnored(room: Room): RoomMember[] { + return usersTyping(room, [MatrixClientPeg.get().getUserId()].concat(MatrixClientPeg.get().getIgnoredUsers())); } -export function usersTypingApartFromMe(room) { - return usersTyping( - room, [MatrixClientPeg.get().credentials.userId], - ); +export function usersTypingApartFromMe(room: Room): RoomMember[] { + return usersTyping(room, [MatrixClientPeg.get().getUserId()]); } /** @@ -34,15 +33,11 @@ export function usersTypingApartFromMe(room) { * to exclude, return a list of user objects who are typing. * @param {Room} room: room object to get users from. * @param {string[]} exclude: list of user mxids to exclude. - * @returns {string[]} list of user objects who are typing. + * @returns {RoomMember[]} list of user objects who are typing. */ -export function usersTyping(room, exclude) { +export function usersTyping(room: Room, exclude: string[] = []): RoomMember[] { const whoIsTyping = []; - if (exclude === undefined) { - exclude = []; - } - const memberKeys = Object.keys(room.currentState.members); for (let i = 0; i < memberKeys.length; ++i) { const userId = memberKeys[i]; @@ -57,20 +52,21 @@ export function usersTyping(room, exclude) { return whoIsTyping; } -export function whoIsTypingString(whoIsTyping, limit) { +export function whoIsTypingString(whoIsTyping: RoomMember[], limit: number): string { let othersCount = 0; if (whoIsTyping.length > limit) { othersCount = whoIsTyping.length - limit + 1; } + if (whoIsTyping.length === 0) { return ''; } else if (whoIsTyping.length === 1) { return _t('%(displayName)s is typing …', {displayName: whoIsTyping[0].name}); } - const names = whoIsTyping.map(function(m) { - return m.name; - }); - if (othersCount>=1) { + + const names = whoIsTyping.map(m => m.name); + + if (othersCount >= 1) { return _t('%(names)s and %(count)s others are typing …', { names: names.slice(0, limit - 1).join(', '), count: othersCount, diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 434b931296..b49a90d175 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -205,7 +205,7 @@ export const RovingTabIndexProvider: React.FC = ({children, handleHomeEn // onFocus should be called when the index gained focus in any manner // isActive should be used to set tabIndex in a manner such as `tabIndex={isActive ? 0 : -1}` // ref should be passed to a DOM node which will be used for DOM compareDocumentPosition -export const useRovingTabIndex = (inputRef: Ref): [FocusHandler, boolean, Ref] => { +export const useRovingTabIndex = (inputRef?: Ref): [FocusHandler, boolean, Ref] => { const context = useContext(RovingTabIndexContext); let ref = useRef(null); diff --git a/src/actions/TagOrderActions.ts b/src/actions/TagOrderActions.ts index c203172874..021cd11b55 100644 --- a/src/actions/TagOrderActions.ts +++ b/src/actions/TagOrderActions.ts @@ -17,14 +17,14 @@ limitations under the License. import Analytics from '../Analytics'; import { asyncAction } from './actionCreators'; -import TagOrderStore from '../stores/TagOrderStore'; +import GroupFilterOrderStore from '../stores/GroupFilterOrderStore'; import { AsyncActionPayload } from "../dispatcher/payloads"; import { MatrixClient } from "matrix-js-sdk/src/client"; export default class TagOrderActions { /** * Creates an action thunk that will do an asynchronous request to - * move a tag in TagOrderStore to destinationIx. + * move a tag in GroupFilterOrderStore to destinationIx. * * @param {MatrixClient} matrixClient the matrix client to set the * account data on. @@ -36,8 +36,8 @@ export default class TagOrderActions { */ public static moveTag(matrixClient: MatrixClient, tag: string, destinationIx: number): AsyncActionPayload { // Only commit tags if the state is ready, i.e. not null - let tags = TagOrderStore.getOrderedTags(); - let removedTags = TagOrderStore.getRemovedTagsAccountData() || []; + let tags = GroupFilterOrderStore.getOrderedTags(); + let removedTags = GroupFilterOrderStore.getRemovedTagsAccountData() || []; if (!tags) { return; } @@ -47,7 +47,7 @@ export default class TagOrderActions { removedTags = removedTags.filter((t) => t !== tag); - const storeId = TagOrderStore.getStoreId(); + const storeId = GroupFilterOrderStore.getStoreId(); return asyncAction('TagOrderActions.moveTag', () => { Analytics.trackEvent('TagOrderActions', 'commitTagOrdering'); @@ -83,8 +83,8 @@ export default class TagOrderActions { */ public static removeTag(matrixClient: MatrixClient, tag: string): AsyncActionPayload { // Don't change tags, just removedTags - const tags = TagOrderStore.getOrderedTags(); - const removedTags = TagOrderStore.getRemovedTagsAccountData() || []; + const tags = GroupFilterOrderStore.getOrderedTags(); + const removedTags = GroupFilterOrderStore.getRemovedTagsAccountData() || []; if (removedTags.includes(tag)) { // Return a thunk that doesn't do anything, we don't even need @@ -94,7 +94,7 @@ export default class TagOrderActions { removedTags.push(tag); - const storeId = TagOrderStore.getStoreId(); + const storeId = GroupFilterOrderStore.getStoreId(); return asyncAction('TagOrderActions.removeTag', () => { Analytics.trackEvent('TagOrderActions', 'removeTag'); diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js index 00aad2a0ce..6819742388 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js @@ -32,6 +32,7 @@ import DialogButtons from "../../../../components/views/elements/DialogButtons"; import InlineSpinner from "../../../../components/views/elements/InlineSpinner"; import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog"; import { getSecureBackupSetupMethods, isSecureBackupRequired } from '../../../../utils/WellKnownUtils'; +import SecurityCustomisations from "../../../../customisations/Security"; const PHASE_LOADING = 0; const PHASE_LOADERROR = 1; @@ -99,7 +100,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this._passphraseField = createRef(); - this._fetchBackupInfo(); + MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatusChange); + if (this.state.accountPassword) { // If we have an account password in memory, let's simplify and // assume it means password auth is also supported for device @@ -110,13 +112,27 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this._queryKeyUploadAuth(); } - MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatusChange); + this._getInitialPhase(); } componentWillUnmount() { MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatusChange); } + _getInitialPhase() { + const keyFromCustomisations = SecurityCustomisations.createSecretStorageKey?.(); + if (keyFromCustomisations) { + console.log("Created key via customisations, jumping to bootstrap step"); + this._recoveryKey = { + privateKey: keyFromCustomisations, + }; + this._bootstrapSecretStorage(); + return; + } + + this._fetchBackupInfo(); + } + async _fetchBackupInfo() { try { const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index 884f77aba5..fa0d6682dd 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -416,8 +416,9 @@ export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None return menuOptions; }; -export const useContextMenu = (): [boolean, RefObject, () => void, () => void, (val: boolean) => void] => { - const button = useRef(null); +type ContextMenuTuple = [boolean, RefObject, () => void, () => void, (val: boolean) => void]; +export const useContextMenu = (): ContextMenuTuple => { + const button = useRef(null); const [isOpen, setIsOpen] = useState(false); const open = () => { setIsOpen(true); diff --git a/src/components/structures/TagPanel.js b/src/components/structures/GroupFilterPanel.js similarity index 86% rename from src/components/structures/TagPanel.js rename to src/components/structures/GroupFilterPanel.js index 135b2a1c5c..96aa1ba728 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/GroupFilterPanel.js @@ -16,7 +16,7 @@ limitations under the License. */ import React from 'react'; -import TagOrderStore from '../../stores/TagOrderStore'; +import GroupFilterOrderStore from '../../stores/GroupFilterOrderStore'; import GroupActions from '../../actions/GroupActions'; @@ -31,7 +31,7 @@ import AutoHideScrollbar from "./AutoHideScrollbar"; import SettingsStore from "../../settings/SettingsStore"; import UserTagTile from "../views/elements/UserTagTile"; -class TagPanel extends React.Component { +class GroupFilterPanel extends React.Component { static contextType = MatrixClientContext; state = { @@ -44,13 +44,13 @@ class TagPanel extends React.Component { this.context.on("Group.myMembership", this._onGroupMyMembership); this.context.on("sync", this._onClientSync); - this._tagOrderStoreToken = TagOrderStore.addListener(() => { + this._groupFilterOrderStoreToken = GroupFilterOrderStore.addListener(() => { if (this.unmounted) { return; } this.setState({ - orderedTags: TagOrderStore.getOrderedTags() || [], - selectedTags: TagOrderStore.getSelectedTags(), + orderedTags: GroupFilterOrderStore.getOrderedTags() || [], + selectedTags: GroupFilterOrderStore.getSelectedTags(), }); }); // This could be done by anything with a matrix client @@ -61,8 +61,8 @@ class TagPanel extends React.Component { this.unmounted = true; this.context.removeListener("Group.myMembership", this._onGroupMyMembership); this.context.removeListener("sync", this._onClientSync); - if (this._tagOrderStoreToken) { - this._tagOrderStoreToken.remove(); + if (this._groupFilterOrderStoreToken) { + this._groupFilterOrderStoreToken.remove(); } } @@ -98,7 +98,7 @@ class TagPanel extends React.Component { return (
-
+
); } @@ -117,8 +117,8 @@ class TagPanel extends React.Component { }); const itemsSelected = this.state.selectedTags.length > 0; - const classes = classNames('mx_TagPanel', { - mx_TagPanel_items_selected: itemsSelected, + const classes = classNames('mx_GroupFilterPanel', { + mx_GroupFilterPanel_items_selected: itemsSelected, }); let createButton = ( @@ -141,7 +141,7 @@ class TagPanel extends React.Component { return
{ (provided, snapshot) => (
{ this.renderGlobalIcon() } @@ -168,4 +168,4 @@ class TagPanel extends React.Component {
; } } -export default TagPanel; +export default GroupFilterPanel; diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 5dadba983a..482b9f6da2 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -620,7 +620,7 @@ export default class GroupView extends React.Component { profileForm: newProfileForm, // Indicate that FlairStore needs to be poked to show this change - // in TagTile (TagPanel), Flair and GroupTile (MyGroups). + // in TagTile (GroupFilterPanel), Flair and GroupTile (MyGroups). avatarChanged: true, }); }).catch((e) => { @@ -649,7 +649,6 @@ export default class GroupView extends React.Component { editing: false, summary: null, }); - dis.dispatch({action: 'panel_disable'}); this._initGroupStore(this.props.groupId); if (this.state.avatarChanged) { @@ -870,10 +869,7 @@ export default class GroupView extends React.Component { { _t('Add rooms to this community') }
) :
; - const roomDetailListClassName = classnames({ - "mx_fadable": true, - "mx_fadable_faded": this.state.editing, - }); + return

@@ -884,9 +880,7 @@ export default class GroupView extends React.Component {

{ this.state.groupRoomsLoading ? : - + }
; } diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 090a64904c..4445ff3ff8 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -16,7 +16,7 @@ limitations under the License. import * as React from "react"; import { createRef } from "react"; -import TagPanel from "./TagPanel"; +import GroupFilterPanel from "./GroupFilterPanel"; import CustomRoomTagPanel from "./CustomRoomTagPanel"; import classNames from "classnames"; import dis from "../../dispatcher/dispatcher"; @@ -38,6 +38,7 @@ import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import { OwnProfileStore } from "../../stores/OwnProfileStore"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import RoomListNumResults from "../views/rooms/RoomListNumResults"; +import LeftPanelWidget from "./LeftPanelWidget"; interface IProps { isMinimized: boolean; @@ -46,7 +47,7 @@ interface IProps { interface IState { showBreadcrumbs: boolean; - showTagPanel: boolean; + showGroupFilterPanel: boolean; } // List of CSS classes which should be included in keyboard navigation within the room list @@ -60,7 +61,7 @@ const cssClasses = [ export default class LeftPanel extends React.Component { private listContainerRef: React.RefObject = createRef(); - private tagPanelWatcherRef: string; + private groupFilterPanelWatcherRef: string; private bgImageWatcherRef: string; private focusedElement = null; private isDoingStickyHeaders = false; @@ -70,7 +71,7 @@ export default class LeftPanel extends React.Component { this.state = { showBreadcrumbs: BreadcrumbsStore.instance.visible, - showTagPanel: SettingsStore.getValue('TagPanel.enableTagPanel'), + showGroupFilterPanel: SettingsStore.getValue('TagPanel.enableTagPanel'), }; BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); @@ -78,8 +79,8 @@ export default class LeftPanel extends React.Component { OwnProfileStore.instance.on(UPDATE_EVENT, this.onBackgroundImageUpdate); this.bgImageWatcherRef = SettingsStore.watchSetting( "RoomList.backgroundImage", null, this.onBackgroundImageUpdate); - this.tagPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => { - this.setState({showTagPanel: SettingsStore.getValue("TagPanel.enableTagPanel")}); + this.groupFilterPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => { + this.setState({showGroupFilterPanel: SettingsStore.getValue("TagPanel.enableTagPanel")}); }); // We watch the middle panel because we don't actually get resized, the middle panel does. @@ -88,7 +89,7 @@ export default class LeftPanel extends React.Component { } public componentWillUnmount() { - SettingsStore.unwatchSetting(this.tagPanelWatcherRef); + SettingsStore.unwatchSetting(this.groupFilterPanelWatcherRef); SettingsStore.unwatchSetting(this.bgImageWatcherRef); BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); @@ -119,8 +120,11 @@ export default class LeftPanel extends React.Component { if (settingBgMxc) { avatarUrl = MatrixClientPeg.get().mxcUrlToHttp(settingBgMxc, avatarSize, avatarSize); } + const avatarUrlProp = `url(${avatarUrl})`; - if (document.body.style.getPropertyValue("--avatar-url") !== avatarUrlProp) { + if (!avatarUrl) { + document.body.style.removeProperty("--avatar-url"); + } else if (document.body.style.getPropertyValue("--avatar-url") !== avatarUrlProp) { document.body.style.setProperty("--avatar-url", avatarUrlProp); } }; @@ -139,7 +143,7 @@ export default class LeftPanel extends React.Component { const bottomEdge = list.offsetHeight + list.scrollTop; const sublists = list.querySelectorAll(".mx_RoomSublist"); - const headerRightMargin = 16; // calculated from margins and widths to align with non-sticky tiles + const headerRightMargin = 15; // calculated from margins and widths to align with non-sticky tiles const headerStickyWidth = list.clientWidth - headerRightMargin; // We track which styles we want on a target before making the changes to avoid @@ -210,10 +214,19 @@ export default class LeftPanel extends React.Component { if (!header.classList.contains("mx_RoomSublist_headerContainer_stickyBottom")) { header.classList.add("mx_RoomSublist_headerContainer_stickyBottom"); } + + const offset = window.innerHeight - (list.parentElement.offsetTop + list.parentElement.offsetHeight); + const newBottom = `${offset}px`; + if (header.style.bottom !== newBottom) { + header.style.bottom = newBottom; + } } else { if (header.classList.contains("mx_RoomSublist_headerContainer_stickyBottom")) { header.classList.remove("mx_RoomSublist_headerContainer_stickyBottom"); } + if (header.style.bottom) { + header.style.removeProperty('bottom'); + } } if (style.stickyTop || style.stickyBottom) { @@ -375,9 +388,9 @@ export default class LeftPanel extends React.Component { } public render(): React.ReactNode { - const tagPanel = !this.state.showTagPanel ? null : ( -
- + const groupFilterPanel = !this.state.showGroupFilterPanel ? null : ( +
+ {SettingsStore.getValue("feature_custom_tags") ? : null}
); @@ -385,7 +398,6 @@ export default class LeftPanel extends React.Component { const roomList = { const containerClasses = classNames({ "mx_LeftPanel": true, - "mx_LeftPanel_hasTagPanel": !!tagPanel, + "mx_LeftPanel_hasGroupFilterPanel": !!groupFilterPanel, "mx_LeftPanel_minimized": this.props.isMinimized, }); @@ -405,7 +417,7 @@ export default class LeftPanel extends React.Component { return (
- {tagPanel} + {groupFilterPanel}
+ { !this.props.isMinimized && }
); diff --git a/src/components/structures/LeftPanelWidget.tsx b/src/components/structures/LeftPanelWidget.tsx new file mode 100644 index 0000000000..4daec76d08 --- /dev/null +++ b/src/components/structures/LeftPanelWidget.tsx @@ -0,0 +1,149 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, {useContext, useEffect, useMemo} from "react"; +import {Resizable} from "re-resizable"; +import classNames from "classnames"; + +import AccessibleButton from "../views/elements/AccessibleButton"; +import {useRovingTabIndex} from "../../accessibility/RovingTabIndex"; +import {Key} from "../../Keyboard"; +import {useLocalStorageState} from "../../hooks/useLocalStorageState"; +import MatrixClientContext from "../../contexts/MatrixClientContext"; +import WidgetUtils, {IWidgetEvent} from "../../utils/WidgetUtils"; +import {useAccountData} from "../../hooks/useAccountData"; +import AppTile from "../views/elements/AppTile"; +import {useSettingValue} from "../../hooks/useSettings"; + +interface IProps { + onResize(): void; +} + +const MIN_HEIGHT = 100; +const MAX_HEIGHT = 500; // or 50% of the window height +const INITIAL_HEIGHT = 280; + +const LeftPanelWidget: React.FC = ({ onResize }) => { + const cli = useContext(MatrixClientContext); + + const mWidgetsEvent = useAccountData>(cli, "m.widgets"); + const leftPanelWidgetId = useSettingValue("Widgets.leftPanel"); + const app = useMemo(() => { + if (!mWidgetsEvent || !leftPanelWidgetId) return null; + const widgetConfig = Object.values(mWidgetsEvent).find(w => w.id === leftPanelWidgetId); + if (!widgetConfig) return null; + + return WidgetUtils.makeAppConfig( + widgetConfig.state_key, + widgetConfig.content, + widgetConfig.sender, + null, + widgetConfig.id); + }, [mWidgetsEvent, leftPanelWidgetId]); + + const [height, setHeight] = useLocalStorageState("left-panel-widget-height", INITIAL_HEIGHT); + const [expanded, setExpanded] = useLocalStorageState("left-panel-widget-expanded", true); + useEffect(onResize, [expanded]); + + const [onFocus, isActive, ref] = useRovingTabIndex(); + const tabIndex = isActive ? 0 : -1; + + if (!app) return null; + + let content; + if (expanded) { + content = { + setHeight(height + d.height); + }} + handleWrapperClass="mx_LeftPanelWidget_resizerHandles" + handleClasses={{top: "mx_LeftPanelWidget_resizerHandle"}} + className="mx_LeftPanelWidget_resizeBox" + enable={{ top: true }} + > + + ; + } + + return
+
{ + switch (ev.key) { + case Key.ARROW_LEFT: + ev.stopPropagation(); + setExpanded(false); + break; + case Key.ARROW_RIGHT: { + ev.stopPropagation(); + setExpanded(true); + break; + } + } + }} + > +
+ { + setExpanded(e => !e); + }} + > + + { WidgetUtils.getWidgetName(app) } + + + {/* Code for the maximise button for once we have full screen widgets */} + {/* { + }} + className="mx_LeftPanelWidget_maximizeButton" + tooltipClassName="mx_LeftPanelWidget_maximizeButtonTooltip" + title={_t("Maximize")} + />*/} +
+
+ + { content } +
; +}; + +export default LeftPanelWidget; diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 79f2916200..03277a84f9 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -52,6 +52,7 @@ import RoomListStore from "../../stores/room-list/RoomListStore"; import NonUrgentToastContainer from "./NonUrgentToastContainer"; import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload"; import { IThreepidInvite } from "../../stores/ThreepidInviteStore"; +import { ICollapseConfig } from "../../resizer/distributors/collapse"; // 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. @@ -71,9 +72,6 @@ interface IProps { viaServers?: string[]; hideToSRUsers: boolean; resizeNotifier: ResizeNotifier; - middleDisabled: boolean; - leftDisabled: boolean; - rightDisabled: boolean; // eslint-disable-next-line camelcase page_type: string; autoJoin: boolean; @@ -100,10 +98,6 @@ interface IUsageLimit { } interface IState { - mouseDown?: { - x: number; - y: number; - }; syncErrorData?: { error: { data: IUsageLimit; @@ -151,7 +145,6 @@ class LoggedInView extends React.Component { super(props, context); this.state = { - mouseDown: undefined, syncErrorData: undefined, // use compact timeline view useCompactLayout: SettingsStore.getValue('useCompactLayout'), @@ -213,12 +206,8 @@ class LoggedInView extends React.Component { }; _createResizer() { - const classNames = { - handle: "mx_ResizeHandle", - vertical: "mx_ResizeHandle_vertical", - reverse: "mx_ResizeHandle_reverse", - }; - const collapseConfig = { + let size; + const collapseConfig: ICollapseConfig = { toggleSize: 260 - 50, onCollapsed: (collapsed) => { if (collapsed) { @@ -228,22 +217,24 @@ class LoggedInView extends React.Component { dis.dispatch({action: "show_left_panel"}, true); } }, - onResized: (size) => { - window.localStorage.setItem("mx_lhs_size", '' + size); + onResized: (_size) => { + size = _size; this.props.resizeNotifier.notifyLeftHandleResized(); }, onResizeStart: () => { this.props.resizeNotifier.startResizing(); }, onResizeStop: () => { + window.localStorage.setItem("mx_lhs_size", '' + size); this.props.resizeNotifier.stopResizing(); }, }; - const resizer = new Resizer( - this._resizeContainer.current, - CollapseDistributor, - collapseConfig); - resizer.setClassNames(classNames); + const resizer = new Resizer(this._resizeContainer.current, CollapseDistributor, collapseConfig); + resizer.setClassNames({ + handle: "mx_ResizeHandle", + vertical: "mx_ResizeHandle_vertical", + reverse: "mx_ResizeHandle_reverse", + }); return resizer; } @@ -518,8 +509,8 @@ class LoggedInView extends React.Component { // Could be "GroupTile +groupId:domain" const draggableId = result.draggableId.split(' ').pop(); - // Dispatch synchronously so that the TagPanel receives an - // optimistic update from TagOrderStore before the previous + // Dispatch synchronously so that the GroupFilterPanel receives an + // optimistic update from GroupFilterOrderStore before the previous // state is shown. dis.dispatch(TagOrderActions.moveTag( this._matrixClient, @@ -550,48 +541,6 @@ class LoggedInView extends React.Component { ), true); }; - _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). - if (this.props.leftDisabled && this.props.rightDisabled) { - const targetClasses = new Set(ev.target.className.split(' ')); - if ( - targetClasses.has('mx_MatrixChat') || - targetClasses.has('mx_MatrixChat_middlePanel') || - targetClasses.has('mx_RoomView') - ) { - this.setState({ - mouseDown: { - x: ev.pageX, - y: ev.pageY, - }, - }); - } - } - }; - - _onMouseUp = (ev) => { - if (!this.state.mouseDown) return; - - const deltaX = ev.pageX - this.state.mouseDown.x; - const deltaY = ev.pageY - this.state.mouseDown.y; - const distance = Math.sqrt((deltaX * deltaX) + (deltaY + deltaY)); - const maxRadius = 5; // People shouldn't be straying too far, hopefully - - // Note: we track how far the user moved their mouse to help - // combat against https://github.com/vector-im/element-web/issues/7158 - - if (distance < maxRadius) { - // This is probably a real click, and not a drag - dis.dispatch({ action: 'close_settings' }); - } - - // Always clear the mouseDown state to ensure we don't accidentally - // use stale values due to the mouseDown checks. - this.setState({mouseDown: null}); - }; - render() { const RoomView = sdk.getComponent('structures.RoomView'); const UserView = sdk.getComponent('structures.UserView'); @@ -611,7 +560,6 @@ class LoggedInView extends React.Component { oobData={this.props.roomOobData} viaServers={this.props.viaServers} key={this.props.currentRoomId || 'roomview'} - disabled={this.props.middleDisabled} resizeNotifier={this.props.resizeNotifier} />; break; @@ -659,8 +607,6 @@ class LoggedInView extends React.Component { onKeyDown={this._onReactKeyDown} className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers} - onMouseDown={this._onMouseDown} - onMouseUp={this._onMouseUp} > diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 4f5489d796..f7665fea8a 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -181,9 +181,6 @@ interface IState { currentUserId?: string; // this is persisted as mx_lhs_size, loaded in LoggedInView collapseLhs: boolean; - leftDisabled: boolean; - middleDisabled: boolean; - // the right panel's disabled state is tracked in its store. // Parameters used in the registration dance with the IS // eslint-disable-next-line camelcase register_client_secret?: string; @@ -236,8 +233,6 @@ export default class MatrixChat extends React.PureComponent { this.state = { view: Views.LOADING, collapseLhs: false, - leftDisabled: false, - middleDisabled: false, hideToSRUsers: false, @@ -710,14 +705,6 @@ export default class MatrixChat extends React.PureComponent { this.state.resizeNotifier.notifyLeftHandleResized(); }); break; - case 'panel_disable': { - this.setState({ - leftDisabled: payload.leftDisabled || payload.sideDisabled || false, - middleDisabled: payload.middleDisabled || false, - // We don't track the right panel being disabled here - it's tracked in the store. - }); - break; - } case 'on_logged_in': if ( !Lifecycle.isSoftLogout() && diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index 021cdb438d..41f4d83743 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -17,7 +17,6 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import classNames from 'classnames'; import {Room} from "matrix-js-sdk/src/models/room"; import * as sdk from '../../index'; @@ -304,14 +303,8 @@ export default class RightPanel extends React.Component { break; } - const classes = classNames("mx_RightPanel", "mx_fadable", { - "collapsed": this.props.collapsed, - "mx_fadable_faded": this.props.disabled, - "dark-panel": true, - }); - return ( -