diff --git a/.eslintrc.js b/.eslintrc.js index a0f57155736..45cd05506f6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -87,32 +87,71 @@ module.exports = { "jsx-a11y/role-supports-aria-props": "off", "jsx-a11y/tabindex-no-positive": "off", }, - overrides: [{ - files: [ - "src/**/*.{ts,tsx}", - "test/**/*.{ts,tsx}", - ], - extends: [ - "plugin:matrix-org/typescript", - "plugin:matrix-org/react", - ], - rules: { - // Things we do that break the ideal style - "prefer-promise-reject-errors": "off", - "quotes": "off", - "no-extra-boolean-cast": "off", + overrides: [ + { + files: [ + "src/**/*.{ts,tsx}", + "test/**/*.{ts,tsx}", + ], + extends: [ + "plugin:matrix-org/typescript", + "plugin:matrix-org/react", + ], + rules: { + // Things we do that break the ideal style + "prefer-promise-reject-errors": "off", + "quotes": "off", + "no-extra-boolean-cast": "off", - // Remove Babel things manually due to override limitations - "@babel/no-invalid-this": ["off"], + // Remove Babel things manually due to override limitations + "@babel/no-invalid-this": ["off"], - // We're okay being explicit at the moment - "@typescript-eslint/no-empty-interface": "off", - // We disable this while we're transitioning - "@typescript-eslint/no-explicit-any": "off", - // We'd rather not do this but we do - "@typescript-eslint/ban-ts-comment": "off", + // We're okay being explicit at the moment + "@typescript-eslint/no-empty-interface": "off", + // We disable this while we're transitioning + "@typescript-eslint/no-explicit-any": "off", + // We'd rather not do this but we do + "@typescript-eslint/ban-ts-comment": "off", + }, }, - }], + // temporary override for offending icon require files + { + files: [ + "src/SdkConfig.ts", + "src/components/structures/FileDropTarget.tsx", + "src/components/structures/RoomStatusBar.tsx", + "src/components/structures/UserMenu.tsx", + "src/components/views/avatars/WidgetAvatar.tsx", + "src/components/views/dialogs/AddExistingToSpaceDialog.tsx", + "src/components/views/dialogs/CreateSpaceFromCommunityDialog.tsx", + "src/components/views/dialogs/ForwardDialog.tsx", + "src/components/views/dialogs/InviteDialog.tsx", + "src/components/views/dialogs/ModalWidgetDialog.tsx", + "src/components/views/dialogs/UploadConfirmDialog.tsx", + "src/components/views/dialogs/security/SetupEncryptionDialog.tsx", + "src/components/views/elements/AddressTile.tsx", + "src/components/views/elements/AppWarning.tsx", + "src/components/views/elements/SSOButtons.tsx", + "src/components/views/messages/MAudioBody.tsx", + "src/components/views/messages/MImageBody.tsx", + "src/components/views/messages/MFileBody.tsx", + "src/components/views/messages/MStickerBody.tsx", + "src/components/views/messages/MVideoBody.tsx", + "src/components/views/messages/MVoiceMessageBody.tsx", + "src/components/views/right_panel/EncryptionPanel.tsx", + "src/components/views/rooms/EntityTile.tsx", + "src/components/views/rooms/LinkPreviewGroup.tsx", + "src/components/views/rooms/MemberList.tsx", + "src/components/views/rooms/MessageComposer.tsx", + "src/components/views/rooms/ReplyPreview.tsx", + "src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx", + "src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx" + ], + rules: { + "@typescript-eslint/no-var-requires": "off", + }, + } + ], settings: { react: { version: "detect", diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 00000000000..6635a775866 --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1,10 @@ +coverage: + status: + project: off + patch: off +comment: + layout: "diff, files" + behavior: default + require_changes: false + require_base: no + require_head: no diff --git a/.github/workflows/develop.yml b/.github/workflows/end-to-end-tests.yaml similarity index 95% rename from .github/workflows/develop.yml rename to .github/workflows/end-to-end-tests.yaml index 456c97d5807..334af1772fd 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/end-to-end-tests.yaml @@ -1,4 +1,4 @@ -name: Develop +name: End-to-end Tests on: # These tests won't work for non-develop branches at the moment as they # won't pull in the right versions of other repos, so they're only enabled @@ -11,6 +11,7 @@ jobs: end-to-end: runs-on: ubuntu-latest env: + # This must be set for fetchdep.sh to get the right branch PR_NUMBER: ${{github.event.number}} container: vectorim/element-web-ci-e2etests-env:latest steps: diff --git a/.github/workflows/layered-build.yaml b/.github/workflows/layered-build.yaml index fd531fc9689..1610f0e66e2 100644 --- a/.github/workflows/layered-build.yaml +++ b/.github/workflows/layered-build.yaml @@ -1,3 +1,5 @@ +# Produce a 'layered build' (a build of element-web with this version of +# react-sdk) and output it as an artifact name: Layered Preview Build on: pull_request: @@ -5,6 +7,7 @@ jobs: build: runs-on: ubuntu-latest env: + # This must be set for fetchdep.sh to get the right branch PR_NUMBER: ${{github.event.number}} steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/netlify.yaml b/.github/workflows/netlify.yaml index 7c472ab24d1..864f029d1ec 100644 --- a/.github/workflows/netlify.yaml +++ b/.github/workflows/netlify.yaml @@ -1,3 +1,5 @@ +# Triggers after the layered build has finished, taking the artifact +# and uploading it to netlify name: Upload Preview Build to Netlify on: workflow_run: diff --git a/.github/workflows/test_coverage.yml b/.github/workflows/test_coverage.yml new file mode 100644 index 00000000000..292b8a98653 --- /dev/null +++ b/.github/workflows/test_coverage.yml @@ -0,0 +1,29 @@ +name: Test coverage +on: + pull_request: {} + push: + branches: [develop, main, master] +jobs: + test-coverage: + runs-on: ubuntu-latest + env: + # This must be set for fetchdep.sh to get the right branch + PR_NUMBER: ${{github.event.number}} + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Yarn cache + uses: c-hive/gha-yarn-cache@v2 + + - name: Install Deps + run: "./scripts/ci/install-deps.sh --ignore-scripts" + + - name: Run tests with coverage + run: "yarn install && yarn reskindex && yarn coverage" + + - name: Upload coverage + uses: codecov/codecov-action@v2 + with: + fail_ci_if_error: false + verbose: true diff --git a/.github/workflows/typecheck.yaml b/.github/workflows/typecheck.yaml index f6ab6439582..60cabb3caba 100644 --- a/.github/workflows/typecheck.yaml +++ b/.github/workflows/typecheck.yaml @@ -6,6 +6,7 @@ jobs: build: runs-on: ubuntu-latest env: + # This must be set for fetchdep.sh to get the right branch PR_NUMBER: ${{github.event.number}} steps: - uses: actions/checkout@v2 diff --git a/__mocks__/maplibre-gl.js b/__mocks__/maplibre-gl.js index b1f114e8eff..8686089825d 100644 --- a/__mocks__/maplibre-gl.js +++ b/__mocks__/maplibre-gl.js @@ -1,20 +1,23 @@ const EventEmitter = require("events"); -const { LngLat } = require('maplibre-gl'); +const { LngLat, NavigationControl } = require('maplibre-gl'); class MockMap extends EventEmitter { addControl = jest.fn(); removeControl = jest.fn(); } -class MockGeolocateControl extends EventEmitter { +const MockMapInstance = new MockMap(); +class MockGeolocateControl extends EventEmitter { + trigger = jest.fn(); } -class MockMarker extends EventEmitter { - setLngLat = jest.fn().mockReturnValue(this); - addTo = jest.fn(); -} +const MockGeolocateInstance = new MockGeolocateControl(); +const MockMarker = {} +MockMarker.setLngLat = jest.fn().mockReturnValue(MockMarker); +MockMarker.addTo = jest.fn().mockReturnValue(MockMarker); module.exports = { - Map: MockMap, - GeolocateControl: MockGeolocateControl, - Marker: MockMarker, + Map: jest.fn().mockReturnValue(MockMapInstance), + GeolocateControl: jest.fn().mockReturnValue(MockGeolocateInstance), + Marker: jest.fn().mockReturnValue(MockMarker), LngLat, + NavigationControl }; diff --git a/docs/features/keyboardShortcuts.md b/docs/features/keyboardShortcuts.md new file mode 100644 index 00000000000..71814023543 --- /dev/null +++ b/docs/features/keyboardShortcuts.md @@ -0,0 +1,59 @@ +# Keyboard shortcuts + +## Using the `KeyBindingManger` + +The `KeyBindingManager` (accessible using `getKeyBindingManager()`) is a class +with several methods that allow you to get a `KeyBindingAction` based on a +`KeyboardEvent | React.KeyboardEvent`. + +The event passed to the `KeyBindingManager` gets compared to the list of +shortcuts that are retrieved from the `IKeyBindingsProvider`s. The +`IKeyBindingsProvider` is in `KeyBindingDefaults`. + +### Examples + +Let's say we want to close a menu when the correct keys were pressed: + +```ts +const onKeyDown = (ev: KeyboardEvent): void => { + let handled = true; + const action = getKeyBindingManager().getAccessibilityAction(ev) + switch (action) { + case KeyBindingAction.Escape: + closeMenu(); + break; + default: + handled = false; + break; + } + + if (handled) { + ev.preventDefault(); + ev.stopPropagation(); + } +} +``` + +## Managing keyboard shortcuts + +There are a few things at play when it comes to keyboard shortcuts. The +`KeyBindingManager` gets `IKeyBindingsProvider`s one of which is +`defaultBindingsProvider` defined in `KeyBindingDefaults`. In +`KeyBindingDefaults` a `getBindingsByCategory()` method is used to create +`KeyBinding`s based on `KeyboardShortcutSetting`s defined in +`KeyboardShortcuts`. + +### Adding keyboard shortcuts + +To add a keyboard shortcut there are two files we have to look at: +`KeyboardShortcuts.ts` and `KeyBindingDefaults.ts`. In most cases we only need +to edit `KeyboardShortcuts.ts`: add a `KeyBindingAction` and add the +`KeyBindingAction` to the `KEYBOARD_SHORTCUTS` object. + +Though, to make matters worse, sometimes we want to add a shortcut that has +multiple keybindings associated with. This keyboard shortcut won't be +customizable as it would be rather difficult to manage both from the point of +the settings and the UI. To do this, we have to add a `KeyBindingAction` and add +the UI representation of that keyboard shortcut to the `getUIOnlyShortcuts()` +method. Then, we also need to add the keybinding to the correct method in +`KeyBindingDefaults`. diff --git a/package.json b/package.json index 957982c474d..0ccb0990ed3 100644 --- a/package.json +++ b/package.json @@ -231,7 +231,8 @@ "/src/**/*.{js,ts,tsx}" ], "coverageReporters": [ - "text" + "text", + "json" ] } } diff --git a/res/css/_components.scss b/res/css/_components.scss index 65ce8742b87..a5ff7e233f8 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -5,6 +5,7 @@ @import "./_font-weights.scss"; @import "./_spacing.scss"; @import "./components/views/location/_LocationShareMenu.scss"; +@import "./components/views/location/_MapError.scss"; @import "./components/views/location/_ShareDialogButtons.scss"; @import "./components/views/location/_ShareType.scss"; @import "./components/views/spaces/_QuickThemeSwitcher.scss"; @@ -22,7 +23,6 @@ @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"; diff --git a/res/css/components/views/location/_MapError.scss b/res/css/components/views/location/_MapError.scss new file mode 100644 index 00000000000..83d6316ec44 --- /dev/null +++ b/res/css/components/views/location/_MapError.scss @@ -0,0 +1,36 @@ +/* +Copyright 2022 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_MapError { + padding: 100px $spacing-32 0; + text-align: center; + + p { + margin: $spacing-16 0 $spacing-32; + } +} + +.mx_MapError_heading { + padding-top: $spacing-24; +} + +.mx_MapError_icon { + height: 58px; + + path { + fill: $secondary-content; + } +} diff --git a/res/css/structures/_LeftPanelWidget.scss b/res/css/structures/_LeftPanelWidget.scss deleted file mode 100644 index 0a01a19b90a..00000000000 --- a/res/css/structures/_LeftPanelWidget.scss +++ /dev/null @@ -1,145 +0,0 @@ -/* -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: $tertiary-content; - 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: $tertiary-content; - 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-content; - } - - .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/element-icons/maximise-expand.svg"); - background: $muted-fg-color; - } - } -} - -.mx_LeftPanelWidget_maximizeButtonTooltip { - margin-top: -3px; -} diff --git a/res/css/structures/_SpaceHierarchy.scss b/res/css/structures/_SpaceHierarchy.scss index a43e538b721..85b9455a745 100644 --- a/res/css/structures/_SpaceHierarchy.scss +++ b/res/css/structures/_SpaceHierarchy.scss @@ -35,25 +35,33 @@ limitations under the License. } .mx_SpaceHierarchy_listHeader { - display: flex; - min-height: 32px; + display: grid; + justify-content: space-between; align-items: center; + column-gap: 10px; + min-height: 32px; font-size: $font-15px; line-height: $font-24px; color: $primary-content; margin-bottom: 12px; - > h4 { + .mx_SpaceHierarchy_listHeader_header { + grid-column-start: 1; font-weight: $font-semi-bold; margin: 0; } - .mx_AccessibleButton { - padding: 4px 12px; - font-weight: normal; + .mx_SpaceHierarchy_listHeader_buttons { + grid-column-start: 2; + display: flex; + flex-flow: wrap; + justify-content: flex-end; + row-gap: 12px; + column-gap: 12px; - & + .mx_AccessibleButton { - margin-left: 16px; + .mx_AccessibleButton { + padding: 4px 12px; + font-weight: normal; } } @@ -61,10 +69,6 @@ limitations under the License. .mx_AccessibleButton_kind_primary_outline { padding: 3px 12px; // to account for the 1px border } - - > span { - margin-left: auto; - } } .mx_SpaceHierarchy_error { diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index 8fbd6fbb703..b0166fa152f 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -152,6 +152,7 @@ $SpaceRoomViewInnerWidth: 428px; right: 24px; top: 32px; } + // XXX remove this when spaces leaves Beta .mx_SpaceRoomView_preview_spaceBetaPrompt { font-weight: $font-semi-bold; @@ -254,13 +255,27 @@ $SpaceRoomViewInnerWidth: 428px; flex-direction: column; min-width: 0; - > .mx_BaseAvatar { - width: 80px; - } + .mx_SpaceRoomView_landing_header { + display: flex; + justify-content: space-between; - > .mx_BaseAvatar_image, - > .mx_BaseAvatar > .mx_BaseAvatar_image { - border-radius: 12px; + .mx_BaseAvatar { + width: 80px; + + .mx_BaseAvatar_image { + border-radius: 12px; + } + } + + // XXX: Temporary for the Spaces release only + .mx_SpaceFeedbackPrompt { + padding: 7px; // 8px - 1px border + border: 1px solid rgba($primary-content, .1); + border-radius: 8px; + width: max-content; + height: fit-content; + margin-left: 24px; + } } .mx_SpaceRoomView_landing_name { @@ -360,14 +375,6 @@ $SpaceRoomViewInnerWidth: 428px; margin: 0 0 20px; flex: 0; } - - .mx_SpaceFeedbackPrompt { - padding: 7px; // 8px - 1px border - border: 1px solid rgba($primary-content, .1); - border-radius: 8px; - width: max-content; - margin: 0 0 -40px auto; // collapse its own height to not push other components down - } } .mx_SpaceRoomView_privateScope { diff --git a/res/css/views/avatars/_DecoratedRoomAvatar.scss b/res/css/views/avatars/_DecoratedRoomAvatar.scss index b912302f382..7927f82c2f5 100644 --- a/res/css/views/avatars/_DecoratedRoomAvatar.scss +++ b/res/css/views/avatars/_DecoratedRoomAvatar.scss @@ -65,6 +65,10 @@ limitations under the License. background-color: $presence-away; } + .mx_DecoratedRoomAvatar_icon_busy::before { + background-color: $presence-busy; + } + .mx_NotificationBadge, .mx_RoomTile_badgeContainer { position: absolute; top: 0; diff --git a/res/css/views/dialogs/_LeaveSpaceDialog.scss b/res/css/views/dialogs/_LeaveSpaceDialog.scss index baae73a90b9..aa20efe89a8 100644 --- a/res/css/views/dialogs/_LeaveSpaceDialog.scss +++ b/res/css/views/dialogs/_LeaveSpaceDialog.scss @@ -19,58 +19,49 @@ limitations under the License. display: flex; flex-direction: column; padding: 24px 32px; - } -} -.mx_LeaveSpaceDialog { - width: 440px; - display: flex; - flex-direction: column; - flex-wrap: nowrap; - height: 520px; + .mx_LeaveSpaceDialog { + width: 440px; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + height: 520px; - .mx_Dialog_content { - flex-grow: 1; - margin: 0; - overflow-y: auto; + .mx_Dialog_content { + flex-grow: 1; + margin: 0; + overflow-y: auto; - .mx_LeaveSpaceDialog_section_warning { - position: relative; - border-radius: 8px; - margin: 12px 0 0; - padding: 12px 8px 12px 42px; - background-color: $header-panel-bg-color; + .mx_LeaveSpaceDialog_section_warning { + position: relative; + border-radius: 8px; + margin: 12px 0 0; + padding: 12px 8px 12px 42px; + background-color: $header-panel-bg-color; - font-size: $font-12px; - line-height: $font-15px; - color: $secondary-content; + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-content; - &::before { - content: ''; - position: absolute; - left: 10px; - top: calc(50% - 8px); // vertical centering - height: 16px; - width: 16px; - background-color: $secondary-content; - mask-repeat: no-repeat; - mask-size: contain; - mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); - mask-position: center; - } - } + &::before { + content: ''; + position: absolute; + left: 10px; + top: calc(50% - 8px); // vertical centering + height: 16px; + width: 16px; + background-color: $secondary-content; + mask-repeat: no-repeat; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); + mask-position: center; + } + } - > p { - color: $primary-content; - } - } - - .mx_Dialog_buttons { - margin-top: 20px; - - .mx_Dialog_primary { - background-color: $alert !important; // override default colour - border-color: $alert; + > p { + color: $primary-content; + } + } } } } diff --git a/res/css/views/dialogs/_SpotlightDialog.scss b/res/css/views/dialogs/_SpotlightDialog.scss index 31358ad8a3b..3041c860424 100644 --- a/res/css/views/dialogs/_SpotlightDialog.scss +++ b/res/css/views/dialogs/_SpotlightDialog.scss @@ -100,6 +100,7 @@ limitations under the License. display: flex; white-space: nowrap; overflow-x: hidden; + margin-right: 1px; // occlude the 1px visible of the very next tile to prevent it looking broken } .mx_AccessibleButton { @@ -109,15 +110,16 @@ limitations under the License. font-size: $font-12px; line-height: $font-15px; display: inline-block; - width: 50px; - min-width: 50px; + width: 58px; + height: 58px; + min-width: 58px; box-sizing: border-box; text-align: center; overflow: hidden; text-overflow: ellipsis; .mx_DecoratedRoomAvatar { - margin: 0 5px 4px; // maintain centering + margin: 0 9px 4px; // maintain centering } & + .mx_AccessibleButton { @@ -150,8 +152,8 @@ limitations under the License. > .mx_DecoratedRoomAvatar, > .mx_BaseAvatar { margin-right: 8px; - width: 20px; - height: 20px; + width: 24px; + height: 24px; .mx_BaseAvatar { width: inherit; @@ -186,8 +188,8 @@ limitations under the License. mask-repeat: no-repeat; mask-position: center; mask-size: contain; - width: 20px; - height: 20px; + width: 24px; + height: 24px; position: absolute; left: 4px; top: 50%; @@ -214,9 +216,8 @@ limitations under the License. .mx_SpotlightDialog_otherSearches_messageSearchIcon { display: inline-block; - margin-left: 8px; - width: 20px; - height: 20px; + width: 24px; + height: 24px; background-color: $secondary-content; vertical-align: text-bottom; mask-repeat: no-repeat; diff --git a/res/css/views/dialogs/security/_AccessSecretStorageDialog.scss b/res/css/views/dialogs/security/_AccessSecretStorageDialog.scss index e8bde18232a..609c022f855 100644 --- a/res/css/views/dialogs/security/_AccessSecretStorageDialog.scss +++ b/res/css/views/dialogs/security/_AccessSecretStorageDialog.scss @@ -18,6 +18,8 @@ limitations under the License. position: relative; padding-left: 24px; // 16px icon + 8px padding margin-top: 7px; // vertical alignment to buttons + margin-bottom: 7px; // space between the buttons and the text when float is activated + text-align: left; &::before { content: ""; diff --git a/res/css/views/elements/_Tooltip.scss b/res/css/views/elements/_Tooltip.scss index 93ee6912b4f..f5621a61d58 100644 --- a/res/css/views/elements/_Tooltip.scss +++ b/res/css/views/elements/_Tooltip.scss @@ -58,7 +58,7 @@ limitations under the License. line-height: $font-14px; font-size: $font-12px; font-weight: 500; - max-width: 200px; + max-width: 300px; word-break: break-word; margin-left: 6px; margin-right: 6px; diff --git a/res/css/views/location/_LocationPicker.scss b/res/css/views/location/_LocationPicker.scss index 76e56eedd9f..53b3655ed8b 100644 --- a/res/css/views/location/_LocationPicker.scss +++ b/res/css/views/location/_LocationPicker.scss @@ -19,21 +19,35 @@ limitations under the License. height: 100%; position: relative; + overflow: hidden; + + // when there are errors loading the map + // the canvas is still inserted + // and can overlap error message/close buttons + // hide it + &.mx_LocationPicker_hasError { + .maplibregl-canvas-container, .maplibregl-control-container { + display: none; + } + } #mx_LocationPicker_map { height: 100%; border-radius: 8px; + .maplibregl-ctrl.maplibregl-ctrl-group, + .maplibregl-ctrl.maplibregl-ctrl-attrib { + margin-right: $spacing-16; + } + .maplibregl-ctrl.maplibregl-ctrl-group { // place below the close button // padding-16 + 24px close button + padding-10 margin-top: 50px; - margin-right: $spacing-16; } .maplibregl-ctrl-bottom-right { - bottom: 68px; - margin-right: $spacing-16; + bottom: 80px; } .maplibregl-user-location-accuracy-circle { @@ -51,10 +65,9 @@ limitations under the License. background-color: $accent; filter: drop-shadow(0px 3px 5px rgba(0, 0, 0, 0.2)); - .mx_BaseAvatar { - margin-top: 2px; - margin-left: 2px; - } + display: flex; + align-items: center; + justify-content: center; } .mx_MLocationBody_pointer { @@ -83,23 +96,42 @@ limitations under the License. position: absolute; bottom: 0px; width: 100%; + box-sizing: border-box; + padding: $spacing-16; + display: flex; + flex-direction: column; + justify-content: stretch; - .mx_Dialog_buttons { - text-align: center; + background-color: $header-panel-bg-color; + } +} - /* Note the `button` prefix and `not()` clauses are needed to make - these selectors more specific than those in _common.scss. */ +.mx_MLocationBody_markerIcon { + color: white; + height: 20px; +} - button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton) { - margin: 0px 0px 16px 0px; - min-width: 328px; - min-height: 48px; - } - } - } +.mx_LocationPicker_pinText { + position: absolute; + top: $spacing-16; + width: 100%; + box-sizing: border-box; + text-align: center; + height: 0; + pointer-events: none; + + span { + box-shadow: 0px 4px 15px rgba(0, 0, 0, 0.15); + border-radius: 8px; + padding: $spacing-8; + background-color: $background; + color: $primary-content; - .mx_LocationPicker_error { - color: red; - margin: auto; + font-size: $font-12px; } } + +.mx_LocationPicker_submitButton { + width: 100%; + height: 48px; +} diff --git a/res/css/views/messages/_MLocationBody.scss b/res/css/views/messages/_MLocationBody.scss index eefb39004ef..e4135d46d44 100644 --- a/res/css/views/messages/_MLocationBody.scss +++ b/res/css/views/messages/_MLocationBody.scss @@ -18,6 +18,7 @@ limitations under the License. .mx_MLocationBody_map { width: 450px; height: 300px; + z-index: 0; // keeps the entire map under the message action bar border-radius: $timeline-image-border-radius; } diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss index a9c6e3b39c7..ab434d8286c 100644 --- a/res/css/views/messages/_MessageActionBar.scss +++ b/res/css/views/messages/_MessageActionBar.scss @@ -65,6 +65,12 @@ limitations under the License. .mx_MessageActionBar_maskButton { width: 28px; height: 28px; + + &:disabled, + &[disabled] { + cursor: not-allowed; + opacity: .75; + } } .mx_MessageActionBar_maskButton::after { diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 586865cb3d0..afa05668ecf 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -723,7 +723,7 @@ $left-gutter: 64px; .mx_ThreadInfo { min-width: 267px; - max-width: min(calc(100% - 64px), 600px); + max-width: min(calc(100% - $left-gutter - 64px), 600px); // leave space on both left & right gutters width: fit-content; height: 40px; position: relative; @@ -740,41 +740,49 @@ $left-gutter: 64px; justify-content: flex-start; clear: both; overflow: hidden; + border: 1px solid $system; // always render a border so the hover effect doesn't require a re-layout - &:hover { - cursor: pointer; - border: 1px solid $quinary-content; - padding-top: 7px; - padding-bottom: 7px; - padding-left: 11px; - padding-right: 15px; - } - - &::after { - content: "›"; + .mx_ThreadInfo_chevron { position: absolute; top: 0; right: 0; bottom: 0; width: 60px; - padding: 0 10px; - font-size: 15px; - line-height: 39px; box-sizing: border-box; - - text-align: right; - font-weight: 600; - background: linear-gradient(270deg, $system 52.6%, transparent 100%); opacity: 0; - transform: translateX(20px); + transform: translateX(60px); transition: all .1s ease-in-out; + + &::before { + content: ''; + position: absolute; + top: 50%; + right: 12px; + transform: translateY(-50%); + width: 12px; + height: 12px; + mask-image: url('$(res)/img/compound/chevron-right-12px.svg'); + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background-color: $secondary-content; + } } - &:hover::after { - opacity: 1; - transform: translateX(0); + &:hover, &:focus { + cursor: pointer; + border-color: $quinary-content; + + .mx_ThreadInfo_chevron { + opacity: 1; + transform: translateX(0); + } + } + + &::before { + align-self: center; // v-align the threads icon } } @@ -784,25 +792,34 @@ $left-gutter: 64px; width: initial; } +$threadInfoLineHeight: calc(2 * $font-12px); + +.mx_ThreadInfo_sender { + font-weight: $font-semi-bold; + line-height: $threadInfoLineHeight; +} + .mx_ThreadInfo_content { text-overflow: ellipsis; overflow: hidden; white-space: nowrap; - padding-left: 8px; + margin-left: 4px; font-size: $font-12px; - line-height: calc(2 * $font-12px); + line-height: $threadInfoLineHeight; color: $secondary-content; } .mx_ThreadInfo_avatar { float: left; + margin-right: 8px; } .mx_ThreadInfo_threads-amount { - font-weight: 600; + font-weight: $font-semi-bold; position: relative; padding: 0 12px 0 8px; white-space: nowrap; + line-height: $threadInfoLineHeight; } .mx_EventTile[data-shape=ThreadsList] { @@ -968,6 +985,7 @@ $left-gutter: 64px; .mx_EventTile_content, .mx_HiddenBody, .mx_RedactedBody, + .mx_MPollBody, .mx_ReplyChain_wrapper { margin-left: 36px; margin-right: 8px; diff --git a/res/css/views/rooms/_RoomSublist.scss b/res/css/views/rooms/_RoomSublist.scss index d5b28d07e2e..c00ac227cf5 100644 --- a/res/css/views/rooms/_RoomSublist.scss +++ b/res/css/views/rooms/_RoomSublist.scss @@ -50,6 +50,10 @@ limitations under the License. height: 24px; color: $tertiary-content; + .mx_RoomSublist_stickableContainer { + width: 100%; + } + .mx_RoomSublist_stickable { flex: 1; max-width: 100%; @@ -176,6 +180,14 @@ limitations under the License. } } + // In the general case, we reserve space for each sublist header to prevent + // scroll jumps when they become sticky. However, that leaves a gap when + // scrolled to the top above the first sublist (whose header can only ever + // stick to top), so we make sure to exclude the first visible sublist. + &:not(.mx_RoomSublist_hidden) ~ .mx_RoomSublist .mx_RoomSublist_stickableContainer { + height: 24px; + } + .mx_RoomSublist_resizeBox { position: relative; diff --git a/res/css/views/spaces/_SpaceCreateMenu.scss b/res/css/views/spaces/_SpaceCreateMenu.scss index 80c1c39da22..9b5954aecfe 100644 --- a/res/css/views/spaces/_SpaceCreateMenu.scss +++ b/res/css/views/spaces/_SpaceCreateMenu.scss @@ -43,6 +43,7 @@ $spacePanelWidth: 68px; color: $secondary-content; } + // XXX: Temporary for the Spaces release only .mx_SpaceFeedbackPrompt { border-top: 1px solid $input-border-color; padding-top: 12px; @@ -112,14 +113,13 @@ $spacePanelWidth: 68px; position: relative; font-size: inherit; line-height: inherit; - margin-right: auto; + margin-right: 8px; } .mx_AccessibleButton_kind_link { color: $accent; position: relative; padding: 0; - margin-left: 8px; font-size: inherit; line-height: inherit; } diff --git a/res/img/compound/chevron-right-12px.svg b/res/img/compound/chevron-right-12px.svg new file mode 100644 index 00000000000..02f61f36ff3 --- /dev/null +++ b/res/img/compound/chevron-right-12px.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index baee895d9f6..7ecf47d64d9 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -137,6 +137,7 @@ $roomtile-selected-bg-color: #fff; $presence-away: #d9b072; $presence-offline: #e3e8f0; +$presence-busy: #FF5B55; // Legacy theme backports $accent: #0DBD8B; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 6d7921eb0b1..11777c00758 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -136,6 +136,7 @@ $rte-code-bg-color: rgba(0, 0, 0, 0.04); // ******************** $presence-away: #d9b072; $presence-offline: $quinary-content; +$presence-busy: $alert; // ******************** // Inputs diff --git a/scripts/ci/Dockerfile b/scripts/ci/Dockerfile index 447ed09b7c9..08c9153578b 100644 --- a/scripts/ci/Dockerfile +++ b/scripts/ci/Dockerfile @@ -1,3 +1,5 @@ +# Docker file for end-to-end tests + # Update on docker hub with the following commands in the directory of this file: # If you're on linux amd64 # docker build -t vectorim/element-web-ci-e2etests-env:latest . diff --git a/scripts/ci/install-deps.sh b/scripts/ci/install-deps.sh index 0741ad2ab34..c10884361e9 100755 --- a/scripts/ci/install-deps.sh +++ b/scripts/ci/install-deps.sh @@ -1,5 +1,12 @@ #!/bin/bash +# This installs other Matrix dependencies that are often +# developed in parallel with react-sdk, using fetchdep.sh +# for branch matching. +# This will set up a working react-sdk environment, so is +# used for running react-sdk standalone tests. To set up a +# build of element-web, use layered.sh + set -ex scripts/fetchdep.sh matrix-org matrix-js-sdk diff --git a/scripts/ci/layered.sh b/scripts/ci/layered.sh index 3e30cc808f7..8ebb7a75489 100755 --- a/scripts/ci/layered.sh +++ b/scripts/ci/layered.sh @@ -1,7 +1,9 @@ #!/bin/bash # Creates a layered environment with the full repo for the app and SDKs cloned -# and linked. +# and linked. This gives an element-web dev environment ready to build with +# the current react-sdk branch and any matching branches of react-sdk's dependencies +# so that changes can be tested in element-web. # Note that this style is different from the recommended developer setup: this # file nests js-sdk and element-web inside react-sdk, while the local @@ -16,7 +18,8 @@ yarn link yarn install --pure-lockfile popd -# Set up the js-sdk first +# Also set up matrix-analytics-events so we get the latest from +# the main branch or a branch with matching name scripts/fetchdep.sh matrix-org matrix-analytics-events main pushd matrix-analytics-events yarn link diff --git a/src/@types/common.ts b/src/@types/common.ts index 991cfb7e085..d70d80fcd56 100644 --- a/src/@types/common.ts +++ b/src/@types/common.ts @@ -34,3 +34,10 @@ type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...0[]]; export type Leaves = [D] extends [never] ? never : T extends object ? { [K in keyof T]-?: Join> }[keyof T] : ""; + +export type RecursivePartial = { + [P in keyof T]?: + T[P] extends (infer U)[] ? RecursivePartial[] : + T[P] extends object ? RecursivePartial : + T[P]; +}; diff --git a/src/ActiveRoomObserver.ts b/src/ActiveRoomObserver.ts index 4da0a6eac3a..ef641922034 100644 --- a/src/ActiveRoomObserver.ts +++ b/src/ActiveRoomObserver.ts @@ -1,5 +1,5 @@ /* -Copyright 2017 New Vector Ltd +Copyright 2017 - 2022 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,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { EventSubscription } from 'fbemitter'; import { logger } from "matrix-js-sdk/src/logger"; import RoomViewStore from './stores/RoomViewStore'; @@ -33,11 +32,10 @@ type Listener = (isActive: boolean) => void; export class ActiveRoomObserver { private listeners: {[key: string]: Listener[]} = {}; private _activeRoomId = RoomViewStore.getRoomId(); - private readonly roomStoreToken: EventSubscription; constructor() { // TODO: We could self-destruct when the last listener goes away, or at least stop listening. - this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); + RoomViewStore.addListener(this.onRoomViewStoreUpdate); } public get activeRoomId(): string { diff --git a/src/Analytics.tsx b/src/Analytics.tsx index 09cb78d980b..d7759562b1a 100644 --- a/src/Analytics.tsx +++ b/src/Analytics.tsx @@ -1,6 +1,6 @@ /* Copyright 2017 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020 - 2022 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. @@ -166,7 +166,6 @@ const HEARTBEAT_INTERVAL = 30 * 1000; // seconds 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; diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 60094262edb..07fc0bb1158 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -1,7 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017, 2018 New Vector Ltd -Copyright 2019 - 2021 The Matrix.org Foundation C.I.C. +Copyright 2019 - 2022 The Matrix.org Foundation C.I.C. Copyright 2021 Šimon Brandner Licensed under the Apache License, Version 2.0 (the "License"); @@ -125,10 +125,6 @@ export default class CallHandler extends EventEmitter { private supportsPstnProtocol = null; private pstnSupportPrefixed = null; // True if the server only support the prefixed pstn protocol private supportsSipNativeVirtual = null; // im.vector.protocol.sip_virtual and im.vector.protocol.sip_native - private pstnSupportCheckTimer: number; - // For rooms we've been invited to, true if they're from virtual user, false if we've checked and they aren't. - private invitedRoomsAreVirtual = new Map(); - private invitedRoomCheckInProgress = false; // Map of the asserted identity users after we've looked them up using the API. // We need to be be able to determine the mapped room synchronously, so we @@ -255,7 +251,7 @@ export default class CallHandler extends EventEmitter { logger.log("Failed to check for protocol support and no retries remain: assuming no support", e); } else { logger.log("Failed to check for protocol support: will retry", e); - this.pstnSupportCheckTimer = setTimeout(() => { + setTimeout(() => { this.checkProtocols(maxTries - 1); }, 10000); } diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 8b0ddc83688..9a2b9ac79ce 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -19,12 +19,13 @@ limitations under the License. import React from "react"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { IUploadOpts } from "matrix-js-sdk/src/@types/requests"; -import { MsgType, RelationType } from "matrix-js-sdk/src/@types/event"; +import { MsgType } from "matrix-js-sdk/src/@types/event"; import encrypt from "browser-encrypt-attachment"; import extractPngChunks from "png-chunks-extract"; import { IAbortablePromise, IImageInfo } from "matrix-js-sdk/src/@types/partials"; import { logger } from "matrix-js-sdk/src/logger"; import { IEventRelation, ISendEventResponse } from "matrix-js-sdk/src/matrix"; +import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread"; import { IEncryptedFile, IMediaEventInfo } from "./customisations/models/IMediaEventContent"; import dis from './dispatcher/dispatcher'; @@ -658,7 +659,7 @@ export default class ContentMessages { return promBefore; }).then(function() { if (upload.canceled) throw new UploadCanceledError(); - const threadId = relation?.rel_type === RelationType.Thread + const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name ? relation.event_id : null; const prom = matrixClient.sendMessage(roomId, threadId, content); diff --git a/src/KeyBindingsDefaults.ts b/src/KeyBindingsDefaults.ts index a41680848dd..9ac6d745c20 100644 --- a/src/KeyBindingsDefaults.ts +++ b/src/KeyBindingsDefaults.ts @@ -26,13 +26,13 @@ import { import { CATEGORIES, CategoryName, - getCustomizableShortcuts, KeyBindingAction, } from "./accessibility/KeyboardShortcuts"; +import { getKeyboardShortcuts } from "./accessibility/KeyboardShortcutUtils"; export const getBindingsByCategory = (category: CategoryName): KeyBinding[] => { return CATEGORIES[category].settingNames.reduce((bindings, name) => { - const value = getCustomizableShortcuts()[name]?.default; + const value = getKeyboardShortcuts()[name]?.default; if (value) { bindings.push({ action: name as KeyBindingAction, diff --git a/src/Keyboard.ts b/src/Keyboard.ts index 7040898872e..8d7d39fc190 100644 --- a/src/Keyboard.ts +++ b/src/Keyboard.ts @@ -45,6 +45,7 @@ export const Key = { SLASH: "/", SQUARE_BRACKET_LEFT: "[", SQUARE_BRACKET_RIGHT: "]", + SEMICOLON: ";", A: "a", B: "b", C: "c", diff --git a/src/Markdown.ts b/src/Markdown.ts index e24c9b72870..9f88fbe41f1 100644 --- a/src/Markdown.ts +++ b/src/Markdown.ts @@ -153,10 +153,27 @@ export default class Markdown { (node.type === 'text' && node.literal === ' ') ) { text = ''; + continue; } + + // Break up text nodes on spaces, so that we don't shoot past them without resetting if (node.type === 'text') { - text += node.literal; + const [thisPart, ...nextParts] = node.literal.split(/( )/); + node.literal = thisPart; + text += thisPart; + + // Add the remaining parts as siblings + nextParts.reverse().forEach(part => { + if (part) { + const nextNode = new commonmark.Node('text'); + nextNode.literal = part; + node.insertAfter(nextNode); + // Make the iterator aware of the newly inserted node + walker.resumeAt(nextNode, true); + } + }); } + // We should not do this if previous node was not a textnode, as we can't combine it then. if ((node.type === 'emph' || node.type === 'strong') && previousNode.type === 'text') { if (event.entering) { diff --git a/src/Notifier.ts b/src/Notifier.ts index c5383488f2f..3f17ae0091d 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -22,7 +22,7 @@ import { Room, RoomEvent } from "matrix-js-sdk/src/models/room"; import { ClientEvent } from "matrix-js-sdk/src/client"; import { logger } from "matrix-js-sdk/src/logger"; import { MsgType } from "matrix-js-sdk/src/@types/event"; -import { LOCATION_EVENT_TYPE } from "matrix-js-sdk/src/@types/location"; +import { M_LOCATION } from "matrix-js-sdk/src/@types/location"; import { MatrixClientPeg } from './MatrixClientPeg'; import SdkConfig from './SdkConfig'; @@ -64,10 +64,10 @@ const msgTypeHandlers = { const name = (event.sender || {}).name; return _t("%(name)s is requesting verification", { name }); }, - [LOCATION_EVENT_TYPE.name]: (event: MatrixEvent) => { + [M_LOCATION.name]: (event: MatrixEvent) => { return TextForEvent.textForLocationEvent(event)(); }, - [LOCATION_EVENT_TYPE.altName]: (event: MatrixEvent) => { + [M_LOCATION.altName]: (event: MatrixEvent) => { return TextForEvent.textForLocationEvent(event)(); }, }; diff --git a/src/PasswordReset.ts b/src/PasswordReset.ts index 7b0b6f9f846..f5d9ab77dcb 100644 --- a/src/PasswordReset.ts +++ b/src/PasswordReset.ts @@ -1,6 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019 - 2022 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. @@ -29,7 +29,6 @@ import { _t } from './languageHandler'; export default class PasswordReset { private client: MatrixClient; private clientSecret: string; - private identityServerDomain: string; private password: string; private sessionId: string; @@ -44,7 +43,6 @@ export default class PasswordReset { idBaseUrl: identityUrl, }); this.clientSecret = this.client.generateClientSecret(); - this.identityServerDomain = identityUrl ? identityUrl.split("://")[1] : null; } /** diff --git a/src/RoomNotifs.ts b/src/RoomNotifs.ts index 1f39eddc17c..39d1d5d4aa7 100644 --- a/src/RoomNotifs.ts +++ b/src/RoomNotifs.ts @@ -61,17 +61,6 @@ export function aggregateNotificationCount(rooms: Room[]): {count: number, highl }, { count: 0, highlight: false }); } -export function getRoomHasBadge(room: Room): boolean { - const roomNotifState = getRoomNotifsState(room.roomId); - const highlight = room.getUnreadNotificationCount(NotificationCountType.Highlight) > 0; - const notificationCount = room.getUnreadNotificationCount(); - - const notifBadges = notificationCount > 0 && shouldShowNotifBadge(roomNotifState); - const mentionBadges = highlight && shouldShowMentionBadge(roomNotifState); - - return notifBadges || mentionBadges; -} - export function getRoomNotifsState(roomId: string): RoomNotifState { if (MatrixClientPeg.get().isGuest()) return RoomNotifState.AllMessages; @@ -88,14 +77,14 @@ export function getRoomNotifsState(roomId: string): RoomNotifState { roomRule = MatrixClientPeg.get().getRoomPushRule('global', roomId); } catch (err) { // Possible that the client doesn't have pushRules yet. If so, it - // hasn't started eiher, so indicate that this room is not notifying. + // hasn't started either, so indicate that this room is not notifying. return null; } // XXX: We have to assume the default is to notify for all messages // (in particular this will be 'wrong' for one to one rooms because // they will notify loudly for all messages) - if (!roomRule || !roomRule.enabled) return RoomNotifState.AllMessages; + if (!roomRule?.enabled) return RoomNotifState.AllMessages; // a mute at the room level will still allow mentions // to notify @@ -213,17 +202,15 @@ function findOverrideMuteRule(roomId: string): IPushRule { return null; } for (const rule of cli.pushRules.global.override) { - if (isRuleForRoom(roomId, rule)) { - if (isMuteRule(rule) && rule.enabled) { - return rule; - } + if (rule.enabled && isRuleForRoom(roomId, rule) && isMuteRule(rule)) { + return rule; } } return null; } function isRuleForRoom(roomId: string, rule: IPushRule): boolean { - if (rule.conditions.length !== 1) { + if (rule.conditions?.length !== 1) { return false; } const cond = rule.conditions[0]; diff --git a/src/SdkConfig.ts b/src/SdkConfig.ts index f0bfafab9fd..c4387d1f237 100644 --- a/src/SdkConfig.ts +++ b/src/SdkConfig.ts @@ -50,7 +50,7 @@ export const DEFAULTS: ConfigOptions = { }, desktopBuilds: { available: true, - logo: require("../res/img/element-desktop-logo.svg"), + logo: require("../res/img/element-desktop-logo.svg").default, url: "https://element.io/get-started", }, }; diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index 49da76b8b85..5b76eb6f205 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -29,7 +29,7 @@ import { M_POLL_END, PollStartEvent, } from "matrix-events-sdk"; -import { LOCATION_EVENT_TYPE } from "matrix-js-sdk/src/@types/location"; +import { M_LOCATION } from "matrix-js-sdk/src/@types/location"; import { _t } from './languageHandler'; import * as Roles from './Roles'; @@ -339,7 +339,7 @@ function textForMessageEvent(ev: MatrixEvent): () => string | null { const content = ev.getContent(); const msgtype = content.msgtype; - if (LOCATION_EVENT_TYPE.matches(type) || LOCATION_EVENT_TYPE.matches(msgtype)) { + if (M_LOCATION.matches(type) || M_LOCATION.matches(msgtype)) { return textForLocationEvent(ev); } diff --git a/src/Unread.ts b/src/Unread.ts index 905798eb038..91e192b371d 100644 --- a/src/Unread.ts +++ b/src/Unread.ts @@ -23,7 +23,7 @@ import shouldHideEvent from './shouldHideEvent'; import { haveTileForEvent } from "./components/views/rooms/EventTile"; /** - * Returns true iff this event arriving in a room should affect the room's + * Returns true if this event arriving in a room should affect the room's * count of unread messages * * @param {Object} ev The event diff --git a/src/accessibility/KeyboardShortcutUtils.ts b/src/accessibility/KeyboardShortcutUtils.ts new file mode 100644 index 00000000000..434116d4303 --- /dev/null +++ b/src/accessibility/KeyboardShortcutUtils.ts @@ -0,0 +1,131 @@ +/* +Copyright 2022 Šimon Brandner + +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 { KeyCombo } from "../KeyBindingsManager"; +import { isMac, Key } from "../Keyboard"; +import { _t, _td } from "../languageHandler"; +import PlatformPeg from "../PlatformPeg"; +import SettingsStore from "../settings/SettingsStore"; +import { + DESKTOP_SHORTCUTS, + DIGITS, + IKeyboardShortcuts, + KeyBindingAction, + KEYBOARD_SHORTCUTS, + MAC_ONLY_SHORTCUTS, +} from "./KeyboardShortcuts"; + +/** + * This function gets the keyboard shortcuts that should be presented in the UI + * but they shouldn't be consumed by KeyBindingDefaults. That means that these + * have to be manually mirrored in KeyBindingDefaults. + */ +const getUIOnlyShortcuts = (): IKeyboardShortcuts => { + const ctrlEnterToSend = SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend'); + + const keyboardShortcuts: IKeyboardShortcuts = { + [KeyBindingAction.SendMessage]: { + default: { + key: Key.ENTER, + ctrlOrCmdKey: ctrlEnterToSend, + }, + displayName: _td("Send message"), + }, + [KeyBindingAction.NewLine]: { + default: { + key: Key.ENTER, + shiftKey: !ctrlEnterToSend, + }, + displayName: _td("New line"), + }, + [KeyBindingAction.CompleteAutocomplete]: { + default: { + key: Key.ENTER, + }, + displayName: _td("Complete"), + }, + [KeyBindingAction.ForceCompleteAutocomplete]: { + default: { + key: Key.TAB, + }, + displayName: _td("Force complete"), + }, + [KeyBindingAction.SearchInRoom]: { + default: { + ctrlOrCmdKey: true, + key: Key.F, + }, + displayName: _td("Search (must be enabled)"), + }, + }; + + if (PlatformPeg.get().overrideBrowserShortcuts()) { + // XXX: This keyboard shortcut isn't manually added to + // KeyBindingDefaults as it can't be easily handled by the + // KeyBindingManager + keyboardShortcuts[KeyBindingAction.SwitchToSpaceByNumber] = { + default: { + ctrlOrCmdKey: true, + key: DIGITS, + }, + displayName: _td("Switch to space by number"), + }; + } + + return keyboardShortcuts; +}; + +/** + * This function gets keyboard shortcuts that can be consumed by the KeyBindingDefaults. + */ +export const getKeyboardShortcuts = (): IKeyboardShortcuts => { + const overrideBrowserShortcuts = PlatformPeg.get().overrideBrowserShortcuts(); + + return Object.keys(KEYBOARD_SHORTCUTS).filter((k: KeyBindingAction) => { + if (KEYBOARD_SHORTCUTS[k]?.controller?.settingDisabled) return false; + if (MAC_ONLY_SHORTCUTS.includes(k) && !isMac) return false; + if (DESKTOP_SHORTCUTS.includes(k) && !overrideBrowserShortcuts) return false; + + return true; + }).reduce((o, key) => { + o[key] = KEYBOARD_SHORTCUTS[key]; + return o; + }, {} as IKeyboardShortcuts); +}; + +/** + * Gets keyboard shortcuts that should be presented to the user in the UI. + */ +export const getKeyboardShortcutsForUI = (): IKeyboardShortcuts => { + const entries = [ + ...Object.entries(getUIOnlyShortcuts()), + ...Object.entries(getKeyboardShortcuts()), + ]; + + return entries.reduce((acc, [key, value]) => { + acc[key] = value; + return acc; + }, {} as IKeyboardShortcuts); +}; + +export const getKeyboardShortcutValue = (name: string): KeyCombo => { + return getKeyboardShortcutsForUI()[name]?.default; +}; + +export const getKeyboardShortcutDisplayName = (name: string): string | null => { + const keyboardShortcutDisplayName = getKeyboardShortcutsForUI()[name]?.displayName; + return keyboardShortcutDisplayName && _t(keyboardShortcutDisplayName); +}; diff --git a/src/accessibility/KeyboardShortcuts.ts b/src/accessibility/KeyboardShortcuts.ts index 9ac4552acea..8d13856f7e6 100644 --- a/src/accessibility/KeyboardShortcuts.ts +++ b/src/accessibility/KeyboardShortcuts.ts @@ -18,9 +18,8 @@ limitations under the License. import { _td } from "../languageHandler"; import { isMac, Key } from "../Keyboard"; import { IBaseSetting } from "../settings/Settings"; -import SettingsStore from "../settings/SettingsStore"; import IncompatibleController from "../settings/controllers/IncompatibleController"; -import PlatformPeg from "../PlatformPeg"; +import { KeyCombo } from "../KeyBindingsManager"; export enum KeyBindingAction { /** Send a message */ @@ -35,6 +34,8 @@ export enum KeyBindingAction { EditNextMessage = 'KeyBinding.editNextMessage', /** Cancel editing a message or cancel replying to a message */ CancelReplyOrEdit = 'KeyBinding.cancelReplyInComposer', + /** Show the sticker picker */ + ShowStickerPicker = 'KeyBinding.showStickerPicker', /** Set bold format the current selection */ FormatBold = 'KeyBinding.toggleBoldInComposer', @@ -149,18 +150,9 @@ export enum KeyBindingAction { ToggleHiddenEventVisibility = 'KeyBinding.toggleHiddenEventVisibility', } -export type KeyBindingConfig = { - key: string; - ctrlOrCmdKey?: boolean; - ctrlKey?: boolean; - altKey?: boolean; - shiftKey?: boolean; - metaKey?: boolean; -}; - -type KeyboardShortcutSetting = IBaseSetting; +type KeyboardShortcutSetting = IBaseSetting; -type IKeyboardShortcuts = { +export type IKeyboardShortcuts = { // TODO: We should figure out what to do with the keyboard shortcuts that are not handled by KeybindingManager [k in (KeyBindingAction)]?: KeyboardShortcutSetting; }; @@ -227,6 +219,7 @@ export const CATEGORIES: Record = { KeyBindingAction.EditPrevMessage, KeyBindingAction.SelectNextSendHistory, KeyBindingAction.SelectPrevSendHistory, + KeyBindingAction.ShowStickerPicker, ], }, [CategoryName.CALLS]: { categoryLabel: _td("Calls"), @@ -307,14 +300,14 @@ export const CATEGORIES: Record = { }, }; -const DESKTOP_SHORTCUTS = [ +export const DESKTOP_SHORTCUTS = [ KeyBindingAction.OpenUserSettings, KeyBindingAction.SwitchToSpaceByNumber, KeyBindingAction.PreviousVisitedRoomOrCommunity, KeyBindingAction.NextVisitedRoomOrCommunity, ]; -const MAC_ONLY_SHORTCUTS = [ +export const MAC_ONLY_SHORTCUTS = [ KeyBindingAction.OpenUserSettings, ]; @@ -392,6 +385,13 @@ export const KEYBOARD_SHORTCUTS: IKeyboardShortcuts = { }, displayName: _td("Navigate to previous message in composer history"), }, + [KeyBindingAction.ShowStickerPicker]: { + default: { + ctrlOrCmdKey: true, + key: Key.SEMICOLON, + }, + displayName: _td("Send a sticker"), + }, [KeyBindingAction.ToggleMicInCall]: { default: { ctrlOrCmdKey: true, @@ -700,86 +700,6 @@ export const KEYBOARD_SHORTCUTS: IKeyboardShortcuts = { }, }; -// XXX: These have to be manually mirrored in KeyBindingDefaults -const getNonCustomizableShortcuts = (): IKeyboardShortcuts => { - const ctrlEnterToSend = SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend'); - - const keyboardShortcuts: IKeyboardShortcuts = { - [KeyBindingAction.SendMessage]: { - default: { - key: Key.ENTER, - ctrlOrCmdKey: ctrlEnterToSend, - }, - displayName: _td("Send message"), - }, - [KeyBindingAction.NewLine]: { - default: { - key: Key.ENTER, - shiftKey: !ctrlEnterToSend, - }, - displayName: _td("New line"), - }, - [KeyBindingAction.CompleteAutocomplete]: { - default: { - key: Key.ENTER, - }, - displayName: _td("Complete"), - }, - [KeyBindingAction.ForceCompleteAutocomplete]: { - default: { - key: Key.TAB, - }, - displayName: _td("Force complete"), - }, - [KeyBindingAction.SearchInRoom]: { - default: { - ctrlOrCmdKey: true, - key: Key.F, - }, - displayName: _td("Search (must be enabled)"), - }, - }; - - if (PlatformPeg.get().overrideBrowserShortcuts()) { - keyboardShortcuts[KeyBindingAction.SwitchToSpaceByNumber] = { - default: { - ctrlOrCmdKey: true, - key: DIGITS, - }, - displayName: _td("Switch to space by number"), - }; - } - - return keyboardShortcuts; -}; - -export const getCustomizableShortcuts = (): IKeyboardShortcuts => { - const overrideBrowserShortcuts = PlatformPeg.get().overrideBrowserShortcuts(); - - return Object.keys(KEYBOARD_SHORTCUTS).filter((k: KeyBindingAction) => { - if (KEYBOARD_SHORTCUTS[k]?.controller?.settingDisabled) return false; - if (MAC_ONLY_SHORTCUTS.includes(k) && !isMac) return false; - if (DESKTOP_SHORTCUTS.includes(k) && !overrideBrowserShortcuts) return false; - - return true; - }).reduce((o, key) => { - o[key] = KEYBOARD_SHORTCUTS[key]; - return o; - }, {} as IKeyboardShortcuts); -}; - -export const getKeyboardShortcuts = (): IKeyboardShortcuts => { - const entries = [ - ...Object.entries(getNonCustomizableShortcuts()), - ...Object.entries(getCustomizableShortcuts()), - ]; - - return entries.reduce((acc, [key, value]) => { - acc[key] = value; - return acc; - }, {} as IKeyboardShortcuts); -}; - // For tests export function mock({ keyboardShortcuts, macOnlyShortcuts, desktopShortcuts }): void { Object.keys(KEYBOARD_SHORTCUTS).forEach((k) => delete KEYBOARD_SHORTCUTS[k]); diff --git a/src/audio/PlaybackQueue.ts b/src/audio/PlaybackQueue.ts index 3b1b001bb91..d470b6b94ce 100644 --- a/src/audio/PlaybackQueue.ts +++ b/src/audio/PlaybackQueue.ts @@ -1,5 +1,5 @@ /* -Copyright 2021 The Matrix.org Foundation C.I.C. +Copyright 2021 - 2022 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,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { Room } from "matrix-js-sdk/src/models/room"; import { EventType } from "matrix-js-sdk/src/@types/event"; @@ -49,7 +48,7 @@ export class PlaybackQueue { private currentPlaybackId: string; // event ID, broken out from above for ease of use private recentFullPlays = new Set(); // event IDs - constructor(private client: MatrixClient, private room: Room) { + constructor(private room: Room) { this.loadClocks(); RoomViewStore.addListener(() => { @@ -71,7 +70,7 @@ export class PlaybackQueue { if (PlaybackQueue.queues.has(room.roomId)) { return PlaybackQueue.queues.get(room.roomId); } - const queue = new PlaybackQueue(cli, room); + const queue = new PlaybackQueue(room); PlaybackQueue.queues.set(room.roomId, queue); return queue; } diff --git a/src/components/structures/FileDropTarget.tsx b/src/components/structures/FileDropTarget.tsx index 72a86978d92..f6572a05e85 100644 --- a/src/components/structures/FileDropTarget.tsx +++ b/src/components/structures/FileDropTarget.tsx @@ -17,7 +17,6 @@ limitations under the License. import React, { useEffect, useState } from "react"; import { _t } from "../../languageHandler"; -import FileDropSvg from '../../../res/img/upload-big.svg'; interface IProps { parent: HTMLElement; @@ -110,7 +109,7 @@ const FileDropTarget: React.FC = ({ parent, onFileDrop }) => { if (state.dragging) { return
- + { _t("Drop file here to upload") }
; } diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index bfeec99ae7c..d5da40b86a9 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -46,12 +46,6 @@ import { createSpaceFromCommunity } from "../../utils/space"; import { Action } from "../../dispatcher/actions"; import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; -import CreateRoomSvg from '../../../res/img/icons-create-room.svg'; -import CancelSmallSvg from '../../../res/img/cancel-small.svg'; -import CancelSvg from '../../../res/img/cancel.svg'; -import ExternalLinkSvg from '../../../res/img/external-link.svg'; -import AddRoomSvg from '../../../res/img/icons-room-add.svg'; -import CameraSvg from '../../../res/img/camera.svg'; const LONG_DESC_PLACEHOLDER = _td( `

HTML for your community's page

@@ -141,7 +135,7 @@ class CategoryRoomList extends React.Component { ( - +
{ _t('Add a Room') }
@@ -241,7 +235,7 @@ class FeaturedRoom extends React.Component { const deleteButton = this.props.editing ? Delete - +
{ _t('Add a User') }
@@ -392,7 +386,7 @@ class FeaturedUser extends React.Component { const deleteButton = this.props.editing ? Delete - + ; } @@ -931,7 +925,7 @@ export default class GroupView extends React.Component { onClick={this._onAddRoomsClick} >
- +
{ _t('Add rooms to this community') } @@ -1263,7 +1257,7 @@ export default class GroupView extends React.Component {
- { !this.props.isMinimized && } ); diff --git a/src/components/structures/LeftPanelWidget.tsx b/src/components/structures/LeftPanelWidget.tsx deleted file mode 100644 index a73d08d29f8..00000000000 --- a/src/components/structures/LeftPanelWidget.tsx +++ /dev/null @@ -1,144 +0,0 @@ -/* -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, useMemo } from "react"; -import { Resizable } from "re-resizable"; -import classNames from "classnames"; - -import AccessibleButton from "../views/elements/AccessibleButton"; -import { useRovingTabIndex } from "../../accessibility/RovingTabIndex"; -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"; -import UIStore from "../../stores/UIStore"; -import { getKeyBindingsManager } from "../../KeyBindingsManager"; -import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts"; - -const MIN_HEIGHT = 100; -const MAX_HEIGHT = 500; // or 50% of the window height -const INITIAL_HEIGHT = 280; - -const LeftPanelWidget: React.FC = () => { - 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); - - 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
-
{ - const action = getKeyBindingsManager().getAccessibilityAction(ev); - switch (action) { - case KeyBindingAction.ArrowLeft: - ev.stopPropagation(); - setExpanded(false); - break; - case KeyBindingAction.ArrowRight: - ev.stopPropagation(); - setExpanded(true); - break; - } - }} - > -
- { - setExpanded(!expanded); - }} - > - - { 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("Maximise")} - />*/ } -
-
- - { content } -
; -}; - -export default LeftPanelWidget; diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 4c868ab15fe..4967954cdd6 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -1,5 +1,5 @@ /* -Copyright 2015 - 2021 The Matrix.org Foundation C.I.C. +Copyright 2015 - 2022 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. @@ -58,6 +58,7 @@ import AudioFeedArrayForCall from '../views/voip/AudioFeedArrayForCall'; import { OwnProfileStore } from '../../stores/OwnProfileStore'; import { UPDATE_EVENT } from "../../stores/AsyncStore"; import RoomView from './RoomView'; +import type { RoomView as RoomViewType } from './RoomView'; import ToastContainer from './ToastContainer'; import MyGroups from "./MyGroups"; import UserView from "./UserView"; @@ -139,7 +140,7 @@ class LoggedInView extends React.Component { static displayName = 'LoggedInView'; protected readonly _matrixClient: MatrixClient; - protected readonly _roomView: React.RefObject; + protected readonly _roomView: React.RefObject; protected readonly _resizeContainer: React.RefObject; protected readonly resizeHandler: React.RefObject; protected layoutWatcherRef: string; diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index ebc90f51ad2..426920f0985 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -229,7 +229,6 @@ export default class MatrixChat extends React.PureComponent { private firstSyncPromise: IDeferred; private screenAfterLogin?: IScreen; - private pageChanging: boolean; private tokenLogin?: boolean; private accountPassword?: string; private accountPasswordTimer?: number; @@ -284,8 +283,6 @@ export default class MatrixChat extends React.PureComponent { this.prevWindowWidth = UIStore.instance.windowWidth || 1000; UIStore.instance.on(UI_EVENTS.Resize, this.handleResize); - this.pageChanging = false; - // For PersistentElement this.state.resizeNotifier.on("middlePanelResized", this.dispatchTimelineResize); diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 20af21a9e59..f5c47e16c97 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -31,7 +31,7 @@ import SettingsStore from '../../settings/SettingsStore'; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import { Layout } from "../../settings/enums/Layout"; import { _t } from "../../languageHandler"; -import EventTile, { haveTileForEvent, IReadReceiptProps } from "../views/rooms/EventTile"; +import EventTile, { UnwrappedEventTile, haveTileForEvent, IReadReceiptProps } from "../views/rooms/EventTile"; import { hasText } from "../../TextForEvent"; import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer"; import DMRoomMap from "../../utils/DMRoomMap"; @@ -251,7 +251,7 @@ export default class MessagePanel extends React.Component { private scrollPanel = createRef(); private readonly showTypingNotificationsWatcherRef: string; - private eventTiles: Record = {}; + private eventTiles: Record = {}; // A map to allow groupers to maintain consistent keys even if their first event is uprooted due to back-pagination. public grouperKeyMap = new WeakMap(); @@ -336,7 +336,7 @@ export default class MessagePanel extends React.Component { return this.eventTiles[eventId]?.ref?.current; } - public getTileForEventId(eventId: string): EventTile { + public getTileForEventId(eventId: string): UnwrappedEventTile { if (!this.eventTiles) { return undefined; } @@ -919,7 +919,7 @@ export default class MessagePanel extends React.Component { return receiptsByEvent; } - private collectEventTile = (eventId: string, node: EventTile): void => { + private collectEventTile = (eventId: string, node: UnwrappedEventTile): void => { this.eventTiles[eventId] = node; }; diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index 1b4e00b5ce8..7efa5335edb 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -26,7 +26,6 @@ import AccessibleButton from '../views/elements/AccessibleButton'; import MatrixClientContext from "../../contexts/MatrixClientContext"; import AutoHideScrollbar from "./AutoHideScrollbar"; import { replaceableComponent } from "../../utils/replaceableComponent"; -import GroupsSvg from '../../../res/img/icons-groups.svg'; @replaceableComponent("structures.MyGroups") export default class MyGroups extends React.Component { @@ -107,7 +106,7 @@ export default class MyGroups extends React.Component { } return
- +
@@ -123,7 +122,7 @@ export default class MyGroups extends React.Component {
{ /*
- +
diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx index b29cc245fd3..fc1fc39df9c 100644 --- a/src/components/structures/RoomDirectory.tsx +++ b/src/components/structures/RoomDirectory.tsx @@ -78,7 +78,6 @@ interface IState { @replaceableComponent("structures.RoomDirectory") export default class RoomDirectory extends React.Component { - private readonly startTime: number; private unmounted = false; private nextBatch: string = null; private filterTimeout: number; diff --git a/src/components/structures/RoomStatusBar.tsx b/src/components/structures/RoomStatusBar.tsx index 546d6add198..514c3a507c6 100644 --- a/src/components/structures/RoomStatusBar.tsx +++ b/src/components/structures/RoomStatusBar.tsx @@ -30,7 +30,6 @@ import { StaticNotificationState } from "../../stores/notifications/StaticNotifi import AccessibleButton from "../views/elements/AccessibleButton"; import InlineSpinner from "../views/elements/InlineSpinner"; import MatrixClientContext from "../../contexts/MatrixClientContext"; -import WarningTriangleSvg from '../../../res/img/feather-customised/warning-triangle.svg'; const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_EXPANDED = 1; @@ -286,7 +285,7 @@ export default class RoomStatusBar extends React.PureComponent {
{ showTopUnreadMessagesBar: false, statusBarVisible: false, canReact: false, - canReply: false, + canSendMessages: false, layout: SettingsStore.getValue("layout"), lowBandwidth: SettingsStore.getValue("lowBandwidth"), alwaysShowTimestamps: SettingsStore.getValue("alwaysShowTimestamps"), @@ -371,12 +372,6 @@ export class RoomView extends React.Component { : MainSplitContentType.Timeline; }; - private onReadReceiptsChange = () => { - this.setState({ - showReadReceipts: SettingsStore.getValue("showReadReceipts", this.state.roomId), - }); - }; - private onRoomViewStoreUpdate = async (initial?: boolean): Promise => { if (this.unmounted) { return; @@ -847,28 +842,13 @@ export class RoomView extends React.Component { }); break; case 'reply_to_event': - if (this.state.searchResults - && payload.event.getRoomId() === this.state.roomId - && !this.unmounted - && payload.context === TimelineRenderingType.Room) { + if (!this.unmounted && + this.state.searchResults && + payload.event?.getRoomId() === this.state.roomId && + payload.context === TimelineRenderingType.Search + ) { this.onCancelSearchClick(); - } - break; - case 'quote': - if (this.state.searchResults) { - const roomId = payload.event.getRoomId(); - if (roomId === this.state.roomId) { - this.onCancelSearchClick(); - } - - setImmediate(() => { - dis.dispatch({ - action: Action.ViewRoom, - room_id: roomId, - deferred_action: payload, - metricsTrigger: "MessageSearch", - }); - }); + // we don't need to re-dispatch as RoomViewStore knows to persist with context=Search also } break; case 'MatrixActions.sync': @@ -1033,10 +1013,15 @@ export class RoomView extends React.Component { this.checkWidgets(room); this.setState({ + tombstone: this.getRoomTombstone(room), liveTimeline: room.getLiveTimeline(), }); }; + private getRoomTombstone(room = this.state.room) { + return room?.currentState.getStateEvents(EventType.RoomTombstone, ""); + } + private async calculateRecommendedVersion(room: Room) { const upgradeRecommendation = await room.getRecommendedVersion(); if (this.unmounted) return; @@ -1167,17 +1152,23 @@ export class RoomView extends React.Component { // ignore if we don't have a room yet if (!this.state.room || this.state.room.roomId !== state.roomId) return; - if (ev.getType() === EventType.RoomCanonicalAlias) { - // re-view the room so MatrixChat can manage the alias in the URL properly - dis.dispatch({ - action: Action.ViewRoom, - room_id: this.state.room.roomId, - metricsTrigger: undefined, // room doesn't change - }); - return; // this event cannot affect permissions so bail - } + switch (ev.getType()) { + case EventType.RoomCanonicalAlias: + // re-view the room so MatrixChat can manage the alias in the URL properly + dis.dispatch({ + action: Action.ViewRoom, + room_id: this.state.room.roomId, + metricsTrigger: undefined, // room doesn't change + }); + break; - this.updatePermissions(this.state.room); + case EventType.RoomTombstone: + this.setState({ tombstone: this.getRoomTombstone() }); + break; + + default: + this.updatePermissions(this.state.room); + } }; private onRoomStateUpdate = (state: RoomState) => { @@ -1201,9 +1192,9 @@ export class RoomView extends React.Component { if (room) { const me = this.context.getUserId(); const canReact = room.getMyMembership() === "join" && room.currentState.maySendEvent("m.reaction", me); - const canReply = room.maySendMessage(); + const canSendMessages = room.maySendMessage(); - this.setState({ canReact, canReply }); + this.setState({ canReact, canSendMessages }); } } @@ -1675,8 +1666,8 @@ export class RoomView extends React.Component { * * We pass it down to the scroll panel. */ - private handleScrollKey = ev => { - let panel; + public handleScrollKey = ev => { + let panel: ScrollPanel | TimelinePanel; if (this.searchResultsPanel.current) { panel = this.searchResultsPanel.current; } else if (this.messagePanel) { diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index 65da850e8e1..0c6ce4cee5e 100644 --- a/src/components/structures/SpaceHierarchy.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -770,8 +770,10 @@ const SpaceHierarchy = ({ content = <>
-

{ query.trim() ? _t("Results") : _t("Rooms and spaces") }

- +

+ { query.trim() ? _t("Results") : _t("Rooms and spaces") } +

+
{ additionalButtons } { hasPermissions && ( ) } - +
{ errorText &&
{ errorText } diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 896c2965040..42e2d3adf23 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -479,8 +479,10 @@ const SpaceLanding = ({ space }: { space: Room }) => { }; return
- - +
+ + +
{ (name) => { @@ -897,10 +899,11 @@ export default class SpaceRoomView extends React.PureComponent { title={_t("What are some things you want to discuss in %(spaceName)s?", { spaceName: this.props.justCreatedOpts?.createOpts?.name || this.props.space.name, })} - description={ - _t("Let's create a room for each of them.") + "\n" + - _t("You can add more later too, including already existing ones.") - } + description={<> + { _t("Let's create a room for each of them.") } +
+ { _t("You can add more later too, including already existing ones.") } + } onFinished={(firstRoomId: string) => this.setState({ phase: Phase.PublicShare, firstRoomId })} />; case Phase.PublicShare: @@ -928,8 +931,11 @@ export default class SpaceRoomView extends React.PureComponent { return + { _t("We'll create rooms for each of them.") } +
+ { _t("You can add more later too, including already existing ones.") } + } onFinished={(firstRoomId: string) => this.setState({ phase: Phase.PrivateInvite, firstRoomId })} />; case Phase.PrivateExistingRooms: diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index a452ee630a8..30669a8eec8 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -17,15 +17,19 @@ limitations under the License. import React, { useContext, useEffect, useRef, useState } from 'react'; import { EventTimelineSet } from 'matrix-js-sdk/src/models/event-timeline-set'; import { Room } from 'matrix-js-sdk/src/models/room'; -import { RelationType } from 'matrix-js-sdk/src/@types/event'; import { MatrixClient } from 'matrix-js-sdk/src/client'; import { Filter, IFilterDefinition, - UNSTABLE_FILTER_RELATED_BY_SENDERS, - UNSTABLE_FILTER_RELATED_BY_REL_TYPES, } from 'matrix-js-sdk/src/filter'; -import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread'; +import { + FILTER_RELATED_BY_REL_TYPES, + FILTER_RELATED_BY_SENDERS, + Thread, + ThreadEvent, + THREAD_RELATION_TYPE, +} from 'matrix-js-sdk/src/models/thread'; +import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline'; import BaseCard from "../views/right_panel/BaseCard"; import ResizeNotifier from '../../utils/ResizeNotifier'; @@ -53,13 +57,13 @@ export async function getThreadTimelineSet( const definition: IFilterDefinition = { "room": { "timeline": { - [UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]: [RelationType.Thread], + [FILTER_RELATED_BY_REL_TYPES.name]: [THREAD_RELATION_TYPE.name], }, }, }; if (filterType === ThreadFilterType.My) { - definition.room.timeline[UNSTABLE_FILTER_RELATED_BY_SENDERS.name] = [myUserId]; + definition.room.timeline[FILTER_RELATED_BY_SENDERS.name] = [myUserId]; } filter.setDefinition(definition); @@ -70,14 +74,16 @@ export async function getThreadTimelineSet( filter.filterId = filterId; const timelineSet = room.getOrCreateFilteredTimelineSet( filter, - { prepopulateTimeline: false }, + { + prepopulateTimeline: false, + pendingEvents: false, + }, ); - timelineSet.resetLiveTimeline(); - await client.paginateEventTimeline( - timelineSet.getLiveTimeline(), - { backwards: true, limit: 20 }, - ); + // An empty pagination token allows to paginate from the very bottom of + // the timeline set. + timelineSet.getLiveTimeline().setPaginationToken("", EventTimeline.BACKWARDS); + return timelineSet; } else { // Filter creation fails if HomeServer does not support the new relation @@ -88,8 +94,8 @@ export async function getThreadTimelineSet( }); Array.from(room.threads) - .sort(([, threadA], [, threadB]) => threadA.replyToEvent.getTs() - threadB.replyToEvent.getTs()) .forEach(([, thread]) => { + if (thread.length === 0) return; const currentUserParticipated = thread.events.some(event => event.getSender() === client.getUserId()); if (filterType !== ThreadFilterType.My || currentUserParticipated) { timelineSet.getLiveTimeline().addEvent(thread.rootEvent, false); @@ -204,8 +210,8 @@ const EmptyThread: React.FC = ({ filterOption, showAllThreads return