diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..c9d11f02c8 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,3 @@ + + + diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index 3f82e61280..3c3807e33b 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -1,4 +1,4 @@ -name: Develop jobs +name: Develop on: push: branches: [develop] @@ -11,17 +11,31 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 - - name: End-to-End tests - run: ./scripts/ci/end-to-end-tests.sh + - name: Prepare End-to-End tests + run: ./scripts/ci/prepare-end-to-end-tests.sh + - name: Run End-to-End tests + run: ./scripts/ci/run-end-to-end-tests.sh - name: Archive logs uses: actions/upload-artifact@v2 + if: ${{ always() }} with: path: | test/end-to-end-tests/logs/**/* test/end-to-end-tests/synapse/installations/consent/homeserver.log retention-days: 14 - - name: Archive performance benchmark - uses: actions/upload-artifact@v2 + - name: Download previous benchmark data + uses: actions/cache@v1 with: - name: performance-entries.json - path: test/end-to-end-tests/performance-entries.json + path: ./cache + key: ${{ runner.os }}-benchmark + - name: Store benchmark result + uses: matrix-org/github-action-benchmark@jsperfentry-1 + with: + tool: 'jsperformanceentry' + output-file-path: test/end-to-end-tests/performance-entries.json + fail-on-alert: false + comment-on-alert: false + # Only temporary to monitor where failures occur + alert-comment-cc-users: '@gsouquet' + github-token: ${{ secrets.DEPLOY_GH_PAGES }} + auto-push: ${{ github.ref == 'refs/heads/develop' }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 94c9530941..0f979b4802 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,133 @@ +Changes in [3.24.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.24.0) (2021-06-21) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.24.0-rc.1...v3.24.0) + + * Upgrade to JS SDK 12.0.0 + * [Release] Keep composer reply when scrolling away from a highlighted event + [\#6211](https://github.com/matrix-org/matrix-react-sdk/pull/6211) + * [Release] Remove stray bullet point in reply preview + [\#6210](https://github.com/matrix-org/matrix-react-sdk/pull/6210) + * [Release] Stop requesting null next replies from the server + [\#6209](https://github.com/matrix-org/matrix-react-sdk/pull/6209) + +Changes in [3.24.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.24.0-rc.1) (2021-06-15) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.23.0...v3.24.0-rc.1) + + * Upgrade to JS SDK 12.0.0-rc.1 + * Translations update from Weblate + [\#6192](https://github.com/matrix-org/matrix-react-sdk/pull/6192) + * Disable comment-on-alert for PR coming from a fork + [\#6189](https://github.com/matrix-org/matrix-react-sdk/pull/6189) + * Add JS benchmark tracking in CI + [\#6177](https://github.com/matrix-org/matrix-react-sdk/pull/6177) + * Upgrade matrix-react-test-utils for React 17 peer deps + [\#6187](https://github.com/matrix-org/matrix-react-sdk/pull/6187) + * Fix display name overlaps on the IRC layout + [\#6186](https://github.com/matrix-org/matrix-react-sdk/pull/6186) + * Small fixes to the spaces experience + [\#6184](https://github.com/matrix-org/matrix-react-sdk/pull/6184) + * Add footer and privacy note to the start dm dialog + [\#6111](https://github.com/matrix-org/matrix-react-sdk/pull/6111) + * Format mxids when disambiguation needed + [\#5880](https://github.com/matrix-org/matrix-react-sdk/pull/5880) + * Move various createRoom types to the js-sdk + [\#6183](https://github.com/matrix-org/matrix-react-sdk/pull/6183) + * Fix HTML tag for Event Tile when not rendered in a list + [\#6175](https://github.com/matrix-org/matrix-react-sdk/pull/6175) + * Remove legacy polyfills and unused dependencies + [\#6176](https://github.com/matrix-org/matrix-react-sdk/pull/6176) + * Fix buggy hovering/selecting of event tiles + [\#6173](https://github.com/matrix-org/matrix-react-sdk/pull/6173) + * Add room intro warning when e2ee is not enabled + [\#5929](https://github.com/matrix-org/matrix-react-sdk/pull/5929) + * Migrate end to end tests to GitHub actions + [\#6156](https://github.com/matrix-org/matrix-react-sdk/pull/6156) + * Fix expanding last collapsed sticky session when zoomed in + [\#6171](https://github.com/matrix-org/matrix-react-sdk/pull/6171) + * ⚛️ Upgrade to React@17 + [\#6165](https://github.com/matrix-org/matrix-react-sdk/pull/6165) + * Revert refreshStickyHeaders optimisations + [\#6168](https://github.com/matrix-org/matrix-react-sdk/pull/6168) + * Add logging for which rooms calls are in + [\#6170](https://github.com/matrix-org/matrix-react-sdk/pull/6170) + * Restore read receipt animation from event to event + [\#6169](https://github.com/matrix-org/matrix-react-sdk/pull/6169) + * Restore copy button icon when sharing permalink + [\#6166](https://github.com/matrix-org/matrix-react-sdk/pull/6166) + * Restore Page Up/Down key bindings when focusing the composer + [\#6167](https://github.com/matrix-org/matrix-react-sdk/pull/6167) + * Timeline rendering optimizations + [\#6143](https://github.com/matrix-org/matrix-react-sdk/pull/6143) + * Bump css-what from 5.0.0 to 5.0.1 + [\#6164](https://github.com/matrix-org/matrix-react-sdk/pull/6164) + * Bump ws from 6.2.1 to 6.2.2 in /test/end-to-end-tests + [\#6145](https://github.com/matrix-org/matrix-react-sdk/pull/6145) + * Bump trim-newlines from 3.0.0 to 3.0.1 + [\#6163](https://github.com/matrix-org/matrix-react-sdk/pull/6163) + * Fix upgrade to element home button in top left menu + [\#6162](https://github.com/matrix-org/matrix-react-sdk/pull/6162) + * Fix unpinning of pinned messages and panel empty state + [\#6140](https://github.com/matrix-org/matrix-react-sdk/pull/6140) + * Better handling for widgets that fail to load + [\#6161](https://github.com/matrix-org/matrix-react-sdk/pull/6161) + * Improved forwarding UI + [\#5999](https://github.com/matrix-org/matrix-react-sdk/pull/5999) + * Fixes for sharing room links + [\#6118](https://github.com/matrix-org/matrix-react-sdk/pull/6118) + * Fix setting watchers + [\#6160](https://github.com/matrix-org/matrix-react-sdk/pull/6160) + * Fix Stickerpicker context menu + [\#6152](https://github.com/matrix-org/matrix-react-sdk/pull/6152) + * Add warning to private space creation flow + [\#6155](https://github.com/matrix-org/matrix-react-sdk/pull/6155) + * Add prop to alwaysShowTimestamps on TimelinePanel + [\#6159](https://github.com/matrix-org/matrix-react-sdk/pull/6159) + * Fix notif panel timestamp padding + [\#6157](https://github.com/matrix-org/matrix-react-sdk/pull/6157) + * Fixes and refactoring for the ImageView + [\#6149](https://github.com/matrix-org/matrix-react-sdk/pull/6149) + * Fix timestamps + [\#6148](https://github.com/matrix-org/matrix-react-sdk/pull/6148) + * Make it easier to pan images in the lightbox + [\#6147](https://github.com/matrix-org/matrix-react-sdk/pull/6147) + * Fix scroll token for EventTile and EventListSummary node type + [\#6154](https://github.com/matrix-org/matrix-react-sdk/pull/6154) + * Convert bunch of things to Typescript + [\#6153](https://github.com/matrix-org/matrix-react-sdk/pull/6153) + * Lint the typescript tests + [\#6142](https://github.com/matrix-org/matrix-react-sdk/pull/6142) + * Fix jumping to bottom without a highlighted event + [\#6146](https://github.com/matrix-org/matrix-react-sdk/pull/6146) + * Repair event status position in timeline + [\#6141](https://github.com/matrix-org/matrix-react-sdk/pull/6141) + * Adapt for js-sdk MatrixClient conversion to TS + [\#6132](https://github.com/matrix-org/matrix-react-sdk/pull/6132) + * Improve pinned messages in Labs + [\#6096](https://github.com/matrix-org/matrix-react-sdk/pull/6096) + * Map phone number lookup results to their native rooms + [\#6136](https://github.com/matrix-org/matrix-react-sdk/pull/6136) + * Fix mx_Event containment rules and empty read avatar row + [\#6138](https://github.com/matrix-org/matrix-react-sdk/pull/6138) + * Improve switch room rendering + [\#6079](https://github.com/matrix-org/matrix-react-sdk/pull/6079) + * Add CSS containment rules for shorter reflow operations + [\#6127](https://github.com/matrix-org/matrix-react-sdk/pull/6127) + * ignore hash/fragment when de-duplicating links for url previews + [\#6135](https://github.com/matrix-org/matrix-react-sdk/pull/6135) + * Clicking jump to bottom resets room hash + [\#5823](https://github.com/matrix-org/matrix-react-sdk/pull/5823) + * Use passive option for scroll handlers + [\#6113](https://github.com/matrix-org/matrix-react-sdk/pull/6113) + * Optimise memberSort performance for large list + [\#6130](https://github.com/matrix-org/matrix-react-sdk/pull/6130) + * Tweak event border radius to match action bar + [\#6133](https://github.com/matrix-org/matrix-react-sdk/pull/6133) + * Log when we ignore a second call in a room + [\#6131](https://github.com/matrix-org/matrix-react-sdk/pull/6131) + * Performance monitoring measurements + [\#6041](https://github.com/matrix-org/matrix-react-sdk/pull/6041) + Changes in [3.23.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.23.0) (2021-06-07) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.23.0-rc.1...v3.23.0) diff --git a/package.json b/package.json index 69fcf2df61..7956c4b47f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.23.0", + "version": "3.24.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -78,7 +78,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "12.0.0", "matrix-widget-api": "^0.1.0-beta.14", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", @@ -132,20 +132,20 @@ "@types/pako": "^1.0.1", "@types/parse5": "^6.0.0", "@types/qrcode": "^1.3.5", - "@types/react": "^16.9", + "@types/react": "^17.0.2", "@types/react-beautiful-dnd": "^13.0.0", - "@types/react-dom": "^16.9.10", + "@types/react-dom": "^17.0.2", "@types/react-transition-group": "^4.4.0", "@types/sanitize-html": "^2.3.1", "@types/zxcvbn": "^4.4.0", "@typescript-eslint/eslint-plugin": "^4.14.0", "@typescript-eslint/parser": "^4.14.0", + "@wojtekmaj/enzyme-adapter-react-17": "^0.6.1", "babel-eslint": "^10.1.0", "babel-jest": "^26.6.3", "chokidar": "^3.5.1", "concurrently": "^5.3.0", "enzyme": "^3.11.0", - "@wojtekmaj/enzyme-adapter-react-17": "^0.6.1", "eslint": "7.18.0", "eslint-config-matrix-org": "^0.2.0", "eslint-plugin-babel": "^5.3.1", @@ -158,7 +158,7 @@ "jest-environment-jsdom-sixteen": "^1.0.3", "jest-fetch-mock": "^3.0.3", "matrix-mock-request": "^1.2.3", - "matrix-react-test-utils": "^0.2.2", + "matrix-react-test-utils": "^0.2.3", "matrix-web-i18n": "github:matrix-org/matrix-web-i18n", "react-test-renderer": "^17.0.2", "rimraf": "^3.0.2", @@ -168,9 +168,6 @@ "typescript": "^4.1.3", "walk": "^2.3.14" }, - "resolutions": { - "**/@types/react": "^16.14" - }, "jest": { "testEnvironment": "./__test-utils__/environment.js", "testMatch": [ diff --git a/res/css/_components.scss b/res/css/_components.scss index 56403ea190..ec3af8655e 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -123,7 +123,6 @@ @import "./views/elements/_EventListSummary.scss"; @import "./views/elements/_FacePile.scss"; @import "./views/elements/_Field.scss"; -@import "./views/elements/_FormButton.scss"; @import "./views/elements/_ImageView.scss"; @import "./views/elements/_InfoTooltip.scss"; @import "./views/elements/_InlineSpinner.scss"; diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index 12762cbc9d..48b565be7f 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -328,7 +328,8 @@ $SpaceRoomViewInnerWidth: 428px; font-size: $font-15px; margin-top: 12px; margin-bottom: 16px; - white-space: pre; + white-space: pre-wrap; + word-wrap: break-word; } > hr { diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss index 09f834a6e3..14e4c01389 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -134,8 +134,9 @@ limitations under the License. .mx_Toast_buttons { float: right; display: flex; + gap: 5px; - .mx_FormButton { + .mx_AccessibleButton { min-width: 96px; box-sizing: border-box; } diff --git a/res/css/views/beta/_BetaCard.scss b/res/css/views/beta/_BetaCard.scss index 3463a653fc..1a8241b65f 100644 --- a/res/css/views/beta/_BetaCard.scss +++ b/res/css/views/beta/_BetaCard.scss @@ -19,49 +19,68 @@ limitations under the License. padding: 24px; background-color: $settings-profile-placeholder-bg-color; border-radius: 8px; - display: flex; box-sizing: border-box; - > div { - .mx_BetaCard_title { - font-weight: $font-semi-bold; - font-size: $font-18px; - line-height: $font-22px; - color: $primary-fg-color; - margin: 4px 0 14px; + .mx_BetaCard_columns { + display: flex; - .mx_BetaCard_betaPill { - margin-left: 12px; + > div { + .mx_BetaCard_title { + font-weight: $font-semi-bold; + font-size: $font-18px; + line-height: $font-22px; + color: $primary-fg-color; + margin: 4px 0 14px; + + .mx_BetaCard_betaPill { + margin-left: 12px; + } + } + + .mx_BetaCard_caption { + font-size: $font-15px; + line-height: $font-20px; + color: $secondary-fg-color; + margin-bottom: 20px; + } + + .mx_BetaCard_buttons .mx_AccessibleButton { + display: block; + margin: 12px 0; + padding: 7px 40px; + width: auto; + } + + .mx_BetaCard_disclaimer { + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + margin-top: 20px; } } - .mx_BetaCard_caption { - font-size: $font-15px; - line-height: $font-20px; - color: $secondary-fg-color; - margin-bottom: 20px; - } - - .mx_AccessibleButton { - display: block; - margin: 12px 0; - padding: 7px 40px; - width: auto; - } - - .mx_BetaCard_disclaimer { - font-size: $font-12px; - line-height: $font-15px; - color: $secondary-fg-color; - margin-top: 20px; + > img { + margin: auto 0 auto 20px; + width: 300px; + object-fit: contain; + height: 100%; } } - > img { - margin: auto 0 auto 20px; - width: 300px; - object-fit: contain; - height: 100%; + .mx_BetaCard_relatedSettings { + .mx_SettingsFlag { + margin: 16px 0 0; + font-size: $font-15px; + line-height: $font-24px; + color: $primary-fg-color; + + .mx_SettingsFlag_microcopy { + margin-top: 4px; + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + } + } } } diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss index d8ff56663a..2e48b5d8e9 100644 --- a/res/css/views/dialogs/_InviteDialog.scss +++ b/res/css/views/dialogs/_InviteDialog.scss @@ -17,6 +17,9 @@ limitations under the License. .mx_InviteDialog_addressBar { display: flex; flex-direction: row; + // Right margin for the design. We could apply this to the whole dialog, but then the scrollbar + // for the user section gets weird. + margin: 8px 45px 0 0; .mx_InviteDialog_editor { flex: 1; @@ -73,7 +76,7 @@ limitations under the License. } .mx_InviteDialog_section { - padding-bottom: 10px; + padding-bottom: 4px; h3 { font-size: $font-12px; @@ -82,6 +85,14 @@ limitations under the License. text-transform: uppercase; } + > p { + margin: 0; + } + + > span { + color: $primary-fg-color; + } + .mx_InviteDialog_subname { margin-bottom: 10px; margin-top: -10px; // HACK: Positioning with margins is bad @@ -90,6 +101,63 @@ limitations under the License. } } +.mx_InviteDialog_section_hidden_suggestions_disclaimer { + padding: 8px 0 16px 0; + font-size: $font-14px; + + > span { + color: $primary-fg-color; + font-weight: 600; + } + + > p { + margin: 0; + } +} + +.mx_InviteDialog_footer { + border-top: 1px solid $input-border-color; + + > h3 { + margin: 12px 0; + font-size: $font-12px; + color: $muted-fg-color; + font-weight: bold; + text-transform: uppercase; + } + + .mx_InviteDialog_footer_link { + display: flex; + justify-content: space-between; + border-radius: 4px; + border: solid 1px $light-fg-color; + padding: 8px; + + > a { + text-decoration: none; + flex-shrink: 1; + overflow: hidden; + text-overflow: ellipsis; + } + } + + .mx_InviteDialog_footer_link_copy { + flex-shrink: 0; + cursor: pointer; + margin-left: 20px; + display: inherit; + + > div { + mask-image: url($copy-button-url); + background-color: $message-action-bar-fg-color; + margin-left: 5px; + width: 20px; + height: 20px; + background-repeat: no-repeat; + } + } +} + .mx_InviteDialog_roomTile { cursor: pointer; padding: 5px 10px; @@ -142,6 +210,7 @@ limitations under the License. .mx_InviteDialog_roomTile_nameStack { display: inline-block; + overflow: hidden; } .mx_InviteDialog_roomTile_name { @@ -157,6 +226,13 @@ limitations under the License. margin-left: 7px; } + .mx_InviteDialog_roomTile_name, + .mx_InviteDialog_roomTile_userId { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .mx_InviteDialog_roomTile_time { text-align: right; font-size: $font-12px; @@ -212,22 +288,29 @@ limitations under the License. .mx_InviteDialog { // Prevent the dialog from jumping around randomly when elements change. - height: 590px; + height: 600px; padding-left: 20px; // the design wants some padding on the left + display: flex; + flex-direction: column; + + .mx_InviteDialog_content { + overflow: hidden; + } } .mx_InviteDialog_userSections { - margin-top: 10px; + margin-top: 4px; overflow-y: auto; - padding-right: 45px; - height: 455px; // mx_InviteDialog's height minus some for the upper elements + padding: 0 45px 4px 0; + height: calc(100% - 115px); // mx_InviteDialog's height minus some for the upper and lower elements } -// Right margin for the design. We could apply this to the whole dialog, but then the scrollbar -// for the user section gets weird. -.mx_InviteDialog_helpText, -.mx_InviteDialog_addressBar { - margin-right: 45px; +.mx_InviteDialog_hasFooter .mx_InviteDialog_userSections { + height: calc(100% - 175px); +} + +.mx_InviteDialog_helpText { + margin: 0; } .mx_InviteDialog_helpText .mx_AccessibleButton_kind_link { diff --git a/res/css/views/elements/_FormButton.scss b/res/css/views/elements/_FormButton.scss deleted file mode 100644 index eda201ff03..0000000000 --- a/res/css/views/elements/_FormButton.scss +++ /dev/null @@ -1,42 +0,0 @@ -/* -Copyright 2019 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_FormButton { - line-height: $font-16px; - padding: 5px 15px; - font-size: $font-12px; - height: min-content; - - &:not(:last-child) { - margin-right: 8px; - } - - &.mx_AccessibleButton_kind_primary { - color: $accent-color; - background-color: $accent-bg-color; - } - - &.mx_AccessibleButton_kind_danger { - color: $notice-primary-color; - background-color: $notice-primary-bg-color; - } - - &.mx_AccessibleButton_kind_secondary { - color: $secondary-fg-color; - border: 1px solid $secondary-fg-color; - background-color: unset; - } -} diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index 87420ae4e7..6632ccddf9 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -259,16 +259,6 @@ limitations under the License. .mx_AccessibleButton.mx_AccessibleButton_hasKind { padding: 8px 18px; - - &.mx_AccessibleButton_kind_primary { - color: $accent-color; - background-color: $accent-bg-color; - } - - &.mx_AccessibleButton_kind_danger { - color: $notice-primary-color; - background-color: $notice-primary-bg-color; - } } .mx_VerificationShowSas .mx_AccessibleButton, diff --git a/res/css/views/right_panel/_VerificationPanel.scss b/res/css/views/right_panel/_VerificationPanel.scss index a8466a1626..12148b09de 100644 --- a/res/css/views/right_panel/_VerificationPanel.scss +++ b/res/css/views/right_panel/_VerificationPanel.scss @@ -58,7 +58,7 @@ limitations under the License. } .mx_VerificationPanel_reciprocate_section { - .mx_FormButton { + .mx_AccessibleButton { width: 100%; box-sizing: border-box; padding: 10px; diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index cf61ce569d..ae8d22a462 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -178,7 +178,7 @@ $irc-line-height: $font-18px; overflow: hidden; display: flex; - > .mx_SenderProfile_name { + > .mx_SenderProfile_displayName { overflow: hidden; text-overflow: ellipsis; min-width: var(--name-width); @@ -207,7 +207,7 @@ $irc-line-height: $font-18px; background: transparent; > span { - > .mx_SenderProfile_name { + > .mx_SenderProfile_displayName { min-width: inherit; } } diff --git a/res/css/views/spaces/_SpaceBasicSettings.scss b/res/css/views/spaces/_SpaceBasicSettings.scss index 204ccab2b7..32454b9530 100644 --- a/res/css/views/spaces/_SpaceBasicSettings.scss +++ b/res/css/views/spaces/_SpaceBasicSettings.scss @@ -73,7 +73,7 @@ limitations under the License. } } - .mx_FormButton { + .mx_AccessibleButton { padding: 8px 22px; margin-left: auto; display: block; diff --git a/res/css/views/voip/_CallContainer.scss b/res/css/views/voip/_CallContainer.scss index 8262075559..168a8bb74b 100644 --- a/res/css/views/voip/_CallContainer.scss +++ b/res/css/views/voip/_CallContainer.scss @@ -98,5 +98,29 @@ limitations under the License. line-height: $font-24px; } } + + .mx_IncomingCallBox_iconButton { + position: absolute; + right: 8px; + + &::before { + content: ''; + + height: 20px; + width: 20px; + background-color: $icon-button-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + } + } + + .mx_IncomingCallBox_silence::before { + mask-image: url('$(res)/img/voip/silence.svg'); + } + + .mx_IncomingCallBox_unSilence::before { + mask-image: url('$(res)/img/voip/un-silence.svg'); + } } } diff --git a/res/img/voip/silence.svg b/res/img/voip/silence.svg new file mode 100644 index 0000000000..332932dfff --- /dev/null +++ b/res/img/voip/silence.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/voip/un-silence.svg b/res/img/voip/un-silence.svg new file mode 100644 index 0000000000..c00b366f84 --- /dev/null +++ b/res/img/voip/un-silence.svg @@ -0,0 +1,4 @@ + + + + diff --git a/scripts/ci/Dockerfile b/scripts/ci/Dockerfile index 3fdd0d7bf6..1d1425c865 100644 --- a/scripts/ci/Dockerfile +++ b/scripts/ci/Dockerfile @@ -3,6 +3,6 @@ # docker push vectorim/element-web-ci-e2etests-env:latest FROM node:14-buster RUN apt-get update -RUN apt-get -y install build-essential python3-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev uuid-runtime +RUN apt-get -y install jq build-essential python3-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev uuid-runtime # dependencies for chrome (installed by puppeteer) RUN apt-get -y install gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget diff --git a/scripts/ci/end-to-end-tests.sh b/scripts/ci/prepare-end-to-end-tests.sh similarity index 65% rename from scripts/ci/end-to-end-tests.sh rename to scripts/ci/prepare-end-to-end-tests.sh index edb8870d8e..147e1f6445 100755 --- a/scripts/ci/end-to-end-tests.sh +++ b/scripts/ci/prepare-end-to-end-tests.sh @@ -1,8 +1,4 @@ #!/bin/bash -# -# script which is run by the CI build (after `yarn test`). -# -# clones element-web develop and runs the tests against our version of react-sdk. set -ev @@ -19,7 +15,7 @@ cd element-web element_web_dir=`pwd` CI_PACKAGE=true yarn build cd .. -# run end to end tests +# prepare end to end tests pushd test/end-to-end-tests ln -s $element_web_dir element/element-web # PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh @@ -28,9 +24,4 @@ echo "--- Install synapse & other dependencies" ./install.sh # install static webserver to server symlinked local copy of element ./element/install-webserver.sh -rm -r logs || true -mkdir logs -echo "+++ Running end-to-end tests" -TESTS_STARTED=1 -./run.sh --no-sandbox --log-directory logs/ popd diff --git a/scripts/ci/run-end-to-end-tests.sh b/scripts/ci/run-end-to-end-tests.sh new file mode 100755 index 0000000000..3c99391fc7 --- /dev/null +++ b/scripts/ci/run-end-to-end-tests.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -ev + +handle_error() { + EXIT_CODE=$? + exit $EXIT_CODE +} + +trap 'handle_error' ERR + +# run end to end tests +pushd test/end-to-end-tests +rm -r logs || true +mkdir logs +echo "--- Running end-to-end tests" +TESTS_STARTED=1 +./run.sh --no-sandbox --log-directory logs/ +popd diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh index fe1f49c361..0990af70ce 100755 --- a/scripts/fetchdep.sh +++ b/scripts/fetchdep.sh @@ -22,29 +22,51 @@ clone() { } # Try the PR author's branch in case it exists on the deps as well. -# First we check if BUILDKITE_BRANCH is defined, -# if it isn't we can assume this is a Netlify build -if [ -z ${BUILDKITE_BRANCH+x} ]; then - # Netlify doesn't give us info about the fork so we have to get it from GitHub API - apiEndpoint="https://api.github.com/repos/matrix-org/matrix-react-sdk/pulls/" - apiEndpoint+=$REVIEW_ID - head=$(curl $apiEndpoint | jq -r '.head.label') -else +# First we check if GITHUB_HEAD_REF is defined, +# Then we check if BUILDKITE_BRANCH is defined, +# if they aren't we can assume this is a Netlify build +if [ -n "$GITHUB_HEAD_REF" ]; then + head=$GITHUB_HEAD_REF +elif [ -n "$BUILDKITE_BRANCH" ]; then head=$BUILDKITE_BRANCH +else + # Netlify doesn't give us info about the fork so we have to get it from GitHub API + apiEndpoint="https://api.github.com/repos/matrix-org/matrix-react-sdk/pulls/" + apiEndpoint+=$REVIEW_ID + head=$(curl $apiEndpoint | jq -r '.head.label') fi -# If head is set, it will contain either: +# If head is set, it will contain on Buildkite either: # * "branch" when the author's branch and target branch are in the same repo # * "fork:branch" when the author's branch is in their fork or if this is a Netlify build # We can split on `:` into an array to check. +# For GitHub Actions we need to inspect GITHUB_REPOSITORY and GITHUB_ACTOR +# to determine whether the branch is from a fork or not BRANCH_ARRAY=(${head//:/ }) if [[ "${#BRANCH_ARRAY[@]}" == "1" ]]; then - clone $deforg $defrepo $BUILDKITE_BRANCH + + if [ -n "$GITHUB_HEAD_REF" ]; then + if [[ "$GITHUB_REPOSITORY" == "$deforg"* ]]; then + clone $deforg $defrepo $GITHUB_HEAD_REF + else + REPO_ARRAY=(${GITHUB_REPOSITORY//\// }) + clone $REPO_ARRAY[0] $defrepo $GITHUB_HEAD_REF + fi + else + clone $deforg $defrepo $BUILDKITE_BRANCH + fi + elif [[ "${#BRANCH_ARRAY[@]}" == "2" ]]; then clone ${BRANCH_ARRAY[0]} $defrepo ${BRANCH_ARRAY[1]} fi + # Try the target branch of the push or PR. -clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH +if [ -n $GITHUB_BASE_REF ]; then + clone $deforg $defrepo $GITHUB_BASE_REF +elif [ -n $BUILDKITE_PULL_REQUEST_BASE_BRANCH ]; then + clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH +fi + # Try HEAD which is the branch name in Netlify (not BRANCH which is pull/xxxx/head for PR builds) clone $deforg $defrepo $HEAD # Use the default branch as the last resort. diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 22280b8a28..7eff341095 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -44,6 +44,7 @@ import { EventIndexPeg } from "../indexing/EventIndexPeg"; import {VoiceRecordingStore} from "../stores/VoiceRecordingStore"; import PerformanceMonitor from "../performance"; import UIStore from "../stores/UIStore"; +import { SetupEncryptionStore } from "../stores/SetupEncryptionStore"; declare global { interface Window { @@ -84,6 +85,7 @@ declare global { mxPerformanceMonitor: PerformanceMonitor; mxPerformanceEntryNames: any; mxUIStore: UIStore; + mxSetupEncryptionStore?: SetupEncryptionStore; } interface Document { @@ -111,19 +113,6 @@ declare global { usageDetails?: {[key: string]: number}; } - export interface ISettledFulfilled { - status: "fulfilled"; - value: T; - } - export interface ISettledRejected { - status: "rejected"; - reason: any; - } - - interface PromiseConstructor { - allSettled(promises: Promise[]): Promise | ISettledRejected>>; - } - interface HTMLAudioElement { type?: string; // sinkId & setSinkId are experimental and typescript doesn't know about them diff --git a/src/Avatar.ts b/src/Avatar.ts index a6499c688e..4c4bd1c265 100644 --- a/src/Avatar.ts +++ b/src/Avatar.ts @@ -14,18 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -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 { 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 { ResizeMethod } from "matrix-js-sdk/src/@types/partials"; import DMRoomMap from './utils/DMRoomMap'; -import {mediaFromMxc} from "./customisations/Media"; +import { mediaFromMxc } from "./customisations/Media"; import SettingsStore from "./settings/SettingsStore"; -export type ResizeMethod = "crop" | "scale"; - // Not to be used for BaseAvatar urls as that has similar default avatar fallback already -export function avatarUrlForMember(member: RoomMember, width: number, height: number, resizeMethod: ResizeMethod) { +export function avatarUrlForMember( + member: RoomMember, + width: number, + height: number, + resizeMethod: ResizeMethod, +): string { let url: string; if (member?.getMxcAvatarUrl()) { url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod); @@ -39,7 +43,12 @@ export function avatarUrlForMember(member: RoomMember, width: number, height: nu return url; } -export function avatarUrlForUser(user: User, width: number, height: number, resizeMethod?: ResizeMethod) { +export function avatarUrlForUser( + user: Pick, + width: number, + height: number, + resizeMethod?: ResizeMethod, +): string | null { if (!user.avatarUrl) return null; return mediaFromMxc(user.avatarUrl).getThumbnailOfSourceHttp(width, height, resizeMethod); } diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index bf7cb3473d..448b1cb780 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -99,7 +99,7 @@ const CHECK_PROTOCOLS_ATTEMPTS = 3; // (and store the ID of their native room) export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room'; -enum AudioID { +export enum AudioID { Ring = 'ringAudio', Ringback = 'ringbackAudio', CallEnd = 'callendAudio', diff --git a/src/Modal.tsx b/src/Modal.tsx index ce11c571b6..2f2d5a2d52 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -385,7 +385,7 @@ export class ModalManager { ); - ReactDOM.render(dialog, ModalManager.getOrCreateContainer()); + setImmediate(() => ReactDOM.render(dialog, ModalManager.getOrCreateContainer())); } else { // This is safe to call repeatedly if we happen to do that ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer()); diff --git a/src/Searching.js b/src/Searching.js index 2b17aee054..596dd2f3d4 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -468,7 +468,7 @@ function restoreEncryptionInfo(searchResultSlice = []) { ev.event.curve25519Key, ev.event.ed25519Key, ); - ev._forwardingCurve25519KeyChain = ev.event.forwardingCurve25519KeyChain; + ev.forwardingCurve25519KeyChain = ev.event.forwardingCurve25519KeyChain; delete ev.event.curve25519Key; delete ev.event.ed25519Key; diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 4a7b37b5e5..bd2133f3d9 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -17,8 +17,8 @@ See the License for the specific language governing permissions and limitations under the License. */ - import * as React from 'react'; +import { User } from "matrix-js-sdk/src/models/user"; import * as ContentHelpers from 'matrix-js-sdk/src/content-helpers'; import {MatrixClientPeg} from './MatrixClientPeg'; @@ -150,6 +150,10 @@ function success(promise?: Promise) { return {promise}; } +function successSync(value: any) { + return success(Promise.resolve(value)); +} + /* Disable the "unexpected this" error for these commands - all of the run * functions are called with `this` bound to the Command instance. */ @@ -160,7 +164,7 @@ export const Commands = [ args: '', description: _td('Sends the given message as a spoiler'), runFn: function(roomId, message) { - return success(ContentHelpers.makeHtmlMessage( + return successSync(ContentHelpers.makeHtmlMessage( message, `${message}`, )); @@ -176,7 +180,7 @@ export const Commands = [ if (args) { message = message + ' ' + args; } - return success(ContentHelpers.makeTextMessage(message)); + return successSync(ContentHelpers.makeTextMessage(message)); }, category: CommandCategories.messages, }), @@ -189,7 +193,7 @@ export const Commands = [ if (args) { message = message + ' ' + args; } - return success(ContentHelpers.makeTextMessage(message)); + return successSync(ContentHelpers.makeTextMessage(message)); }, category: CommandCategories.messages, }), @@ -202,7 +206,7 @@ export const Commands = [ if (args) { message = message + ' ' + args; } - return success(ContentHelpers.makeTextMessage(message)); + return successSync(ContentHelpers.makeTextMessage(message)); }, category: CommandCategories.messages, }), @@ -215,7 +219,7 @@ export const Commands = [ if (args) { message = message + ' ' + args; } - return success(ContentHelpers.makeTextMessage(message)); + return successSync(ContentHelpers.makeTextMessage(message)); }, category: CommandCategories.messages, }), @@ -224,7 +228,7 @@ export const Commands = [ args: '', description: _td('Sends a message as plain text, without interpreting it as markdown'), runFn: function(roomId, messages) { - return success(ContentHelpers.makeTextMessage(messages)); + return successSync(ContentHelpers.makeTextMessage(messages)); }, category: CommandCategories.messages, }), @@ -233,7 +237,7 @@ export const Commands = [ args: '', description: _td('Sends a message as html, without interpreting it as markdown'), runFn: function(roomId, messages) { - return success(ContentHelpers.makeHtmlMessage(messages, messages)); + return successSync(ContentHelpers.makeHtmlMessage(messages, messages)); }, category: CommandCategories.messages, }), @@ -978,7 +982,7 @@ export const Commands = [ args: '', runFn: function(roomId, args) { if (!args) return reject(this.getUserId()); - return success(ContentHelpers.makeHtmlMessage(args, textToHtmlRainbow(args))); + return successSync(ContentHelpers.makeHtmlMessage(args, textToHtmlRainbow(args))); }, category: CommandCategories.messages, }), @@ -988,7 +992,7 @@ export const Commands = [ args: '', runFn: function(roomId, args) { if (!args) return reject(this.getUserId()); - return success(ContentHelpers.makeHtmlEmote(args, textToHtmlRainbow(args))); + return successSync(ContentHelpers.makeHtmlEmote(args, textToHtmlRainbow(args))); }, category: CommandCategories.messages, }), @@ -1015,9 +1019,8 @@ export const Commands = [ const member = MatrixClientPeg.get().getRoom(roomId).getMember(userId); dis.dispatch({ action: Action.ViewUser, - // XXX: We should be using a real member object and not assuming what the - // receiver wants. - member: member || {userId}, + // XXX: We should be using a real member object and not assuming what the receiver wants. + member: member || { userId } as User, }); return success(); }, diff --git a/src/VoipUserMapper.ts b/src/VoipUserMapper.ts index d576a5434c..dacb4262bd 100644 --- a/src/VoipUserMapper.ts +++ b/src/VoipUserMapper.ts @@ -24,7 +24,9 @@ import { Room } from 'matrix-js-sdk/src/models/room'; // is sip virtual: there could be others in the future. export default class VoipUserMapper { - private virtualRoomIdCache = new Set(); + // We store mappings of virtual -> native room IDs here until the local echo for the + // account data arrives. + private virtualToNativeRoomIdCache = new Map(); public static sharedInstance(): VoipUserMapper { if (window.mxVoipUserMapper === undefined) window.mxVoipUserMapper = new VoipUserMapper(); @@ -49,10 +51,20 @@ export default class VoipUserMapper { native_room: roomId, }); + this.virtualToNativeRoomIdCache.set(virtualRoomId, roomId); + return virtualRoomId; } public nativeRoomForVirtualRoom(roomId: string): string { + const cachedNativeRoomId = this.virtualToNativeRoomIdCache.get(roomId); + if (cachedNativeRoomId) { + console.log( + "Returning native room ID " + cachedNativeRoomId + " for virtual room ID " + roomId + " from cache", + ); + return cachedNativeRoomId; + } + const virtualRoom = MatrixClientPeg.get().getRoom(roomId); if (!virtualRoom) return null; const virtualRoomEvent = virtualRoom.getAccountData(VIRTUAL_ROOM_EVENT_TYPE); @@ -67,7 +79,7 @@ export default class VoipUserMapper { public isVirtualRoom(room: Room): boolean { if (this.nativeRoomForVirtualRoom(room.roomId)) return true; - if (this.virtualRoomIdCache.has(room.roomId)) return true; + if (this.virtualToNativeRoomIdCache.has(room.roomId)) return true; // also look in the create event for the claimed native room ID, which is the only // way we can recognise a virtual room we've created when it first arrives down @@ -110,7 +122,7 @@ export default class VoipUserMapper { // also put this room in the virtual room ID cache so isVirtualRoom return the right answer // in however long it takes for the echo of setAccountData to come down the sync - this.virtualRoomIdCache.add(invitedRoom.roomId); + this.virtualToNativeRoomIdCache.set(invitedRoom.roomId, nativeRoom.roomId); } } } diff --git a/src/accessibility/KeyboardShortcuts.tsx b/src/accessibility/KeyboardShortcuts.tsx index 2a3e576e31..1cd5408210 100644 --- a/src/accessibility/KeyboardShortcuts.tsx +++ b/src/accessibility/KeyboardShortcuts.tsx @@ -57,6 +57,8 @@ export enum Modifiers { // Meta-modifier: isMac ? CMD : CONTROL export const CMD_OR_CTRL = isMac ? Modifiers.COMMAND : Modifiers.CONTROL; +// Meta-key representing the digits [0-9] often found at the top of standard keyboard layouts +export const DIGITS = "digits"; interface IKeybind { modifiers?: Modifiers[]; @@ -319,6 +321,7 @@ const alternateKeyName: Record = { [Key.SPACE]: _td("Space"), [Key.HOME]: _td("Home"), [Key.END]: _td("End"), + [DIGITS]: _td("[number]"), }; const keyIcon: Record = { [Key.ARROW_UP]: "↑", diff --git a/src/autocomplete/Autocompleter.ts b/src/autocomplete/Autocompleter.ts index 5409825f45..ea8eddbb8d 100644 --- a/src/autocomplete/Autocompleter.ts +++ b/src/autocomplete/Autocompleter.ts @@ -15,8 +15,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ReactElement} from 'react'; -import Room from 'matrix-js-sdk/src/models/room'; +import { ReactElement } from 'react'; +import { Room } from 'matrix-js-sdk/src/models/room'; + import CommandProvider from './CommandProvider'; import CommunityProvider from './CommunityProvider'; import DuckDuckGoProvider from './DuckDuckGoProvider'; @@ -24,7 +25,7 @@ import RoomProvider from './RoomProvider'; import UserProvider from './UserProvider'; import EmojiProvider from './EmojiProvider'; import NotifProvider from './NotifProvider'; -import {timeout} from "../utils/promise"; +import { timeout } from "../utils/promise"; import AutocompleteProvider, {ICommand} from "./AutocompleteProvider"; import SettingsStore from "../settings/SettingsStore"; import SpaceProvider from "./SpaceProvider"; diff --git a/src/autocomplete/NotifProvider.tsx b/src/autocomplete/NotifProvider.tsx index 0bc7ead097..827f4aa885 100644 --- a/src/autocomplete/NotifProvider.tsx +++ b/src/autocomplete/NotifProvider.tsx @@ -15,7 +15,8 @@ limitations under the License. */ import React from 'react'; -import Room from "matrix-js-sdk/src/models/room"; +import { Room } from "matrix-js-sdk/src/models/room"; + import AutocompleteProvider from './AutocompleteProvider'; import { _t } from '../languageHandler'; import {MatrixClientPeg} from '../MatrixClientPeg'; diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx index ad55b19101..5240db6955 100644 --- a/src/autocomplete/RoomProvider.tsx +++ b/src/autocomplete/RoomProvider.tsx @@ -17,28 +17,24 @@ limitations under the License. */ import React from "react"; -import {uniqBy, sortBy} from "lodash"; -import Room from "matrix-js-sdk/src/models/room"; +import { uniqBy, sortBy } from "lodash"; +import { Room } from "matrix-js-sdk/src/models/room"; import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; -import {MatrixClientPeg} from '../MatrixClientPeg'; +import { MatrixClientPeg } from '../MatrixClientPeg'; import QueryMatcher from './QueryMatcher'; -import {PillCompletion} from './Components'; -import {makeRoomPermalink} from "../utils/permalinks/Permalinks"; -import {ICompletion, ISelectionRange} from "./Autocompleter"; +import { PillCompletion } from './Components'; +import { makeRoomPermalink } from "../utils/permalinks/Permalinks"; +import { ICompletion, ISelectionRange } from "./Autocompleter"; import RoomAvatar from '../components/views/avatars/RoomAvatar'; import SettingsStore from "../settings/SettingsStore"; const ROOM_REGEX = /\B#\S*/g; -function score(query: string, space: string) { - const index = space.indexOf(query); - if (index === -1) { - return Infinity; - } else { - return index; - } +// Prefer canonical aliases over non-canonical ones +function canonicalScore(displayedAlias: string, room: Room): number { + return displayedAlias === room.getCanonicalAlias() ? 0 : 1; } function matcherObject(room: Room, displayedAlias: string, matchName = "") { @@ -106,7 +102,7 @@ export default class RoomProvider extends AutocompleteProvider { const matchedString = command[0]; completions = this.matcher.match(matchedString, limit); completions = sortBy(completions, [ - (c) => score(matchedString, c.displayedAlias), + (c) => canonicalScore(c.displayedAlias, c.room), (c) => c.displayedAlias.length, ]); completions = uniqBy(completions, (match) => match.room); diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index 3cf43d0b84..687b477133 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -20,19 +20,19 @@ limitations under the License. import React from 'react'; import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; -import {PillCompletion} from './Components'; +import { PillCompletion } from './Components'; import * as sdk from '../index'; import QueryMatcher from './QueryMatcher'; -import {sortBy} from 'lodash'; -import {MatrixClientPeg} from '../MatrixClientPeg'; +import { sortBy } from 'lodash'; +import { MatrixClientPeg } from '../MatrixClientPeg'; -import MatrixEvent from "matrix-js-sdk/src/models/event"; -import Room from "matrix-js-sdk/src/models/room"; -import RoomMember from "matrix-js-sdk/src/models/room-member"; -import RoomState from "matrix-js-sdk/src/models/room-state"; -import EventTimeline from "matrix-js-sdk/src/models/event-timeline"; -import {makeUserPermalink} from "../utils/permalinks/Permalinks"; -import {ICompletion, ISelectionRange} from "./Autocompleter"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { RoomState } from "matrix-js-sdk/src/models/room-state"; +import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline"; +import { makeUserPermalink } from "../utils/permalinks/Permalinks"; +import { ICompletion, ISelectionRange } from "./Autocompleter"; const USER_REGEX = /\B@\S*/g; diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 16da9321e2..0af2d3d635 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1953,6 +1953,7 @@ export default class MatrixChat extends React.PureComponent { // Create and start the client await Lifecycle.setLoggedIn(credentials); await this.postLoginSetup(); + PerformanceMonitor.instance.stop(PerformanceEntryNames.LOGIN); PerformanceMonitor.instance.stop(PerformanceEntryNames.REGISTER); }; diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index b2f0c70bd7..7d74229421 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -41,7 +41,7 @@ export function getUnsentMessages(room) { } @replaceableComponent("structures.RoomStatusBar") -export default class RoomStatusBar extends React.Component { +export default class RoomStatusBar extends React.PureComponent { static propTypes = { // the room this statusbar is representing. room: PropTypes.object.isRequired, diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index fe90d2f873..885851e8e6 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -23,8 +23,9 @@ limitations under the License. import React, { createRef } from 'react'; import classNames from 'classnames'; -import { Room } from "matrix-js-sdk/src/models/room"; +import { IRecommendedVersion, NotificationCountType, Room } from "matrix-js-sdk/src/models/room"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { SearchResult } from "matrix-js-sdk/src/models/search-result"; import { EventSubscription } from "fbemitter"; import shouldHideEvent from '../../shouldHideEvent'; @@ -59,7 +60,7 @@ import ScrollPanel from "./ScrollPanel"; import TimelinePanel from "./TimelinePanel"; import ErrorBoundary from "../views/elements/ErrorBoundary"; import RoomPreviewBar from "../views/rooms/RoomPreviewBar"; -import SearchBar from "../views/rooms/SearchBar"; +import SearchBar, { SearchScope } from "../views/rooms/SearchBar"; import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar"; import AuxPanel from "../views/rooms/AuxPanel"; import RoomHeader from "../views/rooms/RoomHeader"; @@ -80,7 +81,6 @@ import { objectHasDiff } from "../../utils/objects"; import SpaceRoomView from "./SpaceRoomView"; import { IOpts } from "../../createRoom"; import { replaceableComponent } from "../../utils/replaceableComponent"; -import { omit } from 'lodash'; import UIStore from "../../stores/UIStore"; const DEBUG = false; @@ -139,11 +139,11 @@ export interface IState { draggingFile: boolean; searching: boolean; searchTerm?: string; - searchScope?: "All" | "Room"; + searchScope?: SearchScope; searchResults?: XOR<{}, { count: number; highlights: string[]; - results: MatrixEvent[]; + results: SearchResult[]; next_batch: string; // eslint-disable-line camelcase }>; searchHighlights?: string[]; @@ -172,11 +172,7 @@ export interface IState { // We load this later by asking the js-sdk to suggest a version for us. // This object is the result of Room#getRecommendedVersion() - upgradeRecommendation?: { - version: string; - needsUpgrade: boolean; - urgent: boolean; - }; + upgradeRecommendation?: IRecommendedVersion; canReact: boolean; canReply: boolean; layout: Layout; @@ -572,16 +568,12 @@ export default class RoomView extends React.Component { shouldComponentUpdate(nextProps, nextState) { const hasPropsDiff = objectHasDiff(this.props, nextProps); - // React only shallow comparison and we only want to trigger - // a component re-render if a room requires an upgrade - const newUpgradeRecommendation = nextState.upgradeRecommendation || {} - - const state = omit(this.state, ['upgradeRecommendation']); - const newState = omit(nextState, ['upgradeRecommendation']) + const { upgradeRecommendation, ...state } = this.state; + const { upgradeRecommendation: newUpgradeRecommendation, ...newState } = nextState; const hasStateDiff = - objectHasDiff(state, newState) || - (newUpgradeRecommendation.needsUpgrade === true) + newUpgradeRecommendation?.needsUpgrade !== upgradeRecommendation?.needsUpgrade || + objectHasDiff(state, newState); return hasPropsDiff || hasStateDiff; } @@ -701,16 +693,11 @@ export default class RoomView extends React.Component { room_id: this.state.room.roomId, event_id: this.state.initialEventId, highlighted: false, + replyingToEvent: this.state.replyToEvent, }); } } - private onLayoutChange = () => { - this.setState({ - layout: SettingsStore.getValue("layout"), - }); - }; - private onRightPanelStoreUpdate = () => { this.setState({ showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, @@ -1276,7 +1263,7 @@ export default class RoomView extends React.Component { }); } - private onSearch = (term: string, scope) => { + private onSearch = (term: string, scope: SearchScope) => { this.setState({ searchTerm: term, searchScope: scope, @@ -1297,7 +1284,7 @@ export default class RoomView extends React.Component { this.searchId = new Date().getTime(); let roomId; - if (scope === "Room") roomId = this.state.room.roomId; + if (scope === SearchScope.Room) roomId = this.state.room.roomId; debuglog("sending search request"); const searchPromise = eventSearch(term, roomId); @@ -1644,29 +1631,27 @@ export default class RoomView extends React.Component { let auxPanelMaxHeight = UIStore.instance.windowHeight - (54 + // height of RoomHeader 36 + // height of the status area - 51 + // minimum height of the message compmoser + 51 + // minimum height of the message composer 120); // amount of desired scrollback // XXX: this is a bit of a hack and might possibly cause the video to push out the page anyway // but it's better than the video going missing entirely if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50; - this.setState({auxPanelMaxHeight: auxPanelMaxHeight}); + if (this.state.auxPanelMaxHeight !== auxPanelMaxHeight) { + this.setState({ auxPanelMaxHeight }); + } }; private onStatusBarVisible = () => { - if (this.unmounted) return; - this.setState({ - statusBarVisible: true, - }); + if (this.unmounted || this.state.statusBarVisible) return; + this.setState({ statusBarVisible: true }); }; private onStatusBarHidden = () => { // This is currently not desired as it is annoying if it keeps expanding and collapsing - if (this.unmounted) return; - this.setState({ - statusBarVisible: false, - }); + if (this.unmounted || !this.state.statusBarVisible) return; + this.setState({ statusBarVisible: false }); }; /** @@ -2069,7 +2054,7 @@ export default class RoomView extends React.Component { if (!this.state.atEndOfLiveTimeline && !this.state.searchResults) { const JumpToBottomButton = sdk.getComponent('rooms.JumpToBottomButton'); jumpToBottom = ( 0} + highlight={this.state.room.getUnreadNotificationCount(NotificationCountType.Highlight) > 0} numUnreadMessages={this.state.numUnreadMessages} onScrollToBottomClick={this.jumpToLiveTimeline} roomId={this.state.roomId} diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 497f525a00..4292b60f41 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -520,6 +520,7 @@ export const SpaceHierarchy: React.FC = ({ setError("Failed to update some suggestions. Try again later"); } setSaving(false); + setSelected(new Map()); }} kind="primary_outline" disabled={disabled} diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 3ccf2e5424..aad770888b 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -59,7 +59,7 @@ import IconizedContextMenu, { } from "../views/context_menus/IconizedContextMenu"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import {BetaPill} from "../views/beta/BetaCard"; -import {USER_LABS_TAB} from "../views/dialogs/UserSettingsDialog"; +import { UserTab } from "../views/dialogs/UserSettingsDialog"; import SettingsStore from "../../settings/SettingsStore"; import dis from "../../dispatcher/dispatcher"; import Modal from "../../Modal"; @@ -166,7 +166,7 @@ const SpaceInfo = ({ space }) => { const onBetaClick = () => { defaultDispatcher.dispatch({ action: Action.ViewUserSettings, - initialTabId: USER_LABS_TAB, + initialTabId: UserTab.Labs, }); }; diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index bb62745d98..03d0b5c6d7 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -18,14 +18,14 @@ limitations under the License. */ import SettingsStore from "../../settings/SettingsStore"; -import {LayoutPropType} from "../../settings/Layout"; -import React, {createRef} from 'react'; +import { LayoutPropType } from "../../settings/Layout"; +import React, { createRef } from 'react'; import ReactDOM from "react-dom"; import PropTypes from 'prop-types'; -import {EventTimeline} from "matrix-js-sdk/src/models/event-timeline"; -import {TimelineWindow} from "matrix-js-sdk/src/timeline-window"; +import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline"; +import { TimelineWindow } from "matrix-js-sdk/src/timeline-window"; import { _t } from '../../languageHandler'; -import {MatrixClientPeg} from "../../MatrixClientPeg"; +import { MatrixClientPeg } from "../../MatrixClientPeg"; import RoomContext from "../../contexts/RoomContext"; import UserActivity from "../../UserActivity"; import Modal from "../../Modal"; @@ -35,10 +35,11 @@ import { Key } from '../../Keyboard'; import Timer from '../../utils/Timer'; import shouldHideEvent from '../../shouldHideEvent'; import EditorStateTransfer from '../../utils/EditorStateTransfer'; -import {haveTileForEvent} from "../views/rooms/EventTile"; -import {UIFeature} from "../../settings/UIFeature"; -import {replaceableComponent} from "../../utils/replaceableComponent"; +import { haveTileForEvent } from "../views/rooms/EventTile"; +import { UIFeature } from "../../settings/UIFeature"; +import { replaceableComponent } from "../../utils/replaceableComponent"; import { arrayFastClone } from "../../utils/arrays"; +import { Action } from "../../dispatcher/actions"; const PAGINATE_SIZE = 20; const INITIAL_SIZE = 20; @@ -439,21 +440,42 @@ class TimelinePanel extends React.Component { }; onAction = payload => { - if (payload.action === 'ignore_state_changed') { - this.forceUpdate(); - } - if (payload.action === "edit_event") { - const editState = payload.event ? new EditorStateTransfer(payload.event) : null; - this.setState({editState}, () => { - if (payload.event && this._messagePanel.current) { - this._messagePanel.current.scrollToEventIfNeeded( - payload.event.getId(), - ); + switch (payload.action) { + case "ignore_state_changed": + this.forceUpdate(); + break; + + case "edit_event": { + const editState = payload.event ? new EditorStateTransfer(payload.event) : null; + this.setState({editState}, () => { + if (payload.event && this._messagePanel.current) { + this._messagePanel.current.scrollToEventIfNeeded( + payload.event.getId(), + ); + } + }); + break; + } + + case Action.ComposerInsert: { + // re-dispatch to the correct composer + if (this.state.editState) { + dis.dispatch({ + ...payload, + action: "edit_composer_insert", + }); + } else { + dis.dispatch({ + ...payload, + action: "send_composer_insert", + }); } - }); - } - if (payload.action === "scroll_to_bottom") { - this.jumpToLiveTimeline(); + break; + } + + case "scroll_to_bottom": + this.jumpToLiveTimeline(); + break; } }; diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 6a449cf1a2..3cf0dc5f84 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -26,7 +26,7 @@ import { ActionPayload } from "../../dispatcher/payloads"; import { Action } from "../../dispatcher/actions"; import { _t } from "../../languageHandler"; import { ContextMenuButton } from "./ContextMenu"; -import { USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB } from "../views/dialogs/UserSettingsDialog"; +import { UserTab } from "../views/dialogs/UserSettingsDialog"; import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; import FeedbackDialog from "../views/dialogs/FeedbackDialog"; import Modal from "../../Modal"; @@ -408,12 +408,12 @@ export default class UserMenu extends React.Component { this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)} + onClick={(e) => this.onSettingsOpen(e, UserTab.Notifications)} /> this.onSettingsOpen(e, USER_SECURITY_TAB)} + onClick={(e) => this.onSettingsOpen(e, UserTab.Security)} /> ; title = _t("Verify this login"); - } else if (phase === PHASE_DONE) { + } else if (phase === Phase.Done) { icon = ; title = _t("Session verified"); - } else if (phase === PHASE_CONFIRM_SKIP) { + } else if (phase === Phase.ConfirmSkip) { icon = ; title = _t("Are you sure?"); - } else if (phase === PHASE_BUSY) { + } else if (phase === Phase.Busy) { icon = ; title = _t("Verify this login"); } else { diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 6feb1e34f7..3a4be6f0d6 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -269,7 +269,7 @@ export default class Registration extends React.Component { ); } - private onUIAuthFinished = async (success, response, extra) => { + private onUIAuthFinished = async (success: boolean, response: any) => { if (!success) { let msg = response.message || response.toString(); // can we give a better error message? diff --git a/src/components/structures/auth/SetupEncryptionBody.js b/src/components/structures/auth/SetupEncryptionBody.js index 803df19d00..90137e084c 100644 --- a/src/components/structures/auth/SetupEncryptionBody.js +++ b/src/components/structures/auth/SetupEncryptionBody.js @@ -21,15 +21,7 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg'; import Modal from '../../../Modal'; import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog'; import * as sdk from '../../../index'; -import { - SetupEncryptionStore, - PHASE_LOADING, - PHASE_INTRO, - PHASE_BUSY, - PHASE_DONE, - PHASE_CONFIRM_SKIP, - PHASE_FINISHED, -} from '../../../stores/SetupEncryptionStore'; +import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore'; import {replaceableComponent} from "../../../utils/replaceableComponent"; function keyHasPassphrase(keyInfo) { @@ -63,7 +55,7 @@ export default class SetupEncryptionBody extends React.Component { _onStoreUpdate = () => { const store = SetupEncryptionStore.sharedInstance(); - if (store.phase === PHASE_FINISHED) { + if (store.phase === Phase.Finished) { this.props.onFinished(); return; } @@ -136,7 +128,7 @@ export default class SetupEncryptionBody extends React.Component { onClose={this.props.onFinished} member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)} />; - } else if (phase === PHASE_INTRO) { + } else if (phase === Phase.Intro) { const store = SetupEncryptionStore.sharedInstance(); let recoveryKeyPrompt; if (store.keyInfo && keyHasPassphrase(store.keyInfo)) { @@ -174,7 +166,7 @@ export default class SetupEncryptionBody extends React.Component { ); - } else if (phase === PHASE_DONE) { + } else if (phase === Phase.Done) { let message; if (this.state.backupInfo) { message =

{_t( @@ -200,7 +192,7 @@ export default class SetupEncryptionBody extends React.Component { ); - } else if (phase === PHASE_CONFIRM_SKIP) { + } else if (phase === Phase.ConfirmSkip) { return (

{_t( @@ -224,7 +216,7 @@ export default class SetupEncryptionBody extends React.Component {

); - } else if (phase === PHASE_BUSY || phase === PHASE_LOADING) { + } else if (phase === Phase.Busy || phase === Phase.Loading) { const Spinner = sdk.getComponent('views.elements.Spinner'); return ; } else { diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx index 6949c14636..f98f8c88a1 100644 --- a/src/components/views/avatars/BaseAvatar.tsx +++ b/src/components/views/avatars/BaseAvatar.tsx @@ -17,16 +17,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useCallback, useContext, useEffect, useState} from 'react'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; import classNames from 'classnames'; +import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials'; + import * as AvatarLogic from '../../../Avatar'; import SettingsStore from "../../../settings/SettingsStore"; import AccessibleButton from '../elements/AccessibleButton'; import RoomContext from "../../../contexts/RoomContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import {useEventEmitter} from "../../../hooks/useEventEmitter"; -import {toPx} from "../../../utils/units"; -import {ResizeMethod} from "../../../Avatar"; +import { useEventEmitter } from "../../../hooks/useEventEmitter"; +import { toPx } from "../../../utils/units"; import { _t } from '../../../languageHandler'; interface IProps { diff --git a/src/components/views/avatars/GroupAvatar.tsx b/src/components/views/avatars/GroupAvatar.tsx index 3734ba9504..13dbbfec09 100644 --- a/src/components/views/avatars/GroupAvatar.tsx +++ b/src/components/views/avatars/GroupAvatar.tsx @@ -15,10 +15,11 @@ limitations under the License. */ import React from 'react'; +import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials'; + import BaseAvatar from './BaseAvatar'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {mediaFromMxc} from "../../../customisations/Media"; -import {ResizeMethod} from "../../../Avatar"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { mediaFromMxc } from "../../../customisations/Media"; export interface IProps { groupId?: string; diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx index 3205ca372c..862563a8b4 100644 --- a/src/components/views/avatars/MemberAvatar.tsx +++ b/src/components/views/avatars/MemberAvatar.tsx @@ -16,14 +16,14 @@ limitations under the License. */ import React from 'react'; -import {RoomMember} from "matrix-js-sdk/src/models/room-member"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials'; import dis from "../../../dispatcher/dispatcher"; -import {Action} from "../../../dispatcher/actions"; +import { Action } from "../../../dispatcher/actions"; import BaseAvatar from "./BaseAvatar"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {mediaFromMxc} from "../../../customisations/Media"; -import {ResizeMethod} from "../../../Avatar"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { mediaFromMxc } from "../../../customisations/Media"; interface IProps extends Omit, "name" | "idName" | "url"> { member: RoomMember; diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx index 4693d907ba..bd820509c5 100644 --- a/src/components/views/avatars/RoomAvatar.tsx +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -13,17 +13,17 @@ 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, {ComponentProps} from 'react'; -import Room from 'matrix-js-sdk/src/models/room'; +import React, { ComponentProps } from 'react'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials'; import BaseAvatar from './BaseAvatar'; import ImageView from '../elements/ImageView'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; import Modal from '../../../Modal'; import * as Avatar from '../../../Avatar'; -import {ResizeMethod} from "../../../Avatar"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {mediaFromMxc} from "../../../customisations/Media"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { mediaFromMxc } from "../../../customisations/Media"; interface IProps extends Omit, "name" | "idName" | "url" | "onClick"> { // Room may be left unset here, but if it is, diff --git a/src/components/views/beta/BetaCard.tsx b/src/components/views/beta/BetaCard.tsx index 821c448f4f..aa4fe49f63 100644 --- a/src/components/views/beta/BetaCard.tsx +++ b/src/components/views/beta/BetaCard.tsx @@ -25,6 +25,7 @@ import TextWithTooltip from "../elements/TextWithTooltip"; import Modal from "../../../Modal"; import BetaFeedbackDialog from "../dialogs/BetaFeedbackDialog"; import SdkConfig from "../../../SdkConfig"; +import SettingsFlag from "../elements/SettingsFlag"; interface IProps { title?: string; @@ -66,7 +67,7 @@ const BetaCard = ({ title: titleOverride, featureId }: IProps) => { const info = SettingsStore.getBetaInfo(featureId); if (!info) return null; // Beta is invalid/disabled - const { title, caption, disclaimer, image, feedbackLabel, feedbackSubheading } = info; + const { title, caption, disclaimer, image, feedbackLabel, feedbackSubheading, extraSettings } = info; const value = SettingsStore.getValue(featureId); let feedbackButton; @@ -82,26 +83,33 @@ const BetaCard = ({ title: titleOverride, featureId }: IProps) => { } return
-
-

- { titleOverride || _t(title) } - -

- { _t(caption) } +
- { feedbackButton } - SettingsStore.setValue(featureId, null, SettingLevel.DEVICE, !value)} - kind={feedbackButton ? "primary_outline" : "primary"} - > - { value ? _t("Leave the beta") : _t("Join the beta") } - +

+ { titleOverride || _t(title) } + +

+ { _t(caption) } +
+ { feedbackButton } + SettingsStore.setValue(featureId, null, SettingLevel.DEVICE, !value)} + kind={feedbackButton ? "primary_outline" : "primary"} + > + { value ? _t("Leave the beta") : _t("Join the beta") } + +
+ { disclaimer &&
+ { disclaimer(value) } +
}
- { disclaimer &&
- { disclaimer(value) } -
} +
- + { extraSettings &&
+ { extraSettings.map(key => ( + + )) } +
}
; }; diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index adfeeb0968..5a1da1376d 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -33,6 +33,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard"; import ForwardDialog from "../dialogs/ForwardDialog"; +import { Action } from "../../../dispatcher/actions"; export function canCancel(eventStatus) { return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; @@ -200,7 +201,7 @@ export default class MessageContextMenu extends React.Component { onQuoteClick = () => { dis.dispatch({ - action: 'quote', + action: Action.ComposerInsert, event: this.props.mxEvent, }); this.closeMenu(); diff --git a/src/components/views/dialogs/AskInviteAnywayDialog.js b/src/components/views/dialogs/AskInviteAnywayDialog.tsx similarity index 73% rename from src/components/views/dialogs/AskInviteAnywayDialog.js rename to src/components/views/dialogs/AskInviteAnywayDialog.tsx index e6cd45ba6b..970883aca2 100644 --- a/src/components/views/dialogs/AskInviteAnywayDialog.js +++ b/src/components/views/dialogs/AskInviteAnywayDialog.tsx @@ -15,39 +15,41 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; -import {SettingLevel} from "../../../settings/SettingLevel"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { SettingLevel } from "../../../settings/SettingLevel"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; + +interface IProps { + unknownProfileUsers: Array<{ + userId: string; + errorText: string; + }>; + onInviteAnyways: () => void; + onGiveUp: () => void; + onFinished: (success: boolean) => void; +} @replaceableComponent("views.dialogs.AskInviteAnywayDialog") -export default class AskInviteAnywayDialog extends React.Component { - static propTypes = { - unknownProfileUsers: PropTypes.array.isRequired, // [ {userId, errorText}... ] - onInviteAnyways: PropTypes.func.isRequired, - onGiveUp: PropTypes.func.isRequired, - onFinished: PropTypes.func.isRequired, - }; - - _onInviteClicked = () => { +export default class AskInviteAnywayDialog extends React.Component { + private onInviteClicked = (): void => { this.props.onInviteAnyways(); this.props.onFinished(true); }; - _onInviteNeverWarnClicked = () => { + private onInviteNeverWarnClicked = (): void => { SettingsStore.setValue("promptBeforeInviteUnknownUsers", null, SettingLevel.ACCOUNT, false); this.props.onInviteAnyways(); this.props.onFinished(true); }; - _onGiveUpClicked = () => { + private onGiveUpClicked = (): void => { this.props.onGiveUp(); this.props.onFinished(false); }; - render() { + public render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const errorList = this.props.unknownProfileUsers @@ -55,11 +57,12 @@ export default class AskInviteAnywayDialog extends React.Component { return (
+ {/* eslint-disable-next-line */}

{_t("Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?")}

    { errorList } @@ -67,13 +70,13 @@ export default class AskInviteAnywayDialog extends React.Component {
- - -
diff --git a/src/components/views/dialogs/BetaFeedbackDialog.tsx b/src/components/views/dialogs/BetaFeedbackDialog.tsx index 1ae50dd66f..b8ff803627 100644 --- a/src/components/views/dialogs/BetaFeedbackDialog.tsx +++ b/src/components/views/dialogs/BetaFeedbackDialog.tsx @@ -29,7 +29,7 @@ import InfoDialog from "./InfoDialog"; import AccessibleButton from "../elements/AccessibleButton"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import {Action} from "../../../dispatcher/actions"; -import {USER_LABS_TAB} from "./UserSettingsDialog"; +import { UserTab } from "./UserSettingsDialog"; interface IProps extends IDialogProps { featureId: string; @@ -44,7 +44,12 @@ const BetaFeedbackDialog: React.FC = ({featureId, onFinished}) => { const sendFeedback = async (ok: boolean) => { if (!ok) return onFinished(false); - submitFeedback(SdkConfig.get().bug_report_endpoint_url, info.feedbackLabel, comment, canContact); + const extraData = SettingsStore.getBetaInfo(featureId)?.extraSettings.reduce((o, k) => { + o[k] = SettingsStore.getValue(k); + return o; + }, {}); + + submitFeedback(SdkConfig.get().bug_report_endpoint_url, info.feedbackLabel, comment, canContact, extraData); onFinished(true); Modal.createTrackedDialog("Beta Dialog Sent", featureId, InfoDialog, { @@ -70,7 +75,7 @@ const BetaFeedbackDialog: React.FC = ({featureId, onFinished}) => { onFinished(false); defaultDispatcher.dispatch({ action: Action.ViewUserSettings, - initialTabId: USER_LABS_TAB, + initialTabId: UserTab.Labs, }); }}> { _t("To leave the beta, visit your settings.") } diff --git a/src/components/views/dialogs/BugReportDialog.js b/src/components/views/dialogs/BugReportDialog.tsx similarity index 79% rename from src/components/views/dialogs/BugReportDialog.js rename to src/components/views/dialogs/BugReportDialog.tsx index cbe0130649..f938340a50 100644 --- a/src/components/views/dialogs/BugReportDialog.js +++ b/src/components/views/dialogs/BugReportDialog.tsx @@ -18,7 +18,6 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import SdkConfig from '../../../SdkConfig'; import Modal from '../../../Modal'; @@ -27,8 +26,27 @@ import sendBugReport, {downloadBugReport} from '../../../rageshake/submit-ragesh import AccessibleButton from "../elements/AccessibleButton"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +interface IProps { + onFinished: (success: boolean) => void; + initialText?: string; + label?: string; +} + +interface IState { + sendLogs: boolean; + busy: boolean; + err: string; + issueUrl: string; + text: string; + progress: string; + downloadBusy: boolean; + downloadProgress: string; +} + @replaceableComponent("views.dialogs.BugReportDialog") -export default class BugReportDialog extends React.Component { +export default class BugReportDialog extends React.Component { + private unmounted: boolean; + constructor(props) { super(props); this.state = { @@ -41,25 +59,18 @@ export default class BugReportDialog extends React.Component { downloadBusy: false, downloadProgress: null, }; - this._unmounted = false; - this._onSubmit = this._onSubmit.bind(this); - this._onCancel = this._onCancel.bind(this); - this._onTextChange = this._onTextChange.bind(this); - this._onIssueUrlChange = this._onIssueUrlChange.bind(this); - this._onSendLogsChange = this._onSendLogsChange.bind(this); - this._sendProgressCallback = this._sendProgressCallback.bind(this); - this._downloadProgressCallback = this._downloadProgressCallback.bind(this); + this.unmounted = false; } - componentWillUnmount() { - this._unmounted = true; + public componentWillUnmount() { + this.unmounted = true; } - _onCancel(ev) { + private onCancel = (): void => { this.props.onFinished(false); } - _onSubmit(ev) { + private onSubmit = (): void => { if ((!this.state.text || !this.state.text.trim()) && (!this.state.issueUrl || !this.state.issueUrl.trim())) { this.setState({ err: _t("Please tell us what went wrong or, better, create a GitHub issue that describes the problem."), @@ -72,15 +83,15 @@ export default class BugReportDialog extends React.Component { (this.state.issueUrl.length > 0 ? this.state.issueUrl : 'No issue link given'); this.setState({ busy: true, progress: null, err: null }); - this._sendProgressCallback(_t("Preparing to send logs")); + this.sendProgressCallback(_t("Preparing to send logs")); sendBugReport(SdkConfig.get().bug_report_endpoint_url, { userText, sendLogs: true, - progressCallback: this._sendProgressCallback, + progressCallback: this.sendProgressCallback, label: this.props.label, }).then(() => { - if (!this._unmounted) { + if (!this.unmounted) { this.props.onFinished(false); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); // N.B. first param is passed to piwik and so doesn't want i18n @@ -91,7 +102,7 @@ export default class BugReportDialog extends React.Component { }); } }, (err) => { - if (!this._unmounted) { + if (!this.unmounted) { this.setState({ busy: false, progress: null, @@ -101,14 +112,14 @@ export default class BugReportDialog extends React.Component { }); } - _onDownload = async (ev) => { + private onDownload = async (): Promise => { this.setState({ downloadBusy: true }); - this._downloadProgressCallback(_t("Preparing to download logs")); + this.downloadProgressCallback(_t("Preparing to download logs")); try { await downloadBugReport({ sendLogs: true, - progressCallback: this._downloadProgressCallback, + progressCallback: this.downloadProgressCallback, label: this.props.label, }); @@ -117,7 +128,7 @@ export default class BugReportDialog extends React.Component { downloadProgress: null, }); } catch (err) { - if (!this._unmounted) { + if (!this.unmounted) { this.setState({ downloadBusy: false, downloadProgress: _t("Failed to send logs: ") + `${err.message}`, @@ -126,33 +137,29 @@ export default class BugReportDialog extends React.Component { } }; - _onTextChange(ev) { - this.setState({ text: ev.target.value }); + private onTextChange = (ev: React.FormEvent): void => { + this.setState({ text: ev.currentTarget.value }); } - _onIssueUrlChange(ev) { - this.setState({ issueUrl: ev.target.value }); + private onIssueUrlChange = (ev: React.FormEvent): void => { + this.setState({ issueUrl: ev.currentTarget.value }); } - _onSendLogsChange(ev) { - this.setState({ sendLogs: ev.target.checked }); - } - - _sendProgressCallback(progress) { - if (this._unmounted) { + private sendProgressCallback = (progress: string): void => { + if (this.unmounted) { return; } - this.setState({progress: progress}); + this.setState({ progress }); } - _downloadProgressCallback(downloadProgress) { - if (this._unmounted) { + private downloadProgressCallback = (downloadProgress: string): void => { + if (this.unmounted) { return; } this.setState({ downloadProgress }); } - render() { + public render() { const Loader = sdk.getComponent("elements.Spinner"); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); @@ -183,7 +190,7 @@ export default class BugReportDialog extends React.Component { } return ( - @@ -213,7 +220,7 @@ export default class BugReportDialog extends React.Component {

- + { _t("Download logs") } {this.state.downloadProgress && {this.state.downloadProgress} ...} @@ -223,7 +230,7 @@ export default class BugReportDialog extends React.Component { type="text" className="mx_BugReportDialog_field_input" label={_t("GitHub issue")} - onChange={this._onIssueUrlChange} + onChange={this.onIssueUrlChange} value={this.state.issueUrl} placeholder="https://github.com/vector-im/element-web/issues/..." /> @@ -232,7 +239,7 @@ export default class BugReportDialog extends React.Component { element="textarea" label={_t("Notes")} rows={5} - onChange={this._onTextChange} + onChange={this.onTextChange} value={this.state.text} placeholder={_t( "If there is additional context that would help in " + @@ -245,17 +252,12 @@ export default class BugReportDialog extends React.Component { {error}
); } } - -BugReportDialog.propTypes = { - onFinished: PropTypes.func.isRequired, - initialText: PropTypes.string, -}; diff --git a/src/components/views/dialogs/ChangelogDialog.js b/src/components/views/dialogs/ChangelogDialog.tsx similarity index 88% rename from src/components/views/dialogs/ChangelogDialog.js rename to src/components/views/dialogs/ChangelogDialog.tsx index efbeba3977..0ded33cdcb 100644 --- a/src/components/views/dialogs/ChangelogDialog.js +++ b/src/components/views/dialogs/ChangelogDialog.tsx @@ -16,21 +16,26 @@ Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> */ import React from 'react'; -import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import request from 'browser-request'; import { _t } from '../../../languageHandler'; +interface IProps { + newVersion: string; + version: string; + onFinished: (success: boolean) => void; +} + const REPOS = ['vector-im/element-web', 'matrix-org/matrix-react-sdk', 'matrix-org/matrix-js-sdk']; -export default class ChangelogDialog extends React.Component { +export default class ChangelogDialog extends React.Component { constructor(props) { super(props); this.state = {}; } - componentDidMount() { + public componentDidMount() { const version = this.props.newVersion.split('-'); const version2 = this.props.version.split('-'); if (version == null || version2 == null) return; @@ -49,7 +54,7 @@ export default class ChangelogDialog extends React.Component { } } - _elementsForCommit(commit) { + private elementsForCommit(commit): JSX.Element { return (
  • @@ -59,7 +64,7 @@ export default class ChangelogDialog extends React.Component { ); } - render() { + public render() { const Spinner = sdk.getComponent('views.elements.Spinner'); const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); @@ -72,7 +77,7 @@ export default class ChangelogDialog extends React.Component { msg: this.state[repo], }); } else { - content = this.state[repo].map(this._elementsForCommit); + content = this.state[repo].map(this.elementsForCommit); } return (
    @@ -99,9 +104,3 @@ export default class ChangelogDialog extends React.Component { ); } } - -ChangelogDialog.propTypes = { - version: PropTypes.string.isRequired, - newVersion: PropTypes.string.isRequired, - onFinished: PropTypes.func.isRequired, -}; diff --git a/src/components/views/dialogs/ConfirmAndWaitRedactDialog.js b/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx similarity index 89% rename from src/components/views/dialogs/ConfirmAndWaitRedactDialog.js rename to src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx index 37d5510756..ae7b23c2c9 100644 --- a/src/components/views/dialogs/ConfirmAndWaitRedactDialog.js +++ b/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx @@ -17,7 +17,17 @@ limitations under the License. import React from 'react'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; + +interface IProps { + redact: () => Promise; + onFinished: (success: boolean) => void; +} + +interface IState { + isRedacting: boolean; + redactionErrorCode: string | number; +} /* * A dialog for confirming a redaction. @@ -32,7 +42,7 @@ import {replaceableComponent} from "../../../utils/replaceableComponent"; * To avoid this, we keep the dialog open as long as /redact is in progress. */ @replaceableComponent("views.dialogs.ConfirmAndWaitRedactDialog") -export default class ConfirmAndWaitRedactDialog extends React.PureComponent { +export default class ConfirmAndWaitRedactDialog extends React.PureComponent { constructor(props) { super(props); this.state = { @@ -41,7 +51,7 @@ export default class ConfirmAndWaitRedactDialog extends React.PureComponent { }; } - onParentFinished = async (proceed) => { + public onParentFinished = async (proceed: boolean): Promise => { if (proceed) { this.setState({isRedacting: true}); try { @@ -60,7 +70,7 @@ export default class ConfirmAndWaitRedactDialog extends React.PureComponent { } }; - render() { + public render() { if (this.state.isRedacting) { if (this.state.redactionErrorCode) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); diff --git a/src/components/views/dialogs/ConfirmRedactDialog.js b/src/components/views/dialogs/ConfirmRedactDialog.tsx similarity index 95% rename from src/components/views/dialogs/ConfirmRedactDialog.js rename to src/components/views/dialogs/ConfirmRedactDialog.tsx index bd63d3acc1..eee05599e8 100644 --- a/src/components/views/dialogs/ConfirmRedactDialog.js +++ b/src/components/views/dialogs/ConfirmRedactDialog.tsx @@ -19,11 +19,15 @@ import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +interface IProps { + onFinished: (success: boolean) => void; +} + /* * A dialog for confirming a redaction. */ @replaceableComponent("views.dialogs.ConfirmRedactDialog") -export default class ConfirmRedactDialog extends React.Component { +export default class ConfirmRedactDialog extends React.Component { render() { const TextInputDialog = sdk.getComponent('views.dialogs.TextInputDialog'); return ( diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.tsx similarity index 74% rename from src/components/views/dialogs/ConfirmUserActionDialog.js rename to src/components/views/dialogs/ConfirmUserActionDialog.tsx index 8059b9172a..5cdb4c664b 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.js +++ b/src/components/views/dialogs/ConfirmUserActionDialog.tsx @@ -15,13 +15,31 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk/src/client'; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import { GroupMemberType } from '../../../groups'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {mediaFromMxc} from "../../../customisations/Media"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { mediaFromMxc } from "../../../customisations/Media"; + +interface IProps { + // matrix-js-sdk (room) member object. Supply either this or 'groupMember' + member: RoomMember; + // group member object. Supply either this or 'member' + groupMember: GroupMemberType; + // needed if a group member is specified + matrixClient?: MatrixClient, + action: string; // eg. 'Ban' + title: string; // eg. 'Ban this user?' + + // Whether to display a text field for a reason + // If true, the second argument to onFinished will + // be the string entered. + askReason?: boolean; + danger?: boolean; + onFinished: (success: boolean, reason?: string) => void; +} /* * A dialog for confirming an operation on another user. @@ -32,53 +50,23 @@ import {mediaFromMxc} from "../../../customisations/Media"; * Also tweaks the style for 'dangerous' actions (albeit only with colour) */ @replaceableComponent("views.dialogs.ConfirmUserActionDialog") -export default class ConfirmUserActionDialog extends React.Component { - static propTypes = { - // matrix-js-sdk (room) member object. Supply either this or 'groupMember' - member: PropTypes.object, - // group member object. Supply either this or 'member' - groupMember: GroupMemberType, - // needed if a group member is specified - matrixClient: PropTypes.instanceOf(MatrixClient), - action: PropTypes.string.isRequired, // eg. 'Ban' - title: PropTypes.string.isRequired, // eg. 'Ban this user?' - - // Whether to display a text field for a reason - // If true, the second argument to onFinished will - // be the string entered. - askReason: PropTypes.bool, - danger: PropTypes.bool, - onFinished: PropTypes.func.isRequired, - }; +export default class ConfirmUserActionDialog extends React.Component { + private reasonField: React.RefObject = React.createRef(); static defaultProps = { danger: false, askReason: false, }; - constructor(props) { - super(props); - - this._reasonField = null; - } - - onOk = () => { - let reason; - if (this._reasonField) { - reason = this._reasonField.value; - } - this.props.onFinished(true, reason); + public onOk = (): void => { + this.props.onFinished(true, this.reasonField.current?.value); }; - onCancel = () => { + public onCancel = (): void => { this.props.onFinished(false); }; - _collectReasonField = e => { - this._reasonField = e; - }; - - render() { + public render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar"); @@ -92,7 +80,7 @@ export default class ConfirmUserActionDialog extends React.Component {
    diff --git a/src/components/views/dialogs/ConfirmWipeDeviceDialog.js b/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx similarity index 88% rename from src/components/views/dialogs/ConfirmWipeDeviceDialog.js rename to src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx index 333e1522f1..d95b1fe358 100644 --- a/src/components/views/dialogs/ConfirmWipeDeviceDialog.js +++ b/src/components/views/dialogs/ConfirmWipeDeviceDialog.tsx @@ -15,22 +15,21 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import {_t} from "../../../languageHandler"; import * as sdk from "../../../index"; import {replaceableComponent} from "../../../utils/replaceableComponent"; -@replaceableComponent("views.dialogs.ConfirmWipeDeviceDialog") -export default class ConfirmWipeDeviceDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - }; +interface IProps { + onFinished: (success: boolean) => void; +} - _onConfirm = () => { +@replaceableComponent("views.dialogs.ConfirmWipeDeviceDialog") +export default class ConfirmWipeDeviceDialog extends React.Component { + private onConfirm = (): void => { this.props.onFinished(true); }; - _onDecline = () => { + private onDecline = (): void => { this.props.onFinished(false); }; @@ -55,10 +54,10 @@ export default class ConfirmWipeDeviceDialog extends React.Component {
    ); diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.tsx similarity index 82% rename from src/components/views/dialogs/CreateGroupDialog.js rename to src/components/views/dialogs/CreateGroupDialog.tsx index e6c7a67aca..60e4f5efb8 100644 --- a/src/components/views/dialogs/CreateGroupDialog.js +++ b/src/components/views/dialogs/CreateGroupDialog.tsx @@ -15,44 +15,51 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import dis from '../../../dispatcher/dispatcher'; import { _t } from '../../../languageHandler'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {replaceableComponent} from "../../../utils/replaceableComponent"; -@replaceableComponent("views.dialogs.CreateGroupDialog") -export default class CreateGroupDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - }; +interface IProps { + onFinished: (success: boolean) => void; +} - state = { +interface IState { + groupName: string; + groupId: string; + groupIdError: string; + creating: boolean; + createError: Error; +} + +@replaceableComponent("views.dialogs.CreateGroupDialog") +export default class CreateGroupDialog extends React.Component { + public state = { groupName: '', groupId: '', - groupError: null, + groupIdError: '', creating: false, createError: null, }; - _onGroupNameChange = e => { + private onGroupNameChange = (e: React.FormEvent): void => { this.setState({ - groupName: e.target.value, + groupName: e.currentTarget.value, }); }; - _onGroupIdChange = e => { + private onGroupIdChange = (e: React.FormEvent): void => { this.setState({ - groupId: e.target.value, + groupId: e.currentTarget.value, }); }; - _onGroupIdBlur = e => { - this._checkGroupId(); + private onGroupIdBlur = (): void => { + this.checkGroupId(); }; - _checkGroupId(e) { + private checkGroupId() { let error = null; if (!this.state.groupId) { error = _t("Community IDs cannot be empty."); @@ -67,12 +74,12 @@ export default class CreateGroupDialog extends React.Component { return error; } - _onFormSubmit = e => { + private onFormSubmit = (e: React.FormEvent) => { e.preventDefault(); - if (this._checkGroupId()) return; + if (this.checkGroupId()) return; - const profile = {}; + const profile: any = {}; if (this.state.groupName !== '') { profile.name = this.state.groupName; } @@ -121,7 +128,7 @@ export default class CreateGroupDialog extends React.Component { - +
    @@ -129,9 +136,9 @@ export default class CreateGroupDialog extends React.Component {
    @@ -144,10 +151,10 @@ export default class CreateGroupDialog extends React.Component { + diff --git a/src/components/views/dialogs/CryptoStoreTooNewDialog.js b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx similarity index 94% rename from src/components/views/dialogs/CryptoStoreTooNewDialog.js rename to src/components/views/dialogs/CryptoStoreTooNewDialog.tsx index 6336c635e4..2bdf732bc5 100644 --- a/src/components/views/dialogs/CryptoStoreTooNewDialog.js +++ b/src/components/views/dialogs/CryptoStoreTooNewDialog.tsx @@ -22,7 +22,11 @@ import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import Modal from '../../../Modal'; -export default (props) => { +interface IProps { + onFinished: (success: boolean) => void; +} + +export default (props: IProps) => { const brand = SdkConfig.get().brand; const _onLogoutClicked = () => { @@ -40,7 +44,7 @@ export default (props) => { onFinished: (doLogout) => { if (doLogout) { dis.dispatch({action: 'logout'}); - props.onFinished(); + props.onFinished(true); } }, }); diff --git a/src/components/views/dialogs/DeactivateAccountDialog.js b/src/components/views/dialogs/DeactivateAccountDialog.tsx similarity index 87% rename from src/components/views/dialogs/DeactivateAccountDialog.js rename to src/components/views/dialogs/DeactivateAccountDialog.tsx index 4e52549d51..cf88802340 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.js +++ b/src/components/views/dialogs/DeactivateAccountDialog.tsx @@ -16,7 +16,6 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import Analytics from '../../../Analytics'; @@ -28,8 +27,25 @@ import {DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry} from "../auth/Interactiv import StyledCheckbox from "../elements/StyledCheckbox"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +interface IProps { + onFinished: (success: boolean) => void; +} + +interface IState { + shouldErase: boolean; + errStr: string; + authData: any; // for UIA + authEnabled: boolean; // see usages for information + + // A few strings that are passed to InteractiveAuth for design or are displayed + // next to the InteractiveAuth component. + bodyText: string; + continueText: string; + continueKind: string; +} + @replaceableComponent("views.dialogs.DeactivateAccountDialog") -export default class DeactivateAccountDialog extends React.Component { +export default class DeactivateAccountDialog extends React.Component { constructor(props) { super(props); @@ -46,10 +62,10 @@ export default class DeactivateAccountDialog extends React.Component { continueKind: null, }; - this._initAuth(/* shouldErase= */false); + this.initAuth(/* shouldErase= */false); } - _onStagePhaseChange = (stage, phase) => { + private onStagePhaseChange = (stage: string, phase: string): void => { const dialogAesthetics = { [SSOAuthEntry.PHASE_PREAUTH]: { body: _t("Confirm your account deactivation by using Single Sign On to prove your identity."), @@ -87,19 +103,19 @@ export default class DeactivateAccountDialog extends React.Component { this.setState({bodyText, continueText, continueKind}); }; - _onUIAuthFinished = (success, result, extra) => { + private onUIAuthFinished = (success: boolean, result: Error) => { if (success) return; // great! makeRequest() will be called too. if (result === ERROR_USER_CANCELLED) { - this._onCancel(); + this.onCancel(); return; } - console.error("Error during UI Auth:", {result, extra}); + console.error("Error during UI Auth:", { result }); this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")}); }; - _onUIAuthComplete = (auth) => { + private onUIAuthComplete = (auth: any): void => { MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase).then(r => { // Deactivation worked - logout & close this dialog Analytics.trackEvent('Account', 'Deactivate Account'); @@ -111,9 +127,9 @@ export default class DeactivateAccountDialog extends React.Component { }); }; - _onEraseFieldChange = (ev) => { + private onEraseFieldChange = (ev: React.FormEvent): void => { this.setState({ - shouldErase: ev.target.checked, + shouldErase: ev.currentTarget.checked, // Disable the auth form because we're going to have to reinitialize the auth // information. We do this because we can't modify the parameters in the UIA @@ -123,14 +139,14 @@ export default class DeactivateAccountDialog extends React.Component { }); // As mentioned above, set up for auth again to get updated UIA session info - this._initAuth(/* shouldErase= */ev.target.checked); + this.initAuth(/* shouldErase= */ev.currentTarget.checked); }; - _onCancel() { + private onCancel(): void { this.props.onFinished(false); } - _initAuth(shouldErase) { + private initAuth(shouldErase: boolean): void { MatrixClientPeg.get().deactivateAccount(null, shouldErase).then(r => { // If we got here, oops. The server didn't require any auth. // Our application lifecycle will catch the error and do the logout bits. @@ -148,7 +164,7 @@ export default class DeactivateAccountDialog extends React.Component { }); } - render() { + public render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); let error = null; @@ -166,9 +182,9 @@ export default class DeactivateAccountDialog extends React.Component { @@ -214,7 +230,7 @@ export default class DeactivateAccountDialog extends React.Component {

    {_t( "Please forget all messages I have sent when my account is deactivated " + @@ -235,7 +251,3 @@ export default class DeactivateAccountDialog extends React.Component { ); } } - -DeactivateAccountDialog.propTypes = { - onFinished: PropTypes.func.isRequired, -}; diff --git a/src/components/views/dialogs/DevtoolsDialog.tsx b/src/components/views/dialogs/DevtoolsDialog.tsx index fdbf6a36fc..2690eb67d7 100644 --- a/src/components/views/dialogs/DevtoolsDialog.tsx +++ b/src/components/views/dialogs/DevtoolsDialog.tsx @@ -525,11 +525,11 @@ class RoomStateExplorer extends React.PureComponent { diff --git a/src/components/views/dialogs/ErrorDialog.js b/src/components/views/dialogs/ErrorDialog.tsx similarity index 81% rename from src/components/views/dialogs/ErrorDialog.js rename to src/components/views/dialogs/ErrorDialog.tsx index 5197c68b5a..d50ec7bf36 100644 --- a/src/components/views/dialogs/ErrorDialog.js +++ b/src/components/views/dialogs/ErrorDialog.tsx @@ -26,37 +26,37 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import {replaceableComponent} from "../../../utils/replaceableComponent"; -@replaceableComponent("views.dialogs.ErrorDialog") -export default class ErrorDialog extends React.Component { - static propTypes = { - title: PropTypes.string, - description: PropTypes.oneOfType([ - PropTypes.element, - PropTypes.string, - ]), - button: PropTypes.string, - focus: PropTypes.bool, - onFinished: PropTypes.func.isRequired, - headerImage: PropTypes.string, - }; +interface IProps { + onFinished: (success: boolean) => void; + title?: string; + description?: React.ReactNode; + button?: string; + focus?: boolean; + headerImage?: string; +} - static defaultProps = { +interface IState { + onFinished: (success: boolean) => void; +} + +@replaceableComponent("views.dialogs.ErrorDialog") +export default class ErrorDialog extends React.Component { + public static defaultProps = { focus: true, title: null, description: null, button: null, }; - onClick = () => { + private onClick = () => { this.props.onFinished(true); }; - render() { + public render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( = ({ matrixClient: cli, event, permalinkCr }); mockEvent.sender = { name: profileInfo.displayname || userId, + rawDisplayName: profileInfo.displayname, userId, getAvatarUrl: (..._) => { return avatarUrlForUser( diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index d90214110f..ffca9a88a7 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -14,7 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {createRef} from 'react'; +import React, { createRef } from 'react'; +import classNames from 'classnames'; + import {_t, _td} from "../../../languageHandler"; import * as sdk from "../../../index"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; @@ -50,6 +52,11 @@ import BaseAvatar from '../avatars/BaseAvatar'; import AccessibleButton from '../elements/AccessibleButton'; import { compare } from '../../../utils/strings'; import { IInvite3PID } from "matrix-js-sdk/src/@types/requests"; +import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; +import { copyPlaintext, selectText } from "../../../utils/strings"; +import * as ContextMenu from "../../structures/ContextMenu"; +import { toRightOf } from "../../structures/ContextMenu"; +import GenericTextContextMenu from "../context_menus/GenericTextContextMenu"; // we have a number of types defined from the Matrix spec which can't reasonably be altered here. /* eslint-disable camelcase */ @@ -146,8 +153,8 @@ class ThreepidMember extends Member { } interface IDMUserTileProps { - member: RoomMember; - onRemove(member: RoomMember): void; + member: Member; + onRemove(member: Member): void; } class DMUserTile extends React.PureComponent { @@ -161,7 +168,7 @@ class DMUserTile extends React.PureComponent { render() { const avatarSize = 20; - const avatar = this.props.member.isEmail + const avatar = (this.props.member as ThreepidMember).isEmail ? { } interface IDMRoomTileProps { - member: RoomMember; + member: Member; lastActiveTs: number; - onToggle(member: RoomMember): void; + onToggle(member: Member): void; highlightWord: string; isSelected: boolean; } @@ -263,7 +270,7 @@ class DMRoomTile extends React.PureComponent { } const avatarSize = 36; - const avatar = this.props.member.isEmail + const avatar = (this.props.member as ThreepidMember).isEmail ? @@ -291,7 +298,7 @@ class DMRoomTile extends React.PureComponent { ); - const caption = this.props.member.isEmail + const caption = (this.props.member as ThreepidMember).isEmail ? _t("Invite by email") : this.highlightName(this.props.member.userId); @@ -327,7 +334,7 @@ interface IInviteDialogProps { } interface IInviteDialogState { - targets: RoomMember[]; // array of Member objects (see interface above) + targets: Member[]; // array of Member objects (see interface above) filterText: string; recents: { user: Member, userId: string }[]; numRecentsShown: number; @@ -351,6 +358,7 @@ export default class InviteDialog extends React.PureComponent void; private debounceTimer: NodeJS.Timeout = null; // actually number because we're in the browser private editorRef = createRef(); private unmounted = false; @@ -403,6 +411,9 @@ export default class InviteDialog extends React.PureComponent { @@ -1238,6 +1249,25 @@ export default class InviteDialog extends React.PureComponent { + e.preventDefault(); + const target = e.target; // copy target before we go async and React throws it away + + const successful = await copyPlaintext(makeUserPermalink(MatrixClientPeg.get().getUserId())); + const buttonRect = target.getBoundingClientRect(); + const { close } = ContextMenu.createMenu(GenericTextContextMenu, { + ...toRightOf(buttonRect, 2), + message: successful ? _t("Copied!") : _t("Failed to copy"), + }); + // Drop a reference to this close handler for componentWillUnmount + this.closeCopiedTooltip = target.onmouseleave = close; + }; + render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); @@ -1248,12 +1278,12 @@ export default class InviteDialog extends React.PureComponent; } - let title; let helpText; let buttonText; let goButtonFn; - let consultSection; + let extraSection; + let footer; let keySharingWarning = ; const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer); @@ -1316,6 +1346,26 @@ export default class InviteDialog extends React.PureComponent + { _t("Some suggestions may be hidden for privacy.") } +

    { _t("If you can't see who you’re looking for, send them your invite link below.") }

    +
    ; + const link = makeUserPermalink(MatrixClientPeg.get().getUserId()); + footer =
    +

    { _t("Or send invite link") }

    +
    } else if (this.props.kind === KIND_INVITE) { const room = MatrixClientPeg.get()?.getRoom(this.props.roomId); const isSpace = SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom(); @@ -1377,7 +1427,7 @@ export default class InviteDialog extends React.PureComponent + footer =
    - {consultSection} + {footer}
    ); diff --git a/src/components/views/dialogs/ReportEventDialog.js b/src/components/views/dialogs/ReportEventDialog.js deleted file mode 100644 index 5454b97287..0000000000 --- a/src/components/views/dialogs/ReportEventDialog.js +++ /dev/null @@ -1,149 +0,0 @@ -/* -Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> - -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, {PureComponent} from 'react'; -import * as sdk from '../../../index'; -import { _t } from '../../../languageHandler'; -import PropTypes from "prop-types"; -import {MatrixEvent} from "matrix-js-sdk/src/models/event"; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; -import SdkConfig from '../../../SdkConfig'; -import Markdown from '../../../Markdown'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; - -/* - * A dialog for reporting an event. - */ -@replaceableComponent("views.dialogs.ReportEventDialog") -export default class ReportEventDialog extends PureComponent { - static propTypes = { - mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired, - onFinished: PropTypes.func.isRequired, - }; - - constructor(props) { - super(props); - - this.state = { - reason: "", - busy: false, - err: null, - }; - } - - _onReasonChange = ({target: {value: reason}}) => { - this.setState({ reason }); - }; - - _onCancel = () => { - this.props.onFinished(false); - }; - - _onSubmit = async () => { - if (!this.state.reason || !this.state.reason.trim()) { - this.setState({ - err: _t("Please fill why you're reporting."), - }); - return; - } - - this.setState({ - busy: true, - err: null, - }); - - try { - const ev = this.props.mxEvent; - await MatrixClientPeg.get().reportEvent(ev.getRoomId(), ev.getId(), -100, this.state.reason.trim()); - this.props.onFinished(true); - } catch (e) { - this.setState({ - busy: false, - err: e.message, - }); - } - }; - - render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - const Loader = sdk.getComponent('elements.Spinner'); - const Field = sdk.getComponent('elements.Field'); - - let error = null; - if (this.state.err) { - error =
    - {this.state.err} -
    ; - } - - let progress = null; - if (this.state.busy) { - progress = ( -
    - -
    - ); - } - - const adminMessageMD = - SdkConfig.get().reportEvent && - SdkConfig.get().reportEvent.adminMessageMD; - let adminMessage; - if (adminMessageMD) { - const html = new Markdown(adminMessageMD).toHTML({ externalLinks: true }); - adminMessage =

    ; - } - - return ( - -

    -

    - { - _t("Reporting this message will send its unique 'event ID' to the administrator of " + - "your homeserver. If messages in this room are encrypted, your homeserver " + - "administrator will not be able to read the message text or view any files or images.") - } -

    - {adminMessage} - - {progress} - {error} -
    - - - ); - } -} diff --git a/src/components/views/dialogs/ReportEventDialog.tsx b/src/components/views/dialogs/ReportEventDialog.tsx new file mode 100644 index 0000000000..8271239f7f --- /dev/null +++ b/src/components/views/dialogs/ReportEventDialog.tsx @@ -0,0 +1,445 @@ +/* +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import * as sdk from '../../../index'; +import { _t } from '../../../languageHandler'; +import { ensureDMExists } from "../../../createRoom"; +import { IDialogProps } from "./IDialogProps"; +import {MatrixEvent} from "matrix-js-sdk/src/models/event"; +import {MatrixClientPeg} from "../../../MatrixClientPeg"; +import SdkConfig from '../../../SdkConfig'; +import Markdown from '../../../Markdown'; +import {replaceableComponent} from "../../../utils/replaceableComponent"; +import SettingsStore from "../../../settings/SettingsStore"; +import StyledRadioButton from "../elements/StyledRadioButton"; + +interface IProps extends IDialogProps { + mxEvent: MatrixEvent; +} + +interface IState { + // A free-form text describing the abuse. + reason: string; + busy: boolean; + err?: string; + // If we know it, the nature of the abuse, as specified by MSC3215. + nature?: EXTENDED_NATURE; +} + + +const MODERATED_BY_STATE_EVENT_TYPE = [ + "org.matrix.msc3215.room.moderation.moderated_by", + /** + * Unprefixed state event. Not ready for prime time. + * + * "m.room.moderation.moderated_by" + */ +]; + +const ABUSE_EVENT_TYPE = "org.matrix.msc3215.abuse.report"; + +// Standard abuse natures. +enum NATURE { + DISAGREEMENT = "org.matrix.msc3215.abuse.nature.disagreement", + TOXIC = "org.matrix.msc3215.abuse.nature.toxic", + ILLEGAL = "org.matrix.msc3215.abuse.nature.illegal", + SPAM = "org.matrix.msc3215.abuse.nature.spam", + OTHER = "org.matrix.msc3215.abuse.nature.other", +} + +enum NON_STANDARD_NATURE { + // Non-standard abuse nature. + // It should never leave the client - we use it to fallback to + // server-wide abuse reporting. + ADMIN = "non-standard.abuse.nature.admin" +} + +type EXTENDED_NATURE = NATURE | NON_STANDARD_NATURE; + +type Moderation = { + // The id of the moderation room. + moderationRoomId: string; + // The id of the bot in charge of forwarding abuse reports to the moderation room. + moderationBotUserId: string; +} +/* + * A dialog for reporting an event. + * + * The actual content of the dialog will depend on two things: + * + * 1. Is `feature_report_to_moderators` enabled? + * 2. Does the room support moderation as per MSC3215, i.e. is there + * a well-formed state event `m.room.moderation.moderated_by` + * /`org.matrix.msc3215.room.moderation.moderated_by`? + */ +@replaceableComponent("views.dialogs.ReportEventDialog") +export default class ReportEventDialog extends React.Component { + // If the room supports moderation, the moderation information. + private moderation?: Moderation; + + constructor(props: IProps) { + super(props); + + let moderatedByRoomId = null; + let moderatedByUserId = null; + + if (SettingsStore.getValue("feature_report_to_moderators")) { + // The client supports reporting to moderators. + // Does the room support it, too? + + // Extract state events to determine whether we should display + const client = MatrixClientPeg.get(); + const room = client.getRoom(props.mxEvent.getRoomId()); + + for (const stateEventType of MODERATED_BY_STATE_EVENT_TYPE) { + const stateEvent = room.currentState.getStateEvents(stateEventType, stateEventType); + if (!stateEvent) { + continue; + } + if (Array.isArray(stateEvent)) { + // Internal error. + throw new TypeError(`getStateEvents(${stateEventType}, ${stateEventType}) ` + + "should return at most one state event"); + } + const event = stateEvent.event; + if (!("content" in event) || typeof event["content"] != "object") { + // The room is improperly configured. + // Display this debug message for the sake of moderators. + console.debug("Moderation error", "state event", stateEventType, + "should have an object field `content`, got", event); + continue; + } + const content = event["content"]; + if (!("room_id" in content) || typeof content["room_id"] != "string") { + // The room is improperly configured. + // Display this debug message for the sake of moderators. + console.debug("Moderation error", "state event", stateEventType, + "should have a string field `content.room_id`, got", event); + continue; + } + if (!("user_id" in content) || typeof content["user_id"] != "string") { + // The room is improperly configured. + // Display this debug message for the sake of moderators. + console.debug("Moderation error", "state event", stateEventType, + "should have a string field `content.user_id`, got", event); + continue; + } + moderatedByRoomId = content["room_id"]; + moderatedByUserId = content["user_id"]; + } + + if (moderatedByRoomId && moderatedByUserId) { + // The room supports moderation. + this.moderation = { + moderationRoomId: moderatedByRoomId, + moderationBotUserId: moderatedByUserId, + }; + } + } + + this.state = { + // A free-form text describing the abuse. + reason: "", + busy: false, + err: null, + // If specified, the nature of the abuse, as specified by MSC3215. + nature: null, + }; + } + + // The user has written down a freeform description of the abuse. + private onReasonChange = ({target: {value: reason}}): void => { + this.setState({ reason }); + }; + + // The user has clicked on a nature. + private onNatureChosen = (e: React.FormEvent): void => { + this.setState({ nature: e.currentTarget.value as EXTENDED_NATURE}); + }; + + // The user has clicked "cancel". + private onCancel = (): void => { + this.props.onFinished(false); + }; + + // The user has clicked "submit". + private onSubmit = async () => { + let reason = this.state.reason || ""; + reason = reason.trim(); + if (this.moderation) { + // This room supports moderation. + // We need a nature. + // If the nature is `NATURE.OTHER` or `NON_STANDARD_NATURE.ADMIN`, we also need a `reason`. + if (!this.state.nature || + ((this.state.nature == NATURE.OTHER || this.state.nature == NON_STANDARD_NATURE.ADMIN) + && !reason) + ) { + this.setState({ + err: _t("Please fill why you're reporting."), + }); + return; + } + } else { + // This room does not support moderation. + // We need a `reason`. + if (!reason) { + this.setState({ + err: _t("Please fill why you're reporting."), + }); + return; + } + } + + this.setState({ + busy: true, + err: null, + }); + + try { + const client = MatrixClientPeg.get(); + const ev = this.props.mxEvent; + if (this.moderation && this.state.nature != NON_STANDARD_NATURE.ADMIN) { + const nature: NATURE = this.state.nature; + + // Report to moderators through to the dedicated bot, + // as configured in the room's state events. + const dmRoomId = await ensureDMExists(client, this.moderation.moderationBotUserId); + await client.sendEvent(dmRoomId, ABUSE_EVENT_TYPE, { + event_id: ev.getId(), + room_id: ev.getRoomId(), + moderated_by_id: this.moderation.moderationRoomId, + nature, + reporter: client.getUserId(), + comment: this.state.reason.trim(), + }); + } else { + // Report to homeserver admin through the dedicated Matrix API. + await client.reportEvent(ev.getRoomId(), ev.getId(), -100, this.state.reason.trim()); + } + this.props.onFinished(true); + } catch (e) { + this.setState({ + busy: false, + err: e.message, + }); + } + }; + + render() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + const Loader = sdk.getComponent('elements.Spinner'); + const Field = sdk.getComponent('elements.Field'); + + let error = null; + if (this.state.err) { + error =
    + {this.state.err} +
    ; + } + + let progress = null; + if (this.state.busy) { + progress = ( +
    + +
    + ); + } + + const adminMessageMD = + SdkConfig.get().reportEvent && + SdkConfig.get().reportEvent.adminMessageMD; + let adminMessage; + if (adminMessageMD) { + const html = new Markdown(adminMessageMD).toHTML({ externalLinks: true }); + adminMessage =

    ; + } + + if (this.moderation) { + // Display report-to-moderator dialog. + // We let the user pick a nature. + const client = MatrixClientPeg.get(); + const homeServerName = SdkConfig.get()["validated_server_config"].hsName; + let subtitle; + switch (this.state.nature) { + case NATURE.DISAGREEMENT: + subtitle = _t("What this user is writing is wrong.\n" + + "This will be reported to the room moderators."); + break; + case NATURE.TOXIC: + subtitle = _t("This user is displaying toxic behaviour, " + + "for instance by insulting other users or sharing " + + " adult-only content in a family-friendly room " + + " or otherwise violating the rules of this room.\n" + + "This will be reported to the room moderators."); + break; + case NATURE.ILLEGAL: + subtitle = _t("This user is displaying illegal behaviour, " + + "for instance by doxing people or threatening violence.\n" + + "This will be reported to the room moderators who may escalate this to legal authorities."); + break; + case NATURE.SPAM: + subtitle = _t("This user is spamming the room with ads, links to ads or to propaganda.\n" + + "This will be reported to the room moderators."); + break; + case NON_STANDARD_NATURE.ADMIN: + if (client.isRoomEncrypted(this.props.mxEvent.getRoomId())) { + subtitle = _t("This room is dedicated to illegal or toxic content " + + "or the moderators fail to moderate illegal or toxic content.\n" + + "This will be reported to the administrators of %(homeserver)s. " + + "The administrators will NOT be able to read the encrypted content of this room.", + { homeserver: homeServerName }); + } else { + subtitle = _t("This room is dedicated to illegal or toxic content " + + "or the moderators fail to moderate illegal or toxic content.\n" + + " This will be reported to the administrators of %(homeserver)s.", + { homeserver: homeServerName }); + } + break; + case NATURE.OTHER: + subtitle = _t("Any other reason. Please describe the problem.\n" + + "This will be reported to the room moderators."); + break; + default: + subtitle = _t("Please pick a nature and describe what makes this message abusive."); + break; + } + + return ( + +

    + + {_t('Disagree')} + + + {_t('Toxic Behaviour')} + + + {_t('Illegal Content')} + + + {_t('Spam or propaganda')} + + + {_t('Report the entire room')} + + + {_t('Other')} + +

    + {subtitle} +

    + + {progress} + {error} +
    + + + ); + } + // Report to homeserver admin. + // Currently, the API does not support natures. + return ( + +
    +

    + { + _t("Reporting this message will send its unique 'event ID' to the administrator of " + + "your homeserver. If messages in this room are encrypted, your homeserver " + + "administrator will not be able to read the message text or view any files " + + "or images.") + } +

    + {adminMessage} + + {progress} + {error} +
    + +
    + ); + } +} diff --git a/src/components/views/dialogs/TermsDialog.js b/src/components/views/dialogs/TermsDialog.tsx similarity index 72% rename from src/components/views/dialogs/TermsDialog.js rename to src/components/views/dialogs/TermsDialog.tsx index e8625ec6cb..ace5316323 100644 --- a/src/components/views/dialogs/TermsDialog.js +++ b/src/components/views/dialogs/TermsDialog.tsx @@ -16,22 +16,21 @@ limitations under the License. import url from 'url'; import React from 'react'; -import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import { _t, pickBestLanguage } from '../../../languageHandler'; import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types"; +import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types"; -class TermsCheckbox extends React.PureComponent { - static propTypes = { - onChange: PropTypes.func.isRequired, - url: PropTypes.string.isRequired, - checked: PropTypes.bool.isRequired, - } +interface ITermsCheckboxProps { + onChange: (url: string, checked: boolean) => void; + url: string; + checked: boolean; +} - onChange = (ev) => { - this.props.onChange(this.props.url, ev.target.checked); +class TermsCheckbox extends React.PureComponent { + private onChange = (ev: React.FormEvent): void => { + this.props.onChange(this.props.url, ev.currentTarget.checked); } render() { @@ -42,30 +41,34 @@ class TermsCheckbox extends React.PureComponent { } } +interface ITermsDialogProps { + /** + * Array of [Service, policies] pairs, where policies is the response from the + * /terms endpoint for that service + */ + policiesAndServicePairs: any[], + + /** + * urls that the user has already agreed to + */ + agreedUrls?: string[], + + /** + * Called with: + * * success {bool} True if the user accepted any douments, false if cancelled + * * agreedUrls {string[]} List of agreed URLs + */ + onFinished: (success: boolean, agreedUrls?: string[]) => void, +} + +interface IState { + agreedUrls: any; +} + @replaceableComponent("views.dialogs.TermsDialog") -export default class TermsDialog extends React.PureComponent { - static propTypes = { - /** - * Array of [Service, policies] pairs, where policies is the response from the - * /terms endpoint for that service - */ - policiesAndServicePairs: PropTypes.array.isRequired, - - /** - * urls that the user has already agreed to - */ - agreedUrls: PropTypes.arrayOf(PropTypes.string), - - /** - * Called with: - * * success {bool} True if the user accepted any douments, false if cancelled - * * agreedUrls {string[]} List of agreed URLs - */ - onFinished: PropTypes.func.isRequired, - } - +export default class TermsDialog extends React.PureComponent { constructor(props) { - super(); + super(props); this.state = { // url -> boolean agreedUrls: {}, @@ -75,15 +78,15 @@ export default class TermsDialog extends React.PureComponent { } } - _onCancelClick = () => { + private onCancelClick = (): void => { this.props.onFinished(false); } - _onNextClick = () => { + private onNextClick = (): void => { this.props.onFinished(true, Object.keys(this.state.agreedUrls).filter((url) => this.state.agreedUrls[url])); } - _nameForServiceType(serviceType, host) { + private nameForServiceType(serviceType: SERVICE_TYPES, host: string): JSX.Element { switch (serviceType) { case SERVICE_TYPES.IS: return
    {_t("Identity Server")}
    ({host})
    ; @@ -92,7 +95,7 @@ export default class TermsDialog extends React.PureComponent { } } - _summaryForServiceType(serviceType) { + private summaryForServiceType(serviceType: SERVICE_TYPES): JSX.Element { switch (serviceType) { case SERVICE_TYPES.IS: return
    @@ -107,13 +110,13 @@ export default class TermsDialog extends React.PureComponent { } } - _onTermsCheckboxChange = (url, checked) => { + private onTermsCheckboxChange = (url: string, checked: boolean) => { this.setState({ agreedUrls: Object.assign({}, this.state.agreedUrls, { [url]: checked }), }); } - render() { + public render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); @@ -128,8 +131,8 @@ export default class TermsDialog extends React.PureComponent { let serviceName; let summary; if (i === 0) { - serviceName = this._nameForServiceType(policiesAndService.service.serviceType, parsedBaseUrl.host); - summary = this._summaryForServiceType( + serviceName = this.nameForServiceType(policiesAndService.service.serviceType, parsedBaseUrl.host); + summary = this.summaryForServiceType( policiesAndService.service.serviceType, ); } @@ -137,12 +140,15 @@ export default class TermsDialog extends React.PureComponent { rows.push( {serviceName} {summary} - {termDoc[termsLang].name} - - + + {termDoc[termsLang].name} + + + + ); @@ -176,7 +182,7 @@ export default class TermsDialog extends React.PureComponent { return ( diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.tsx similarity index 75% rename from src/components/views/dialogs/UserSettingsDialog.js rename to src/components/views/dialogs/UserSettingsDialog.tsx index fe29b85aea..1a62a4ff22 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.tsx @@ -16,11 +16,10 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import TabbedView, {Tab} from "../../structures/TabbedView"; import {_t, _td} from "../../../languageHandler"; import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab"; -import SettingsStore from "../../../settings/SettingsStore"; +import SettingsStore, { CallbackFn } from "../../../settings/SettingsStore"; import LabsUserSettingsTab from "../settings/tabs/user/LabsUserSettingsTab"; import AppearanceUserSettingsTab from "../settings/tabs/user/AppearanceUserSettingsTab"; import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab"; @@ -35,41 +34,49 @@ import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab import {UIFeature} from "../../../settings/UIFeature"; import {replaceableComponent} from "../../../utils/replaceableComponent"; -export const USER_GENERAL_TAB = "USER_GENERAL_TAB"; -export const USER_APPEARANCE_TAB = "USER_APPEARANCE_TAB"; -export const USER_FLAIR_TAB = "USER_FLAIR_TAB"; -export const USER_NOTIFICATIONS_TAB = "USER_NOTIFICATIONS_TAB"; -export const USER_PREFERENCES_TAB = "USER_PREFERENCES_TAB"; -export const USER_VOICE_TAB = "USER_VOICE_TAB"; -export const USER_SECURITY_TAB = "USER_SECURITY_TAB"; -export const USER_LABS_TAB = "USER_LABS_TAB"; -export const USER_MJOLNIR_TAB = "USER_MJOLNIR_TAB"; -export const USER_HELP_TAB = "USER_HELP_TAB"; +export enum UserTab { + General = "USER_GENERAL_TAB", + Appearance = "USER_APPEARANCE_TAB", + Flair = "USER_FLAIR_TAB", + Notifications = "USER_NOTIFICATIONS_TAB", + Preferences = "USER_PREFERENCES_TAB", + Voice = "USER_VOICE_TAB", + Security = "USER_SECURITY_TAB", + Labs = "USER_LABS_TAB", + Mjolnir = "USER_MJOLNIR_TAB", + Help = "USER_HELP_TAB", +} + +interface IProps { + onFinished: (success: boolean) => void; + initialTabId?: string; +} + +interface IState { + mjolnirEnabled: boolean; +} @replaceableComponent("views.dialogs.UserSettingsDialog") -export default class UserSettingsDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - initialTabId: PropTypes.string, - }; +export default class UserSettingsDialog extends React.Component { + private mjolnirWatcher: string; - constructor() { - super(); + constructor(props) { + super(props); this.state = { mjolnirEnabled: SettingsStore.getValue("feature_mjolnir"), }; } - componentDidMount(): void { - this._mjolnirWatcher = SettingsStore.watchSetting("feature_mjolnir", null, this._mjolnirChanged.bind(this)); + public componentDidMount(): void { + this.mjolnirWatcher = SettingsStore.watchSetting("feature_mjolnir", null, this.mjolnirChanged); } - componentWillUnmount(): void { - SettingsStore.unwatchSetting(this._mjolnirWatcher); + public componentWillUnmount(): void { + SettingsStore.unwatchSetting(this.mjolnirWatcher); } - _mjolnirChanged(settingName, roomId, atLevel, newValue) { + private mjolnirChanged: CallbackFn = (settingName, roomId, atLevel, newValue) => { // We can cheat because we know what levels a feature is tracked at, and how it is tracked this.setState({mjolnirEnabled: newValue}); } @@ -78,33 +85,33 @@ export default class UserSettingsDialog extends React.Component { const tabs = []; tabs.push(new Tab( - USER_GENERAL_TAB, + UserTab.General, _td("General"), "mx_UserSettingsDialog_settingsIcon", , )); tabs.push(new Tab( - USER_APPEARANCE_TAB, + UserTab.Appearance, _td("Appearance"), "mx_UserSettingsDialog_appearanceIcon", , )); if (SettingsStore.getValue(UIFeature.Flair)) { tabs.push(new Tab( - USER_FLAIR_TAB, + UserTab.Flair, _td("Flair"), "mx_UserSettingsDialog_flairIcon", , )); } tabs.push(new Tab( - USER_NOTIFICATIONS_TAB, + UserTab.Notifications, _td("Notifications"), "mx_UserSettingsDialog_bellIcon", , )); tabs.push(new Tab( - USER_PREFERENCES_TAB, + UserTab.Preferences, _td("Preferences"), "mx_UserSettingsDialog_preferencesIcon", , @@ -112,7 +119,7 @@ export default class UserSettingsDialog extends React.Component { if (SettingsStore.getValue(UIFeature.Voip)) { tabs.push(new Tab( - USER_VOICE_TAB, + UserTab.Voice, _td("Voice & Video"), "mx_UserSettingsDialog_voiceIcon", , @@ -120,7 +127,7 @@ export default class UserSettingsDialog extends React.Component { } tabs.push(new Tab( - USER_SECURITY_TAB, + UserTab.Security, _td("Security & Privacy"), "mx_UserSettingsDialog_securityIcon", , @@ -130,7 +137,7 @@ export default class UserSettingsDialog extends React.Component { || SettingsStore.getFeatureSettingNames().some(k => SettingsStore.getBetaInfo(k)) ) { tabs.push(new Tab( - USER_LABS_TAB, + UserTab.Labs, _td("Labs"), "mx_UserSettingsDialog_labsIcon", , @@ -138,17 +145,17 @@ export default class UserSettingsDialog extends React.Component { } if (this.state.mjolnirEnabled) { tabs.push(new Tab( - USER_MJOLNIR_TAB, + UserTab.Mjolnir, _td("Ignored users"), "mx_UserSettingsDialog_mjolnirIcon", , )); } tabs.push(new Tab( - USER_HELP_TAB, + UserTab.Help, _td("Help & About"), "mx_UserSettingsDialog_helpIcon", - , + this.props.onFinished(true)} />, )); return tabs; diff --git a/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js b/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.tsx similarity index 83% rename from src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js rename to src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.tsx index e71983b074..6272302a76 100644 --- a/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js +++ b/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.tsx @@ -15,22 +15,21 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import {_t} from "../../../../languageHandler"; +import { _t } from "../../../../languageHandler"; import * as sdk from "../../../../index"; -import {replaceableComponent} from "../../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../../utils/replaceableComponent"; + +interface IProps { + onFinished: (success: boolean) => void; +} @replaceableComponent("views.dialogs.security.ConfirmDestroyCrossSigningDialog") -export default class ConfirmDestroyCrossSigningDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - }; - - _onConfirm = () => { +export default class ConfirmDestroyCrossSigningDialog extends React.Component { + private onConfirm = (): void => { this.props.onFinished(true); }; - _onDecline = () => { + private onDecline = (): void => { this.props.onFinished(false); }; @@ -57,10 +56,10 @@ export default class ConfirmDestroyCrossSigningDialog extends React.Component {
    ); diff --git a/src/components/views/dialogs/security/CreateCrossSigningDialog.js b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx similarity index 85% rename from src/components/views/dialogs/security/CreateCrossSigningDialog.js rename to src/components/views/dialogs/security/CreateCrossSigningDialog.tsx index fedcc02f89..840390f6fb 100644 --- a/src/components/views/dialogs/security/CreateCrossSigningDialog.js +++ b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx @@ -16,7 +16,6 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { MatrixClientPeg } from '../../../../MatrixClientPeg'; import { _t } from '../../../../languageHandler'; import Modal from '../../../../Modal'; @@ -25,7 +24,19 @@ import DialogButtons from '../../elements/DialogButtons'; import BaseDialog from '../BaseDialog'; import Spinner from '../../elements/Spinner'; import InteractiveAuthDialog from '../InteractiveAuthDialog'; -import {replaceableComponent} from "../../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../../utils/replaceableComponent"; + +interface IProps { + accountPassword?: string; + tokenLogin?: boolean; + onFinished?: (success: boolean) => void; +} + +interface IState { + error: Error | null; + canUploadKeysWithPasswordOnly?: boolean; + accountPassword: string; +} /* * Walks the user through the process of creating a cross-signing keys. In most @@ -33,39 +44,32 @@ import {replaceableComponent} from "../../../../utils/replaceableComponent"; * may need to complete some steps to proceed. */ @replaceableComponent("views.dialogs.security.CreateCrossSigningDialog") -export default class CreateCrossSigningDialog extends React.PureComponent { - static propTypes = { - accountPassword: PropTypes.string, - tokenLogin: PropTypes.bool, - }; - - constructor(props) { +export default class CreateCrossSigningDialog extends React.PureComponent { + constructor(props: IProps) { super(props); this.state = { error: null, // Does the server offer a UI auth flow with just m.login.password // for /keys/device_signing/upload? - canUploadKeysWithPasswordOnly: null, - accountPassword: props.accountPassword || "", - }; - - 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 // signing key upload as well. This avoids hitting the server to // test auth flows, which may be slow under high load. - this.state.canUploadKeysWithPasswordOnly = true; - } else { - this._queryKeyUploadAuth(); + canUploadKeysWithPasswordOnly: props.accountPassword ? true : null, + accountPassword: props.accountPassword || "", + }; + + if (!this.state.accountPassword) { + this.queryKeyUploadAuth(); } } - componentDidMount() { - this._bootstrapCrossSigning(); + public componentDidMount(): void { + this.bootstrapCrossSigning(); } - async _queryKeyUploadAuth() { + private async queryKeyUploadAuth(): Promise { try { await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {}); // We should never get here: the server should always require @@ -86,7 +90,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent { } } - _doBootstrapUIAuth = async (makeRequest) => { + private doBootstrapUIAuth = async (makeRequest: (authData: any) => void): Promise => { if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) { await makeRequest({ type: 'm.login.password', @@ -137,7 +141,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent { } } - _bootstrapCrossSigning = async () => { + private bootstrapCrossSigning = async (): Promise => { this.setState({ error: null, }); @@ -146,13 +150,13 @@ export default class CreateCrossSigningDialog extends React.PureComponent { try { await cli.bootstrapCrossSigning({ - authUploadDeviceSigningKeys: this._doBootstrapUIAuth, + authUploadDeviceSigningKeys: this.doBootstrapUIAuth, }); this.props.onFinished(true); } catch (e) { if (this.props.tokenLogin) { // ignore any failures, we are relying on grace period here - this.props.onFinished(); + this.props.onFinished(false); return; } @@ -161,7 +165,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent { } } - _onCancel = () => { + private onCancel = (): void => { this.props.onFinished(false); } @@ -172,8 +176,8 @@ export default class CreateCrossSigningDialog extends React.PureComponent {

    {_t("Unable to set up keys")}

    ; diff --git a/src/components/views/dialogs/security/SetupEncryptionDialog.js b/src/components/views/dialogs/security/SetupEncryptionDialog.tsx similarity index 72% rename from src/components/views/dialogs/security/SetupEncryptionDialog.js rename to src/components/views/dialogs/security/SetupEncryptionDialog.tsx index 3c15ea9f1d..19c7af01ff 100644 --- a/src/components/views/dialogs/security/SetupEncryptionDialog.js +++ b/src/components/views/dialogs/security/SetupEncryptionDialog.tsx @@ -15,47 +15,52 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import SetupEncryptionBody from '../../../structures/auth/SetupEncryptionBody'; import BaseDialog from '../BaseDialog'; import { _t } from '../../../../languageHandler'; -import { SetupEncryptionStore, PHASE_DONE } from '../../../../stores/SetupEncryptionStore'; +import { SetupEncryptionStore, Phase } from '../../../../stores/SetupEncryptionStore'; import {replaceableComponent} from "../../../../utils/replaceableComponent"; -function iconFromPhase(phase) { - if (phase === PHASE_DONE) { +function iconFromPhase(phase: Phase) { + if (phase === Phase.Done) { return require("../../../../../res/img/e2e/verified.svg"); } else { return require("../../../../../res/img/e2e/warning.svg"); } } -@replaceableComponent("views.dialogs.security.SetupEncryptionDialog") -export default class SetupEncryptionDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - }; +interface IProps { + onFinished: (success: boolean) => void; +} - constructor() { - super(); +interface IState { + icon: Phase; +} + +@replaceableComponent("views.dialogs.security.SetupEncryptionDialog") +export default class SetupEncryptionDialog extends React.Component { + private store: SetupEncryptionStore; + + constructor(props: IProps) { + super(props); this.store = SetupEncryptionStore.sharedInstance(); this.state = {icon: iconFromPhase(this.store.phase)}; } - componentDidMount() { - this.store.on("update", this._onStoreUpdate); + public componentDidMount() { + this.store.on("update", this.onStoreUpdate); } - componentWillUnmount() { - this.store.removeListener("update", this._onStoreUpdate); + public componentWillUnmount() { + this.store.removeListener("update", this.onStoreUpdate); } - _onStoreUpdate = () => { + private onStoreUpdate = (): void => { this.setState({icon: iconFromPhase(this.store.phase)}); }; - render() { + public render() { return + {_t("Message search initialisation failed, check your settings for more information", {}, { + a: sub => ( { + evt.preventDefault(); + dis.dispatch({ + action: Action.ViewUserSettings, + initialTabId: UserTab.Security, + }); + }}> + {sub} + ), + })} + ; + } + const {desktopBuilds, brand} = SdkConfig.get(); let text = null; diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index cf3b7a6e61..d39557c9bb 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -101,7 +101,8 @@ export default class EventTilePreview extends React.Component { // Fake it more event.sender = { - name: this.props.displayName, + name: this.props.displayName || this.props.userId, + rawDisplayName: this.props.displayName, userId: this.props.userId, getAvatarUrl: (..._) => { return Avatar.avatarUrlForUser( diff --git a/src/components/views/elements/FormButton.js b/src/components/views/elements/FormButton.js deleted file mode 100644 index f6b4c986f5..0000000000 --- a/src/components/views/elements/FormButton.js +++ /dev/null @@ -1,28 +0,0 @@ -/* -Copyright 2019 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; -import AccessibleButton from "./AccessibleButton"; - -export default function FormButton(props) { - const {className, label, kind, ...restProps} = props; - const newClassName = (className || "") + " mx_FormButton"; - const allProps = Object.assign({}, restProps, - {className: newClassName, kind: kind || "primary", children: [label]}); - return React.createElement(AccessibleButton, allProps); -} - -FormButton.propTypes = AccessibleButton.propTypes; diff --git a/src/components/views/elements/MemberEventListSummary.tsx b/src/components/views/elements/MemberEventListSummary.tsx index 0290ef6d83..f10884ce9d 100644 --- a/src/components/views/elements/MemberEventListSummary.tsx +++ b/src/components/views/elements/MemberEventListSummary.tsx @@ -24,7 +24,7 @@ import { _t } from '../../../languageHandler'; import { formatCommaSeparatedList } from '../../../utils/FormattingUtils'; import { isValid3pidInvite } from "../../../RoomInvite"; import EventListSummary from "./EventListSummary"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { // An array of member events to summarise @@ -303,7 +303,7 @@ export default class MemberEventListSummary extends React.Component { return res; } - private static getTransitionSequence(events: MatrixEvent[]) { + private static getTransitionSequence(events: IUserEvents[]) { return events.map(MemberEventListSummary.getTransition); } @@ -315,7 +315,7 @@ export default class MemberEventListSummary extends React.Component { * @returns {string?} the transition type given to this event. This defaults to `null` * if a transition is not recognised. */ - private static getTransition(e: MatrixEvent): TransitionType { + private static getTransition(e: IUserEvents): TransitionType { if (e.mxEvent.getType() === 'm.room.third_party_invite') { // Handle 3pid invites the same as invites so they get bundled together if (!isValid3pidInvite(e.mxEvent)) { diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index 81ed360b17..ebf4a18aa9 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -297,6 +297,7 @@ export default class ReplyThread extends React.Component { } async getEvent(eventId) { + if (!eventId) return null; const event = this.room.findEventById(eventId); if (event) return event; @@ -392,6 +393,7 @@ export default class ReplyThread extends React.Component { alwaysShowTimestamps={this.props.alwaysShowTimestamps} enableFlair={SettingsStore.getValue(UIFeature.Flair)} replacingEventId={ev.replacingEventId()} + as="div" /> ; }); diff --git a/src/components/views/elements/RoomName.tsx b/src/components/views/elements/RoomName.tsx index 9178155d19..cdd83aedc2 100644 --- a/src/components/views/elements/RoomName.tsx +++ b/src/components/views/elements/RoomName.tsx @@ -14,10 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {useEffect, useState} from "react"; -import {Room} from "matrix-js-sdk/src/models/room"; +import React, { useEffect, useState } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; -import {useEventEmitter} from "../../../hooks/useEventEmitter"; +import { useEventEmitter } from "../../../hooks/useEventEmitter"; interface IProps { room: Room; @@ -34,7 +34,7 @@ const RoomName = ({ room, children }: IProps): JSX.Element => { }, [room]); if (children) return children(name); - return name || ""; + return <>{ name || "" }; }; export default RoomName; diff --git a/src/components/views/elements/SettingsFlag.tsx b/src/components/views/elements/SettingsFlag.tsx index 4f885ab47d..24a21e1a33 100644 --- a/src/components/views/elements/SettingsFlag.tsx +++ b/src/components/views/elements/SettingsFlag.tsx @@ -77,9 +77,10 @@ export default class SettingsFlag extends React.Component { public render() { const canChange = SettingsStore.canSetValue(this.props.name, this.props.roomId, this.props.level); - let label = this.props.label; - if (!label) label = SettingsStore.getDisplayName(this.props.name, this.props.level); - else label = _t(label); + const label = this.props.label + ? _t(this.props.label) + : SettingsStore.getDisplayName(this.props.name, this.props.level); + const description = SettingsStore.getDescription(this.props.name); if (this.props.useCheckbox) { return { disabled={this.props.disabled || !canChange} aria-label={label} /> + { description &&
    + { description } +
    }
    ); } diff --git a/src/components/views/messages/MKeyVerificationRequest.js b/src/components/views/messages/MKeyVerificationRequest.tsx similarity index 76% rename from src/components/views/messages/MKeyVerificationRequest.js rename to src/components/views/messages/MKeyVerificationRequest.tsx index 988606a766..69467cfa50 100644 --- a/src/components/views/messages/MKeyVerificationRequest.js +++ b/src/components/views/messages/MKeyVerificationRequest.tsx @@ -15,41 +15,40 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; +import { MatrixEvent } from 'matrix-js-sdk/src'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; -import {getNameForEventRoom, userLabelForEventRoom} +import { getNameForEventRoom, userLabelForEventRoom } from '../../../utils/KeyVerificationStateObserver'; import dis from "../../../dispatcher/dispatcher"; -import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; -import {Action} from "../../../dispatcher/actions"; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; +import { Action } from "../../../dispatcher/actions"; import EventTileBubble from "./EventTileBubble"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; + +interface IProps { + mxEvent: MatrixEvent +} @replaceableComponent("views.messages.MKeyVerificationRequest") -export default class MKeyVerificationRequest extends React.Component { - constructor(props) { - super(props); - this.state = {}; - } - - componentDidMount() { +export default class MKeyVerificationRequest extends React.Component { + public componentDidMount() { const request = this.props.mxEvent.verificationRequest; if (request) { - request.on("change", this._onRequestChanged); + request.on("change", this.onRequestChanged); } } - componentWillUnmount() { + public componentWillUnmount() { const request = this.props.mxEvent.verificationRequest; if (request) { - request.off("change", this._onRequestChanged); + request.off("change", this.onRequestChanged); } } - _openRequest = () => { - const {verificationRequest} = this.props.mxEvent; + private openRequest = () => { + const { verificationRequest } = this.props.mxEvent; const member = MatrixClientPeg.get().getUser(verificationRequest.otherUserId); dis.dispatch({ action: Action.SetRightPanelPhase, @@ -58,15 +57,15 @@ export default class MKeyVerificationRequest extends React.Component { }); }; - _onRequestChanged = () => { + private onRequestChanged = () => { this.forceUpdate(); }; - _onAcceptClicked = async () => { + private onAcceptClicked = async () => { const request = this.props.mxEvent.verificationRequest; if (request) { try { - this._openRequest(); + this.openRequest(); await request.accept(); } catch (err) { console.error(err.message); @@ -74,7 +73,7 @@ export default class MKeyVerificationRequest extends React.Component { } }; - _onRejectClicked = async () => { + private onRejectClicked = async () => { const request = this.props.mxEvent.verificationRequest; if (request) { try { @@ -85,7 +84,7 @@ export default class MKeyVerificationRequest extends React.Component { } }; - _acceptedLabel(userId) { + private acceptedLabel(userId: string) { const client = MatrixClientPeg.get(); const myUserId = client.getUserId(); if (userId === myUserId) { @@ -95,7 +94,7 @@ export default class MKeyVerificationRequest extends React.Component { } } - _cancelledLabel(userId) { + private cancelledLabel(userId: string) { const client = MatrixClientPeg.get(); const myUserId = client.getUserId(); const {cancellationCode} = this.props.mxEvent.verificationRequest; @@ -115,9 +114,8 @@ export default class MKeyVerificationRequest extends React.Component { } } - render() { + public render() { const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); - const FormButton = sdk.getComponent("elements.FormButton"); const {mxEvent} = this.props; const request = mxEvent.verificationRequest; @@ -134,11 +132,11 @@ export default class MKeyVerificationRequest extends React.Component { let stateLabel; const accepted = request.ready || request.started || request.done; if (accepted) { - stateLabel = ( - {this._acceptedLabel(request.receivingUserId)} + stateLabel = ( + {this.acceptedLabel(request.receivingUserId)} ); } else if (request.cancelled) { - stateLabel = this._cancelledLabel(request.cancellingUserId); + stateLabel = this.cancelledLabel(request.cancellingUserId); } else if (request.accepting) { stateLabel = _t("Accepting …"); } else if (request.declining) { @@ -153,8 +151,12 @@ export default class MKeyVerificationRequest extends React.Component { subtitle = userLabelForEventRoom(request.requestingUserId, mxEvent.getRoomId()); if (request.canAccept) { stateNode = (
    - - + + {_t("Decline")} + + + {_t("Accept")} +
    ); } } else { // request sent by us @@ -174,8 +176,3 @@ export default class MKeyVerificationRequest extends React.Component { return null; } } - -MKeyVerificationRequest.propTypes = { - /* the MatrixEvent to show */ - mxEvent: PropTypes.object.isRequired, -}; diff --git a/src/components/views/messages/MVoiceMessageBody.tsx b/src/components/views/messages/MVoiceMessageBody.tsx index d65de7697a..a7e3b1cd86 100644 --- a/src/components/views/messages/MVoiceMessageBody.tsx +++ b/src/components/views/messages/MVoiceMessageBody.tsx @@ -24,6 +24,7 @@ import {_t} from "../../../languageHandler"; import {mediaFromContent} from "../../../customisations/Media"; import {decryptFile} from "../../../utils/DecryptFile"; import RecordingPlayback from "../voice_messages/RecordingPlayback"; +import {IMediaEventContent} from "../../../customisations/models/IMediaEventContent"; interface IProps { mxEvent: MatrixEvent; @@ -45,7 +46,7 @@ export default class MVoiceMessageBody extends React.PureComponent { public render() { - const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice']; + // MSC2516 is a legacy identifier. See https://github.com/matrix-org/matrix-doc/pull/3245 + const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice'] + || !!this.props.mxEvent.getContent()['org.matrix.msc3245.voice']; const voiceMessagesEnabled = SettingsStore.getValue("feature_voice_messages"); if (isVoiceMessage && voiceMessagesEnabled) { return ; diff --git a/src/components/views/messages/MessageTimestamp.js b/src/components/views/messages/MessageTimestamp.tsx similarity index 67% rename from src/components/views/messages/MessageTimestamp.js rename to src/components/views/messages/MessageTimestamp.tsx index a7f350adcd..8b02f6b38e 100644 --- a/src/components/views/messages/MessageTimestamp.js +++ b/src/components/views/messages/MessageTimestamp.tsx @@ -16,20 +16,19 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import {formatFullDate, formatTime, formatFullTime} from '../../../DateUtils'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { formatFullDate, formatTime, formatFullTime } from '../../../DateUtils'; +import { replaceableComponent } from "../../../utils/replaceableComponent"; + +interface IProps { + ts: number; + showTwelveHour?: boolean; + showFullDate?: boolean; + showSeconds?: boolean; +} @replaceableComponent("views.messages.MessageTimestamp") -export default class MessageTimestamp extends React.Component { - static propTypes = { - ts: PropTypes.number.isRequired, - showTwelveHour: PropTypes.bool, - showFullDate: PropTypes.bool, - showSeconds: PropTypes.bool, - }; - - render() { +export default class MessageTimestamp extends React.Component { + public render() { const date = new Date(this.props.ts); let timestamp; if (this.props.showFullDate) { @@ -41,7 +40,11 @@ export default class MessageTimestamp extends React.Component { } return ( - + {timestamp} ); diff --git a/src/components/views/messages/SenderProfile.tsx b/src/components/views/messages/SenderProfile.tsx index 805f842fbc..883b2bd8a7 100644 --- a/src/components/views/messages/SenderProfile.tsx +++ b/src/components/views/messages/SenderProfile.tsx @@ -17,10 +17,10 @@ import React from 'react'; import Flair from '../elements/Flair.js'; import FlairStore from '../../../stores/FlairStore'; -import {getUserNameColorClass} from '../../../utils/FormattingUtils'; +import { getUserNameColorClass } from '../../../utils/FormattingUtils'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import MatrixEvent from "matrix-js-sdk/src/models/event"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; interface IProps { mxEvent: MatrixEvent; diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 3adfea6ee6..ebc4ce7ce8 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -16,12 +16,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {createRef} from 'react'; +import React, { createRef } from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import highlight from 'highlight.js'; import * as HtmlUtils from '../../../HtmlUtils'; -import {formatDate} from '../../../DateUtils'; +import { formatDate } from '../../../DateUtils'; import * as sdk from '../../../index'; import Modal from '../../../Modal'; import dis from '../../../dispatcher/dispatcher'; @@ -29,14 +29,16 @@ import { _t } from '../../../languageHandler'; import * as ContextMenu from '../../structures/ContextMenu'; import SettingsStore from "../../../settings/SettingsStore"; import ReplyThread from "../elements/ReplyThread"; -import {pillifyLinks, unmountPills} from '../../../utils/pillify'; -import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; -import {isPermalinkHost} from "../../../utils/permalinks/Permalinks"; -import {toRightOf} from "../../structures/ContextMenu"; -import {copyPlaintext} from "../../../utils/strings"; +import { pillifyLinks, unmountPills } from '../../../utils/pillify'; +import { IntegrationManagers } from "../../../integrations/IntegrationManagers"; +import { isPermalinkHost } from "../../../utils/permalinks/Permalinks"; +import { toRightOf } from "../../structures/ContextMenu"; +import { copyPlaintext } from "../../../utils/strings"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; import UIStore from "../../../stores/UIStore"; +import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; +import { Action } from "../../../dispatcher/actions"; @replaceableComponent("views.messages.TextualBody") export default class TextualBody extends React.Component { @@ -390,9 +392,9 @@ export default class TextualBody extends React.Component { onEmoteSenderClick = event => { const mxEvent = this.props.mxEvent; - dis.dispatch({ - action: 'insert_mention', - user_id: mxEvent.getSender(), + dis.dispatch({ + action: Action.ComposerInsert, + userId: mxEvent.getSender(), }); }; diff --git a/src/components/views/right_panel/EncryptionInfo.tsx b/src/components/views/right_panel/EncryptionInfo.tsx index aa51965ac6..db59a88967 100644 --- a/src/components/views/right_panel/EncryptionInfo.tsx +++ b/src/components/views/right_panel/EncryptionInfo.tsx @@ -17,8 +17,9 @@ limitations under the License. import React from "react"; import * as sdk from "../../../index"; -import {_t} from "../../../languageHandler"; -import {RoomMember} from "matrix-js-sdk/src/models/room-member"; +import { _t } from "../../../languageHandler"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { User } from "matrix-js-sdk/src/models/user"; export const PendingActionSpinner = ({text}) => { const Spinner = sdk.getComponent('elements.Spinner'); @@ -31,7 +32,7 @@ export const PendingActionSpinner = ({text}) => { interface IProps { waitingForOtherParty: boolean; waitingForNetwork: boolean; - member: RoomMember; + member: RoomMember | User; onStartVerification: () => Promise; isRoomEncrypted: boolean; inDialog: boolean; @@ -55,7 +56,7 @@ const EncryptionInfo: React.FC = ({ text = _t("Accept on your other login…"); } else { text = _t("Waiting for %(displayName)s to accept…", { - displayName: member.displayName || member.name || member.userId, + displayName: (member as User).displayName || (member as RoomMember).name || member.userId, }); } } else { diff --git a/src/components/views/right_panel/EncryptionPanel.tsx b/src/components/views/right_panel/EncryptionPanel.tsx index c237a4ade6..3a26427246 100644 --- a/src/components/views/right_panel/EncryptionPanel.tsx +++ b/src/components/views/right_panel/EncryptionPanel.tsx @@ -14,28 +14,29 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useCallback, useEffect, useState} from "react"; +import React, { useCallback, useEffect, useState } from "react"; +import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { User } from "matrix-js-sdk/src/models/user"; +import { PHASE_REQUESTED, PHASE_UNSENT } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import EncryptionInfo from "./EncryptionInfo"; import VerificationPanel from "./VerificationPanel"; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; -import {ensureDMExists} from "../../../createRoom"; -import {useEventEmitter} from "../../../hooks/useEventEmitter"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { ensureDMExists } from "../../../createRoom"; +import { useEventEmitter } from "../../../hooks/useEventEmitter"; import Modal from "../../../Modal"; -import {PHASE_REQUESTED, PHASE_UNSENT} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import * as sdk from "../../../index"; -import {_t} from "../../../languageHandler"; -import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; -import {RoomMember} from "matrix-js-sdk/src/models/room-member"; +import { _t } from "../../../languageHandler"; import dis from "../../../dispatcher/dispatcher"; -import {Action} from "../../../dispatcher/actions"; -import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; +import { Action } from "../../../dispatcher/actions"; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; // cancellation codes which constitute a key mismatch const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"]; interface IProps { - member: RoomMember; + member: RoomMember | User; onClose: () => void; verificationRequest: VerificationRequest; verificationRequestPromise: Promise; diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index d6c97f9cf2..79302e2aa0 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -38,7 +38,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import RoomViewStore from "../../../stores/RoomViewStore"; import MultiInviter from "../../../utils/MultiInviter"; import GroupStore from "../../../stores/GroupStore"; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; import E2EIcon from "../rooms/E2EIcon"; import { useEventEmitter } from "../../../hooks/useEventEmitter"; import { textualPowerLevel } from '../../../Roles'; @@ -48,7 +48,7 @@ import EncryptionPanel from "./EncryptionPanel"; import { useAsyncMemo } from '../../../hooks/useAsyncMemo'; import { legacyVerifyUser, verifyDevice, verifyUser } from '../../../verification'; import { Action } from "../../../dispatcher/actions"; -import { USER_SECURITY_TAB } from "../dialogs/UserSettingsDialog"; +import { UserTab } from "../dialogs/UserSettingsDialog"; import { useIsEncrypted } from "../../../hooks/useIsEncrypted"; import BaseCard from "./BaseCard"; import { E2EStatus } from "../../../utils/ShieldUtils"; @@ -68,6 +68,7 @@ import RoomAvatar from "../avatars/RoomAvatar"; import RoomName from "../elements/RoomName"; import { mediaFromMxc } from "../../../customisations/Media"; import UIStore from "../../../stores/UIStore"; +import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; export interface IDevice { deviceId: string; @@ -146,7 +147,7 @@ async function openDMForUser(matrixClient: MatrixClient, userId: string) { type SetUpdating = (updating: boolean) => void; -function useHasCrossSigningKeys(cli: MatrixClient, member: RoomMember, canVerify: boolean, setUpdating: SetUpdating) { +function useHasCrossSigningKeys(cli: MatrixClient, member: User, canVerify: boolean, setUpdating: SetUpdating) { return useAsyncMemo(async () => { if (!canVerify) { return undefined; @@ -368,9 +369,9 @@ const UserOptionsSection: React.FC<{ }; const onInsertPillButton = function() { - dis.dispatch({ - action: 'insert_mention', - user_id: member.userId, + dis.dispatch({ + action: Action.ComposerInsert, + userId: member.userId, }); }; @@ -971,7 +972,7 @@ interface IRoomPermissions { canInvite: boolean; } -function useRoomPermissions(cli: MatrixClient, room: Room, user: User): IRoomPermissions { +function useRoomPermissions(cli: MatrixClient, room: Room, user: RoomMember): IRoomPermissions { const [roomPermissions, setRoomPermissions] = useState({ // modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL modifyLevelMax: -1, @@ -1028,7 +1029,7 @@ function useRoomPermissions(cli: MatrixClient, room: Room, user: User): IRoomPer } const PowerLevelSection: React.FC<{ - user: User; + user: RoomMember; room: Room; roomPermissions: IRoomPermissions; powerLevels: IPowerLevelsContent; @@ -1037,7 +1038,7 @@ const PowerLevelSection: React.FC<{ return (); } else { const powerLevelUsersDefault = powerLevels.users_default || 0; - const powerLevel = parseInt(user.powerLevel, 10); + const powerLevel = user.powerLevel; const role = textualPowerLevel(powerLevel, powerLevelUsersDefault); return (
    @@ -1048,13 +1049,13 @@ const PowerLevelSection: React.FC<{ }; const PowerLevelEditor: React.FC<{ - user: User; + user: RoomMember; room: Room; roomPermissions: IRoomPermissions; }> = ({user, room, roomPermissions}) => { const cli = useContext(MatrixClientContext); - const [selectedPowerLevel, setSelectedPowerLevel] = useState(parseInt(user.powerLevel, 10)); + const [selectedPowerLevel, setSelectedPowerLevel] = useState(user.powerLevel); const onPowerChange = useCallback(async (powerLevelStr: string) => { const powerLevel = parseInt(powerLevelStr, 10); setSelectedPowerLevel(powerLevel); @@ -1231,7 +1232,7 @@ const BasicUserInfo: React.FC<{ setPendingUpdateCount(pendingUpdateCount - 1); }, [pendingUpdateCount]); - const roomPermissions = useRoomPermissions(cli, room, member); + const roomPermissions = useRoomPermissions(cli, room, member as RoomMember); const onSynapseDeactivate = useCallback(async () => { const {finished} = Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, { @@ -1275,12 +1276,26 @@ const BasicUserInfo: React.FC<{ ); } + let memberDetails; let adminToolsContainer; - if (room && member.roomId) { + if (room && (member as RoomMember).roomId) { + // hide the Roles section for DMs as it doesn't make sense there + if (!DMRoomMap.shared().getUserIdForRoomId((member as RoomMember).roomId)) { + memberDetails =
    +

    { _t("Role") }

    + +
    ; + } + adminToolsContainer = ( @@ -1309,20 +1324,6 @@ const BasicUserInfo: React.FC<{ spinner = ; } - let memberDetails; - // hide the Roles section for DMs as it doesn't make sense there - if (room && member.roomId && !DMRoomMap.shared().getUserIdForRoomId(member.roomId)) { - memberDetails =
    -

    { _t("Role") }

    - -
    ; - } - // only display the devices list if our client supports E2E const cryptoEnabled = cli.isCryptoEnabled(); @@ -1349,8 +1350,7 @@ const BasicUserInfo: React.FC<{ const setUpdating = (updating) => { setPendingUpdateCount(count => count + (updating ? 1 : -1)); }; - const hasCrossSigningKeys = - useHasCrossSigningKeys(cli, member, canVerify, setUpdating ); + const hasCrossSigningKeys = useHasCrossSigningKeys(cli, member as User, canVerify, setUpdating); const showDeviceListSpinner = devices === undefined; if (canVerify) { @@ -1359,9 +1359,9 @@ const BasicUserInfo: React.FC<{ verifyButton = ( { if (hasCrossSigningKeys) { - verifyUser(member); + verifyUser(member as User); } else { - legacyVerifyUser(member); + legacyVerifyUser(member as User); } }}> {_t("Verify")} @@ -1381,7 +1381,7 @@ const BasicUserInfo: React.FC<{ { dis.dispatch({ action: Action.ViewUserSettings, - initialTabId: USER_SECURITY_TAB, + initialTabId: UserTab.Security, }); }}> { _t("Edit devices") } @@ -1409,7 +1409,7 @@ const BasicUserInfo: React.FC<{ @@ -1428,13 +1428,15 @@ const UserInfoHeader: React.FC<{ const cli = useContext(MatrixClientContext); const onMemberAvatarClick = useCallback(() => { - const avatarUrl = member.getMxcAvatarUrl ? member.getMxcAvatarUrl() : member.avatarUrl; + const avatarUrl = (member as RoomMember).getMxcAvatarUrl + ? (member as RoomMember).getMxcAvatarUrl() + : (member as User).avatarUrl; if (!avatarUrl) return; const httpUrl = mediaFromMxc(avatarUrl).srcHttp; const params = { src: httpUrl, - name: member.name, + name: (member as RoomMember).name || (member as User).displayName, }; Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true); @@ -1446,13 +1448,13 @@ const UserInfoHeader: React.FC<{
    + urls={(member as User).avatarUrl ? [(member as User).avatarUrl] : undefined} />
  • @@ -1469,7 +1471,11 @@ const UserInfoHeader: React.FC<{ presenceCurrentlyActive = member.user.currentlyActive; if (SettingsStore.getValue("feature_custom_status")) { - statusMessage = member.user._unstable_statusMessage; + if ((member as RoomMember).user) { + statusMessage = member.user.unstable_statusMessage; + } else { + statusMessage = (member as unknown as User).unstable_statusMessage; + } } } @@ -1500,7 +1506,7 @@ const UserInfoHeader: React.FC<{ e2eIcon = ; } - const displayName = member.rawDisplayName || member.displayname; + const displayName = (member as RoomMember).rawDisplayName || (member as GroupMember).displayname; return { avatarElement } @@ -1588,7 +1594,7 @@ const UserInfo: React.FC = ({ content = ( @@ -1599,7 +1605,7 @@ const UserInfo: React.FC = ({ content = ( } - member={member} + member={member as User | RoomMember} onClose={onEncryptionPanelClose} isRoomEncrypted={isRoomEncrypted} /> diff --git a/src/components/views/right_panel/VerificationPanel.tsx b/src/components/views/right_panel/VerificationPanel.tsx index ac01c953b9..d3f2ba8cbf 100644 --- a/src/components/views/right_panel/VerificationPanel.tsx +++ b/src/components/views/right_panel/VerificationPanel.tsx @@ -22,6 +22,7 @@ import {verificationMethods} from 'matrix-js-sdk/src/crypto'; import {SCAN_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode"; import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import {RoomMember} from "matrix-js-sdk/src/models/room-member"; +import { User } from "matrix-js-sdk/src/models/user"; import {ReciprocateQRCode} from "matrix-js-sdk/src/crypto/verification/QRCode"; import {SAS} from "matrix-js-sdk/src/crypto/verification/SAS"; @@ -51,7 +52,7 @@ enum VerificationPhase { interface IProps { layout: string; request: VerificationRequest; - member: RoomMember; + member: RoomMember | User; phase: VerificationPhase; onClose: () => void; isRoomEncrypted: boolean; @@ -134,7 +135,7 @@ export default class VerificationPanel extends React.PureComponent

    {_t("Verify by scanning")}

    {_t("Ask %(displayName)s to scan your code:", { - displayName: member.displayName || member.name || member.userId, + displayName: (member as User).displayName || (member as RoomMember).name || member.userId, })}

    @@ -194,37 +195,33 @@ export default class VerificationPanel extends React.PureComponent

    {description}

    - - + onClick={this.onReciprocateYesClick} + > + { _t("Yes") } +
    ; } else { @@ -264,7 +261,7 @@ export default class VerificationPanel extends React.PureComponent { // This one is the vertical, ie. change height of apps drawer this.setState({ resizingVertical: resizing }); @@ -141,7 +134,10 @@ export default class AppsDrawer extends React.Component { _getAppsHash = (apps) => apps.map(app => app.id).join("~"); componentDidUpdate(prevProps, prevState) { - if (this._getAppsHash(this.state.apps) !== this._getAppsHash(prevState.apps)) { + if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) { + // Room has changed, update apps + this._updateApps(); + } else if (this._getAppsHash(this.state.apps) !== this._getAppsHash(prevState.apps)) { this._loadResizerPreferences(); } } diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx index 6d2ae39059..f6cc0f4d45 100644 --- a/src/components/views/rooms/AuxPanel.tsx +++ b/src/components/views/rooms/AuxPanel.tsx @@ -15,19 +15,20 @@ limitations under the License. */ import React from 'react'; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; -import { Room } from 'matrix-js-sdk/src/models/room' -import dis from "../../../dispatcher/dispatcher"; -import AppsDrawer from './AppsDrawer'; import classNames from 'classnames'; +import { lexicographicCompare } from 'matrix-js-sdk/src/utils'; +import { Room } from 'matrix-js-sdk/src/models/room' + +import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import AppsDrawer from './AppsDrawer'; import RateLimitedFunc from '../../../ratelimitedfunc'; import SettingsStore from "../../../settings/SettingsStore"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; -import {UIFeature} from "../../../settings/UIFeature"; +import { UIFeature } from "../../../settings/UIFeature"; import { ResizeNotifier } from "../../../utils/ResizeNotifier"; import CallViewForRoom from '../voip/CallViewForRoom'; -import {objectHasDiff} from "../../../utils/objects"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { objectHasDiff } from "../../../utils/objects"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { // js-sdk room object @@ -69,19 +70,21 @@ export default class AuxPanel extends React.Component { super(props); this.state = { - counters: this._computeCounters(), + counters: this.computeCounters(), }; } componentDidMount() { const cli = MatrixClientPeg.get(); - cli.on("RoomState.events", this._rateLimitedUpdate); + if (SettingsStore.getValue("feature_state_counters")) { + cli.on("RoomState.events", this.rateLimitedUpdate); + } } componentWillUnmount() { const cli = MatrixClientPeg.get(); - if (cli) { - cli.removeListener("RoomState.events", this._rateLimitedUpdate); + if (cli && SettingsStore.getValue("feature_state_counters")) { + cli.removeListener("RoomState.events", this.rateLimitedUpdate); } } @@ -96,30 +99,16 @@ export default class AuxPanel extends React.Component { } } - onConferenceNotificationClick = (ev, type) => { - dis.dispatch({ - action: 'place_call', - type: type, - room_id: this.props.room.roomId, - }); - ev.stopPropagation(); - ev.preventDefault(); - }; - - _rateLimitedUpdate = new RateLimitedFunc(() => { - if (SettingsStore.getValue("feature_state_counters")) { - this.setState({counters: this._computeCounters()}); - } + private rateLimitedUpdate = new RateLimitedFunc(() => { + this.setState({ counters: this.computeCounters() }); }, 500); - _computeCounters() { + private computeCounters() { const counters = []; if (this.props.room && SettingsStore.getValue("feature_state_counters")) { const stateEvs = this.props.room.currentState.getStateEvents('re.jki.counter'); - stateEvs.sort((a, b) => { - return a.getStateKey() < b.getStateKey(); - }); + stateEvs.sort((a, b) => lexicographicCompare(a.getStateKey(), b.getStateKey())); for (const ev of stateEvs) { const title = ev.getContent().title; @@ -225,7 +214,7 @@ export default class AuxPanel extends React.Component { } return ( - + { stateViews } { appsDrawer } { callView } diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index e83f066bd0..981ff1b4ae 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -16,38 +16,39 @@ limitations under the License. */ import classNames from 'classnames'; -import React, {createRef, ClipboardEvent} from 'react'; -import {Room} from 'matrix-js-sdk/src/models/room'; +import React, { createRef, ClipboardEvent } from 'react'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import EMOTICON_REGEX from 'emojibase-regex/emoticon'; import EditorModel from '../../../editor/model'; import HistoryManager from '../../../editor/history'; -import {Caret, setSelection} from '../../../editor/caret'; +import { Caret, setSelection } from '../../../editor/caret'; import { formatRangeAsQuote, formatRangeAsCode, toggleInlineFormat, replaceRangeAndMoveCaret, } from '../../../editor/operations'; -import {getCaretOffsetAndText, getRangeForSelection} from '../../../editor/dom'; -import Autocomplete, {generateCompletionDomId} from '../rooms/Autocomplete'; -import {getAutoCompleteCreator} from '../../../editor/parts'; -import {parsePlainTextMessage} from '../../../editor/deserialize'; -import {renderModel} from '../../../editor/render'; +import { getCaretOffsetAndText, getRangeForSelection } from '../../../editor/dom'; +import Autocomplete, { generateCompletionDomId } from '../rooms/Autocomplete'; +import { getAutoCompleteCreator } from '../../../editor/parts'; +import { parseEvent, parsePlainTextMessage } from '../../../editor/deserialize'; +import { renderModel } from '../../../editor/render'; import TypingStore from "../../../stores/TypingStore"; import SettingsStore from "../../../settings/SettingsStore"; -import {Key} from "../../../Keyboard"; -import {EMOTICON_TO_EMOJI} from "../../../emoji"; -import {CommandCategories, CommandMap, parseCommandString} from "../../../SlashCommands"; +import { Key } from "../../../Keyboard"; +import { EMOTICON_TO_EMOJI } from "../../../emoji"; +import { CommandCategories, CommandMap, parseCommandString } from "../../../SlashCommands"; import Range from "../../../editor/range"; import MessageComposerFormatBar from "./MessageComposerFormatBar"; import DocumentOffset from "../../../editor/offset"; -import {IDiff} from "../../../editor/diff"; +import { IDiff } from "../../../editor/diff"; import AutocompleteWrapperModel from "../../../editor/autocomplete"; import DocumentPosition from "../../../editor/position"; -import {ICompletion} from "../../../autocomplete/Autocompleter"; +import { ICompletion } from "../../../autocomplete/Autocompleter"; import { AutocompleteAction, getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; // matches emoticons which follow the start of a line or whitespace const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$'); @@ -716,4 +717,48 @@ export default class BasicMessageEditor extends React.Component focus() { this.editorRef.current.focus(); } + + public insertMention(userId: string) { + const {model} = this.props; + const {partCreator} = model; + const member = this.props.room.getMember(userId); + const displayName = member ? + member.rawDisplayName : userId; + const caret = this.getCaret(); + const position = model.positionForOffset(caret.offset, caret.atNodeEnd); + // Insert suffix only if the caret is at the start of the composer + const parts = partCreator.createMentionParts(caret.offset === 0, displayName, userId); + model.transform(() => { + const addedLen = model.insert(parts, position); + return model.positionForOffset(caret.offset + addedLen, true); + }); + // refocus on composer, as we just clicked "Mention" + this.focus(); + } + + public insertQuotedMessage(event: MatrixEvent) { + const {model} = this.props; + const {partCreator} = model; + const quoteParts = parseEvent(event, partCreator, {isQuotedMessage: true}); + // add two newlines + quoteParts.push(partCreator.newline()); + quoteParts.push(partCreator.newline()); + model.transform(() => { + const addedLen = model.insert(quoteParts, model.positionForOffset(0)); + return model.positionForOffset(addedLen, true); + }); + // refocus on composer, as we just clicked "Quote" + this.focus(); + } + + public insertPlaintext(text: string) { + const {model} = this.props; + const {partCreator} = model; + const caret = this.getCaret(); + const position = model.positionForOffset(caret.offset, caret.atNodeEnd); + model.transform(() => { + const addedLen = model.insert([partCreator.plain(text)], position); + return model.positionForOffset(caret.offset + addedLen, true); + }); + } } diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index f0980af7ae..914f08eac7 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -16,25 +16,25 @@ limitations under the License. */ import React from 'react'; import * as sdk from '../../../index'; -import {_t, _td} from '../../../languageHandler'; +import { _t, _td } from '../../../languageHandler'; import PropTypes from 'prop-types'; import dis from '../../../dispatcher/dispatcher'; import EditorModel from '../../../editor/model'; -import {getCaretOffsetAndText} from '../../../editor/dom'; -import {htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand} from '../../../editor/serialize'; -import {findEditableEvent} from '../../../utils/EventUtils'; -import {parseEvent} from '../../../editor/deserialize'; -import {CommandPartCreator} from '../../../editor/parts'; +import { getCaretOffsetAndText } from '../../../editor/dom'; +import { htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand } from '../../../editor/serialize'; +import { findEditableEvent } from '../../../utils/EventUtils'; +import { parseEvent } from '../../../editor/deserialize'; +import { CommandPartCreator } from '../../../editor/parts'; import EditorStateTransfer from '../../../utils/EditorStateTransfer'; import classNames from 'classnames'; -import {EventStatus} from 'matrix-js-sdk/src/models/event'; +import { EventStatus } from 'matrix-js-sdk/src/models/event'; import BasicMessageComposer from "./BasicMessageComposer"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import {CommandCategories, getCommand} from '../../../SlashCommands'; -import {Action} from "../../../dispatcher/actions"; +import { CommandCategories, getCommand } from '../../../SlashCommands'; +import { Action } from "../../../dispatcher/actions"; import CountlyAnalytics from "../../../CountlyAnalytics"; -import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager'; +import { replaceableComponent } from "../../../utils/replaceableComponent"; import SendHistoryManager from '../../../SendHistoryManager'; import Modal from '../../../Modal'; @@ -124,6 +124,7 @@ export default class EditMessageComposer extends React.Component { }; this._createEditorModel(); window.addEventListener("beforeunload", this._saveStoredEditorState); + this.dispatcherRef = dis.register(this.onAction); } _setEditorRef = ref => { @@ -399,6 +400,7 @@ export default class EditMessageComposer extends React.Component { if (this._shouldSaveStoredEditorState) { this._saveStoredEditorState(); } + dis.unregister(this.dispatcherRef); } _createEditorModel() { @@ -443,6 +445,18 @@ export default class EditMessageComposer extends React.Component { }); }; + onAction = payload => { + if (payload.action === "edit_composer_insert" && this._editorRef) { + if (payload.userId) { + this._editorRef.insertMention(payload.userId); + } else if (payload.event) { + this._editorRef.insertQuotedMessage(payload.event); + } else if (payload.text) { + this._editorRef.insertPlaintext(payload.text); + } + } + }; + render() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); return (
    diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 85b9cac2c4..0099bf73fb 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -46,6 +46,8 @@ import { EditorStateTransfer } from "../../../utils/EditorStateTransfer"; import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState"; import NotificationBadge from "./NotificationBadge"; +import {ComposerInsertPayload} from "../../../dispatcher/payloads/ComposerInsertPayload"; +import { Action } from '../../../dispatcher/actions'; const eventTileTypes = { [EventType.RoomMessage]: 'messages.MessageEvent', @@ -376,7 +378,7 @@ export default class EventTile extends React.Component { EventType.RoomMessage, EventType.RoomMessageEncrypted, ]; - if (!simpleSendableEvents.includes(this.props.mxEvent.getType())) return false; + if (!simpleSendableEvents.includes(this.props.mxEvent.getType() as EventType)) return false; // Default case return true; @@ -727,9 +729,9 @@ export default class EventTile extends React.Component { onSenderProfileClick = event => { const mxEvent = this.props.mxEvent; - dis.dispatch({ - action: 'insert_mention', - user_id: mxEvent.getSender(), + dis.dispatch({ + action: Action.ComposerInsert, + userId: mxEvent.getSender(), }); }; diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 3671069903..f7d562fca0 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -16,11 +16,11 @@ limitations under the License. import React from 'react'; import classNames from 'classnames'; import { _t } from '../../../languageHandler'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; import * as sdk from '../../../index'; -import {MatrixEvent} from "matrix-js-sdk/src/models/event"; -import {Room} from "matrix-js-sdk/src/models/room"; -import {RoomMember} from "matrix-js-sdk/src/models/room-member"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import dis from '../../../dispatcher/dispatcher'; import { ActionPayload } from "../../../dispatcher/payloads"; import Stickerpicker from './Stickerpicker'; @@ -28,19 +28,21 @@ import { makeRoomPermalink, RoomPermalinkCreator } from '../../../utils/permalin import ContentMessages from '../../../ContentMessages'; import E2EIcon from './E2EIcon'; import SettingsStore from "../../../settings/SettingsStore"; -import {aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu} from "../../structures/ContextMenu"; +import { aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import ReplyPreview from "./ReplyPreview"; -import {UIFeature} from "../../../settings/UIFeature"; -import {UPDATE_EVENT} from "../../../stores/AsyncStore"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { UIFeature } from "../../../settings/UIFeature"; +import { UPDATE_EVENT } from "../../../stores/AsyncStore"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; import VoiceRecordComposerTile from "./VoiceRecordComposerTile"; -import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore"; -import {RecordingState} from "../../../voice/VoiceRecording"; -import Tooltip, {Alignment} from "../elements/Tooltip"; +import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore"; +import { RecordingState } from "../../../voice/VoiceRecording"; +import Tooltip, { Alignment } from "../elements/Tooltip"; import ResizeNotifier from "../../../utils/ResizeNotifier"; import { E2EStatus } from '../../../utils/ShieldUtils'; import SendMessageComposer from "./SendMessageComposer"; +import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; +import { Action } from "../../../dispatcher/actions"; interface IComposerAvatarProps { me: object; @@ -316,10 +318,10 @@ export default class MessageComposer extends React.Component { } } - addEmoji(emoji) { - dis.dispatch({ - action: "insert_emoji", - emoji, + addEmoji(emoji: string) { + dis.dispatch({ + action: Action.ComposerInsert, + text: emoji, }); } diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx index 3bf9a9db33..cae86846d9 100644 --- a/src/components/views/rooms/NewRoomIntro.tsx +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -14,30 +14,31 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useContext} from "react"; -import {EventType} from "matrix-js-sdk/src/@types/event"; +import React, { useContext } from "react"; +import { EventType } from "matrix-js-sdk/src/@types/event"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { User } from "matrix-js-sdk/src/models/user"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import RoomContext from "../../../contexts/RoomContext"; import DMRoomMap from "../../../utils/DMRoomMap"; -import {_t} from "../../../languageHandler"; +import { _t } from "../../../languageHandler"; import AccessibleButton from "../elements/AccessibleButton"; -import MiniAvatarUploader, {AVATAR_SIZE} from "../elements/MiniAvatarUploader"; +import MiniAvatarUploader, { AVATAR_SIZE } from "../elements/MiniAvatarUploader"; import RoomAvatar from "../avatars/RoomAvatar"; import defaultDispatcher from "../../../dispatcher/dispatcher"; -import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload"; -import {Action} from "../../../dispatcher/actions"; +import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload"; +import { Action } from "../../../dispatcher/actions"; import dis from "../../../dispatcher/dispatcher"; import SpaceStore from "../../../stores/SpaceStore"; -import {showSpaceInvite} from "../../../utils/space"; - +import { showSpaceInvite } from "../../../utils/space"; import { privateShouldBeEncrypted } from "../../../createRoom"; - import EventTileBubble from "../messages/EventTileBubble"; import { ROOM_SECURITY_TAB } from "../dialogs/RoomSettingsDialog"; -function hasExpectedEncryptionSettings(room): boolean { - const isEncrypted: boolean = room._client?.isRoomEncrypted(room.roomId); +function hasExpectedEncryptionSettings(matrixClient: MatrixClient, room: Room): boolean { + const isEncrypted: boolean = matrixClient.isRoomEncrypted(room.roomId); const isPublic: boolean = room.getJoinRule() === "public"; return isPublic || !privateShouldBeEncrypted() || isEncrypted; } @@ -61,7 +62,7 @@ const NewRoomIntro = () => { defaultDispatcher.dispatch({ action: Action.ViewUser, // XXX: We should be using a real member object and not assuming what the receiver wants. - member: member || {userId: dmPartner}, + member: member || { userId: dmPartner } as User, }); }} /> @@ -194,7 +195,7 @@ const NewRoomIntro = () => { return
    - { !hasExpectedEncryptionSettings(room) && ( + { !hasExpectedEncryptionSettings(cli, room) && ( { }; private onLocalEchoUpdated = (ev: MatrixEvent, room: Room) => { - if (!room?.roomId === this.props.room.roomId) return; + if (room?.roomId !== this.props.room.roomId) return; this.setState({hasUnsentEvents: this.countUnsentEvents() > 0}); }; @@ -316,7 +316,7 @@ export default class RoomTile extends React.PureComponent { 0, )); } else { - console.warn(`Unexpected tag ${tagId} applied to ${this.props.room.room_id}`); + console.warn(`Unexpected tag ${tagId} applied to ${this.props.room.roomId}`); } if ((ev as React.KeyboardEvent).key === Key.ENTER) { diff --git a/src/components/views/rooms/RoomUpgradeWarningBar.js b/src/components/views/rooms/RoomUpgradeWarningBar.js index a2d4f92d35..66e76903eb 100644 --- a/src/components/views/rooms/RoomUpgradeWarningBar.js +++ b/src/components/views/rooms/RoomUpgradeWarningBar.js @@ -24,7 +24,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {replaceableComponent} from "../../../utils/replaceableComponent"; @replaceableComponent("views.rooms.RoomUpgradeWarningBar") -export default class RoomUpgradeWarningBar extends React.Component { +export default class RoomUpgradeWarningBar extends React.PureComponent { static propTypes = { room: PropTypes.object.isRequired, recommendation: PropTypes.object.isRequired, diff --git a/src/components/views/rooms/SearchBar.js b/src/components/views/rooms/SearchBar.js deleted file mode 100644 index 029516c932..0000000000 --- a/src/components/views/rooms/SearchBar.js +++ /dev/null @@ -1,99 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React, {createRef} from 'react'; -import AccessibleButton from "../elements/AccessibleButton"; -import classNames from "classnames"; -import { _t } from '../../../languageHandler'; -import {Key} from "../../../Keyboard"; -import DesktopBuildsNotice, {WarningKind} from "../elements/DesktopBuildsNotice"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; - -@replaceableComponent("views.rooms.SearchBar") -export default class SearchBar extends React.Component { - constructor(props) { - super(props); - - this._search_term = createRef(); - - this.state = { - scope: 'Room', - }; - } - - onThisRoomClick = () => { - this.setState({ scope: 'Room' }, () => this._searchIfQuery()); - }; - - onAllRoomsClick = () => { - this.setState({ scope: 'All' }, () => this._searchIfQuery()); - }; - - onSearchChange = (e) => { - switch (e.key) { - case Key.ENTER: - this.onSearch(); - break; - case Key.ESCAPE: - this.props.onCancelClick(); - break; - } - }; - - _searchIfQuery() { - if (this._search_term.current.value) { - this.onSearch(); - } - } - - onSearch = () => { - this.props.onSearch(this._search_term.current.value, this.state.scope); - }; - - render() { - const searchButtonClasses = classNames("mx_SearchBar_searchButton", { - mx_SearchBar_searching: this.props.searchInProgress, - }); - const thisRoomClasses = classNames("mx_SearchBar_button", { - mx_SearchBar_unselected: this.state.scope !== 'Room', - }); - const allRoomsClasses = classNames("mx_SearchBar_button", { - mx_SearchBar_unselected: this.state.scope !== 'All', - }); - - return ( - <> -
    -
    - - {_t("This Room")} - - - {_t("All Rooms")} - -
    -
    - - -
    - -
    - - - ); - } -} diff --git a/src/components/views/rooms/SearchBar.tsx b/src/components/views/rooms/SearchBar.tsx new file mode 100644 index 0000000000..d71bb8da73 --- /dev/null +++ b/src/components/views/rooms/SearchBar.tsx @@ -0,0 +1,130 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { createRef, RefObject } from 'react'; +import AccessibleButton from "../elements/AccessibleButton"; +import classNames from "classnames"; +import { _t } from '../../../languageHandler'; +import { Key } from "../../../Keyboard"; +import DesktopBuildsNotice, { WarningKind } from "../elements/DesktopBuildsNotice"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; + +interface IProps { + onCancelClick: () => void; + onSearch: (query: string, scope: string) => void; + searchInProgress?: boolean; + isRoomEncrypted?: boolean; +} + +interface IState { + scope: SearchScope; +} + +export enum SearchScope { + Room = "Room", + All = "All", +} + +@replaceableComponent("views.rooms.SearchBar") +export default class SearchBar extends React.Component { + private searchTerm: RefObject = createRef(); + + constructor(props: IProps) { + super(props); + this.state = { + scope: SearchScope.Room, + }; + } + + private onThisRoomClick = () => { + this.setState({ scope: SearchScope.Room }, () => this.searchIfQuery()); + }; + + private onAllRoomsClick = () => { + this.setState({ scope: SearchScope.All }, () => this.searchIfQuery()); + }; + + private onSearchChange = (e: React.KeyboardEvent) => { + switch (e.key) { + case Key.ENTER: + this.onSearch(); + break; + case Key.ESCAPE: + this.props.onCancelClick(); + break; + } + }; + + private searchIfQuery(): void { + if (this.searchTerm.current.value) { + this.onSearch(); + } + } + + private onSearch = (): void => { + this.props.onSearch(this.searchTerm.current.value, this.state.scope); + }; + + public render() { + const searchButtonClasses = classNames("mx_SearchBar_searchButton", { + mx_SearchBar_searching: this.props.searchInProgress, + }); + const thisRoomClasses = classNames("mx_SearchBar_button", { + mx_SearchBar_unselected: this.state.scope !== SearchScope.Room, + }); + const allRoomsClasses = classNames("mx_SearchBar_button", { + mx_SearchBar_unselected: this.state.scope !== SearchScope.All, + }); + + return ( + <> +
    +
    + + {_t("This Room")} + + + {_t("All Rooms")} + +
    +
    + + +
    + +
    + + + ); + } +} diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index d60eba00ec..10ef91c689 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -27,27 +27,26 @@ import { startsWith, stripPrefix, } from '../../../editor/serialize'; -import {CommandPartCreator} from '../../../editor/parts'; +import { CommandPartCreator } from '../../../editor/parts'; import BasicMessageComposer from "./BasicMessageComposer"; import ReplyThread from "../elements/ReplyThread"; -import {parseEvent} from '../../../editor/deserialize'; -import {findEditableEvent} from '../../../utils/EventUtils'; +import { findEditableEvent } from '../../../utils/EventUtils'; import SendHistoryManager from "../../../SendHistoryManager"; -import {CommandCategories, getCommand} from '../../../SlashCommands'; +import { CommandCategories, getCommand } from '../../../SlashCommands'; import * as sdk from '../../../index'; import Modal from '../../../Modal'; -import {_t, _td} from '../../../languageHandler'; +import { _t, _td } from '../../../languageHandler'; import ContentMessages from '../../../ContentMessages'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import RateLimitedFunc from '../../../ratelimitedfunc'; -import {Action} from "../../../dispatcher/actions"; -import {containsEmoji} from "../../../effects/utils"; -import {CHAT_EFFECTS} from '../../../effects'; +import { Action } from "../../../dispatcher/actions"; +import { containsEmoji } from "../../../effects/utils"; +import { CHAT_EFFECTS } from '../../../effects'; import CountlyAnalytics from "../../../CountlyAnalytics"; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; import EMOJI_REGEX from 'emojibase-regex'; -import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager'; +import { replaceableComponent } from "../../../utils/replaceableComponent"; import SettingsStore from '../../../settings/SettingsStore'; function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { @@ -486,62 +485,18 @@ export default class SendMessageComposer extends React.Component { case Action.FocusComposer: this._editorRef && this._editorRef.focus(); break; - case 'insert_mention': - this._insertMention(payload.user_id); - break; - case 'quote': - this._insertQuotedMessage(payload.event); - break; - case 'insert_emoji': - this._insertEmoji(payload.emoji); + case "send_composer_insert": + if (payload.userId) { + this._editorRef && this._editorRef.insertMention(payload.userId); + } else if (payload.event) { + this._editorRef && this._editorRef.insertQuotedMessage(payload.event); + } else if (payload.text) { + this._editorRef && this._editorRef.insertPlaintext(payload.text); + } break; } }; - _insertMention(userId) { - const {model} = this; - const {partCreator} = model; - const member = this.props.room.getMember(userId); - const displayName = member ? - member.rawDisplayName : userId; - const caret = this._editorRef.getCaret(); - const position = model.positionForOffset(caret.offset, caret.atNodeEnd); - // Insert suffix only if the caret is at the start of the composer - const parts = partCreator.createMentionParts(caret.offset === 0, displayName, userId); - model.transform(() => { - const addedLen = model.insert(parts, position); - return model.positionForOffset(caret.offset + addedLen, true); - }); - // refocus on composer, as we just clicked "Mention" - this._editorRef && this._editorRef.focus(); - } - - _insertQuotedMessage(event) { - const {model} = this; - const {partCreator} = model; - const quoteParts = parseEvent(event, partCreator, {isQuotedMessage: true}); - // add two newlines - quoteParts.push(partCreator.newline()); - quoteParts.push(partCreator.newline()); - model.transform(() => { - const addedLen = model.insert(quoteParts, model.positionForOffset(0)); - return model.positionForOffset(addedLen, true); - }); - // refocus on composer, as we just clicked "Quote" - this._editorRef && this._editorRef.focus(); - } - - _insertEmoji = (emoji) => { - const {model} = this; - const {partCreator} = model; - const caret = this._editorRef.getCaret(); - const position = model.positionForOffset(caret.offset, caret.atNodeEnd); - model.transform(() => { - const addedLen = model.insert([partCreator.plain(emoji)], position); - return model.positionForOffset(caret.offset + addedLen, true); - }); - }; - _onPaste = (event) => { const {clipboardData} = event; // Prioritize text on the clipboard over files as Office on macOS puts a bitmap diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index 2102071bf3..20d8c9c5d4 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -77,7 +77,8 @@ export default class VoiceRecordComposerTile extends React.PureComponent Math.round(v * 1024)), }, - "org.matrix.msc2516.voice": {}, // No content, this is a rendering hint + "org.matrix.msc3245.voice": {}, // No content, this is a rendering hint }); await this.disposeRecording(); } diff --git a/src/components/views/rooms/WhoIsTypingTile.tsx b/src/components/views/rooms/WhoIsTypingTile.tsx index 3a1d2051b4..93078ff645 100644 --- a/src/components/views/rooms/WhoIsTypingTile.tsx +++ b/src/components/views/rooms/WhoIsTypingTile.tsx @@ -16,7 +16,7 @@ limitations under the License. */ import React from 'react'; -import Room from "matrix-js-sdk/src/models/room"; +import { Room } from "matrix-js-sdk/src/models/room"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; diff --git a/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx b/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx index 8d886a191e..25dfe2f151 100644 --- a/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/BridgeSettingsTab.tsx @@ -44,14 +44,11 @@ export default class BridgeSettingsTab extends React.Component { return ; } - static getBridgeStateEvents(roomId: string) { + static getBridgeStateEvents(roomId: string): MatrixEvent[] { const client = MatrixClientPeg.get(); const roomState = client.getRoom(roomId).currentState; - return BRIDGE_EVENT_TYPES.map(typeName => { - const events = roomState.events.get(typeName); - return events ? Array.from(events.values()) : []; - }).flat(1); + return BRIDGE_EVENT_TYPES.map(typeName => roomState.getStateEvents(typeName)).flat(1); } render() { diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index 3fa0be478c..beff033001 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -32,7 +32,7 @@ import * as ContextMenu from "../../../../structures/ContextMenu"; import { toRightOf } from "../../../../structures/ContextMenu"; interface IProps { - closeSettingsFn: () => {}; + closeSettingsFn: () => void; } interface IState { diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index 6a935ab276..977cd4a9aa 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -29,7 +29,7 @@ import AccessibleButton from "../elements/AccessibleButton"; import {BetaPill} from "../beta/BetaCard"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import {Action} from "../../../dispatcher/actions"; -import {USER_LABS_TAB} from "../dialogs/UserSettingsDialog"; +import { UserTab } from "../dialogs/UserSettingsDialog"; import Field from "../elements/Field"; import withValidation from "../elements/Validation"; import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView"; @@ -224,7 +224,7 @@ const SpaceCreateMenu = ({ onFinished }) => { onFinished(); defaultDispatcher.dispatch({ action: Action.ViewUserSettings, - initialTabId: USER_LABS_TAB, + initialTabId: UserTab.Labs, }); }} /> { body } diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index 2e3bfd157a..b33fcf8915 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -27,6 +27,7 @@ import { SpaceItem } from "./SpaceTreeLevel"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { useEventEmitter } from "../../../hooks/useEventEmitter"; import SpaceStore, { + HOME_SPACE, UPDATE_INVITED_SPACES, UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES, @@ -41,6 +42,7 @@ import { import { Key } from "../../../Keyboard"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { NotificationState } from "../../../stores/notifications/NotificationState"; +import SettingsStore from "../../../settings/SettingsStore"; interface IButtonProps { space?: Room; @@ -132,13 +134,16 @@ const InnerSpacePanel = React.memo(({ children, isPanelCo const [invites, spaces, activeSpace] = useSpaces(); const activeSpaces = activeSpace ? [activeSpace] : []; + const homeNotificationState = SettingsStore.getValue("feature_spaces.all_rooms") + ? RoomNotificationStateStore.instance.globalState : SpaceStore.instance.getNotificationState(HOME_SPACE); + return
    SpaceStore.instance.setActiveSpace(null)} selected={!activeSpace} - tooltip={_t("All rooms")} - notificationState={RoomNotificationStateStore.instance.globalState} + tooltip={SettingsStore.getValue("feature_spaces.all_rooms") ? _t("All rooms") : _t("Home")} + notificationState={homeNotificationState} isNarrow={isPanelCollapsed} /> { invites.map(s => ( diff --git a/src/components/views/toasts/GenericToast.tsx b/src/components/views/toasts/GenericToast.tsx index 209babbf9d..45b65ae1fb 100644 --- a/src/components/views/toasts/GenericToast.tsx +++ b/src/components/views/toasts/GenericToast.tsx @@ -15,8 +15,8 @@ limitations under the License. */ import React, {ReactNode} from "react"; +import AccessibleButton from "../elements/AccessibleButton"; -import FormButton from "../elements/FormButton"; import {XOR} from "../../../@types/common"; export interface IProps { @@ -50,8 +50,12 @@ const GenericToast: React.FC> = ({ {detailContent}
    - {onReject && rejectLabel && } - + {onReject && rejectLabel && + { rejectLabel } + } + + { acceptLabel } +
    ; }; diff --git a/src/components/views/voip/IncomingCallBox.tsx b/src/components/views/voip/IncomingCallBox.tsx index 2abdc0641d..c09043da24 100644 --- a/src/components/views/voip/IncomingCallBox.tsx +++ b/src/components/views/voip/IncomingCallBox.tsx @@ -21,17 +21,20 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg'; import dis from '../../../dispatcher/dispatcher'; import { _t } from '../../../languageHandler'; import { ActionPayload } from '../../../dispatcher/payloads'; -import CallHandler from '../../../CallHandler'; +import CallHandler, { AudioID } from '../../../CallHandler'; import RoomAvatar from '../avatars/RoomAvatar'; -import FormButton from '../elements/FormButton'; +import AccessibleButton from '../elements/AccessibleButton'; import { CallState } from 'matrix-js-sdk/src/webrtc/call'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import AccessibleTooltipButton from '../elements/AccessibleTooltipButton'; +import classNames from 'classnames'; interface IProps { } interface IState { incomingCall: any; + silenced: boolean; } @replaceableComponent("views.voip.IncomingCallBox") @@ -44,6 +47,7 @@ export default class IncomingCallBox extends React.Component { this.dispatcherRef = dis.register(this.onAction); this.state = { incomingCall: null, + silenced: false, }; } @@ -58,6 +62,7 @@ export default class IncomingCallBox extends React.Component { if (call && call.state === CallState.Ringing) { this.setState({ incomingCall: call, + silenced: false, // Reset silenced state for new call }); } else { this.setState({ @@ -84,6 +89,13 @@ export default class IncomingCallBox extends React.Component { }); }; + private onSilenceClick: React.MouseEventHandler = (e) => { + e.stopPropagation(); + const newState = !this.state.silenced + this.setState({silenced: newState}); + newState ? CallHandler.sharedInstance().pause(AudioID.Ring) : CallHandler.sharedInstance().play(AudioID.Ring); + } + public render() { if (!this.state.incomingCall) { return null; @@ -107,6 +119,12 @@ export default class IncomingCallBox extends React.Component { } } + const silenceClass = classNames({ + "mx_IncomingCallBox_iconButton": true, + "mx_IncomingCallBox_unSilence": this.state.silenced, + "mx_IncomingCallBox_silence": !this.state.silenced, + }); + return
    {

    {caller}

    {incomingCallText}

    +
    - + > + { _t("Decline") } +
    - + > + { _t("Accept") } +
    ; } } - diff --git a/src/customisations/Media.ts b/src/customisations/Media.ts index f9d957b60c..37e91fc54b 100644 --- a/src/customisations/Media.ts +++ b/src/customisations/Media.ts @@ -14,10 +14,11 @@ * limitations under the License. */ -import {MatrixClientPeg} from "../MatrixClientPeg"; -import {IMediaEventContent, IPreparedMedia, prepEventContentAsMedia} from "./models/IMediaEventContent"; -import {ResizeMethod} from "../Avatar"; -import {MatrixClient} from "matrix-js-sdk/src/client"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { ResizeMethod } from "matrix-js-sdk/src/@types/partials"; + +import { MatrixClientPeg } from "../MatrixClientPeg"; +import { IMediaEventContent, IPreparedMedia, prepEventContentAsMedia } from "./models/IMediaEventContent"; // Populate this class with the details of your customisations when copying it. diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 300eed2b98..6438ecc0f2 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -159,4 +159,14 @@ export enum Action { * Fired when joining a room failed */ JoinRoomError = "join_room_error", + + /** + * Inserts content into the active composer. Should be used with ComposerInsertPayload + */ + ComposerInsert = "composer_insert", + + /** + * Switches space. Should be used with SwitchSpacePayload. + */ + SwitchSpace = "switch_space", } diff --git a/src/dispatcher/payloads/ComposerInsertPayload.ts b/src/dispatcher/payloads/ComposerInsertPayload.ts new file mode 100644 index 0000000000..9702855432 --- /dev/null +++ b/src/dispatcher/payloads/ComposerInsertPayload.ts @@ -0,0 +1,42 @@ +/* +Copyright 2021 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 { MatrixEvent } from "matrix-js-sdk/src/models/event"; + +import { ActionPayload } from "../payloads"; +import { Action } from "../actions"; + +interface IBaseComposerInsertPayload extends ActionPayload { + action: Action.ComposerInsert, +} + +interface IComposerInsertMentionPayload extends IBaseComposerInsertPayload { + userId: string; +} + +interface IComposerInsertQuotePayload extends IBaseComposerInsertPayload { + event: MatrixEvent; +} + +interface IComposerInsertPlaintextPayload extends IBaseComposerInsertPayload { + text: string; +} + +export type ComposerInsertPayload = + IComposerInsertMentionPayload | + IComposerInsertQuotePayload | + IComposerInsertPlaintextPayload; + diff --git a/src/dispatcher/payloads/SetRightPanelPhasePayload.ts b/src/dispatcher/payloads/SetRightPanelPhasePayload.ts index 430fad6145..a4dcb8cfe1 100644 --- a/src/dispatcher/payloads/SetRightPanelPhasePayload.ts +++ b/src/dispatcher/payloads/SetRightPanelPhasePayload.ts @@ -17,6 +17,7 @@ limitations under the License. import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import { Room } from "matrix-js-sdk/src/models/room"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { User } from "matrix-js-sdk/src/models/user"; import { RightPanelPhases } from "../../stores/RightPanelStorePhases"; import { ActionPayload } from "../payloads"; import { Action } from "../actions"; @@ -29,7 +30,7 @@ export interface SetRightPanelPhasePayload extends ActionPayload { } export interface SetRightPanelPhaseRefireParams { - member?: RoomMember; + member?: RoomMember | User; verificationRequest?: VerificationRequest; groupId?: string; groupRoomId?: string; diff --git a/src/dispatcher/payloads/SwitchSpacePayload.ts b/src/dispatcher/payloads/SwitchSpacePayload.ts new file mode 100644 index 0000000000..04eb744334 --- /dev/null +++ b/src/dispatcher/payloads/SwitchSpacePayload.ts @@ -0,0 +1,27 @@ +/* +Copyright 2021 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 { ActionPayload } from "../payloads"; +import { Action } from "../actions"; + +export interface SwitchSpacePayload extends ActionPayload { + action: Action.SwitchSpace; + + /** + * The number of the space to switch to, 1-indexed, 0 is Home. + */ + num: number; +} diff --git a/src/dispatcher/payloads/ViewUserPayload.ts b/src/dispatcher/payloads/ViewUserPayload.ts index c2838d0dbb..c4d73aea6a 100644 --- a/src/dispatcher/payloads/ViewUserPayload.ts +++ b/src/dispatcher/payloads/ViewUserPayload.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { User } from "matrix-js-sdk/src/models/user"; import { ActionPayload } from "../payloads"; import { Action } from "../actions"; @@ -25,5 +26,5 @@ export interface ViewUserPayload extends ActionPayload { * The member to view. May be null or falsy to indicate that no member * should be shown (hide whichever relevant components). */ - member?: RoomMember; + member?: RoomMember | User; } diff --git a/src/editor/model.ts b/src/editor/model.ts index f1b6f90957..7925a43164 100644 --- a/src/editor/model.ts +++ b/src/editor/model.ts @@ -15,13 +15,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {diffAtCaret, diffDeletion, IDiff} from "./diff"; -import DocumentPosition, {IPosition} from "./position"; +import { diffAtCaret, diffDeletion, IDiff } from "./diff"; +import DocumentPosition, { IPosition } from "./position"; import Range from "./range"; -import {SerializedPart, Part, PartCreator} from "./parts"; -import AutocompleteWrapperModel, {ICallback} from "./autocomplete"; +import { SerializedPart, Part, PartCreator } from "./parts"; +import AutocompleteWrapperModel, { ICallback } from "./autocomplete"; import DocumentOffset from "./offset"; -import {Caret} from "./caret"; +import { Caret } from "./caret"; /** * @callback ModelCallback @@ -390,7 +390,7 @@ export default class EditorModel { return addLen; } - positionForOffset(totalOffset: number, atPartEnd: boolean) { + positionForOffset(totalOffset: number, atPartEnd = false) { let currentOffset = 0; const index = this._parts.findIndex(part => { const partLen = part.text.length; diff --git a/src/hooks/useAccountData.ts b/src/hooks/useAccountData.ts index fe71ed9ecd..0384b3bf77 100644 --- a/src/hooks/useAccountData.ts +++ b/src/hooks/useAccountData.ts @@ -21,11 +21,11 @@ import {Room} from "matrix-js-sdk/src/models/room"; import {useEventEmitter} from "./useEventEmitter"; -const tryGetContent = (ev?: MatrixEvent) => ev ? ev.getContent() : undefined; +const tryGetContent = (ev?: MatrixEvent) => ev ? ev.getContent() : undefined; // Hook to simplify listening to Matrix account data export const useAccountData = (cli: MatrixClient, eventType: string) => { - const [value, setValue] = useState(() => tryGetContent(cli.getAccountData(eventType))); + const [value, setValue] = useState(() => tryGetContent(cli.getAccountData(eventType))); const handler = useCallback((event) => { if (event.getType() !== eventType) return; @@ -38,7 +38,7 @@ export const useAccountData = (cli: MatrixClient, eventType: strin // Hook to simplify listening to Matrix room account data export const useRoomAccountData = (room: Room, eventType: string) => { - const [value, setValue] = useState(() => tryGetContent(room.getAccountData(eventType))); + const [value, setValue] = useState(() => tryGetContent(room.getAccountData(eventType))); const handler = useCallback((event) => { if (event.getType() !== eventType) return; diff --git a/src/i18n/strings/ca.json b/src/i18n/strings/ca.json index 9a9e0efaa7..945b5a10cc 100644 --- a/src/i18n/strings/ca.json +++ b/src/i18n/strings/ca.json @@ -78,13 +78,13 @@ "Invite new community members": "Convida nous membres a la comunitat", "Invite to Community": "Convida a la comunitat", "Which rooms would you like to add to this community?": "Quines sales vols afegir a aquesta comunitat?", - "Show these rooms to non-members on the community page and room list?": "Vols mostrar aquestes sales a la pàgina de la comunitat i a la llista de sales per als que no hi son membres?", + "Show these rooms to non-members on the community page and room list?": "Voleu mostrar aquestes sales a la pàgina de la comunitat i al llistat de sales, als qui no en siguin membres?", "Add rooms to the community": "Afegeix sales a la comunitat", "Add to community": "Afegeix a la comunitat", "Failed to invite the following users to %(groupId)s:": "No s'han pogut convidar a %(groupId)s els següents usuaris:", - "Failed to invite users to community": "No s'han pogut convidar els usuaris a la comunitat", + "Failed to invite users to community": "No s'ha pogut convidar els usuaris a la comunitat", "Failed to invite users to %(groupId)s": "No s'han pogut convidar els usuaris a %(groupId)s", - "Failed to add the following rooms to %(groupId)s:": "No s'han pogut afegir a %(groupId)s les següents sales:", + "Failed to add the following rooms to %(groupId)s:": "No s'ha pogut afegir a %(groupId)s les següents sales:", "%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s no té permís per enviar-te notificacions, comprova la configuració del teu navegador", "%(brand)s was not given permission to send notifications - please try again": "%(brand)s no ha rebut cap permís per enviar notificacions, torna-ho a provar", "Unable to enable Notifications": "No s'han pogut activar les notificacions", @@ -371,23 +371,23 @@ "Communities": "Comunitats", "Home": "Inici", "Manage Integrations": "Gestiona les integracions", - "%(nameList)s %(transitionList)s": "%(transitionList)s%(nameList)s", + "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s s'hi han unit", - "%(oneUser)sjoined %(count)s times|one": "%(oneUser)s s'ha unit", + "%(oneUser)sjoined %(count)s times|one": "%(oneUser)ss'ha unit", "%(severalUsers)sleft %(count)s times|one": "%(severalUsers)s han sortit", - "%(oneUser)sleft %(count)s times|one": "%(oneUser)s ha sortit", + "%(oneUser)sleft %(count)s times|one": "%(oneUser)sha sortit", "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)s s'hi han unit i han sortit %(count)s vegades", "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)s s'hi han unit i han sortit", - "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s ha entrat i ha sortit %(count)s vegades", - "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)s ha entrat i ha sortit", + "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)sha entrat i ha sortit %(count)s vegades", + "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)sha entrat i ha sortit", "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)s han sortit i han tornat a entrar %(count)s vegades", "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)s han sortit i han tornat a entrar", - "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s ha sortit i ha tornat a entrar %(count)s vegades", - "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)s ha sortit i ha tornat a entrar", + "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)sha sortit i ha tornat a entrar %(count)s vegades", + "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)sha sortit i ha tornat a entrar", "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)s han rebutjat les seves invitacions %(count)s vegades", "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)s han rebutjat les seves invitacions", - "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)s ha rebutjat la seva invitació %(count)s vegades", - "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)s ha rebutjat la seva invitació", + "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)sha rebutjat la seva invitació %(count)s vegades", + "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)sha rebutjat la seva invitació", "%(severalUsers)shad their invitations withdrawn %(count)s times|other": "S'han retirat les invitacions de %(severalUsers)s %(count)s vegades", "%(severalUsers)shad their invitations withdrawn %(count)s times|one": "S'han retirat les invitacions de %(severalUsers)s", "%(oneUser)shad their invitation withdrawn %(count)s times|other": "S'ha retirat la invitació de %(oneUser)s %(count)s vegades", @@ -410,12 +410,12 @@ "was kicked %(count)s times|one": "l'han fet fora", "%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)s han canviat el seu nom %(count)s vegades", "%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)s han canviat el seu nom", - "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)s ha canviat el seu nom %(count)s vegades", + "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)sha canviat el seu nom %(count)s vegades", "%(oneUser)schanged their name %(count)s times|one": "%(oneUser)s ha canviat el seu nom", "%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)s han canviat el seu avatar %(count)s vegades", "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)s han canviat el seu avatar", - "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)s han canviat el seu avatar %(count)s vegades", - "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)s ha canviat el seu avatar", + "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)shan canviat el seu avatar %(count)s vegades", + "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)sha canviat el seu avatar", "%(items)s and %(count)s others|other": "%(items)s i %(count)s altres", "%(items)s and %(count)s others|one": "%(items)s i un altre", "%(items)s and %(lastItem)s": "%(items)s i %(lastItem)s", @@ -437,9 +437,9 @@ "Showing flair for these communities:": "Mostra els talents d'aquestes comunitats:", "Display your community flair in rooms configured to show it.": "Mostra els talents de la vostra comunitat dins les sales configurades per a mostrar-los.", "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)s han entrat %(count)s vegades", - "%(oneUser)sjoined %(count)s times|other": "%(oneUser)s ha entrat %(count)s vegades", + "%(oneUser)sjoined %(count)s times|other": "%(oneUser)sha entrat %(count)s vegades", "%(severalUsers)sleft %(count)s times|other": "%(severalUsers)s han sortit %(count)s vegades", - "%(oneUser)sleft %(count)s times|other": "%(oneUser)s ha sortit %(count)s vegades", + "%(oneUser)sleft %(count)s times|other": "%(oneUser)sha sortit %(count)s vegades", "Community IDs may only contain characters a-z, 0-9, or '=_-./'": "Les ID de les comunitats només poden contendre caràcters a-z, 0-9, o '=_-./'", "Community IDs cannot be empty.": "Les ID de les comunitats no poden estar buides.", "Something went wrong whilst creating your community": "S'ha produït un error mentre es creava la comunitat", @@ -722,7 +722,7 @@ "Failed to invite users to the room:": "No s'han pogut convidar els usuaris a la sala:", "Missing roomId.": "Falta l'ID de sala.", "Searches DuckDuckGo for results": "Cerca al DuckDuckGo els resultats", - "Changes your display nickname": "Canvia el teu àlies de visualització", + "Changes your display nickname": "Canvia l'àlies a mostrar", "Invites user with given id to current room": "Convida a la sala actual l'usuari amb l'ID indicat", "Kicks user with given id": "Expulsa l'usuari amb l'ID indicat", "Bans user with given id": "Bandeja l'usuari amb l'ID indicat", @@ -854,7 +854,7 @@ "Changes your avatar in all rooms": "Canvia el teu avatar en totes les sales", "Changes your avatar in this current room only": "Canvia el teu avatar només en aquesta sala actual", "Changes the avatar of the current room": "Canvia l'avatar de la sala actual", - "Changes your display nickname in the current room only": "Canvia el teu àlies de visualització només en la sala actual", + "Changes your display nickname in the current room only": "Canvia el teu àlies a mostrar només en la sala actual", "Double check that your server supports the room version chosen and try again.": "Comprova que el teu servidor és compatible amb la versió de sala que has triat i torna-ho a intentar.", "You do not have the required permissions to use this command.": "No disposes dels permisos necessaris per utilitzar aquesta ordre.", "Sends a message as html, without interpreting it as markdown": "Envia un missatge com a html sense interpretar-lo com a markdown", @@ -951,5 +951,7 @@ "Click the button below to confirm adding this email address.": "Fes clic al botó de sota per confirmar l'addició d'aquesta adreça de correu electrònic.", "Unable to access webcam / microphone": "No s'ha pogut accedir a la càmera web / micròfon", "Unable to access microphone": "No s'ha pogut accedir al micròfon", - "Explore rooms": "Explora sales" + "Explore rooms": "Explora sales", + "%(oneUser)smade no changes %(count)s times|one": "%(oneUser)sno ha fet canvis", + "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)sno ha fet canvis %(count)s cops" } diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index 75472b4d38..a84a10f21c 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -3292,5 +3292,11 @@ "See when people join, leave, or are invited to your active room": "Zjistěte, kdy se lidé připojí, odejdou nebo jsou pozváni do vaší aktivní místnosti", "Kick, ban, or invite people to this room, and make you leave": "Vykopnout, vykázat, pozvat lidi do této místnosti nebo odejít", "Kick, ban, or invite people to your active room, and make you leave": "Vykopnout, vykázat, pozvat lidi do vaší aktivní místnosti nebo odejít", - "See when people join, leave, or are invited to this room": "Zjistěte, kdy se lidé připojí, odejdou nebo jsou pozváni do této místnosti" + "See when people join, leave, or are invited to this room": "Zjistěte, kdy se lidé připojí, odejdou nebo jsou pozváni do této místnosti", + "Currently joining %(count)s rooms|one": "Momentálně se připojuje %(count)s místnost", + "Currently joining %(count)s rooms|other": "Momentálně se připojuje %(count)s místností", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Vyzkoušejte jiná slova nebo zkontrolujte překlepy. Některé výsledky nemusí být viditelné, protože jsou soukromé a potřebujete k nim pozvánku.", + "No results for \"%(query)s\"": "Žádné výsledky pro \"%(query)s\"", + "The user you called is busy.": "Volaný uživatel je zaneprázdněn.", + "User Busy": "Uživatel zaneprázdněn" } diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index dcc5343af4..9d1c1fa071 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -3343,5 +3343,14 @@ "Your feedback will help make spaces better. The more detail you can go into, the better.": "Dein Feedback hilfst uns, die Spaces zu verbessern. Je genauer, desto besser.", "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Durchs Verlassen lädt %(brand)s mit deaktivierten Spaces neu. Danach kannst du Communities und Custom Tags wieder verwenden.", "sends space invaders": "sendet Space Invaders", - "Sends the given message with a space themed effect": "Sendet die Nachricht mit Raumschiffen" + "Sends the given message with a space themed effect": "Sendet die Nachricht mit Raumschiffen", + "Space Autocomplete": "Spaces automatisch vervollständigen", + "Currently joining %(count)s rooms|one": "Betrete %(count)s Raum", + "Currently joining %(count)s rooms|other": "Betrete %(count)s Räume", + "Go to my space": "Zu meinem Space", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Überprüfe auf Tippfehler oder verwende andere Suchbegriffe. Beachte, dass Ergebnisse aus privaten Räumen, in die du nicht eingeladen wurdest, nicht angezeigt werden.", + "See when people join, leave, or are invited to this room": "Anzeigen, wenn Leute eingeladen werden oder den Raum betreten und verlassen", + "The user you called is busy.": "Der angerufene Benutzer ist momentan beschäftigt.", + "User Busy": "Benutzer beschäftigt", + "No results for \"%(query)s\"": "Keine Ergebnisse für \"%(query)s\"" } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 610344798f..a94b608f2b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -784,6 +784,7 @@ "%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s", "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s", "Change notification settings": "Change notification settings", + "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators", "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.", "Spaces": "Spaces", "Spaces are a new way to group rooms and people.": "Spaces are a new way to group rooms and people.", @@ -793,6 +794,10 @@ "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "You can leave the beta any time from settings or tapping on a beta badge, like the one above.", "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.", "Your feedback will help make spaces better. The more detail you can go into, the better.": "Your feedback will help make spaces better. The more detail you can go into, the better.", + "Show all rooms in Home": "Show all rooms in Home", + "Show people in spaces": "Show people in spaces", + "If disabled, you can still add Direct Messages to Personal Spaces. If enabled, you'll automatically see everyone who is a member of the Space.": "If disabled, you can still add Direct Messages to Personal Spaces. If enabled, you'll automatically see everyone who is a member of the Space.", + "Show notification badges for People in Spaces": "Show notification badges for People in Spaces", "Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode", "Send and receive voice messages": "Send and receive voice messages", "Render LaTeX maths in messages": "Render LaTeX maths in messages", @@ -905,6 +910,8 @@ "Incoming voice call": "Incoming voice call", "Incoming video call": "Incoming video call", "Incoming call": "Incoming call", + "Sound on": "Sound on", + "Silence call": "Silence call", "Decline": "Decline", "Accept": "Accept", "Pause": "Pause", @@ -1018,9 +1025,10 @@ "You can change these anytime.": "You can change these anytime.", "Creating...": "Creating...", "Create": "Create", - "All rooms": "All rooms", "Expand space panel": "Expand space panel", "Collapse space panel": "Collapse space panel", + "All rooms": "All rooms", + "Home": "Home", "Click to copy": "Click to copy", "Copied!": "Copied!", "Failed to copy": "Failed to copy", @@ -1930,6 +1938,7 @@ "Error loading Widget": "Error loading Widget", "Error - Mixed content": "Error - Mixed content", "Popout widget": "Popout widget", + "Message search initialisation failed, check your settings for more information": "Message search initialisation failed, check your settings for more information", "Use the Desktop app to see all encrypted files": "Use the Desktop app to see all encrypted files", "Use the Desktop app to search encrypted messages": "Use the Desktop app to search encrypted messages", "This version of %(brand)s does not support viewing some encrypted files": "This version of %(brand)s does not support viewing some encrypted files", @@ -2027,7 +2036,6 @@ "Continue with %(provider)s": "Continue with %(provider)s", "Sign in with single sign-on": "Sign in with single sign-on", "And %(count)s more...|other": "And %(count)s more...", - "Home": "Home", "Enter a server name": "Enter a server name", "Looks good": "Looks good", "You are not allowed to view this server's rooms list": "You are not allowed to view this server's rooms list", @@ -2257,6 +2265,9 @@ "Start a conversation with someone using their name or username (like ).": "Start a conversation with someone using their name or username (like ).", "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here": "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here", "Go": "Go", + "Some suggestions may be hidden for privacy.": "Some suggestions may be hidden for privacy.", + "If you can't see who you’re looking for, send them your invite link below.": "If you can't see who you’re looking for, send them your invite link below.", + "Or send invite link": "Or send invite link", "Unnamed Space": "Unnamed Space", "Invite to %(roomName)s": "Invite to %(roomName)s", "Invite someone using their name, email address, username (like ) or share this space.": "Invite someone using their name, email address, username (like ) or share this space.", @@ -2313,9 +2324,23 @@ "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.": "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.", "Email (optional)": "Email (optional)", "Please fill why you're reporting.": "Please fill why you're reporting.", + "What this user is writing is wrong.\nThis will be reported to the room moderators.": "What this user is writing is wrong.\nThis will be reported to the room moderators.", + "This user is displaying toxic behaviour, for instance by insulting other users or sharing adult-only content in a family-friendly room or otherwise violating the rules of this room.\nThis will be reported to the room moderators.": "This user is displaying toxic behaviour, for instance by insulting other users or sharing adult-only content in a family-friendly room or otherwise violating the rules of this room.\nThis will be reported to the room moderators.", + "This user is displaying illegal behaviour, for instance by doxing people or threatening violence.\nThis will be reported to the room moderators who may escalate this to legal authorities.": "This user is displaying illegal behaviour, for instance by doxing people or threatening violence.\nThis will be reported to the room moderators who may escalate this to legal authorities.", + "This user is spamming the room with ads, links to ads or to propaganda.\nThis will be reported to the room moderators.": "This user is spamming the room with ads, links to ads or to propaganda.\nThis will be reported to the room moderators.", + "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s. The administrators will NOT be able to read the encrypted content of this room.": "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s. The administrators will NOT be able to read the encrypted content of this room.", + "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\n This will be reported to the administrators of %(homeserver)s.": "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\n This will be reported to the administrators of %(homeserver)s.", + "Any other reason. Please describe the problem.\nThis will be reported to the room moderators.": "Any other reason. Please describe the problem.\nThis will be reported to the room moderators.", + "Please pick a nature and describe what makes this message abusive.": "Please pick a nature and describe what makes this message abusive.", + "Report Content": "Report Content", + "Disagree": "Disagree", + "Toxic Behaviour": "Toxic Behaviour", + "Illegal Content": "Illegal Content", + "Spam or propaganda": "Spam or propaganda", + "Report the entire room": "Report the entire room", + "Send report": "Send report", "Report Content to Your Homeserver Administrator": "Report Content to Your Homeserver Administrator", "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.", - "Send report": "Send report", "Room Settings - %(roomName)s": "Room Settings - %(roomName)s", "Failed to upgrade room": "Failed to upgrade room", "The room upgrade could not be completed": "The room upgrade could not be completed", @@ -2482,13 +2507,10 @@ "Share Message": "Share Message", "Source URL": "Source URL", "Collapse Reply Thread": "Collapse Reply Thread", - "Report Content": "Report Content", "Clear status": "Clear status", "Update status": "Update status", "Set status": "Set status", "Set a new status...": "Set a new status...", - "Move up": "Move up", - "Move down": "Move down", "View Community": "View Community", "Unable to start audio streaming.": "Unable to start audio streaming.", "Failed to start livestream": "Failed to start livestream", @@ -2635,7 +2657,7 @@ "%(count)s messages deleted.|one": "%(count)s message deleted.", "Your Communities": "Your Communities", "Did you know: you can use communities to filter your %(brand)s experience!": "Did you know: you can use communities to filter your %(brand)s experience!", - "You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.", + "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.", "Error whilst fetching joined communities": "Error whilst fetching joined communities", "Create a new community": "Create a new community", "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.", @@ -2990,5 +3012,6 @@ "Esc": "Esc", "Enter": "Enter", "Space": "Space", - "End": "End" + "End": "End", + "[number]": "[number]" } diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index fc8c00fd19..41bb44ed83 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -1591,10 +1591,10 @@ "Room Autocomplete": "Memkompletigo de ĉambroj", "User Autocomplete": "Memkompletigo de uzantoj", "Custom (%(level)s)": "Propra (%(level)s)", - "%(senderName)s placed a voice call.": "%(senderName)s metis voĉvokon.", - "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s metis voĉvokon. (mankas subteno en ĉi tiu foliumilo)", - "%(senderName)s placed a video call.": "%(senderName)s metis vidvokon.", - "%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s metis vidvokon. (mankas subteno en ĉi tiu foliumilo)", + "%(senderName)s placed a voice call.": "%(senderName)s ekigis voĉvokon.", + "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s ekigis voĉvokon. (mankas subteno en ĉi tiu foliumilo)", + "%(senderName)s placed a video call.": "%(senderName)s ekigis vidvokon.", + "%(senderName)s placed a video call. (not supported by this browser)": "%(senderName)s ekigis vidvokon. (mankas subteno en ĉi tiu foliumilo)", "Try out new ways to ignore people (experimental)": "Elprovi novajn manierojn malatenti personojn (eksperimente)", "Match system theme": "Similiĝi la sisteman haŭton", "My Ban List": "Mia listo de forbaroj", @@ -3320,5 +3320,11 @@ "Reset event store": "Restarigi deponejon de okazoj", "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few moments whilst the index is recreated": "Se vi tamen tion faras, sciu ke neniu el viaj mesaĝoj foriĝos, sed via sperto pri serĉado povas malboniĝi momente, dum la indekso estas refarata", "You most likely do not want to reset your event index store": "Plej probable, vi ne volas restarigi vian deponejon de indeksoj de okazoj", - "Reset event store?": "Ĉu restarigi deponejon de okazoj?" + "Reset event store?": "Ĉu restarigi deponejon de okazoj?", + "Currently joining %(count)s rooms|one": "Nun aliĝante al %(count)s ĉambro", + "Currently joining %(count)s rooms|other": "Nun aliĝante al %(count)s ĉambroj", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Provu aliajn vortojn aŭ kontorolu, ĉu vi ne tajperaris. Iuj rezultoj eble ne videblos, ĉar ili estas privataj kaj vi bezonus inviton por aliĝi.", + "No results for \"%(query)s\"": "Neniuj rezultoj por «%(query)s»", + "The user you called is busy.": "La uzanto, kiun vi vokis, estas okupata.", + "User Busy": "Uzanto estas okupata" } diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index 60f5d06bec..5e8e57bacd 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -3315,5 +3315,11 @@ "See when people join, leave, or are invited to your active room": "Ver cuando alguien se una, salga o se le invite a tu sala activa", "Kick, ban, or invite people to this room, and make you leave": "Expulsar, vetar o invitar personas a esta sala, y hacerte salir de ella", "Kick, ban, or invite people to your active room, and make you leave": "Expulsar, vetar o invitar a gente a tu sala activa, o hacerte salir", - "See when people join, leave, or are invited to this room": "Ver cuando alguien se une, sale o se le invita a la sala" + "See when people join, leave, or are invited to this room": "Ver cuando alguien se une, sale o se le invita a la sala", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Prueba con sinónimos o revisa si te has equivocado al escribir. Puede que algunos resultados no sean visibles si son privados y necesites que te inviten para verlos.", + "Currently joining %(count)s rooms|one": "Entrando en %(count)s sala", + "Currently joining %(count)s rooms|other": "Entrando en %(count)s salas", + "No results for \"%(query)s\"": "Ningún resultado para «%(query)s»", + "The user you called is busy.": "La persona a la que has llamado está ocupada.", + "User Busy": "Persona ocupada" } diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index 5e8d744cca..a8e267fb01 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -3345,5 +3345,14 @@ "Send and receive voice messages": "Saada ja võta vastu häälsõnumeid", "Your feedback will help make spaces better. The more detail you can go into, the better.": "Sinu tagasiside aitab teha kogukonnakeskuseid paremaks. Mida detailsemalt sa oma arvamust kirjeldad, seda parem.", "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Kui sa lahkud, siis käivitame %(brand)s uuesti nii, et kogukonnakeskused ei ole kasutusel. Vana tüüpi kogukonnad ja kohandatud sildid saavad jälle olema kasutusel.", - "Message search initialisation failed": "Sõnumite otsingu alustamine ei õnnestunud" + "Message search initialisation failed": "Sõnumite otsingu alustamine ei õnnestunud", + "sends space invaders": "korraldab ühe pisikese tulnukate vallutusretke", + "Sends the given message with a space themed effect": "Saadab antud sõnumi kosmoseteemalise efektiga", + "Go to my space": "Palun vaata minu kogukonnakeskust", + "User Busy": "Kasutaja on hõivatud", + "The user you called is busy.": "Kasutaja, kellele sa helistasid, on hõivatud.", + "No results for \"%(query)s\"": "Päringule „%(query)s“ pole vastuseid", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Proovi muid otsingusõnu või kontrolli, et neis polnud trükivigu. Kuna mõned otsingutulemused on privaatsed ja sa vajad kutset nende nägemiseks, siis kõiki tulemusi siin ei pruugi näha olla.", + "Currently joining %(count)s rooms|other": "Parasjagu liitun %(count)s jututoaga", + "Currently joining %(count)s rooms|one": "Parasjagu liitun %(count)s jututoaga" } diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 5a8208f50b..e199e094e7 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -1873,7 +1873,7 @@ "This user has not verified all of their sessions.": "Cet utilisateur n’a pas vérifié toutes ses sessions.", "You have verified this user. This user has verified all of their sessions.": "Vous avez vérifié cet utilisateur. Cet utilisateur a vérifié toutes ses sessions.", "Someone is using an unknown session": "Quelqu’un utilise une session inconnue", - "Mod": "Modo", + "Mod": "Modérateur", "Your key share request has been sent - please check your other sessions for key share requests.": "Votre demande de partage de clé a été envoyée − vérifiez les demandes de partage de clé sur vos autres sessions.", "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "Les demandes de partage de clé sont envoyées à vos autres sessions automatiquement. Si vous avez rejeté ou ignoré la demande de partage de clé sur vos autres sessions, cliquez ici pour redemander les clés pour cette session.", "If your other sessions do not have the key for this message you will not be able to decrypt them.": "Si vos autres sessions n’ont pas la clé pour ce message vous ne pourrez pas le déchiffrer.", @@ -2842,7 +2842,7 @@ "Change the topic of your active room": "Changer le sujet dans le salon actuel", "Change the topic of this room": "Changer le sujet de ce salon", "Send stickers into this room": "Envoyer des autocollants dans ce salon", - "Remain on your screen when viewing another room, when running": "Reste sur votre écran quand vous regardez un autre salon lors de l’appel", + "Remain on your screen when viewing another room, when running": "Reste sur votre écran lors de l’appel quand vous regardez un autre salon", "Takes the call in the current room off hold": "Reprend l’appel en attente dans ce salon", "Places the call in the current room on hold": "Met l’appel dans ce salon en attente", "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Ajoute (╯°□°)╯︵ ┻━┻ en préfixe du message", @@ -2861,14 +2861,14 @@ "Send videos as you in this room": "Envoie des vidéos sous votre nom dans ce salon", "See images posted to this room": "Voir les images publiées dans ce salon", "See images posted to your active room": "Voir les images publiées dans votre salon actif", - "See messages posted to your active room": "Voir les messages publiés dans votre salon actif", + "See messages posted to your active room": "Voir les messages envoyés dans le salon actuel", "See messages posted to this room": "Voir les messages publiés dans ce salon", "Send messages as you in your active room": "Envoie des messages sous votre nom dans votre salon actif", "Send messages as you in this room": "Envoie des messages sous votre nom dans ce salon", "The %(capability)s capability": "La capacité %(capability)s", "See %(eventType)s events posted to your active room": "Voir les événements %(eventType)s publiés dans votre salon actuel", "Send %(eventType)s events as you in your active room": "Envoie des événements %(eventType)s sous votre nom dans votre salon actuel", - "See %(eventType)s events posted to this room": "Voir les événements %(eventType)s publiés dans ce salon", + "See %(eventType)s events posted to this room": "Voir les événements %(eventType)s envoyés dans ce salon", "Send %(eventType)s events as you in this room": "Envoie des événements %(eventType)s sous votre nom dans ce salon", "Send stickers to your active room as you": "Envoie des autocollants sous votre nom dans le salon actuel", "Continue with %(ssoButtons)s": "Continuer avec %(ssoButtons)s", @@ -2930,7 +2930,7 @@ "Don't miss a reply": "Ne ratez pas une réponse", "See %(msgtype)s messages posted to your active room": "Voir les messages de type %(msgtype)s publiés dans le salon actuel", "See %(msgtype)s messages posted to this room": "Voir les messages de type %(msgtype)s publiés dans ce salon", - "Send %(msgtype)s messages as you in this room": "Envoie des messages de type%(msgtype)s sous votre nom dans ce salon", + "Send %(msgtype)s messages as you in this room": "Envoie les messages de type %(msgtype)s sous votre nom dans ce salon", "Send %(msgtype)s messages as you in your active room": "Envoie des messages de type %(msgtype)s sous votre nom dans votre salon actif", "See general files posted to your active room": "Voir les fichiers postés dans votre salon actuel", "See general files posted to this room": "Voir les fichiers postés dans ce salon", @@ -3034,7 +3034,7 @@ "Send text messages as you in this room": "Envoyez des messages textuels sous votre nom dans ce salon", "See when the name changes in your active room": "Suivre les changements de nom dans le salon actif", "Change which room, message, or user you're viewing": "Changer le salon, message, ou la personne que vous visualisez", - "Change which room you're viewing": "Changer le salon que vous visualisez", + "Change which room you're viewing": "Changer le salon que vous êtes en train de lire", "Remain on your screen while running": "Reste sur votre écran pendant l’exécution", "%(senderName)s has updated the widget layout": "%(senderName)s a mis à jour la disposition du widget", "Converts the DM to a room": "Transforme la conversation privée en salon", @@ -3111,7 +3111,7 @@ "No permissions": "Aucune permission", "Remove from Space": "Supprimer de l’espace", "Undo": "Annuler", - "Your message wasn't sent because this homeserver has been blocked by it's administrator. Please contact your service administrator to continue using the service.": "Votre message n’a pas été envoyé car ce serveur d’accueil a été banni par son administrateur. Merci de contacter votre administrateur de service pour poursuivre l’usage de ce service.", + "Your message wasn't sent because this homeserver has been blocked by it's administrator. Please contact your service administrator to continue using the service.": "Votre message n’a pas été envoyé car ce serveur d’accueil a été bloqué par son administrateur. Merci de contacter votre administrateur de service pour continuer à utiliser le service.", "Are you sure you want to leave the space '%(spaceName)s'?": "Êtes-vous sûr de vouloir quitter l’espace « %(spaceName)s » ?", "This space is not public. You will not be able to rejoin without an invite.": "Cet espace n’est pas public. Vous ne pourrez pas le rejoindre sans invitation.", "Start audio stream": "Démarrer une diffusion audio", @@ -3352,5 +3352,11 @@ "sends space invaders": "Envoie les Space Invaders", "Sends the given message with a space themed effect": "Envoyer le message avec un effet lié au thème de l’espace", "See when people join, leave, or are invited to your active room": "Afficher quand des personnes rejoignent, partent, ou sont invités dans votre salon actif", - "Kick, ban, or invite people to your active room, and make you leave": "Expulser, bannir ou inviter des personnes dans votre salon actif et en partir" + "Kick, ban, or invite people to your active room, and make you leave": "Expulser, bannir ou inviter des personnes dans votre salon actif et en partir", + "Currently joining %(count)s rooms|one": "Vous êtes en train de rejoindre %(count)s salon", + "Currently joining %(count)s rooms|other": "Vous êtes en train de rejoindre %(count)s salons", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Essayez d'autres mots ou vérifiez les fautes de frappe. Certains salons peuvent ne pas être visibles car ils sont privés et vous devez être invité pour les rejoindre.", + "No results for \"%(query)s\"": "Aucun résultat pour « %(query)s »", + "The user you called is busy.": "L’utilisateur que vous avez appelé est indisponible.", + "User Busy": "Utilisateur indisponible" } diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 12a2dcd8c3..abb4776a55 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -3375,5 +3375,11 @@ "See when people join, leave, or are invited to your active room": "Mira cando alguén se une, sae ou é convidada á túa sala activa", "Kick, ban, or invite people to your active room, and make you leave": "Expulsa, veta ou convida a persoas á túa sala activa, e fai que saias", "See when people join, leave, or are invited to this room": "Mira cando se une alguén, sae ou é convidada a esta sala", - "Kick, ban, or invite people to this room, and make you leave": "Expulsa, veta, ou convida persoas a esta sala, e fai que saias" + "Kick, ban, or invite people to this room, and make you leave": "Expulsa, veta, ou convida persoas a esta sala, e fai que saias", + "Currently joining %(count)s rooms|one": "Neste intre estás en %(count)s sala", + "Currently joining %(count)s rooms|other": "Neste intre estás en %(count)s salas", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Intentao con outras palabras e fíxate nos erros de escritura. Algúns resultados poderían non ser visibles porque son privados e precisas un convite.", + "No results for \"%(query)s\"": "Sen resultados para \"%(query)s\"", + "The user you called is busy.": "A persoa á que chamas está ocupada.", + "User Busy": "Usuaria ocupada" } diff --git a/src/i18n/strings/hr.json b/src/i18n/strings/hr.json index 2511771578..8070757426 100644 --- a/src/i18n/strings/hr.json +++ b/src/i18n/strings/hr.json @@ -5,5 +5,205 @@ "The platform you're on": "Platforma na kojoj se nalazite", "The version of %(brand)s": "Verzija %(brand)s", "Your language of choice": "Izabrani jezik", - "Dismiss": "Odbaci" + "Dismiss": "Odbaci", + "France": "Francuska", + "Finland": "Finska", + "Fiji": "Fiji", + "Faroe Islands": "Farski otoci", + "Falkland Islands": "Falklandski otoci", + "Ethiopia": "Etiopija", + "Estonia": "Estonija", + "Eritrea": "Eritreja", + "Equatorial Guinea": "Ekvatorska Gvineja", + "El Salvador": "El Salvador", + "Egypt": "Egipat", + "Ecuador": "Ekvador", + "Dominican Republic": "Dominikanska Republika", + "Dominica": "Dominika", + "Djibouti": "Džibuti", + "Denmark": "Danska", + "Côte d’Ivoire": "Obala Bjelokosti", + "Czech Republic": "Češka", + "Cyprus": "Cipar", + "Curaçao": "Curaçao", + "Cuba": "Kuba", + "Croatia": "Hrvatska", + "Costa Rica": "Kostarika", + "Cook Islands": "Cookovo Otočje", + "Congo - Kinshasa": "Kongo - Kinshasa", + "Congo - Brazzaville": "Republika Kongo", + "Comoros": "Komori", + "Colombia": "Kolumbija", + "Cocos (Keeling) Islands": "Kokosovi (Keeling) otoci", + "Christmas Island": "Uskršnji otoci", + "China": "Kina", + "Chile": "Čile", + "Chad": "Čad", + "Central African Republic": "Srednjoafrička Republika", + "Cayman Islands": "Kajmanski otoci", + "Caribbean Netherlands": "Karipska Nizozemska", + "Cape Verde": "Zelenortski Otoci", + "Canada": "Kanada", + "Cameroon": "Kamerun", + "Cambodia": "Kambodža", + "Burundi": "Burundi", + "Burkina Faso": "Burkina Faso", + "Bulgaria": "Bugarska", + "Brunei": "Brunej", + "British Virgin Islands": "Britanski djevičanski otoci", + "British Indian Ocean Territory": "Britanski teritorij Indijskog oceana", + "Brazil": "Brazil", + "Bouvet Island": "Otok Bouvet", + "Botswana": "Bocvana", + "Bosnia": "Bosna i Hercegovina", + "Bolivia": "Bolivija", + "Bhutan": "Butan", + "Bermuda": "Bermuda", + "Benin": "Benin", + "Belize": "Belize", + "Belgium": "Belgija", + "Belarus": "Bjelorusija", + "Barbados": "Barbados", + "Bangladesh": "Bangladeš", + "Bahrain": "Bahrein", + "Bahamas": "Bahami", + "Azerbaijan": "Azerbejdžan", + "Austria": "Austrija", + "Australia": "Australija", + "Aruba": "Aruba", + "Armenia": "Armenija", + "Argentina": "Argentina", + "Antigua & Barbuda": "Antigva i Barbuda", + "Antarctica": "Antartika", + "Anguilla": "Angvila", + "Angola": "Angola", + "Andorra": "Andora", + "American Samoa": "Američka Samoa", + "Algeria": "Alžir", + "Albania": "Albanija", + "Åland Islands": "Alandski otoci", + "Afghanistan": "Afganistan", + "United States": "Sjedinjene Države", + "United Kingdom": "Ujedinjeno Kraljevstvo", + "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Čini se da Vaša email adresa nije povezana s Matrix ID-om na ovom kućnom poslužitelju.", + "This email address was not found": "Ova email adresa nije pronađena", + "Unable to enable Notifications": "Omogućavanje notifikacija nije uspjelo", + "%(brand)s was not given permission to send notifications - please try again": "%(brand)s nema dopuštenje slati Vam notifikacije - molimo pokušajte ponovo", + "%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s nema dopuštenje slati Vam notifikacije - molimo provjerite postavke pretraživača", + "%(name)s is requesting verification": "%(name)s traži potvrdu", + "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "Vaš je kućni poslužitelj odbio vaš pokušaj prijave. Razlog je možda da je jednostavno sve predugo trajalo. Molimo pokušajte ponovno. Ako se ovo nastavi, obratite se administratoru kućnog poslužitelja.", + "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Vaš kućni poslužitelj nije bio dostupan i nije vas mogao prijaviti. Pokušajte ponovo. Ako se ovo nastavi, obratite se administratoru kućnog poslužitelja.", + "Try again": "Pokušaj ponovo", + "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "Tražili smo od preglednika da zapamti koji kućni poslužitelj koristite za prijavu, ali ga je Vaš preglednik nažalost zaboravio. Idite na stranicu za prijavu i pokušajte ponovo.", + "We couldn't log you in": "Nismo Vas mogli ulogirati", + "Trust": "Vjeruj", + "Only continue if you trust the owner of the server.": "Nastavite samo ako vjerujete vlasniku poslužitelja.", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Ova radnja zahtijeva pristup zadanom poslužitelju identiteta radi provjere adrese e-pošte ili telefonskog broja, no poslužitelj nema nikakve uvjete usluge.", + "Identity server has no terms of service": "Poslužitelj identiteta nema uvjete usluge", + "Unnamed Room": "Neimenovana soba", + "Failed to add the following rooms to %(groupId)s:": "Neuspješno dodavanje sljedećih soba u %(groupId)s:", + "Failed to invite users to %(groupId)s": "Neuspješno dodavanje korisnika u %(groupId)s", + "Failed to invite users to community": "Dodavanje korisnika u zajednicu nije uspjelo", + "Failed to invite the following users to %(groupId)s:": "Neuspješno dodavanje sljedećih korisnika u %(groupId)s:", + "Add to community": "Dodaj u zajednicu", + "Room name or address": "Ime ili adresa sobe", + "Add rooms to the community": "Dodaj sobe zajednici", + "Show these rooms to non-members on the community page and room list?": "Prikaži ove sobe osobama koje nisu članovi na stranici zajednice i popisu soba?", + "Which rooms would you like to add to this community?": "Koju sobu biste željeli dodati u ovu zajednicu?", + "Invite to Community": "Pozovi u zajednicu", + "Name or Matrix ID": "Ime ili Matrix ID", + "Invite new community members": "Pozovite nove članove zajednice", + "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Upozorenje: svaka osoba koju dodate u zajednicu bit će javno vidljiva svima koji znaju ID zajednice", + "Who would you like to add to this community?": "Koga biste željeli dodati u ovu zajednicu?", + "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(day)s. %(monthName)s %(fullYear)s, %(time)s", + "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(day)s. %(monthName)s %(fullYear)s", + "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(day)s. %(monthName)s, %(time)s", + "%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s", + "AM": "Prijepodne", + "PM": "Poslijepodne", + "Dec": "Pro", + "Nov": "Stu", + "Oct": "Lis", + "Sep": "Ruj", + "Aug": "Kol", + "Jul": "Srp", + "Jun": "Lip", + "May": "Svi", + "Apr": "Tra", + "Mar": "Ožu", + "Feb": "Velj", + "Jan": "Sij", + "Sat": "Sub", + "Fri": "Pet", + "Thu": "Čet", + "Wed": "Sri", + "Tue": "Uto", + "Mon": "Pon", + "Sun": "Ned", + "Failure to create room": "Stvaranje sobe neuspješno", + "The server does not support the room version specified.": "Poslužitelj ne podržava navedenu verziju sobe.", + "Server may be unavailable, overloaded, or you hit a bug.": "Poslužitelj je možda nedostupan, preopterećen, ili ste pronašli grešku u aplikaciji.", + "Upload Failed": "Prijenos neuspješan", + "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Datoteka '%(fileName)s' premašuje maksimalnu veličinu ovog kućnog poslužitelja za prijenose", + "The file '%(fileName)s' failed to upload.": "Prijenos datoteke '%(fileName)s' nije uspio.", + "Continue": "Nastavi", + "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Trenutno nije moguće odgovoriti datotekom. Želite li prenijeti ovu datoteku bez odgovora?", + "Replying With Files": "Odgovaranje datotekama", + "This will end the conference for everyone. Continue?": "To će prekinuti konferenciju za sve. Nastaviti?", + "End conference": "Završi konferenciju", + "You do not have permission to start a conference call in this room": "Nemate dopuštenje uspostaviti konferencijski poziv u ovoj sobi", + "Permission Required": "Potrebno dopuštenje", + "A call is currently being placed!": "Poziv se upravo uspostavlja!", + "Call in Progress": "Poziv u tijeku", + "You cannot place a call with yourself.": "Ne možete uspostaviti poziv sami sa sobom.", + "You're already in a call with this person.": "Već ste u pozivu sa tom osobom.", + "Already in call": "Već u pozivu", + "You've reached the maximum number of simultaneous calls.": "Dosegli ste maksimalan broj istodobnih poziva.", + "Too Many Calls": "Previše poziva", + "You cannot place VoIP calls in this browser.": "Ne možete uspostaviti VoIP pozive u ovom pretraživaču.", + "VoIP is unsupported": "VoIP nije podržan", + "Unable to capture screen": "Nije moguće snimanje zaslona", + "No other application is using the webcam": "Da ni jedna druga aplikacija već ne koristi web kameru", + "Permission is granted to use the webcam": "Jeli dopušteno korištenje web kamere", + "A microphone and webcam are plugged in and set up correctly": "Jesu li mikrofon i web kamera priključeni i pravilno postavljeni", + "Unable to access webcam / microphone": "Nije moguće pristupiti web kameri / mikrofonu", + "Call failed because webcam or microphone could not be accessed. Check that:": "Poziv nije uspio jer nije bilo moguće pristupiti web kameri ili mikrofonu. Provjerite:", + "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Poziv nije uspio jer nije bilo moguće pristupiti mikrofonu. Provjerite je li mikrofon priključen i ispravno postavljen.", + "Unable to access microphone": "Nije moguće pristupiti mikrofonu", + "OK": "OK", + "Try using turn.matrix.org": "Pokušajte koristiti turn.matrix.org", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternativno, možete pokušati koristiti javni poslužitelj na turn.matrix.org, no to bi moglo biti manje pouzdano i Vaša IP adresa će biti podijeljena s tim poslužiteljem. Time također možete upravljati u Postavkama.", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Zamolite administratora Vašeg kućnog poslužitelja (%(homeserverDomain)s) da konfigurira TURN poslužitelj kako bi pozivi mogli pouzdano funkcionirati.", + "Call failed due to misconfigured server": "Poziv neuspješan radi pogrešno konfiguriranog poslužitelja", + "The call was answered on another device.": "Na poziv je odgovoreno sa drugog uređaja.", + "Answered Elsewhere": "Odgovoreno je drugdje", + "The call could not be established": "Poziv se nije mogao uspostaviti", + "The remote side failed to pick up": "Sugovornik nije odgovorio na poziv", + "The user you called is busy.": "Pozvani korisnik je zauzet.", + "User Busy": "Korisnik zauzet", + "The other party declined the call.": "Sugovornik je odbio poziv.", + "Call Declined": "Poziv odbijen", + "Call Failed": "Poziv neuspješan", + "Unable to load! Check your network connectivity and try again.": "Učitavanje nije moguće! Provjerite mrežnu povezanost i pokušajte ponovo.", + "Error": "Geška", + "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Gdje ova stranica uključuje identificirajuće podatke, poput ID-a sobe, korisnika ili grupe, ti se podaci uklanjaju prije slanja na poslužitelj.", + "The information being sent to us to help make %(brand)s better includes:": "Podaci koji nam se šalju radi poboljšanja %(brand)s uključuju:", + "Analytics": "Analitika", + "Your device resolution": "Razlučivost vašeg uređaja", + "Your user agent": "Vaš korisnički agent", + "e.g. ": "npr. ", + "Every page you use in the app": "Svaka stranica koju upotrebljavate u aplikaciji", + "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Bez obzira upotrebljavate li %(brand)s na uređaju na kojem je dodir primarni mehanizam unosa", + "Confirm adding this phone number by using Single Sign On to prove your identity.": "Potvrdite dodavanje ovog telefonskog broja koristeći jedinstvenu prijavu (SSO) da biste dokazali Vaš identitet.", + "Confirm adding this email address by using Single Sign On to prove your identity.": "Potvrdite dodavanje ove email adrese koristeći jedinstvenu prijavu (SSO) da biste dokazali Vaš identitet.", + "Single Sign On": "Jedinstvena prijava (SSO)", + "Use Single Sign On to continue": "Koristite jedinstvenu prijavu (SSO) za nastavak", + "Whether or not you're logged in (we don't record your username)": "Bez obzira jeste li ulogirani ili ne (ne snimamo vaše korisničko ime)", + "Add Phone Number": "Dodaj telefonski broj", + "Click the button below to confirm adding this phone number.": "Kliknite gumb ispod da biste potvrdili dodavanje ovog telefonskog broja.", + "Confirm adding phone number": "Potvrdite dodavanje telefonskog broja", + "Add Email Address": "Dodaj email adresu", + "Confirm": "Potvrdi", + "Click the button below to confirm adding this email address.": "Kliknite gumb ispod da biste potvrdili dodavanje ove email adrese.", + "Confirm adding email": "Potvrdite dodavanje email adrese" } diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index a6e9992866..b290f20076 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -26,12 +26,12 @@ "Admin Tools": "Admin. Eszközök", "No Microphones detected": "Nem található mikrofon", "No Webcams detected": "Nem található webkamera", - "No media permissions": "Nincs media jogosultság", + "No media permissions": "Nincs média jogosultság", "You may need to manually permit %(brand)s to access your microphone/webcam": "Lehet hogy kézileg kell engedélyeznie a %(brand)snak, hogy hozzáférjen a mikrofonjához és webkamerájához", "Default Device": "Alapértelmezett eszköz", "Microphone": "Mikrofon", "Camera": "Kamera", - "Advanced": "Haladó", + "Advanced": "Speciális", "Always show message timestamps": "Üzenet időbélyeg folyamatos megjelenítése", "Authentication": "Azonosítás", "Failed to change password. Is your password correct?": "Nem sikerült megváltoztatni a jelszót. Helyesen írtad be a jelszavadat?", @@ -66,7 +66,7 @@ "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s megváltoztatta a hozzáférési szintjét erre: %(powerLevelDiffText)s.", "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s megváltoztatta a szoba nevét erre: %(roomName)s.", "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s törölte a szoba nevét.", - "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s megváltoztatta a témát erre \"%(topic)s\".", + "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s megváltoztatta a témát erre: „%(topic)s”.", "Changes your display nickname": "Megváltoztatja a becenevedet", "Click here to fix": "A javításhoz kattints ide", "Click to mute audio": "Hang némításához kattints ide", @@ -116,7 +116,7 @@ "Failed to set display name": "Megjelenítési nevet nem sikerült beállítani", "Failed to unban": "Kizárás visszavonása sikertelen", "Failed to upload profile picture!": "Profil kép feltöltése sikertelen!", - "Failed to verify email address: make sure you clicked the link in the email": "E-mail cím ellenőrzése sikertelen: ellenőrizze, hogy az e-mailben lévő hivatkozásra kattintott-e", + "Failed to verify email address: make sure you clicked the link in the email": "Az e-mail-cím ellenőrzése sikertelen: ellenőrizze, hogy az e-mailben lévő hivatkozásra kattintott-e", "Failure to create room": "Szoba létrehozása sikertelen", "Favourites": "Kedvencek", "Fill screen": "Képernyő kitöltése", @@ -230,7 +230,7 @@ "Submit": "Elküld", "Success": "Sikeres", "The phone number entered looks invalid": "A megadott telefonszám érvénytelennek tűnik", - "This email address is already in use": "Ez az e-mail cím már használatban van", + "This email address is already in use": "Ez az e-mail-cím már használatban van", "This email address was not found": "Az e-mail cím nem található", "The email address linked to your account must be entered.": "A fiókodhoz kötött e-mail címet add meg.", "The remote side failed to pick up": "A hívott fél nem vette fel", @@ -1030,7 +1030,7 @@ "Room list": "Szoba lista", "Timeline": "Idővonal", "Autocomplete delay (ms)": "Automatikus kiegészítés késleltetése (ms)", - "Roles & Permissions": "Szerepek & Jogosultságok", + "Roles & Permissions": "Szerepek és jogosultságok", "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "A üzenetek olvashatóságának változtatása csak az új üzenetekre lesz érvényes. A régi üzenetek láthatósága nem fog változni.", "Security & Privacy": "Biztonság és adatvédelem", "Encryption": "Titkosítás", @@ -1373,7 +1373,7 @@ "Message edits": "Üzenet szerkesztések", "Upgrading this room requires closing down the current instance of the room and creating a new room in its place. To give room members the best possible experience, we will:": "A szoba frissítéséhez be kell zárnod ezt a szobát és egy újat kell nyitnod e helyett. A szoba tagjainak a legjobb felhasználói élmény nyújtásához az alábbit fogjuk tenni:", "Loading room preview": "Szoba előnézetének a betöltése", - "Show all": "Mindet mutat", + "Show all": "Mind megjelenítése", "%(senderName)s made no change.": "%(senderName)s nem változtatott semmit.", "%(severalUsers)smade no changes %(count)s times|other": "%(severalUsers)s %(count)s alkalommal nem változtattak semmit", "%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)s nem változtattak semmit", @@ -1531,7 +1531,7 @@ "%(count)s unread messages including mentions.|other": "%(count)s olvasatlan üzenet megemlítéssel.", "%(count)s unread messages.|other": "%(count)s olvasatlan üzenet.", "Unread mentions.": "Olvasatlan megemlítés.", - "Show image": "Képek megmutatása", + "Show image": "Kép megjelenítése", "Please create a new issue on GitHub so that we can investigate this bug.": "Ahhoz hogy a hibát megvizsgálhassuk kérlek készíts egy új hibajegyet a GitHubon.", "To continue you need to accept the terms of this service.": "A folytatáshoz el kell fogadnod a felhasználási feltételeket.", "Document": "Dokumentum", @@ -1545,7 +1545,7 @@ "Remove %(count)s messages|one": "1 üzenet törlése", "Your email address hasn't been verified yet": "Az e-mail-címe még nincs ellenőrizve", "Click the link in the email you received to verify and then click continue again.": "Ellenőrzéshez kattints a linkre az e-mailben amit kaptál és itt kattints a folytatásra újra.", - "Add Email Address": "E-mail cím hozzáadása", + "Add Email Address": "E-mail-cím hozzáadása", "Add Phone Number": "Telefonszám hozzáadása", "%(creator)s created and configured the room.": "%(creator)s elkészítette és beállította a szobát.", "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "Először töröld a személyes adatokat az azonosítási szerverről () mielőtt lecsatlakozol. Sajnos az azonosítási szerver () jelenleg elérhetetlen.", @@ -1622,7 +1622,7 @@ "Subscribing to a ban list will cause you to join it!": "A feliratkozás egy tiltó listára azzal jár, hogy csatlakozol hozzá!", "If this isn't what you want, please use a different tool to ignore users.": "Ha nem ez az amit szeretnél, kérlek használj más eszközt a felhasználók figyelmen kívül hagyásához.", "Subscribe": "Feliratkozás", - "You have ignored this user, so their message is hidden. Show anyways.": "Ezt a felhasználót figyelmen kívül hagyod, így az üzenetei el lesznek rejtve. Mindenképpen megmutat.", + "You have ignored this user, so their message is hidden. Show anyways.": "Ezt a felhasználót figyelmen kívül hagyod, így az üzenetei el lesznek rejtve. Megjelenítés mindenképpen.", "Custom (%(level)s)": "Egyéni (%(level)s)", "Trusted": "Megbízható", "Not trusted": "Megbízhatatlan", @@ -1733,7 +1733,7 @@ "Country Dropdown": "Ország lenyíló menü", "The message you are trying to send is too large.": "Túl nagy képet próbálsz elküldeni.", "Help": "Súgó", - "Show more": "Mutass többet", + "Show more": "Több megjelenítése", "Recent Conversations": "Legújabb Beszélgetések", "Direct Messages": "Közvetlen Beszélgetések", "Go": "Menj", @@ -1795,7 +1795,7 @@ "This bridge was provisioned by .": "Ezt a hidat az alábbi felhasználó készítette: .", "Workspace: %(networkName)s": "Munkahely: %(networkName)s", "Channel: %(channelName)s": "Csatorna: %(channelName)s", - "Show less": "Kevesebbet mutat", + "Show less": "Kevesebb megjelenítése", "Securely cache encrypted messages locally for them to appear in search results, using ": "A titkosított üzenetek kereséséhez azokat biztonságos módon helyileg kell tárolnod, felhasználva: ", " to store messages from ": " üzenetek tárolásához ", "rooms.": "szobából.", @@ -2109,7 +2109,7 @@ "Server did not return valid authentication information.": "A szerver semmilyen érvényes azonosítási információt sem küldött vissza.", "There was a problem communicating with the server. Please try again.": "A szerverrel való kommunikációval probléma történt. Kérlek próbáld újra.", "Sign in with SSO": "Belépés SSO-val", - "Welcome to %(appName)s": "Üdvözöl az %(appName)s", + "Welcome to %(appName)s": "Üdvözöl a(z) %(appName)s", "Liberate your communication": "Kommunikálj szabadon", "Send a Direct Message": "Közvetlen üzenet küldése", "Explore Public Rooms": "Nyilvános szobák felfedezése", @@ -2237,7 +2237,7 @@ "Sort by": "Rendezés", "Unread rooms": "Olvasatlan szobák", "Always show first": "Mindig az elsőt mutatja", - "Show": "Mutat", + "Show": "Megjelenítés", "Message preview": "Üzenet előnézet", "List options": "Lista beállításai", "Show %(count)s more|other": "Még %(count)s megjelenítése", @@ -2945,7 +2945,7 @@ "Learn more": "Tudj meg többet", "Matrix.org is the biggest public homeserver in the world, so it’s a good place for many.": "A matrix.org a legnagyobb nyilvános Matrix szerver a világon, és sok felhasználónak megfelelő választás.", "About homeservers": "A Matrix szerverekről", - "Use your preferred Matrix homeserver if you have one, or host your own.": "Add meg az általad választott Matrix szerver címét, ha van ilyen, vagy üzemeltess egy sajátot!", + "Use your preferred Matrix homeserver if you have one, or host your own.": "Add meg az általad választott Matrix szerver címét, ha van ilyen, vagy üzemeltess egy sajátot.", "Other homeserver": "Másik Matrix szerver", "Host account on": "Fiók létrehozása itt:", "We call the places where you can host your account ‘homeservers’.": "Matrix szervereknek nevezzük azokat a helyeket, ahol fiókot lehet létrehozni.", @@ -3107,7 +3107,7 @@ "Room name": "Szoba neve", "Support": "Támogatás", "Random": "Véletlen", - "Welcome to ": "Üdvözlöm itt: ", + "Welcome to ": "Üdvözöl a(z) ", "Your private space ": "Privát tere: ", "Your public space ": "Nyilvános tere: ", "You have been invited to ": "Meghívták ide: ", @@ -3370,5 +3370,11 @@ "See when people join, leave, or are invited to your active room": "Emberek belépésének, távozásának vagy meghívásának a megjelenítése az aktív szobájában", "Kick, ban, or invite people to your active room, and make you leave": "Kirúgni, kitiltani vagy meghívni embereket az aktív szobába és, hogy ön elhagyja a szobát", "See when people join, leave, or are invited to this room": "Emberek belépésének, távozásának vagy meghívásának a megjelenítése ebben a szobában", - "Kick, ban, or invite people to this room, and make you leave": "Kirúgni, kitiltani vagy meghívni embereket ebbe a szobába és, hogy ön elhagyja a szobát" + "Kick, ban, or invite people to this room, and make you leave": "Kirúgni, kitiltani vagy meghívni embereket ebbe a szobába és, hogy ön elhagyja a szobát", + "Currently joining %(count)s rooms|one": "%(count)s szobába lép be", + "Currently joining %(count)s rooms|other": "%(count)s szobába lép be", + "No results for \"%(query)s\"": "Nincs találat ehhez: %(query)s", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Próbáljon ki más szavakat vagy keressen elgépelést. Néhány találat azért nem látszik, mert privát és meghívóra van szüksége, hogy csatlakozhasson.", + "The user you called is busy.": "A hívott felhasználó foglalt.", + "User Busy": "Felhasználó foglalt" } diff --git a/src/i18n/strings/is.json b/src/i18n/strings/is.json index 35f5342b30..e8718c941a 100644 --- a/src/i18n/strings/is.json +++ b/src/i18n/strings/is.json @@ -720,5 +720,13 @@ "%(count)s messages deleted.|one": "%(count)s skilaboð eytt.", "%(count)s messages deleted.|other": "%(count)s skilaboðum eytt.", "Message deleted on %(date)s": "Skilaboð eytt á %(date)s", - "Message edits": "Skilaboðs breytingar" + "Message edits": "Skilaboðs breytingar", + "List options": "Lista valkosti", + "Create a Group Chat": "Búa Til Hópspjall", + "Explore Public Rooms": "Kanna Almenningsherbergi", + "Explore public rooms": "Kanna almenningsherbergi", + "Explore all public rooms": "Kanna öll almenningsherbergi", + "Liberate your communication": "Frelsaðu samskipti þín", + "Welcome to ": "Velkomin til ", + "Welcome to %(appName)s": "Velkomin til %(appName)s" } diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 585ee8ba3a..c83800e82a 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -3375,5 +3375,11 @@ "Kick, ban, or invite people to your active room, and make you leave": "Buttare fuori, bandire o invitare persone nella tua stanza attiva e farti uscire", "See when people join, leave, or are invited to this room": "Vedere quando le persone entrano, escono o sono invitate in questa stanza", "Kick, ban, or invite people to this room, and make you leave": "Buttare fuori, bandire o invitare persone in questa stanza e farti uscire", - "See when people join, leave, or are invited to your active room": "Vedere quando le persone entrano, escono o sono invitate nella tua stanza attiva" + "See when people join, leave, or are invited to your active room": "Vedere quando le persone entrano, escono o sono invitate nella tua stanza attiva", + "Currently joining %(count)s rooms|one": "Stai entrando in %(count)s stanza", + "Currently joining %(count)s rooms|other": "Stai entrando in %(count)s stanze", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Prova parole diverse o controlla errori di battitura. Alcuni risultati potrebbero non essere visibili dato che sono privati e ti servirebbe un invito per unirti.", + "No results for \"%(query)s\"": "Nessun risultato per \"%(query)s\"", + "The user you called is busy.": "L'utente che hai chiamato è occupato.", + "User Busy": "Utente occupato" } diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 16f74e7b2d..7299e9d161 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -3261,5 +3261,11 @@ "See when people join, leave, or are invited to your active room": "Zie wanneer personen deelnemen, vertrekken of worden uitgenodigd in uw actieve gesprek", "Kick, ban, or invite people to your active room, and make you leave": "Verwijder, verban of nodig personen uit voor uw actieve gesprek en uzelf laten vertrekken", "See when people join, leave, or are invited to this room": "Zie wanneer personen deelnemen, vertrekken of worden uitgenodigd voor dit gesprek", - "Kick, ban, or invite people to this room, and make you leave": "Verwijder, verban of verwijder personen uit dit gesprek en uzelf laten vertrekken" + "Kick, ban, or invite people to this room, and make you leave": "Verwijder, verban of verwijder personen uit dit gesprek en uzelf laten vertrekken", + "Currently joining %(count)s rooms|one": "Momenteel aan het toetreden tot %(count)s gesprek", + "Currently joining %(count)s rooms|other": "Momenteel aan het toetreden tot %(count)s gesprekken", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Probeer andere woorden of controleer op typefouten. Sommige resultaten zijn mogelijk niet zichtbaar omdat ze privé zijn of u een uitnodiging nodig heeft om deel te nemen.", + "No results for \"%(query)s\"": "Geen resultaten voor \"%(query)s\"", + "The user you called is busy.": "De gebruiker die u belde is bezet.", + "User Busy": "Gebruiker Bezet" } diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index 83c6c25833..51ba733cfa 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -2271,5 +2271,86 @@ "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "Poprosiliśmy przeglądarkę o zapamiętanie, z którego serwera głównego korzystasz, aby umożliwić Ci logowanie, ale niestety Twoja przeglądarka o tym zapomniała. Przejdź do strony logowania i spróbuj ponownie.", "We couldn't log you in": "Nie mogliśmy Cię zalogować", "You're already in a call with this person.": "Prowadzisz już rozmowę z tą osobą.", - "Already in call": "Już dzwoni" + "Already in call": "Już dzwoni", + "Never send encrypted messages to unverified sessions in this room from this session": "Nigdy nie wysyłaj zaszyfrowanych wiadomości do niezweryfikowanych sesji z tej sesji w tym pokoju", + "Use the Desktop app to see all encrypted files": "Użyj aplikacji desktopowej, aby zobaczyć wszystkie szyfrowane pliki", + "You’re all caught up": "Jesteś na bieżąco", + "Verification requested": "Zażądano weryfikacji", + "You have no visible notifications.": "Nie masz widocznych powiadomień.", + "Connecting": "Łączenie", + "Your Security Key has been copied to your clipboard, paste it to:": "Twój klucz bezpieczeństwa został skopiowany do schowka, wklej go:", + "Your Security Key": "Twój klucz bezpieczeństwa", + "Create key backup": "Utwórz kopię zapasową klucza", + "Generate a Security Key": "Wygeneruj klucz bezpieczeństwa", + "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.": "Zabezpiecz się przed utratą dostępu do szyfrowanych wiadomości i danych, tworząc kopię zapasową kluczy szyfrowania na naszym serwerze.", + "Safeguard against losing access to encrypted messages & data": "Zabezpiecz się przed utratą dostępu do szyfrowanych wiadomości i danych", + "Access Token": "Token dostępu", + "Your access token gives full access to your account. Do not share it with anyone.": "Twój token dostępu daje pełen dostęp do Twojego konta. Nie dziel się nim z nikim.", + "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Możesz opuścić betę w każdej chwili z ustawień lub klikając plakietę Beta, taką jak powyższa.", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta dostępna w przeglądarce, na komputerach i na Androidzie. Niektóre funkcje mogą nie być dostępne na Twoim serwerze.", + "Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta dostępna w przeglądarce, na komputerach i na Androidzie. Dziękujemy za wypróbowanie bety.", + "Avatar": "Awatar", + "Leave the beta": "Opuść betę", + "Beta": "Beta", + "Tap for more info": "Naciśnij aby dowiedzieć się więcej", + "Move right": "Przenieś w prawo", + "Move left": "Przenieś w lewo", + "Join the beta": "Dołącz do bety", + "To join %(spaceName)s, turn on the Spaces beta": "Aby dołączyć do %(spaceName)s, włącz betę Przestrzeni", + "No results found": "Nie znaleziono wyników", + "Create room": "Utwórz pokój", + "Removing...": "Usuwanie…", + "Your server does not support showing space hierarchies.": "Twój serwer nie obsługuje wyświetlania hierarchii przestrzeni.", + "You can select all or individual messages to retry or delete": "Możesz zaznaczyć wszystkie lub wybrane wiadomości aby spróbować ponownie lub usunąć je", + "Sending": "Wysyłanie", + "Delete all": "Usuń wszystkie", + "Some of your messages have not been sent": "Niektóre z Twoich wiadomości nie zostały wysłane", + "Filter rooms and people": "Filtruj pokoje i ludzi", + "No results for \"%(query)s\"": "Brak wyników dla „%(query)s”", + "Explore rooms in %(communityName)s": "Eksploruj pokoje w %(communityName)s", + "Retry all": "Spróbuj ponownie wszystkie", + "Suggested": "Polecany", + "This room is suggested as a good one to join": "Ten pokój jest polecany jako dobry do dołączenia", + "%(count)s rooms|one": "%(count)s pokój", + "%(count)s rooms|other": "%(count)s pokoi", + "%(count)s members|one": "%(count)s członek", + "%(count)s members|other": "%(count)s członkowie", + "You don't have permission": "Nie masz uprawnień", + "Failed to remove some rooms. Try again later": "Nie udało się usunąć niektórych pokoi. Spróbuj ponownie później", + "Select a room below first": "Najpierw wybierz poniższy pokój", + "%(count)s rooms and 1 space|one": "%(count)s pokój i jedna przestrzeń", + "%(count)s rooms and 1 space|other": "%(count)s pokoi i jedna przestrzeń", + "To view %(spaceName)s, turn on the Spaces beta": "Aby wyświetlić %(spaceName)s, włącz betę Przestrzeni", + "Spaces are a beta feature.": "Przestrzenie są funkcją beta.", + "%(count)s rooms and %(numSpaces)s spaces|one": "%(count)s pokój i %(numSpaces)s przestrzeni", + "%(count)s rooms and %(numSpaces)s spaces|other": "%(count)s pokoi i %(numSpaces)s przestrzeni", + "Filter all spaces": "Filtruj wszystkie przestrzenie", + "Communities are changing to Spaces": "Społeczności zmieniają się na Przestrzenie", + "Spaces is a beta feature": "Przestrzenie są funkcją beta", + "You can add existing spaces to a space.": "Możesz dodać istniejące przestrzenie do przestrzeni.", + "Filter your rooms and spaces": "Filtruj pokoje i przestrzenie", + "%(count)s results in all spaces|one": "%(count)s wynik we wszystkich przestrzeniach", + "%(count)s results in all spaces|other": "%(count)s wyników we wszystkich przestrzeniach", + "Spaces are a new way to group rooms and people. To join an existing space you'll need an invite.": "Przestrzenie to nowy sposób na grupowanie pokoi i ludzi. Aby dołączyć do istniejącej przestrzeni, potrzebujesz zaproszenia.", + "Your feedback will help make spaces better. The more detail you can go into, the better.": "Twoje opinie pomogą uczynić Przestrzenie lepszymi. Im bardziej szczegółowo je opiszesz, tym lepiej.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s odświeży się z włączonymi Przestrzeniami. Społeczności i niestandardowe tagi zostaną ukryte.", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Jeżeli opuścisz, %(brand)s odświeży się z wyłączonymi Przestrzeniami. Społeczności i niestandardowe tagi będą z powrotem widoczne.", + "Spaces are a new way to group rooms and people.": "Przestrzenie to nowy sposób grupowania pokoi i ludzi.", + "Spaces": "Przestrzenie", + "Feeling experimental?": "Chcesz eksperymentować?", + "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "Chcesz eksperymentować? Laboratoria to najlepszy sposób na uzyskanie nowości wcześniej, przetestowanie nowych funkcji i pomoc w kształtowaniu ich zanim będą ogólnodostępne. Dowiedz się więcej.", + "We'll store an encrypted copy of your keys on our server. Secure your backup with a Security Phrase.": "Będziemy przechowywać zaszyfrowaną kopię Twoich kluczy na naszym serwerze. Zabezpiecz swoją kopię zapasową frazą bezpieczeństwa.", + "Secure Backup": "Bezpieczna kopia zapasowa", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Pozwól na wykorzystanie peer-to-peer w rozmowach 1:1 (jeżeli włączono, druga strona może zobaczyć Twój adres IP)", + "Jump to the bottom of the timeline when you send a message": "Przejdź na dół osi czasu po wysłaniu wiadomości", + "Use Ctrl + F to search": "Używaj Ctrl + F do wyszukiwania", + "Show line numbers in code blocks": "Pokazuj numery wierszy w blokach kodu", + "Expand code blocks by default": "Domyślnie rozwijaj bloki kodu", + "Show stickers button": "Pokaż przycisk naklejek", + "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Zarządcy integracji otrzymują dane konfiguracji, mogą modyfikować widżety, wysyłać zaproszenia do pokoi i ustawiać poziom uprawnień w Twoim imieniu.", + "Converts the DM to a room": "Zmienia wiadomości bezpośrednie w pokój", + "Converts the room to a DM": "Zmienia pokój w wiadomość bezpośrednią", + "Sends the given message as a spoiler": "Wysyła podaną wiadomość jako spoiler", + "User Busy": "Użytkownik zajęty", + "The user you called is busy.": "Użytkownik do którego zadzwoniłeś(-aś) jest zajęty." } diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json index 8497ae7164..e19febd6ef 100644 --- a/src/i18n/strings/pt_BR.json +++ b/src/i18n/strings/pt_BR.json @@ -163,7 +163,7 @@ "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s enviou um convite para %(targetDisplayName)s entrar na sala.", "%(senderName)s set a profile picture.": "%(senderName)s definiu uma foto de perfil.", "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s definiu o nome e sobrenome para %(displayName)s.", - "This email address is already in use": "Este endereço de e-mail já está em uso", + "This email address is already in use": "Este endereço de email já está em uso", "This email address was not found": "Este endereço de e-mail não foi encontrado", "The remote side failed to pick up": "A pessoa não atendeu a chamada", "This room is not recognised.": "Esta sala não é reconhecida.", @@ -284,7 +284,7 @@ "ex. @bob:example.com": "p.ex: @joao:exemplo.com", "Add User": "Adicionar usuária(o)", "Custom Server Options": "Opções para Servidor Personalizado", - "Dismiss": "Descartar", + "Dismiss": "Dispensar", "Please check your email to continue registration.": "Por favor, confirme o seu e-mail para continuar a inscrição.", "Token incorrect": "Token incorreto", "Please enter the code it contains:": "Por favor, entre com o código que está na mensagem:", @@ -1176,7 +1176,7 @@ "Sign In or Create Account": "Faça login ou crie uma conta", "Use your account or create a new one to continue.": "Use sua conta ou crie uma nova para continuar.", "Create Account": "Criar Conta", - "Sign In": "Entrar", + "Sign In": "Fazer signin", "Custom (%(level)s)": "Personalizado (%(level)s)", "Messages": "Mensagens", "Actions": "Ações", diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index bd294093f9..4996decbaf 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -3361,5 +3361,14 @@ "sends space invaders": "dërgon pushtues hapësire", "Sends the given message with a space themed effect": "E dërgon mesazhin e dhënë me një efekt teme hapësinore", "See when people join, leave, or are invited to your active room": "Shihni kur persona vijnë, ikin ose janë ftuar në dhomën tuaj aktive", - "See when people join, leave, or are invited to this room": "Shihni kur persona vijnë, ikin ose janë ftuar në këtë dhomë" + "See when people join, leave, or are invited to this room": "Shihni kur persona vijnë, ikin ose janë ftuar në këtë dhomë", + "Space Autocomplete": "Vetëplotësim Hapësire", + "Currently joining %(count)s rooms|one": "Aktualisht duke hyrë në %(count)s dhomë", + "Currently joining %(count)s rooms|other": "Aktualisht duke hyrë në %(count)s dhoma", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Provoni fjalë të ndryshme, ose kontrolloni për gabime shkrimi. Disa përfundime mund të mos jenë të dukshme, ngaqë janë private dhe ju duhet një ftesë për të marrë pjesë në to.", + "No results for \"%(query)s\"": "S’ka përfundime për \"%(query)s\"", + "The user you called is busy.": "Përdoruesi që thirrët është i zënë.", + "User Busy": "Përdoruesi Është i Zënë", + "Kick, ban, or invite people to your active room, and make you leave": "Përzini, dëboni, ose ftoni persona te dhoma juaj aktive, dhe bëni largimin tuaj", + "Kick, ban, or invite people to this room, and make you leave": "Përzini, dëboni, ose ftoni persona në këtë dhomë, dhe bëni largimin tuaj" } diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index a50c039e9e..f026e0fe02 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -3305,5 +3305,11 @@ "See when people join, leave, or are invited to your active room": "Se när folk går med, lämnar eller bjuds in till ditt aktiva rum", "Kick, ban, or invite people to your active room, and make you leave": "Kicka, banna eller bjuda in folk till ditt aktiva rum, och tvinga dig att lämna", "See when people join, leave, or are invited to this room": "Se när folk går med, lämnar eller bjuds in till det här rummet", - "Kick, ban, or invite people to this room, and make you leave": "Kicka, banna eller bjuda in folk till det här rummet, och tvinga dig att lämna" + "Kick, ban, or invite people to this room, and make you leave": "Kicka, banna eller bjuda in folk till det här rummet, och tvinga dig att lämna", + "Currently joining %(count)s rooms|one": "Går just nu med i %(count)s rum", + "Currently joining %(count)s rooms|other": "Går just nu med i %(count)s rum", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Testa andra ord eller kolla efter felskrivningar. Vissa resultat kanske inte visas för att de är privata och du behöver en inbjudan för att gå med i dem.", + "No results for \"%(query)s\"": "Inga resultat för \"%(query)s\"", + "The user you called is busy.": "Användaren du ringde är upptagen.", + "User Busy": "Användare upptagen" } diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json index db5ce9b360..92da704837 100644 --- a/src/i18n/strings/uk.json +++ b/src/i18n/strings/uk.json @@ -267,8 +267,8 @@ "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s %(time)s", "Who would you like to add to this community?": "Кого ви хочете додати до цієї спільноти?", "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Там, де ця сторінка містить ототожненну інформацію, як-от назва кімнати, користувача чи групи, ці дані будуть вилучені перед надсиланням на сервер.", - "Call in Progress": "Іде виклик", - "A call is currently being placed!": "Зараз іде виклик!", + "Call in Progress": "Триває виклик", + "A call is currently being placed!": "Зараз триває виклик!", "A call is already in progress!": "Вже здійснюється дзвінок!", "Permission Required": "Потрібен дозвіл", "You do not have permission to start a conference call in this room": "У вас немає дозволу, щоб розпочати дзвінок-конференцію в цій кімнаті", @@ -306,8 +306,8 @@ "Missing user_id in request": "У запиті пропущено user_id", "Usage": "Використання", "Searches DuckDuckGo for results": "Здійснює пошук через DuckDuckGo", - "/ddg is not a command": "/ddg — це не команда", - "To use it, just wait for autocomplete results to load and tab through them.": "Щоб цим скористатися, просто почекайте на підказки доповнення й перемикайтеся між ними клавішею TAB.", + "/ddg is not a command": "/ddg не є командою", + "To use it, just wait for autocomplete results to load and tab through them.": "Щоб цим скористатися, просто почекайте на підказки автодоповнення й перемикайтеся між ними клавішею TAB.", "Changes your display nickname": "Змінює ваш нік", "Invites user with given id to current room": "Запрошує користувача з вказаним ідентифікатором до кімнати", "Leave room": "Залишити кімнату", @@ -457,7 +457,7 @@ "Actions": "Дії", "Other": "Інше", "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Додає ¯\\_(ツ)_/¯ на початку текстового повідомлення", - "Sends a message as plain text, without interpreting it as markdown": "Надсилає повідомлення як чистий текст, не використовуючи markdown", + "Sends a message as plain text, without interpreting it as markdown": "Надсилає повідомлення у вигляді звичайного тексту, не інтерпретуючи його як розмітку", "Upgrades a room to a new version": "Покращує кімнату до нової версії", "You do not have the required permissions to use this command.": "Вам бракує дозволу на використання цієї команди.", "Warning: Upgrading a room will not automatically migrate room members to the new version of the room. We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "Увага!: Поліпшення кімнати не перенесе автоматично усіх учасників до нової версії кімнати. Ми опублікуємо посилання на нову кімнату у старій версії кімнати, а учасники мають власноруч клацнути це посилання, щоб приєднатися до нової кімнати.", @@ -624,7 +624,7 @@ "Riot is now Element!": "Riot тепер - Element!", "Learn More": "Дізнатися більше", "Command error": "Помилка команди", - "Sends a message as html, without interpreting it as markdown": "Надсилає повідомлення як HTML, не інтерпритуючи Markdown", + "Sends a message as html, without interpreting it as markdown": "Надсилає повідомлення у вигляді HTML, не інтерпретуючи його як розмітку", "Failed to set topic": "Не вдалося встановити тему", "Once enabled, encryption cannot be disabled.": "Після увімкнення шифрування не можна буде вимкнути.", "Please enter verification code sent via text.": "Будь ласка, введіть звірювальний код, відправлений у текстовому повідомленні.", @@ -1602,5 +1602,33 @@ "Share Link to User": "Поділитися посиланням на користувача", "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Повідомлення тут захищено наскрізним шифруванням. Підтвердьте %(displayName)s у їхньому профілі — натиснувши на їх аватар.", "Open": "Відкрити", - "In reply to ": "У відповідь на " + "In reply to ": "У відповідь на ", + "The user you called is busy.": "Користувач, якого ви викликаєте, зайнятий.", + "User Busy": "Користувач зайнятий", + "Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message": "Додає ┬──┬ ノ( ゜-゜ノ) на початку текстового повідомлення", + "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Додає (╯°□°)╯︵ ┻━┻ на початку текстового повідомлення", + "We couldn't log you in": "Нам не вдалося виконати вхід", + "You're already in a call with this person.": "Ви вже спілкуєтесь із цією особою.", + "Already in call": "Вже у виклику", + "You can't send any messages until you review and agree to our terms and conditions.": "Ви не можете надсилати жодних повідомлень, поки не переглянете та не погодитесь з нашими умовами та положеннями.", + "Send message": "Надіслати повідомлення", + "Sending your message...": "Надсилання повідомлення...", + "You can use /help to list available commands. Did you mean to send this as a message?": "Ви можете скористатися /help для перегляду доступних команд. Ви мали намір надіслати це як повідомлення?", + "Send messages": "Надіслати повідомлення", + "sends confetti": "надсилає конфеті", + "sends fireworks": "надсилає феєрверк", + "sends space invaders": "надсилає тему про космічних загарбників", + "Sends the given message with a space themed effect": "Надсилає це повідомлення з космічними ефектами", + "unknown person": "невідома особа", + "Sends the given message with snowfall": "Надсилає це повідомлення зі снігопадом", + "Sends the given message with fireworks": "Надсилає це повідомлення з феєрверком", + "Sends the given message with confetti": "Надсилає це повідомлення з конфеті", + "Use Ctrl + Enter to send a message": "Натисніть Ctrl + Enter, щоб надіслати повідомлення", + "Use Command + Enter to send a message": "Натисніть Command + Enter, щоб надіслати повідомлення", + "Use Ctrl + F to search": "Натисніть Ctrl + F, щоб шукати", + "Use Command + F to search": "Натисніть Command + F, щоб шукати", + "Send text messages as you in this room": "Надіслати текстові повідомлення у цю кімнату від свого імені", + "Send messages as you in your active room": "Надіслати повідомлення у свою активну кімнату від свого імені", + "Send messages as you in this room": "Надіслати повідомлення у цю кімнату від свого імені", + "Sends the given message as a spoiler": "Надсилає вказане повідомлення згорненим" } diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json index af02a40587..3267060d64 100644 --- a/src/i18n/strings/zh_Hans.json +++ b/src/i18n/strings/zh_Hans.json @@ -3274,5 +3274,11 @@ "Send stickers to your active room as you": "发送贴纸到你所活跃的聊天室", "See when people join, leave, or are invited to your active room": "查看人们何时加入、离开或被邀请到你所活跃的聊天室", "See when people join, leave, or are invited to this room": "查看人们何时加入、离开或被邀请到这个房间", - "Kick, ban, or invite people to this room, and make you leave": "移除、封禁或邀请用户到此聊天室,并让你离开" + "Kick, ban, or invite people to this room, and make you leave": "移除、封禁或邀请用户到此聊天室,并让你离开", + "Currently joining %(count)s rooms|one": "目前正在加入 %(count)s 个聊天室", + "Currently joining %(count)s rooms|other": "目前正在加入 %(count)s 个聊天室", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "尝试不同的单词或检查拼写错误。某些结果可能不可见,因为它们属于私有的,你需要一个邀请才能加入。", + "No results for \"%(query)s\"": "「%(query)s」没有结果", + "The user you called is busy.": "你所拨打的用户正在忙碌中。", + "User Busy": "用户正在忙" } diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 5c27fb3878..053839f937 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -3378,5 +3378,11 @@ "See when people join, leave, or are invited to your active room": "檢視人們何時加入、離開或被邀請至您活躍的聊天室", "Kick, ban, or invite people to your active room, and make you leave": "踢除、封鎖或邀請人們到您作用中的聊天室,然後讓您離開", "See when people join, leave, or are invited to this room": "檢視人們何時加入、離開或被邀請至此聊天室", - "Kick, ban, or invite people to this room, and make you leave": "踢除、封鎖或邀請人們到此聊天室,然後讓您離開" + "Kick, ban, or invite people to this room, and make you leave": "踢除、封鎖或邀請人們到此聊天室,然後讓您離開", + "Currently joining %(count)s rooms|one": "目前正在加入 %(count)s 個聊天室", + "Currently joining %(count)s rooms|other": "目前正在加入 %(count)s 個聊天室", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "嘗試不同的詞或是檢查拼字。某些結果可能不可見,因為其為私人的,您必須要有邀請才能加入。", + "No results for \"%(query)s\"": "「%(query)s」沒有結果", + "The user you called is busy.": "您想要通話的使用者目前忙碌中。", + "User Busy": "使用者忙碌" } diff --git a/src/indexing/EventIndex.ts b/src/indexing/EventIndex.ts index b6289969bd..c36f96f368 100644 --- a/src/indexing/EventIndex.ts +++ b/src/indexing/EventIndex.ts @@ -300,7 +300,7 @@ export default class EventIndex extends EventEmitter { } private eventToJson(ev: MatrixEvent) { - const jsonEvent = ev.toJSON(); + const jsonEvent: any = ev.toJSON(); const e = ev.isEncrypted() ? jsonEvent.decrypted : jsonEvent; if (ev.isEncrypted()) { diff --git a/src/mjolnir/BanList.ts b/src/mjolnir/BanList.ts index 21cd5d4cf7..89eec89500 100644 --- a/src/mjolnir/BanList.ts +++ b/src/mjolnir/BanList.ts @@ -92,7 +92,7 @@ export class BanList { if (!room) return; for (const eventType of ALL_RULE_TYPES) { - const events = room.currentState.getStateEvents(eventType, undefined); + const events = room.currentState.getStateEvents(eventType); for (const ev of events) { if (!ev.getStateKey()) continue; diff --git a/src/performance/entry-names.ts b/src/performance/entry-names.ts index effd9506f6..6cb193b1b1 100644 --- a/src/performance/entry-names.ts +++ b/src/performance/entry-names.ts @@ -37,17 +37,17 @@ export enum PerformanceEntryNames { SWITCH_ROOM = "mx_SwithRoom", JUMP_TO_ROOM = "mx_JumpToRoom", - JOIN_ROOM = "mx_JoinRoom", - CREATE_DM = "mx_CreateDM", + JOIN_ROOM = "mx_JoinRoom", // ✅ + CREATE_DM = "mx_CreateDM", // ✅ PEEK_ROOM = "mx_PeekRoom", /** * User */ - VERIFY_E2EE_USER = "mx_VerifyE2EEUser", - LOGIN = "mx_Login", - REGISTER = "mx_Register", + VERIFY_E2EE_USER = "mx_VerifyE2EEUser", // ✅ + LOGIN = "mx_Login", // ✅ + REGISTER = "mx_Register", // ✅ /** * VoIP diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts index 08d8ccfd13..859fdf046a 100644 --- a/src/rageshake/submit-rageshake.ts +++ b/src/rageshake/submit-rageshake.ts @@ -263,7 +263,13 @@ function uint8ToString(buf: Buffer) { return out; } -export async function submitFeedback(endpoint: string, label: string, comment: string, canContact = false) { +export async function submitFeedback( + endpoint: string, + label: string, + comment: string, + canContact = false, + extraData: Record = {}, +) { let version = "UNKNOWN"; try { version = await PlatformPeg.get().getAppVersion(); @@ -279,6 +285,10 @@ export async function submitFeedback(endpoint: string, label: string, comment: s body.append("platform", PlatformPeg.get().getHumanReadableName()); body.append("user_id", MatrixClientPeg.get()?.getUserId()); + for (const k in extraData) { + body.append(k, extraData[k]); + } + await _submitReport(SdkConfig.get().bug_report_endpoint_url, body, () => {}); } diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 155d039572..6ed5e0c3d8 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -1,6 +1,6 @@ /* Copyright 2017 Travis Ralston -Copyright 2018, 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2018 - 2021 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. @@ -94,6 +94,9 @@ export interface ISetting { [level: SettingLevel]: string; }; + // Optional description which will be shown as microCopy under SettingsFlags + description?: string; + // The supported levels are required. Preferably, use the preset arrays // at the top of this file to define this rather than a custom array. supportedLevels?: SettingLevel[]; @@ -127,10 +130,18 @@ export interface ISetting { image: string; // require(...) feedbackSubheading?: string; feedbackLabel?: string; + extraSettings?: string[]; }; } export const SETTINGS: {[setting: string]: ISetting} = { + "feature_report_to_moderators": { + isFeature: true, + displayName: _td("Report to moderators prototype. " + + "In rooms that support moderation, the `report` button will let you report abuse to room moderators"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "feature_spaces": { isFeature: true, displayName: _td("Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. " + @@ -167,8 +178,33 @@ export const SETTINGS: {[setting: string]: ISetting} = { feedbackSubheading: _td("Your feedback will help make spaces better. " + "The more detail you can go into, the better."), feedbackLabel: "spaces-feedback", + extraSettings: [ + "feature_spaces.all_rooms", + "feature_spaces.space_member_dms", + "feature_spaces.space_dm_badges", + ], }, }, + "feature_spaces.all_rooms": { + displayName: _td("Show all rooms in Home"), + supportedLevels: LEVELS_FEATURE, + default: true, + controller: new ReloadOnChangeController(), + }, + "feature_spaces.space_member_dms": { + displayName: _td("Show people in spaces"), + description: _td("If disabled, you can still add Direct Messages to Personal Spaces. " + + "If enabled, you'll automatically see everyone who is a member of the Space."), + supportedLevels: LEVELS_FEATURE, + default: true, + controller: new ReloadOnChangeController(), + }, + "feature_spaces.space_dm_badges": { + displayName: _td("Show notification badges for People in Spaces"), + supportedLevels: LEVELS_FEATURE, + default: false, + controller: new ReloadOnChangeController(), + }, "feature_dnd": { isFeature: true, displayName: _td("Show options to enable 'Do not disturb' mode"), diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index e1e300e185..44f3d5d838 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -248,6 +248,16 @@ export default class SettingsStore { return _t(displayName as string); } + /** + * Gets the translated description for a given setting + * @param {string} settingName The setting to look up. + * @return {String} The description for the setting, or null if not found. + */ + public static getDescription(settingName: string) { + if (!SETTINGS[settingName]?.description) return null; + return _t(SETTINGS[settingName].description); + } + /** * Determines if a setting is also a feature. * @param {string} settingName The setting to look up. diff --git a/src/stores/CommunityPrototypeStore.ts b/src/stores/CommunityPrototypeStore.ts index 023845c9ee..a6f4574a58 100644 --- a/src/stores/CommunityPrototypeStore.ts +++ b/src/stores/CommunityPrototypeStore.ts @@ -107,8 +107,9 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient { const pl = generalChat.currentState.getStateEvents("m.room.power_levels", ""); if (!pl) return this.isAdminOf(communityId); + const plContent = pl.getContent(); - const invitePl = isNullOrUndefined(pl.invite) ? 50 : Number(pl.invite); + const invitePl = isNullOrUndefined(plContent.invite) ? 50 : Number(plContent.invite); return invitePl <= myMember.powerLevel; } @@ -159,10 +160,16 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient { if (SettingsStore.getValue("feature_communities_v2_prototypes")) { const data = this.matrixClient.getAccountData("im.vector.group_info." + roomId); if (data && data.getContent()) { - return {displayName: data.getContent().name, avatarMxc: data.getContent().avatar_url}; + return { + displayName: data.getContent().name, + avatarMxc: data.getContent().avatar_url, + }; } } - return {displayName: room.name, avatarMxc: room.avatar_url}; + return { + displayName: room.name, + avatarMxc: room.getMxcAvatarUrl(), + }; } protected async onReady(): Promise { diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index cc3eafffcd..87978df471 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -276,7 +276,7 @@ class RoomViewStore extends Store { const address = this.state.roomAlias || this.state.roomId; const viaServers = this.state.viaServers || []; try { - await retry(() => cli.joinRoom(address, { + await retry(() => cli.joinRoom(address, { viaServers, ...payload.opts, }), NUM_JOIN_RETRY, (err) => { diff --git a/src/stores/SetupEncryptionStore.js b/src/stores/SetupEncryptionStore.ts similarity index 73% rename from src/stores/SetupEncryptionStore.js rename to src/stores/SetupEncryptionStore.ts index b768ae69df..88385d0399 100644 --- a/src/stores/SetupEncryptionStore.js +++ b/src/stores/SetupEncryptionStore.ts @@ -15,29 +15,42 @@ limitations under the License. */ import EventEmitter from 'events'; +import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; +import { IKeyBackupVersion } from "matrix-js-sdk/src/crypto/keybackup"; +import { ISecretStorageKeyInfo } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from '../MatrixClientPeg'; import { accessSecretStorage, AccessCancelledError } from '../SecurityManager'; import { PHASE_DONE as VERIF_PHASE_DONE } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; -export const PHASE_LOADING = 0; -export const PHASE_INTRO = 1; -export const PHASE_BUSY = 2; -export const PHASE_DONE = 3; //final done stage, but still showing UX -export const PHASE_CONFIRM_SKIP = 4; -export const PHASE_FINISHED = 5; //UX can be closed +export enum Phase { + Loading = 0, + Intro = 1, + Busy = 2, + Done = 3, // final done stage, but still showing UX + ConfirmSkip = 4, + Finished = 5, // UX can be closed +} export class SetupEncryptionStore extends EventEmitter { - static sharedInstance() { - if (!global.mx_SetupEncryptionStore) global.mx_SetupEncryptionStore = new SetupEncryptionStore(); - return global.mx_SetupEncryptionStore; + private started: boolean; + public phase: Phase; + public verificationRequest: VerificationRequest; + public backupInfo: IKeyBackupVersion; + public keyId: string; + public keyInfo: ISecretStorageKeyInfo; + public hasDevicesToVerifyAgainst: boolean; + + public static sharedInstance() { + if (!window.mxSetupEncryptionStore) window.mxSetupEncryptionStore = new SetupEncryptionStore(); + return window.mxSetupEncryptionStore; } - start() { - if (this._started) { + public start(): void { + if (this.started) { return; } - this._started = true; - this.phase = PHASE_LOADING; + this.started = true; + this.phase = Phase.Loading; this.verificationRequest = null; this.backupInfo = null; @@ -48,34 +61,34 @@ export class SetupEncryptionStore extends EventEmitter { const cli = MatrixClientPeg.get(); cli.on("crypto.verification.request", this.onVerificationRequest); - cli.on('userTrustStatusChanged', this._onUserTrustStatusChanged); + cli.on('userTrustStatusChanged', this.onUserTrustStatusChanged); const requestsInProgress = cli.getVerificationRequestsToDeviceInProgress(cli.getUserId()); if (requestsInProgress.length) { // If there are multiple, we take the most recent. Equally if the user sends another request from // another device after this screen has been shown, we'll switch to the new one, so this // generally doesn't support multiple requests. - this._setActiveVerificationRequest(requestsInProgress[requestsInProgress.length - 1]); + this.setActiveVerificationRequest(requestsInProgress[requestsInProgress.length - 1]); } this.fetchKeyInfo(); } - stop() { - if (!this._started) { + public stop(): void { + if (!this.started) { return; } - this._started = false; + this.started = false; if (this.verificationRequest) { this.verificationRequest.off("change", this.onVerificationRequestChange); } if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("crypto.verification.request", this.onVerificationRequest); - MatrixClientPeg.get().removeListener('userTrustStatusChanged', this._onUserTrustStatusChanged); + MatrixClientPeg.get().removeListener('userTrustStatusChanged', this.onUserTrustStatusChanged); } } - async fetchKeyInfo() { + public async fetchKeyInfo(): Promise { const cli = MatrixClientPeg.get(); const keys = await cli.isSecretStored('m.cross_signing.master', false); if (keys === null || Object.keys(keys).length === 0) { @@ -97,15 +110,15 @@ export class SetupEncryptionStore extends EventEmitter { if (!this.hasDevicesToVerifyAgainst && !this.keyInfo) { // skip before we can even render anything. - this.phase = PHASE_FINISHED; + this.phase = Phase.Finished; } else { - this.phase = PHASE_INTRO; + this.phase = Phase.Intro; } this.emit("update"); } - async usePassPhrase() { - this.phase = PHASE_BUSY; + public async usePassPhrase(): Promise { + this.phase = Phase.Busy; this.emit("update"); const cli = MatrixClientPeg.get(); try { @@ -120,7 +133,7 @@ export class SetupEncryptionStore extends EventEmitter { // passphase cached for that work. This dialog itself will only wait // on the first trust check, and the key backup restore will happen // in the background. - await new Promise((resolve, reject) => { + await new Promise((resolve: (value?: unknown) => void, reject: (reason?: any) => void) => { accessSecretStorage(async () => { await cli.checkOwnCrossSigningTrust(); resolve(); @@ -134,7 +147,7 @@ export class SetupEncryptionStore extends EventEmitter { }); if (cli.getCrossSigningId()) { - this.phase = PHASE_DONE; + this.phase = Phase.Done; this.emit("update"); } } catch (e) { @@ -142,25 +155,25 @@ export class SetupEncryptionStore extends EventEmitter { console.log(e); } // this will throw if the user hits cancel, so ignore - this.phase = PHASE_INTRO; + this.phase = Phase.Intro; this.emit("update"); } } - _onUserTrustStatusChanged = (userId) => { + private onUserTrustStatusChanged = (userId: string) => { if (userId !== MatrixClientPeg.get().getUserId()) return; const publicKeysTrusted = MatrixClientPeg.get().getCrossSigningId(); if (publicKeysTrusted) { - this.phase = PHASE_DONE; + this.phase = Phase.Done; this.emit("update"); } } - onVerificationRequest = (request) => { - this._setActiveVerificationRequest(request); + public onVerificationRequest = (request: VerificationRequest): void => { + this.setActiveVerificationRequest(request); } - onVerificationRequestChange = () => { + public onVerificationRequestChange = (): void => { if (this.verificationRequest.cancelled) { this.verificationRequest.off("change", this.onVerificationRequestChange); this.verificationRequest = null; @@ -172,34 +185,34 @@ export class SetupEncryptionStore extends EventEmitter { // cross signing to be ready to use, so wait for the user trust status to // change (or change to DONE if it's already ready). const publicKeysTrusted = MatrixClientPeg.get().getCrossSigningId(); - this.phase = publicKeysTrusted ? PHASE_DONE : PHASE_BUSY; + this.phase = publicKeysTrusted ? Phase.Done : Phase.Busy; this.emit("update"); } } - skip() { - this.phase = PHASE_CONFIRM_SKIP; + public skip(): void { + this.phase = Phase.ConfirmSkip; this.emit("update"); } - skipConfirm() { - this.phase = PHASE_FINISHED; + public skipConfirm(): void { + this.phase = Phase.Finished; this.emit("update"); } - returnAfterSkip() { - this.phase = PHASE_INTRO; + public returnAfterSkip(): void { + this.phase = Phase.Intro; this.emit("update"); } - done() { - this.phase = PHASE_FINISHED; + public done(): void { + this.phase = Phase.Finished; this.emit("update"); // async - ask other clients for keys, if necessary MatrixClientPeg.get().crypto.cancelAndResendAllOutgoingKeyRequests(); } - async _setActiveVerificationRequest(request) { + private async setActiveVerificationRequest(request: VerificationRequest): Promise { if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return; if (this.verificationRequest) { diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index b0099b3306..8b4e3a8f3a 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -14,38 +14,44 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ListIteratee, Many, sortBy, throttle} from "lodash"; -import {EventType, RoomType} from "matrix-js-sdk/src/@types/event"; -import {Room} from "matrix-js-sdk/src/models/room"; -import {MatrixEvent} from "matrix-js-sdk/src/models/event"; +import { ListIteratee, Many, sortBy, throttle } from "lodash"; +import { EventType, RoomType } from "matrix-js-sdk/src/@types/event"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import {AsyncStoreWithClient} from "./AsyncStoreWithClient"; +import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; import defaultDispatcher from "../dispatcher/dispatcher"; -import {ActionPayload} from "../dispatcher/payloads"; +import { ActionPayload } from "../dispatcher/payloads"; import RoomListStore from "./room-list/RoomListStore"; import SettingsStore from "../settings/SettingsStore"; import DMRoomMap from "../utils/DMRoomMap"; -import {FetchRoomFn} from "./notifications/ListNotificationState"; -import {SpaceNotificationState} from "./notifications/SpaceNotificationState"; -import {RoomNotificationStateStore} from "./notifications/RoomNotificationStateStore"; -import {DefaultTagID} from "./room-list/models"; -import {EnhancedMap, mapDiff} from "../utils/maps"; -import {setHasDiff} from "../utils/sets"; -import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory"; +import { FetchRoomFn } from "./notifications/ListNotificationState"; +import { SpaceNotificationState } from "./notifications/SpaceNotificationState"; +import { RoomNotificationStateStore } from "./notifications/RoomNotificationStateStore"; +import { DefaultTagID } from "./room-list/models"; +import { EnhancedMap, mapDiff } from "../utils/maps"; +import { setHasDiff } from "../utils/sets"; +import { ISpaceSummaryEvent, ISpaceSummaryRoom } from "../components/structures/SpaceRoomDirectory"; import RoomViewStore from "./RoomViewStore"; +import { Action } from "../dispatcher/actions"; +import { arrayHasDiff } from "../utils/arrays"; +import { objectDiff } from "../utils/objects"; import { arrayHasOrderChange } from "../utils/arrays"; import { reorderLexicographically } from "../utils/stringOrderField"; +type SpaceKey = string | symbol; + interface IState {} const ACTIVE_SPACE_LS_KEY = "mx_active_space"; +export const HOME_SPACE = Symbol("home-space"); export const SUGGESTED_ROOMS = Symbol("suggested-rooms"); export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces"); export const UPDATE_INVITED_SPACES = Symbol("invited-spaces"); export const UPDATE_SELECTED_SPACE = Symbol("selected-space"); -// Space Room ID will be emitted when a Space's children change +// Space Room ID/HOME_SPACE will be emitted when a Space's children change export interface ISuggestedRoom extends ISpaceSummaryRoom { viaServers: string[]; @@ -53,7 +59,8 @@ export interface ISuggestedRoom extends ISpaceSummaryRoom { const MAX_SUGGESTED_ROOMS = 20; -const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "ALL_ROOMS"}`; +const homeSpaceKey = SettingsStore.getValue("feature_spaces.all_rooms") ? "ALL_ROOMS" : "HOME_SPACE"; +const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || homeSpaceKey}`; const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms] return arr.reduce((result, room: Room) => { @@ -87,13 +94,15 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // The spaces representing the roots of the various tree-like hierarchies private rootSpaces: Room[] = []; + // The list of rooms not present in any currently joined spaces + private orphanedRooms = new Set(); // Map from room ID to set of spaces which list it as a child private parentMap = new EnhancedMap>(); - // Map from spaceId to SpaceNotificationState instance representing that space - private notificationStateMap = new Map(); + // Map from SpaceKey to SpaceNotificationState instance representing that space + private notificationStateMap = new Map(); // Map from space key to Set of room IDs that should be shown as part of that space's filter - private spaceFilteredRooms = new Map>(); - // The space currently selected in the Space Panel - if null then All Rooms is selected + private spaceFilteredRooms = new Map>(); + // The space currently selected in the Space Panel - if null then Home is selected private _activeSpace?: Room = null; private _suggestedRooms: ISuggestedRoom[] = []; private _invitedSpaces = new Set(); @@ -136,7 +145,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // if the space being selected is an invite then always view that invite // else if the last viewed room in this space is joined then view that // else view space home or home depending on what is being clicked on - if (space?.getMyMembership !== "invite" && + if (space?.getMyMembership() !== "invite" && this.matrixClient?.getRoom(roomId)?.getMyMembership() === "join" ) { defaultDispatcher.dispatch({ @@ -254,10 +263,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } public getSpaceFilteredRoomIds = (space: Room | null): Set => { - if (!space) { + if (!space && SettingsStore.getValue("feature_spaces.all_rooms")) { return new Set(this.matrixClient.getVisibleRooms().map(r => r.roomId)); } - return this.spaceFilteredRooms.get(space.roomId) || new Set(); + return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set(); }; private rebuild = throttle(() => { @@ -288,7 +297,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { }); }); - const [rootSpaces] = partitionSpacesAndRooms(Array.from(unseenChildren)); + const [rootSpaces, orphanedRooms] = partitionSpacesAndRooms(Array.from(unseenChildren)); // somewhat algorithm to handle full-cycles const detachedNodes = new Set(spaces); @@ -329,6 +338,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // rootSpaces.push(space); // }); + this.orphanedRooms = new Set(orphanedRooms.map(r => r.roomId)); this.rootSpaces = this.sortRootSpaces(rootSpaces); this.parentMap = backrefs; @@ -345,10 +355,30 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces); }, 100, {trailing: true, leading: true}); - onSpaceUpdate = () => { + private onSpaceUpdate = () => { this.rebuild(); } + private showInHomeSpace = (room: Room) => { + if (SettingsStore.getValue("feature_spaces.all_rooms")) return true; + if (room.isSpaceRoom()) return false; + return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space + || DMRoomMap.shared().getUserIdForRoomId(room.roomId) // put all DMs in the Home Space + || RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite) // show all favourites + }; + + // Update a given room due to its tag changing (e.g DM-ness or Fav-ness) + // This can only change whether it shows up in the HOME_SPACE or not + private onRoomUpdate = (room: Room) => { + if (this.showInHomeSpace(room)) { + this.spaceFilteredRooms.get(HOME_SPACE)?.add(room.roomId); + this.emit(HOME_SPACE); + } else if (!this.orphanedRooms.has(room.roomId)) { + this.spaceFilteredRooms.get(HOME_SPACE)?.delete(room.roomId); + this.emit(HOME_SPACE); + } + }; + private onSpaceMembersChange = (ev: MatrixEvent) => { // skip this update if we do not have a DM with this user if (DMRoomMap.shared().getDMRoomsForUserId(ev.getStateKey()).length < 1) return; @@ -362,6 +392,18 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const oldFilteredRooms = this.spaceFilteredRooms; this.spaceFilteredRooms = new Map(); + if (!SettingsStore.getValue("feature_spaces.all_rooms")) { + // put all room invites in the Home Space + const invites = visibleRooms.filter(r => !r.isSpaceRoom() && r.getMyMembership() === "invite"); + this.spaceFilteredRooms.set(HOME_SPACE, new Set(invites.map(room => room.roomId))); + + visibleRooms.forEach(room => { + if (this.showInHomeSpace(room)) { + this.spaceFilteredRooms.get(HOME_SPACE).add(room.roomId); + } + }); + } + this.rootSpaces.forEach(s => { // traverse each space tree in DFS to build up the supersets as you go up, // reusing results from like subtrees. @@ -377,13 +419,15 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const roomIds = new Set(childRooms.map(r => r.roomId)); const space = this.matrixClient?.getRoom(spaceId); - // Add relevant DMs - space?.getMembers().forEach(member => { - if (member.membership !== "join" && member.membership !== "invite") return; - DMRoomMap.shared().getDMRoomsForUserId(member.userId).forEach(roomId => { - roomIds.add(roomId); + if (SettingsStore.getValue("feature_spaces.space_member_dms")) { + // Add relevant DMs + space?.getMembers().forEach(member => { + if (member.membership !== "join" && member.membership !== "invite") return; + DMRoomMap.shared().getDMRoomsForUserId(member.userId).forEach(roomId => { + roomIds.add(roomId); + }); }); - }); + } const newPath = new Set(parentPath).add(spaceId); childSpaces.forEach(childSpace => { @@ -409,6 +453,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // Update NotificationStates this.getNotificationState(s)?.setRooms(visibleRooms.filter(room => { if (roomIds.has(room.roomId)) { + if (s !== HOME_SPACE && SettingsStore.getValue("feature_spaces.space_dm_badges")) return true; + return !DMRoomMap.shared().getUserIdForRoomId(room.roomId) || RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite); } @@ -426,8 +472,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient { parent = this.rootSpaces.find(s => this.spaceFilteredRooms.get(s.roomId)?.has(roomId)); } if (!parent) { - const parents = Array.from(this.parentMap.get(roomId) || []); - parent = parents.find(p => this.matrixClient.getRoom(p)); + const parentIds = Array.from(this.parentMap.get(roomId) || []); + for (const parentId of parentIds) { + const room = this.matrixClient.getRoom(parentId); + if (room) { + parent = room; + break; + } + } } // don't trigger a context switch when we are switching a space to match the chosen room @@ -511,6 +563,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // TODO confirm this after implementing parenting behaviour if (room.isSpaceRoom()) { this.onSpaceUpdate(); + } else if (!SettingsStore.getValue("feature_spaces.all_rooms")) { + this.onRoomUpdate(room); } this.emit(room.roomId); break; @@ -523,8 +577,38 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } }; + private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEvent?: MatrixEvent) => { + if (ev.getType() === EventType.Tag && !room.isSpaceRoom()) { + // If the room was in favourites and now isn't or the opposite then update its position in the trees + const oldTags = lastEvent?.getContent()?.tags || {}; + const newTags = ev.getContent()?.tags || {}; + if (!!oldTags[DefaultTagID.Favourite] !== !!newTags[DefaultTagID.Favourite]) { + this.onRoomUpdate(room); + } + } + } + + private onAccountData = (ev: MatrixEvent, lastEvent: MatrixEvent) => { + if (ev.getType() === EventType.Direct) { + const lastContent = lastEvent.getContent(); + const content = ev.getContent(); + + const diff = objectDiff>(lastContent, content); + // filter out keys which changed by reference only by checking whether the sets differ + const changed = diff.changed.filter(k => arrayHasDiff(lastContent[k], content[k])); + // DM tag changes, refresh relevant rooms + new Set([...diff.added, ...diff.removed, ...changed]).forEach(roomId => { + const room = this.matrixClient?.getRoom(roomId); + if (room) { + this.onRoomUpdate(room); + } + }); + } + }; + protected async reset() { this.rootSpaces = []; + this.orphanedRooms = new Set(); this.parentMap = new EnhancedMap(); this.notificationStateMap = new Map(); this.spaceFilteredRooms = new Map(); @@ -540,6 +624,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.matrixClient.removeListener("Room.myMembership", this.onRoom); this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData); this.matrixClient.removeListener("RoomState.events", this.onRoomState); + if (!SettingsStore.getValue("feature_spaces.all_rooms")) { + this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData); + this.matrixClient.removeListener("accountData", this.onAccountData); + } } await this.reset(); } @@ -550,6 +638,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.matrixClient.on("Room.myMembership", this.onRoom); this.matrixClient.on("Room.accountData", this.onRoomAccountData); this.matrixClient.on("RoomState.events", this.onRoomState); + if (!SettingsStore.getValue("feature_spaces.all_rooms")) { + this.matrixClient.on("Room.accountData", this.onRoomAccountData); + this.matrixClient.on("accountData", this.onAccountData); + } await this.onSpaceUpdate(); // trigger an initial update @@ -574,7 +666,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // Don't context switch when navigating to the space room // as it will cause you to end up in the wrong room this.setActiveSpace(room, false); - } else if (this.activeSpace && !this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId)) { + } else if ( + (!SettingsStore.getValue("feature_spaces.all_rooms") || this.activeSpace) && + !this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId) + ) { this.switchToRelatedSpace(roomId); } @@ -589,10 +684,16 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.setActiveSpace(null, false); } break; + case Action.SwitchSpace: + if (payload.num === 0) { + this.setActiveSpace(null); + } else if (this.spacePanelSpaces.length >= payload.num) { + this.setActiveSpace(this.spacePanelSpaces[payload.num - 1]); + } } } - public getNotificationState(key: string): SpaceNotificationState { + public getNotificationState(key: SpaceKey): SpaceNotificationState { if (this.notificationStateMap.has(key)) { return this.notificationStateMap.get(key); } diff --git a/src/stores/WidgetEchoStore.ts b/src/stores/WidgetEchoStore.ts index 09120d6108..0b0be50541 100644 --- a/src/stores/WidgetEchoStore.ts +++ b/src/stores/WidgetEchoStore.ts @@ -16,8 +16,8 @@ limitations under the License. import EventEmitter from 'events'; import { IWidget } from 'matrix-widget-api'; -import MatrixEvent from "matrix-js-sdk/src/models/event"; -import {WidgetType} from "../widgets/WidgetType"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { WidgetType } from "../widgets/WidgetType"; /** * Acts as a place to get & set widget state, storing local echo state and diff --git a/src/stores/room-list/SpaceWatcher.ts b/src/stores/room-list/SpaceWatcher.ts index 0b1b78bc75..a1f7786578 100644 --- a/src/stores/room-list/SpaceWatcher.ts +++ b/src/stores/room-list/SpaceWatcher.ts @@ -19,6 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { RoomListStoreClass } from "./RoomListStore"; import { SpaceFilterCondition } from "./filters/SpaceFilterCondition"; import SpaceStore, { UPDATE_SELECTED_SPACE } from "../SpaceStore"; +import SettingsStore from "../../settings/SettingsStore"; /** * Watches for changes in spaces to manage the filter on the provided RoomListStore @@ -28,6 +29,11 @@ export class SpaceWatcher { private activeSpace: Room = SpaceStore.instance.activeSpace; constructor(private store: RoomListStoreClass) { + if (!SettingsStore.getValue("feature_spaces.all_rooms")) { + this.filter = new SpaceFilterCondition(); + this.updateFilter(); + store.addFilter(this.filter); + } SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated); } @@ -35,7 +41,7 @@ export class SpaceWatcher { this.activeSpace = activeSpace; if (this.filter) { - if (activeSpace) { + if (activeSpace || !SettingsStore.getValue("feature_spaces.all_rooms")) { this.updateFilter(); } else { this.store.removeFilter(this.filter); @@ -49,9 +55,11 @@ export class SpaceWatcher { }; private updateFilter = () => { - SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => { - this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded(); - }); + if (this.activeSpace) { + SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => { + this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded(); + }); + } this.filter.updateSpace(this.activeSpace); }; } diff --git a/src/stores/room-list/filters/SpaceFilterCondition.ts b/src/stores/room-list/filters/SpaceFilterCondition.ts index 6a06bee0d8..0e6965d843 100644 --- a/src/stores/room-list/filters/SpaceFilterCondition.ts +++ b/src/stores/room-list/filters/SpaceFilterCondition.ts @@ -19,7 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition"; import { IDestroyable } from "../../../utils/IDestroyable"; -import SpaceStore from "../../SpaceStore"; +import SpaceStore, { HOME_SPACE } from "../../SpaceStore"; import { setHasDiff } from "../../../utils/sets"; /** @@ -29,7 +29,7 @@ import { setHasDiff } from "../../../utils/sets"; * + All DMs */ export class SpaceFilterCondition extends EventEmitter implements IFilterCondition, IDestroyable { - private roomIds = new Set(); + private roomIds = new Set(); private space: Room = null; public get kind(): FilterKind { @@ -55,12 +55,10 @@ export class SpaceFilterCondition extends EventEmitter implements IFilterConditi } }; - private getSpaceEventKey = (space: Room) => space.roomId; + private getSpaceEventKey = (space: Room | null) => space ? space.roomId : HOME_SPACE; public updateSpace(space: Room) { - if (this.space) { - SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate); - } + SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate); SpaceStore.instance.on(this.getSpaceEventKey(this.space = space), this.onStoreUpdate); this.onStoreUpdate(); // initial update from the change to the space } diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index 397d637125..6dcaf7abd7 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -51,7 +51,7 @@ import ThemeWatcher from "../../settings/watchers/ThemeWatcher"; import {getCustomTheme} from "../../theme"; import CountlyAnalytics from "../../CountlyAnalytics"; import { ElementWidgetCapabilities } from "./ElementWidgetCapabilities"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { MatrixEvent, IEvent } from "matrix-js-sdk/src/models/event"; import { ELEMENT_CLIENT_ID } from "../../identifiers"; import { getUserLanguage } from "../../languageHandler"; @@ -415,7 +415,7 @@ export class StopGapWidget extends EventEmitter { private feedEvent(ev: MatrixEvent) { if (!this.messaging) return; - const raw = ev.event; + const raw = ev.event as IEvent; this.messaging.feedEvent(raw).catch(e => { console.error("Error sending event to widget: ", e); }); diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index 25e81c47a2..5218e4a0bc 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -145,7 +145,7 @@ export class StopGapWidgetDriver extends WidgetDriver { return {roomId, eventId: r.event_id}; } - public async readRoomEvents(eventType: string, msgtype: string | undefined, limit: number): Promise { + public async readRoomEvents(eventType: string, msgtype: string | undefined, limit: number): Promise { limit = limit > 0 ? Math.min(limit, 25) : 25; // arbitrary choice const client = MatrixClientPeg.get(); @@ -167,9 +167,7 @@ export class StopGapWidgetDriver extends WidgetDriver { return results.map(e => e.event); } - public async readStateEvents( - eventType: string, stateKey: string | undefined, limit: number, - ): Promise { + public async readStateEvents(eventType: string, stateKey: string | undefined, limit: number): Promise { limit = limit > 0 ? Math.min(limit, 100) : 100; // arbitrary choice const client = MatrixClientPeg.get(); @@ -178,7 +176,7 @@ export class StopGapWidgetDriver extends WidgetDriver { if (!client || !roomId || !room) throw new Error("Not in a room or not attached to a client"); const results: MatrixEvent[] = []; - const state = room.currentState.events.get(eventType); + const state: Map = room.currentState.events.get(eventType); if (state) { if (stateKey === "" || !!stateKey) { const forKey = state.get(stateKey); diff --git a/src/toasts/UnverifiedSessionToast.ts b/src/toasts/UnverifiedSessionToast.ts index c856d39d1f..05425b93c0 100644 --- a/src/toasts/UnverifiedSessionToast.ts +++ b/src/toasts/UnverifiedSessionToast.ts @@ -21,7 +21,7 @@ import DeviceListener from '../DeviceListener'; import ToastStore from "../stores/ToastStore"; import GenericToast from "../components/views/toasts/GenericToast"; import { Action } from "../dispatcher/actions"; -import { USER_SECURITY_TAB } from "../components/views/dialogs/UserSettingsDialog"; +import { UserTab } from "../components/views/dialogs/UserSettingsDialog"; function toastKey(deviceId: string) { return "unverified_session_" + deviceId; @@ -34,7 +34,7 @@ export const showToast = async (deviceId: string) => { DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]); dis.dispatch({ action: Action.ViewUserSettings, - initialTabId: USER_SECURITY_TAB, + initialTabId: UserTab.Security, }); }; diff --git a/src/utils/DMRoomMap.ts b/src/utils/DMRoomMap.ts index b166674043..3e554f145d 100644 --- a/src/utils/DMRoomMap.ts +++ b/src/utils/DMRoomMap.ts @@ -14,11 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {MatrixClientPeg} from '../MatrixClientPeg'; -import {uniq} from "lodash"; -import {Room} from "matrix-js-sdk/src/models/room"; -import {Event} from "matrix-js-sdk/src/models/event"; -import {MatrixClient} from "matrix-js-sdk/src/client"; +import { uniq } from "lodash"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixClient } from "matrix-js-sdk/src/client"; + +import { MatrixClientPeg } from '../MatrixClientPeg'; /** * Class that takes a Matrix Client and flips the m.direct map @@ -30,15 +30,13 @@ import {MatrixClient} from "matrix-js-sdk/src/client"; export default class DMRoomMap { private static sharedInstance: DMRoomMap; - private matrixClient: MatrixClient; // TODO: convert these to maps private roomToUser: {[key: string]: string} = null; private userToRooms: {[key: string]: string[]} = null; private hasSentOutPatchDirectAccountDataPatch: boolean; - private mDirectEvent: Event; + private mDirectEvent: object; - constructor(matrixClient) { - this.matrixClient = matrixClient; + constructor(private readonly matrixClient: MatrixClient) { // see onAccountData this.hasSentOutPatchDirectAccountDataPatch = false; diff --git a/src/utils/ShieldUtils.ts b/src/utils/ShieldUtils.ts index 5fe653fed0..c855b81bf8 100644 --- a/src/utils/ShieldUtils.ts +++ b/src/utils/ShieldUtils.ts @@ -1,30 +1,31 @@ +/* +Copyright 2021 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 { MatrixClient } from "matrix-js-sdk/src/client"; +import { Room } from "matrix-js-sdk/src/models/room"; + import DMRoomMap from './DMRoomMap'; -/* For now, a cut-down type spec for the client */ -interface Client { - getUserId: () => string; - checkUserTrust: (userId: string) => { - isCrossSigningVerified: () => boolean - wasCrossSigningVerified: () => boolean - }; - getStoredDevicesForUser: (userId: string) => [{ deviceId: string }]; - checkDeviceTrust: (userId: string, deviceId: string) => { - isVerified: () => boolean - }; -} - -interface Room { - getEncryptionTargetMembers: () => Promise<[{userId: string}]>; - roomId: string; -} - export enum E2EStatus { Warning = "warning", Verified = "verified", Normal = "normal" } -export async function shieldStatusForRoom(client: Client, room: Room): Promise { +export async function shieldStatusForRoom(client: MatrixClient, room: Room): Promise { const members = (await room.getEncryptionTargetMembers()).map(({userId}) => userId); const inDMMap = !!DMRoomMap.shared().getUserIdForRoomId(room.roomId); diff --git a/src/utils/WidgetUtils.ts b/src/utils/WidgetUtils.ts index c67f3bad13..926278a20a 100644 --- a/src/utils/WidgetUtils.ts +++ b/src/utils/WidgetUtils.ts @@ -16,19 +16,20 @@ limitations under the License. */ import * as url from "url"; +import { Capability, IWidget, IWidgetData, MatrixCapabilities } from "matrix-widget-api"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import {MatrixClientPeg} from '../MatrixClientPeg'; +import { MatrixClientPeg } from '../MatrixClientPeg'; import SdkConfig from "../SdkConfig"; import dis from '../dispatcher/dispatcher'; import WidgetEchoStore from '../stores/WidgetEchoStore'; import SettingsStore from "../settings/SettingsStore"; -import {IntegrationManagers} from "../integrations/IntegrationManagers"; -import {Room} from "matrix-js-sdk/src/models/room"; -import {WidgetType} from "../widgets/WidgetType"; -import {objectClone} from "./objects"; -import {_t} from "../languageHandler"; -import {Capability, IWidget, IWidgetData, MatrixCapabilities} from "matrix-widget-api"; -import {IApp} from "../stores/WidgetStore"; +import { IntegrationManagers } from "../integrations/IntegrationManagers"; +import { WidgetType } from "../widgets/WidgetType"; +import { objectClone } from "./objects"; +import { _t } from "../languageHandler"; +import { IApp } from "../stores/WidgetStore"; // How long we wait for the state event echo to come back from the server // before waitFor[Room/User]Widget rejects its promise @@ -377,9 +378,9 @@ export default class WidgetUtils { return widgets.filter(w => w.content && w.content.type === "m.integration_manager"); } - static getRoomWidgetsOfType(room: Room, type: WidgetType): IWidgetEvent[] { - const widgets = WidgetUtils.getRoomWidgets(room); - return (widgets || []).filter(w => { + static getRoomWidgetsOfType(room: Room, type: WidgetType): MatrixEvent[] { + const widgets = WidgetUtils.getRoomWidgets(room) || []; + return widgets.filter(w => { const content = w.getContent(); return content.url && type.matches(content.type); }); @@ -392,7 +393,7 @@ export default class WidgetUtils { } const widgets = client.getAccountData('m.widgets'); if (!widgets) return; - const userWidgets: IWidgetEvent[] = widgets.getContent() || {}; + const userWidgets: Record = widgets.getContent() || {}; Object.entries(userWidgets).forEach(([key, widget]) => { if (widget.content && widget.content.type === "m.integration_manager") { delete userWidgets[key]; diff --git a/test/DecryptionFailureTracker-test.js b/test/DecryptionFailureTracker-test.js index 7a6a42ef55..bc751ba44e 100644 --- a/test/DecryptionFailureTracker-test.js +++ b/test/DecryptionFailureTracker-test.js @@ -30,9 +30,7 @@ function createFailedDecryptionEvent() { const event = new MatrixEvent({ event_id: "event-id-" + Math.random().toString(16).slice(2), }); - event._setClearData( - event._badEncryptedMessage(":("), - ); + event.setClearData(event.badEncryptedMessage(":(")); return event; } @@ -67,7 +65,7 @@ describe('DecryptionFailureTracker', function() { tracker.eventDecrypted(decryptedEvent, err); // Indicate successful decryption: clear data can be anything where the msgtype is not m.bad.encrypted - decryptedEvent._setClearData({}); + decryptedEvent.setClearData({}); tracker.eventDecrypted(decryptedEvent, null); // Pretend "now" is Infinity diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index 8f0242eb30..d32970a278 100644 --- a/test/components/structures/MessagePanel-test.js +++ b/test/components/structures/MessagePanel-test.js @@ -42,7 +42,7 @@ import DMRoomMap from "../../../src/utils/DMRoomMap"; configure({ adapter: new Adapter() }); let client; -const room = new Matrix.Room(); +const room = new Matrix.Room("!roomId:server_name"); // wrap MessagePanel with a component which provides the MatrixClient in the context. class WrappedMessagePanel extends React.Component { diff --git a/test/end-to-end-tests/src/scenarios/e2e-encryption.js b/test/end-to-end-tests/src/scenarios/e2e-encryption.js index 20e8af2947..b20874fdaf 100644 --- a/test/end-to-end-tests/src/scenarios/e2e-encryption.js +++ b/test/end-to-end-tests/src/scenarios/e2e-encryption.js @@ -20,9 +20,11 @@ const acceptInvite = require('../usecases/accept-invite'); const {receiveMessage} = require('../usecases/timeline'); const {createDm} = require('../usecases/create-room'); const {checkRoomSettings} = require('../usecases/room-settings'); -const {startSasVerifcation, acceptSasVerification} = require('../usecases/verify'); +const {startSasVerification, acceptSasVerification} = require('../usecases/verify'); const { setupSecureBackup } = require('../usecases/security'); const assert = require('assert'); +const { measureStart, measureStop } = require('../util'); + module.exports = async function e2eEncryptionScenarios(alice, bob) { console.log(" creating an e2e encrypted DM and join through invite:"); @@ -31,12 +33,14 @@ module.exports = async function e2eEncryptionScenarios(alice, bob) { await acceptInvite(alice, 'bob'); // do sas verifcation bob.log.step(`starts SAS verification with ${alice.username}`); - const bobSasPromise = startSasVerifcation(bob, alice.username); + await measureStart(bob, "mx_VerifyE2EEUser"); + const bobSasPromise = startSasVerification(bob, alice.username); const aliceSasPromise = acceptSasVerification(alice, bob.username); // wait in parallel, so they don't deadlock on each other // the logs get a bit messy here, but that's fine enough for debugging (hopefully) const [bobSas, aliceSas] = await Promise.all([bobSasPromise, aliceSasPromise]); assert.deepEqual(bobSas, aliceSas); + await measureStop(bob, "mx_VerifyE2EEUser"); bob.log.done(`done (match for ${bobSas.join(", ")})`); const aliceMessage = "Guess what I just heard?!"; await sendMessage(alice, aliceMessage); diff --git a/test/end-to-end-tests/src/usecases/create-room.js b/test/end-to-end-tests/src/usecases/create-room.js index 3830e3e0da..36b9ed21ec 100644 --- a/test/end-to-end-tests/src/usecases/create-room.js +++ b/test/end-to-end-tests/src/usecases/create-room.js @@ -15,6 +15,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +const { measureStart, measureStop } = require('../util'); + async function openRoomDirectory(session) { const roomDirectoryButton = await session.query('.mx_LeftPanel_exploreButton'); await roomDirectoryButton.click(); @@ -52,6 +54,8 @@ async function createRoom(session, roomName, encrypted=false) { async function createDm(session, invitees) { session.log.step(`creates DM with ${JSON.stringify(invitees)}`); + await measureStart(session, "mx_CreateDM"); + const dmsSublist = await findSublist(session, "people"); const startChatButton = await dmsSublist.$(".mx_RoomSublist_auxButton"); await startChatButton.click(); @@ -76,6 +80,8 @@ async function createDm(session, invitees) { await session.query('.mx_MessageComposer'); session.log.done(); + + await measureStop(session, "mx_CreateDM"); } module.exports = {openRoomDirectory, findSublist, createRoom, createDm}; diff --git a/test/end-to-end-tests/src/usecases/join.js b/test/end-to-end-tests/src/usecases/join.js index 655c0be686..cf0f67be44 100644 --- a/test/end-to-end-tests/src/usecases/join.js +++ b/test/end-to-end-tests/src/usecases/join.js @@ -16,9 +16,12 @@ limitations under the License. */ const {openRoomDirectory} = require('./create-room'); +const { measureStart, measureStop } = require('../util'); + module.exports = async function join(session, roomName) { session.log.step(`joins room "${roomName}"`); + await measureStart(session, "mx_JoinRoom"); await openRoomDirectory(session); const roomInput = await session.query('.mx_DirectorySearchBox input'); await session.replaceInputText(roomInput, roomName); @@ -26,5 +29,6 @@ module.exports = async function join(session, roomName) { const joinFirstLink = await session.query('.mx_RoomDirectory_table .mx_RoomDirectory_join .mx_AccessibleButton'); await joinFirstLink.click(); await session.query('.mx_MessageComposer'); + await measureStop(session, "mx_JoinRoom"); session.log.done(); }; diff --git a/test/end-to-end-tests/src/usecases/verify.js b/test/end-to-end-tests/src/usecases/verify.js index ea5b9961a4..a66c8c1b1c 100644 --- a/test/end-to-end-tests/src/usecases/verify.js +++ b/test/end-to-end-tests/src/usecases/verify.js @@ -74,7 +74,7 @@ async function doSasVerification(session) { return sasCodes; } -module.exports.startSasVerifcation = async function(session, name) { +module.exports.startSasVerification = async function(session, name) { session.log.startGroup("starts verification"); await startVerification(session, name); diff --git a/test/end-to-end-tests/src/util.js b/test/end-to-end-tests/src/util.js index cc7391fa9f..5abb110df4 100644 --- a/test/end-to-end-tests/src/util.js +++ b/test/end-to-end-tests/src/util.js @@ -26,3 +26,15 @@ module.exports.range = function(start, amount, step = 1) { module.exports.delay = function(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }; + +module.exports.measureStart = function(session, name) { + return session.page.evaluate(_name => { + window.mxPerformanceMonitor.start(_name); + }, name); +}; + +module.exports.measureStop = function(session, name) { + return session.page.evaluate(_name => { + window.mxPerformanceMonitor.stop(_name); + }, name); +}; diff --git a/test/end-to-end-tests/start.js b/test/end-to-end-tests/start.js index f29b485c84..04df0c51c0 100644 --- a/test/end-to-end-tests/start.js +++ b/test/end-to-end-tests/start.js @@ -79,7 +79,7 @@ async function runTests() { await new Promise((resolve) => setTimeout(resolve, 5 * 60 * 1000)); } - const performanceEntries = {}; + let performanceEntries; await Promise.all(sessions.map(async (session) => { // Collecting all performance monitoring data before closing the session @@ -88,6 +88,10 @@ async function runTests() { window.mxPerformanceMonitor.addPerformanceDataCallback({ entryNames: [ window.mxPerformanceEntryNames.REGISTER, + window.mxPerformanceEntryNames.LOGIN, + window.mxPerformanceEntryNames.JOIN_ROOM, + window.mxPerformanceEntryNames.CREATE_DM, + window.mxPerformanceEntryNames.VERIFY_E2EE_USER, ], callback: (events) => { measurements = JSON.stringify(events); @@ -95,7 +99,11 @@ async function runTests() { }, true); return measurements; }); - performanceEntries[session.username] = JSON.parse(measurements); + + /** + * TODO: temporary only use one user session data + */ + performanceEntries = JSON.parse(measurements); return session.close(); })); fs.writeFileSync(`performance-entries.json`, JSON.stringify(performanceEntries)); diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index 01bd528b87..4cbd9f43c8 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -123,8 +123,15 @@ describe("SpaceStore", () => { jest.runAllTimers(); client.getVisibleRooms.mockReturnValue(rooms = []); getValue.mockImplementation(settingName => { - if (settingName === "feature_spaces") { - return true; + switch (settingName) { + case "feature_spaces": + return true; + case "feature_spaces.all_rooms": + return true; + case "feature_spaces.space_member_dms": + return true; + case "feature_spaces.space_dm_badges": + return false; } }); }); diff --git a/yarn.lock b/yarn.lock index 289d33088f..b19a188014 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1017,20 +1017,13 @@ pirates "^4.0.0" source-map-support "^0.5.16" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.12.1", "@babel/runtime@^7.9.2": - version "7.14.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.0.tgz#46794bc20b612c5f75e62dd071e24dfd95f1cbe6" - integrity sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA== - dependencies: - regenerator-runtime "^0.13.4" - "@babel/template@^7.10.4", "@babel/template@^7.12.7", "@babel/template@^7.3.3": version "7.12.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc" @@ -1511,14 +1504,6 @@ dependencies: "@types/node" "*" -"@types/hoist-non-react-statics@^3.3.0": - version "3.3.1" - resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" - integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== - dependencies: - "@types/react" "*" - hoist-non-react-statics "^3.3.0" - "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762" @@ -1635,30 +1620,13 @@ dependencies: "@types/node" "*" -"@types/react-beautiful-dnd@^13.0.0": - version "13.0.0" - resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#e60d3d965312fcf1516894af92dc3e9249587db4" - integrity sha512-by80tJ8aTTDXT256Gl+RfLRtFjYbUWOnZuEigJgNsJrSEGxvFe5eY6k3g4VIvf0M/6+xoLgfYWoWonlOo6Wqdg== +"@types/react-dom@^17.0.2": + version "17.0.8" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.8.tgz#3180de6d79bf53762001ad854e3ce49f36dd71fc" + integrity sha512-0ohAiJAx1DAUEcY9UopnfwCE9sSMDGnY/oXjWMax6g3RpzmTt2GMyMVAXcbn0mo8XAff0SbQJl2/SBU+hjSZ1A== dependencies: "@types/react" "*" -"@types/react-dom@^16.9.10": - version "16.9.10" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.10.tgz#4485b0bec3d41f856181b717f45fd7831101156f" - integrity sha512-ItatOrnXDMAYpv6G8UCk2VhbYVTjZT9aorLtA/OzDN9XJ2GKcfam68jutoAcILdRjsRUO8qb7AmyObF77Q8QFw== - dependencies: - "@types/react" "^16" - -"@types/react-redux@^7.1.16": - version "7.1.16" - resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.16.tgz#0fbd04c2500c12105494c83d4a3e45c084e3cb21" - integrity sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw== - dependencies: - "@types/hoist-non-react-statics" "^3.3.0" - "@types/react" "*" - hoist-non-react-statics "^3.3.0" - redux "^4.0.0" - "@types/react-transition-group@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.0.tgz#882839db465df1320e4753e6e9f70ca7e9b4d46d" @@ -1666,12 +1634,13 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^16", "@types/react@^16.14", "@types/react@^16.9": - version "16.14.2" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.2.tgz#85dcc0947d0645349923c04ccef6018a1ab7538c" - integrity sha512-BzzcAlyDxXl2nANlabtT4thtvbbnhee8hMmH/CcJrISDBVcJS1iOsP1f0OAgSdGE0MsY9tqcrb9YoZcOFv9dbQ== +"@types/react@*", "@types/react@^17.0.2": + version "17.0.11" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.11.tgz#67fcd0ddbf5a0b083a0f94e926c7d63f3b836451" + integrity sha512-yFRQbD+whVonItSk7ZzP/L+gPTJVBkL/7shLEF+i9GC/1cV3JmUxEQz6+9ylhUpWSDuqo1N9qEvqS6vTj4USUA== dependencies: "@types/prop-types" "*" + "@types/scheduler" "*" csstype "^3.0.2" "@types/sanitize-html@^2.3.1": @@ -1681,6 +1650,11 @@ dependencies: htmlparser2 "^6.0.0" +"@types/scheduler@*": + version "0.16.1" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275" + integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA== + "@types/stack-utils@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" @@ -2148,6 +2122,14 @@ babel-preset-jest@^26.6.2: babel-plugin-jest-hoist "^26.6.2" babel-preset-current-node-syntax "^1.0.0" +babel-runtime@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" + integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.11.0" + bail@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776" @@ -2666,6 +2648,11 @@ core-js@^1.0.0: resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY= +core-js@^2.4.0: + version "2.6.12" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" + integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== + core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -2725,13 +2712,6 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2: shebang-command "^2.0.0" which "^2.0.1" -css-box-model@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" - integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== - dependencies: - tiny-invariant "^1.0.6" - css-select@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.2.tgz#8b52b6714ed3a80d8221ec971c543f3b12653286" @@ -4261,7 +4241,7 @@ highlight.js@^10.5.0: resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.5.0.tgz#3f09fede6a865757378f2d9ebdcbc15ba268f98f" integrity sha512-xTmvd9HiIHR6L53TMC7TKolEj65zG1XU+Onr8oi86mYa+nLcIbxTTWkpW7CsEwv/vK7u1zb8alZIMLDqqN6KTw== -hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.3.0: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -4476,6 +4456,13 @@ internal-slot@^1.0.2: has "^1.0.3" side-channel "^1.0.2" +invariant@^2.2.2, invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + ip-regex@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" @@ -5613,6 +5600,11 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lodash-es@^4.2.1: + version "4.17.20" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.20.tgz#29f6332eefc60e849f869c264bc71126ad61e8f7" + integrity sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA== + lodash.escape@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98" @@ -5633,7 +5625,7 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= -lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20: +lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.2.1: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -5725,9 +5717,10 @@ mathml-tag-names@^2.1.3: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": - version "11.2.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/35ecbed29d16982deff27a8c37b05167738225a2" +matrix-js-sdk@12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-12.0.0.tgz#8ee7cc37661476341d0c792a1a12bc78b19f9fdd" + integrity sha512-DHeq87Sx9Dv37FYyvZkmA1VYsQUNaVgc3QzMUkFwoHt1T4EZzgyYpdsp3uYruJzUW0ACvVJcwFdrU4e1VS97dQ== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" @@ -5747,10 +5740,10 @@ matrix-mock-request@^1.2.3: bluebird "^3.5.0" expect "^1.20.2" -matrix-react-test-utils@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.2.tgz#c87144d3b910c7edc544a6699d13c7c2bf02f853" - integrity sha512-49+7gfV6smvBIVbeloql+37IeWMTD+fiywalwCqk8Dnz53zAFjKSltB3rmWHso1uecLtQEcPtCijfhzcLXAxTQ== +matrix-react-test-utils@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.3.tgz#27653f9d6bbfddd1856e51860fad1503b039d617" + integrity sha512-NKZDlMEQzDZDQhBYyKBUtqidRvpkww3n9/GmGICkxtU2D6NetyBIfvm1Lf9o7167KSkPHJUVvDS9dzaS55jUnA== "matrix-web-i18n@github:matrix-org/matrix-web-i18n": version "1.1.2" @@ -5801,10 +5794,10 @@ mdurl@~1.0.1: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= -memoize-one@^5.1.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" - integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== +memoize-one@^3.0.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-3.1.1.tgz#ef609811e3bc28970eac2884eece64d167830d17" + integrity sha512-YqVh744GsMlZu6xkhGslPSqSurOv6P+kLN2J3ysBZfagLcL5FdRK/0UpgLoL8hwjjEvvAVkjJZyFP+1T6p1vgA== meow@^9.0.0: version "9.0.0" @@ -6450,6 +6443,11 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +performance-now@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" + integrity sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU= + performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" @@ -6592,9 +6590,9 @@ postcss-value-parser@^4.1.0: integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== postcss@^7.0.14, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0.32, postcss@^7.0.35, postcss@^7.0.6: - version "7.0.35" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24" - integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg== + version "7.0.36" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.36.tgz#056f8cffa939662a8f5905950c07d5285644dfcb" + integrity sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw== dependencies: chalk "^2.4.2" source-map "^0.6.1" @@ -6659,7 +6657,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.6.2, prop-types@^15.7.0, prop-types@^15.7.2: +prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.0, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -6736,12 +6734,12 @@ quick-lru@^4.0.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== -raf-schd@^4.0.2: - version "4.0.3" - resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" - integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== +raf-schd@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-2.1.2.tgz#ec622b5167f2912089f054dc03ebd5bcf33c8f62" + integrity sha512-Orl0IEvMtUCgPddgSxtxreK77UiQz4nPYJy9RggVzu4mKsZkQWiAaG1y9HlYWdvm9xtN348xRaT37qkvL/+A+g== -raf@^3.4.1: +raf@^3.1.0, raf@^3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== @@ -6768,18 +6766,21 @@ re-resizable@^6.9.0: dependencies: fast-memoize "^2.5.1" -react-beautiful-dnd@^13.1.0: - version "13.1.0" - resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz#ec97c81093593526454b0de69852ae433783844d" - integrity sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA== +react-beautiful-dnd@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-4.0.1.tgz#3b0a49bf6be75af351176c904f012611dd292b81" + integrity sha512-d73RMu4QOFCyjUELLWFyY/EuclnfqulI9pECx+2gIuJvV0ycf1uR88o+1x0RSB9ILD70inHMzCBKNkWVbbt+vA== dependencies: - "@babel/runtime" "^7.9.2" - css-box-model "^1.2.0" - memoize-one "^5.1.1" - raf-schd "^4.0.2" - react-redux "^7.2.0" - redux "^4.0.4" - use-memo-one "^1.1.1" + babel-runtime "^6.26.0" + invariant "^2.2.2" + memoize-one "^3.0.1" + prop-types "^15.6.0" + raf-schd "^2.1.0" + react-motion "^0.5.2" + react-redux "^5.0.6" + redux "^3.7.2" + redux-thunk "^2.2.0" + reselect "^3.0.1" react-clientside-effect@^1.2.2: version "1.2.3" @@ -6814,7 +6815,7 @@ react-focus-lock@^2.5.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1: +react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -6824,17 +6825,32 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== -react-redux@^7.2.0: - version "7.2.4" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225" - integrity sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA== +react-lifecycles-compat@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + +react-motion@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316" + integrity sha512-9q3YAvHoUiWlP3cK0v+w1N5Z23HXMj4IF4YuvjvWegWqNPfLXsOBE/V7UvQGpXxHFKRQQcNcVQE31g9SB/6qgQ== dependencies: - "@babel/runtime" "^7.12.1" - "@types/react-redux" "^7.1.16" - hoist-non-react-statics "^3.3.2" - loose-envify "^1.4.0" - prop-types "^15.7.2" - react-is "^16.13.1" + performance-now "^0.2.0" + prop-types "^15.5.8" + raf "^3.1.0" + +react-redux@^5.0.6: + version "5.1.2" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.1.2.tgz#b19cf9e21d694422727bf798e934a916c4080f57" + integrity sha512-Ns1G0XXc8hDyH/OcBHOxNgQx9ayH3SPxBnFCOidGKSle8pKihysQw2rG/PmciUQRoclhVBO8HMhiRmGXnDja9Q== + dependencies: + "@babel/runtime" "^7.1.2" + hoist-non-react-statics "^3.3.0" + invariant "^2.2.4" + loose-envify "^1.1.0" + prop-types "^15.6.1" + react-is "^16.6.0" + react-lifecycles-compat "^3.0.0" react-shallow-renderer@^16.13.1: version "16.14.1" @@ -6963,12 +6979,20 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" -redux@^4.0.0, redux@^4.0.4: - version "4.1.0" - resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.0.tgz#eb049679f2f523c379f1aff345c8612f294c88d4" - integrity sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g== +redux-thunk@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" + integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== + +redux@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b" + integrity sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A== dependencies: - "@babel/runtime" "^7.9.2" + lodash "^4.2.1" + lodash-es "^4.2.1" + loose-envify "^1.1.0" + symbol-observable "^1.0.3" regenerate-unicode-properties@^8.2.0: version "8.2.0" @@ -6982,6 +7006,11 @@ regenerate@^1.4.0: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== +regenerator-runtime@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" + integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== + regenerator-runtime@^0.13.4: version "0.13.7" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" @@ -7139,6 +7168,11 @@ require-main-filename@^2.0.0: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== +reselect@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-3.0.1.tgz#efdaa98ea7451324d092b2b2163a6a1d7a9a2147" + integrity sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc= + resize-observer-polyfill@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" @@ -7861,6 +7895,11 @@ svg-tags@^1.0.0: resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764" integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q= +symbol-observable@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" + integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== + symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" @@ -7923,11 +7962,6 @@ through@^2.3.6: resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= -tiny-invariant@^1.0.6: - version "1.1.0" - resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" - integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== - tmatch@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/tmatch/-/tmatch-2.0.1.tgz#0c56246f33f30da1b8d3d72895abaf16660f38cf" @@ -8243,11 +8277,6 @@ use-callback-ref@^1.2.1: resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.5.tgz#6115ed242cfbaed5915499c0a9842ca2912f38a5" integrity sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg== -use-memo-one@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20" - integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ== - use-sidecar@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.0.4.tgz#38398c3723727f9f924bed2343dfa3db6aaaee46"