diff --git a/.eslintrc.js b/.eslintrc.js
index 67e6ab1e642..f3e38f94c3c 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -86,6 +86,8 @@ module.exports = {
"jsx-a11y/no-static-element-interactions": "off",
"jsx-a11y/role-supports-aria-props": "off",
"jsx-a11y/tabindex-no-positive": "off",
+
+ "matrix-org/require-copyright-header": "error",
},
overrides: [
{
diff --git a/.github/workflows/element-build-and-test.yaml b/.github/workflows/element-build-and-test.yaml
index 9b3d0f373a0..a1047094158 100644
--- a/.github/workflows/element-build-and-test.yaml
+++ b/.github/workflows/element-build-and-test.yaml
@@ -67,14 +67,20 @@ jobs:
- name: Run Cypress tests
uses: cypress-io/github-action@v2
with:
- # The built in Electron runner seems to grind to a halt trying
+ # The built-in Electron runner seems to grind to a halt trying
# to run the tests, so use chrome.
browser: chrome
start: npx serve -p 8080 webapp
record: true
+ command-prefix: 'yarn percy exec --'
env:
# pass the Dashboard record key as an environment variable
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
+ # pass the Percy token as an environment variable
+ PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
+ # Use existing chromium rather than downloading another
+ PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
+ PERCY_BROWSER_EXECUTABLE: /usr/bin/chromium-browser
# pass GitHub token to allow accurately detecting a build vs a re-run build
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -88,6 +94,20 @@ jobs:
cypress/videos
cypress/synapselogs
+ - name: Store benchmark result
+ if: github.ref == 'refs/heads/develop'
+ uses: matrix-org/github-action-benchmark@jsperfentry-1
+ with:
+ name: Cypress measurements
+ tool: 'jsperformanceentry'
+ output-file-path: cypress/performance/measurements.json
+ # The dashboard is available at https://matrix-org.github.io/matrix-react-sdk/cypress/bench/
+ benchmark-data-dir-path: cypress/bench
+ fail-on-alert: false
+ comment-on-alert: false
+ github-token: ${{ secrets.DEPLOY_GH_PAGES }}
+ auto-push: ${{ github.ref == 'refs/heads/develop' }}
+
app-tests:
name: Element Web Integration Tests
runs-on: ubuntu-latest
diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml
index 22a92bf0b56..8d115062ea6 100644
--- a/.github/workflows/pull_request.yaml
+++ b/.github/workflows/pull_request.yaml
@@ -1,10 +1,11 @@
name: Pull Request
on:
pull_request_target:
- types: [ opened, edited, labeled, unlabeled ]
+ types: [ opened, edited, labeled, unlabeled, synchronize ]
jobs:
changelog:
name: Preview Changelog
+ if: github.event.action != 'synchronize'
runs-on: ubuntu-latest
steps:
- uses: matrix-org/allchange@main
diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml
index 7029be97f3b..95b06bab6b5 100644
--- a/.github/workflows/sonarqube.yml
+++ b/.github/workflows/sonarqube.yml
@@ -4,44 +4,34 @@ on:
workflows: [ "Tests" ]
types:
- completed
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
jobs:
- sonarqube:
- name: SonarQube
- runs-on: ubuntu-latest
- if: github.event.workflow_run.conclusion == 'success'
- steps:
- - uses: actions/checkout@v2
- with:
- fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
-
- # There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
- # (/~https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
- - name: Download Coverage Report
- uses: actions/github-script@v3.1.0
- with:
- script: |
- const artifacts = await github.actions.listWorkflowRunArtifacts({
- owner: context.repo.owner,
- repo: context.repo.repo,
- run_id: ${{ github.event.workflow_run.id }},
- });
- const matchArtifact = artifacts.data.artifacts.filter((artifact) => {
- return artifact.name == "coverage"
- })[0];
- const download = await github.actions.downloadArtifact({
- owner: context.repo.owner,
- repo: context.repo.repo,
- artifact_id: matchArtifact.id,
- archive_format: 'zip',
- });
- const fs = require('fs');
- fs.writeFileSync('${{github.workspace}}/coverage.zip', Buffer.from(download.data));
+ prdetails:
+ name: ℹ️ PR Details
+ if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
+ uses: matrix-org/matrix-js-sdk/.github/workflows/pr_details.yml@develop
+ with:
+ owner: ${{ github.event.workflow_run.head_repository.owner.login }}
+ branch: ${{ github.event.workflow_run.head_branch }}
- - name: Extract Coverage Report
- run: unzip -d coverage coverage.zip && rm coverage.zip
-
- - name: SonarCloud Scan
- uses: SonarSource/sonarcloud-github-action@master
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ sonarqube:
+ name: 🩻 SonarQube
+ needs: prdetails
+ # Only wait for prdetails if it isn't skipped
+ if: |
+ always() &&
+ (needs.prdetails.result == 'success' || needs.prdetails.result == 'skipped') &&
+ github.event.workflow_run.conclusion == 'success'
+ uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
+ with:
+ repo: ${{ github.event.workflow_run.head_repository.full_name }}
+ pr_id: ${{ needs.prdetails.outputs.pr_id }}
+ head_branch: ${{ needs.prdetails.outputs.head_branch || github.event.workflow_run.head_branch }}
+ base_branch: ${{ needs.prdetails.outputs.base_branch }}
+ revision: ${{ github.event.workflow_run.head_sha }}
+ coverage_workflow_name: tests.yml
+ coverage_run_id: ${{ github.event.workflow_run.id }}
+ secrets:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
diff --git a/.github/workflows/static_analysis.yaml b/.github/workflows/static_analysis.yaml
index 5e2f27f68b8..266f7c728ad 100644
--- a/.github/workflows/static_analysis.yaml
+++ b/.github/workflows/static_analysis.yaml
@@ -47,7 +47,7 @@ jobs:
- name: "Get modified files"
id: changed_files
- if: github.event_name == 'pull_request'
+ if: github.event_name == 'pull_request' && github.actor != 'RiotTranslateBot'
uses: tj-actions/changed-files@v19
with:
files: |
@@ -56,7 +56,10 @@ jobs:
src/i18n/strings/en_EN.json
- name: "Assert only en_EN was modified"
- if: github.event_name == 'pull_request' && steps.changed_files.outputs.any_modified == 'true'
+ if: |
+ github.event_name == 'pull_request' &&
+ github.actor != 'RiotTranslateBot' &&
+ steps.changed_files.outputs.any_modified == 'true'
run: |
echo "You can only modify en_EN.json, do not touch any of the other i18n files as Weblate will be confused"
exit 1
diff --git a/.github/workflows/upgrade_dependencies.yml b/.github/workflows/upgrade_dependencies.yml
new file mode 100644
index 00000000000..a4a0fedc0d9
--- /dev/null
+++ b/.github/workflows/upgrade_dependencies.yml
@@ -0,0 +1,8 @@
+name: Upgrade Dependencies
+on:
+ workflow_dispatch: { }
+jobs:
+ upgrade:
+ uses: matrix-org/matrix-js-sdk/.github/workflows/upgrade_dependencies.yml@develop
+ secrets:
+ ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 8dcb983ec84..1c4eb114e7d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,3 +28,4 @@ package-lock.json
# These could have files in them but don't currently
# Cypress will still auto-create them though...
/cypress/fixtures
+/cypress/performance
diff --git a/.percy.yml b/.percy.yml
new file mode 100644
index 00000000000..e50f0b0dbba
--- /dev/null
+++ b/.percy.yml
@@ -0,0 +1,5 @@
+version: 2
+snapshot:
+ widths:
+ - 1024
+ - 1920
diff --git a/CHANGELOG.md b/CHANGELOG.md
index da198272a24..d6ad5a02863 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,93 @@
+Changes in [3.45.0](/~https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.45.0) (2022-05-24)
+=====================================================================================================
+
+## ✨ Features
+ * Go to space landing page when clicking on a selected space ([\#6442](/~https://github.com/matrix-org/matrix-react-sdk/pull/6442)). Fixes vector-im/element-web#20296.
+ * Fall back to untranslated string rather than showing missing translation error ([\#8609](/~https://github.com/matrix-org/matrix-react-sdk/pull/8609)).
+ * Show file name and size on images on hover ([\#6511](/~https://github.com/matrix-org/matrix-react-sdk/pull/6511)). Fixes vector-im/element-web#18197.
+ * Iterate on search results for message bubbles ([\#7047](/~https://github.com/matrix-org/matrix-react-sdk/pull/7047)). Fixes vector-im/element-web#20315.
+ * registration: redesign email verification page ([\#8554](/~https://github.com/matrix-org/matrix-react-sdk/pull/8554)). Fixes vector-im/element-web#21984.
+ * Show full thread message in hover title on thread summary ([\#8568](/~https://github.com/matrix-org/matrix-react-sdk/pull/8568)). Fixes vector-im/element-web#22037.
+ * Tweak video rooms copy ([\#8582](/~https://github.com/matrix-org/matrix-react-sdk/pull/8582)). Fixes vector-im/element-web#22176.
+ * Live location share - beacon tooltip in maximised view ([\#8572](/~https://github.com/matrix-org/matrix-react-sdk/pull/8572)).
+ * Add dialog to navigate long room topics ([\#8517](/~https://github.com/matrix-org/matrix-react-sdk/pull/8517)). Fixes vector-im/element-web#9623.
+ * Change spaceroomfacepile tooltip if memberlist is shown ([\#8571](/~https://github.com/matrix-org/matrix-react-sdk/pull/8571)). Fixes vector-im/element-web#17406.
+ * Improve message editing UI ([\#8483](/~https://github.com/matrix-org/matrix-react-sdk/pull/8483)). Fixes vector-im/element-web#9752 and vector-im/element-web#22108.
+ * Make date changes more obvious ([\#6410](/~https://github.com/matrix-org/matrix-react-sdk/pull/6410)). Fixes vector-im/element-web#16221.
+ * Enable forwarding static locations ([\#8553](/~https://github.com/matrix-org/matrix-react-sdk/pull/8553)).
+ * Log `TimelinePanel` debugging info when opening the bug report modal ([\#8502](/~https://github.com/matrix-org/matrix-react-sdk/pull/8502)).
+ * Improve welcome screen, add opt-out analytics ([\#8474](/~https://github.com/matrix-org/matrix-react-sdk/pull/8474)). Fixes vector-im/element-web#21946.
+ * Converting selected text to MD link when pasting a URL ([\#8242](/~https://github.com/matrix-org/matrix-react-sdk/pull/8242)). Fixes vector-im/element-web#21634. Contributed by @Sinharitik589.
+ * Support Inter on custom themes ([\#8399](/~https://github.com/matrix-org/matrix-react-sdk/pull/8399)). Fixes vector-im/element-web#16293.
+ * Add a `Copy link` button to the right-click message context-menu labs feature ([\#8527](/~https://github.com/matrix-org/matrix-react-sdk/pull/8527)).
+ * Move widget screenshots labs flag to devtools ([\#8522](/~https://github.com/matrix-org/matrix-react-sdk/pull/8522)).
+ * Remove some labs features which don't get used or create maintenance burden: custom status, multiple integration managers, and do not disturb ([\#8521](/~https://github.com/matrix-org/matrix-react-sdk/pull/8521)).
+ * Add a way to toggle `ScrollPanel` and `TimelinePanel` debug logs ([\#8513](/~https://github.com/matrix-org/matrix-react-sdk/pull/8513)).
+ * Spaces: remove blue beta dot ([\#8511](/~https://github.com/matrix-org/matrix-react-sdk/pull/8511)). Fixes vector-im/element-web#22061.
+ * Order new search dialog results by recency ([\#8444](/~https://github.com/matrix-org/matrix-react-sdk/pull/8444)).
+ * Improve pills ([\#6398](/~https://github.com/matrix-org/matrix-react-sdk/pull/6398)). Fixes vector-im/element-web#16948 and vector-im/element-web#21281.
+ * Add a way to maximize/pin widget from the PiP view ([\#7672](/~https://github.com/matrix-org/matrix-react-sdk/pull/7672)). Fixes vector-im/element-web#20723.
+ * Iterate video room designs in labs ([\#8499](/~https://github.com/matrix-org/matrix-react-sdk/pull/8499)).
+ * Improve UI/UX in calls ([\#7791](/~https://github.com/matrix-org/matrix-react-sdk/pull/7791)). Fixes vector-im/element-web#19937.
+ * Add ability to change audio and video devices during a call ([\#7173](/~https://github.com/matrix-org/matrix-react-sdk/pull/7173)). Fixes vector-im/element-web#15595.
+
+## 🐛 Bug Fixes
+ * Fix click behavior of notification badges on spaces ([\#8627](/~https://github.com/matrix-org/matrix-react-sdk/pull/8627)). Fixes vector-im/element-web#22241.
+ * Add missing return values in Read Receipt animation code ([\#8625](/~https://github.com/matrix-org/matrix-react-sdk/pull/8625)). Fixes vector-im/element-web#22175.
+ * Fix 'continue' button not working after accepting identity server terms of service ([\#8619](/~https://github.com/matrix-org/matrix-react-sdk/pull/8619)). Fixes vector-im/element-web#20003.
+ * Proactively fix stuck devices in video rooms ([\#8587](/~https://github.com/matrix-org/matrix-react-sdk/pull/8587)). Fixes vector-im/element-web#22131.
+ * Fix position of the message action bar on left side bubbles ([\#8398](/~https://github.com/matrix-org/matrix-react-sdk/pull/8398)). Fixes vector-im/element-web#21879. Contributed by @luixxiul.
+ * Fix edge case thread summaries around events without a msgtype ([\#8576](/~https://github.com/matrix-org/matrix-react-sdk/pull/8576)).
+ * Fix favourites metaspace not updating ([\#8594](/~https://github.com/matrix-org/matrix-react-sdk/pull/8594)). Fixes vector-im/element-web#22156.
+ * Stop spaces from displaying as rooms in new breadcrumbs ([\#8595](/~https://github.com/matrix-org/matrix-react-sdk/pull/8595)). Fixes vector-im/element-web#22165.
+ * Fix avatar position of hidden event on ThreadView ([\#8592](/~https://github.com/matrix-org/matrix-react-sdk/pull/8592)). Fixes vector-im/element-web#22199. Contributed by @luixxiul.
+ * Fix MessageTimestamp position next to redacted messages on IRC/modern layout ([\#8591](/~https://github.com/matrix-org/matrix-react-sdk/pull/8591)). Fixes vector-im/element-web#22181. Contributed by @luixxiul.
+ * Fix padding of messages in threads ([\#8574](/~https://github.com/matrix-org/matrix-react-sdk/pull/8574)). Contributed by @luixxiul.
+ * Enable overflow of hidden events content ([\#8585](/~https://github.com/matrix-org/matrix-react-sdk/pull/8585)). Fixes vector-im/element-web#22187. Contributed by @luixxiul.
+ * Increase composer line height to avoid cutting off emoji ([\#8583](/~https://github.com/matrix-org/matrix-react-sdk/pull/8583)). Fixes vector-im/element-web#22170.
+ * Don't consider threads for breaking continuation until actually created ([\#8581](/~https://github.com/matrix-org/matrix-react-sdk/pull/8581)). Fixes vector-im/element-web#22164.
+ * Fix displaying hidden events on threads ([\#8555](/~https://github.com/matrix-org/matrix-react-sdk/pull/8555)). Fixes vector-im/element-web#22058. Contributed by @luixxiul.
+ * Fix button width and align 絵文字 (emoji) on the user panel ([\#8562](/~https://github.com/matrix-org/matrix-react-sdk/pull/8562)). Fixes vector-im/element-web#22142. Contributed by @luixxiul.
+ * Standardise the margin for settings tabs ([\#7963](/~https://github.com/matrix-org/matrix-react-sdk/pull/7963)). Fixes vector-im/element-web#20767. Contributed by @yuktea.
+ * Fix room history not being visible even if we have historical keys ([\#8563](/~https://github.com/matrix-org/matrix-react-sdk/pull/8563)). Fixes vector-im/element-web#16983.
+ * Fix oblong avatars in video room lobbies ([\#8565](/~https://github.com/matrix-org/matrix-react-sdk/pull/8565)).
+ * Update thread summary when latest event gets decrypted ([\#8564](/~https://github.com/matrix-org/matrix-react-sdk/pull/8564)). Fixes vector-im/element-web#22151.
+ * Fix codepath which can wrongly cause automatic space switch from all rooms ([\#8560](/~https://github.com/matrix-org/matrix-react-sdk/pull/8560)). Fixes vector-im/element-web#21373.
+ * Fix effect of URL preview toggle not updating live ([\#8561](/~https://github.com/matrix-org/matrix-react-sdk/pull/8561)). Fixes vector-im/element-web#22148.
+ * Fix visual bugs on AccessSecretStorageDialog ([\#8160](/~https://github.com/matrix-org/matrix-react-sdk/pull/8160)). Fixes vector-im/element-web#19426. Contributed by @luixxiul.
+ * Fix the width bounce of the clock on the AudioPlayer ([\#8320](/~https://github.com/matrix-org/matrix-react-sdk/pull/8320)). Fixes vector-im/element-web#21788. Contributed by @luixxiul.
+ * Hide the verification left stroke only on the thread list ([\#8525](/~https://github.com/matrix-org/matrix-react-sdk/pull/8525)). Fixes vector-im/element-web#22132. Contributed by @luixxiul.
+ * Hide recently_viewed dropdown when other modal opens ([\#8538](/~https://github.com/matrix-org/matrix-react-sdk/pull/8538)). Contributed by @yaya-usman.
+ * Only jump to date after pressing the 'go' button ([\#8548](/~https://github.com/matrix-org/matrix-react-sdk/pull/8548)). Fixes vector-im/element-web#20799.
+ * Fix download button not working on events that were decrypted too late ([\#8556](/~https://github.com/matrix-org/matrix-react-sdk/pull/8556)). Fixes vector-im/element-web#19427.
+ * Align thread summary button with bubble messages on the left side ([\#8388](/~https://github.com/matrix-org/matrix-react-sdk/pull/8388)). Fixes vector-im/element-web#21873. Contributed by @luixxiul.
+ * Fix unresponsive notification toggles ([\#8549](/~https://github.com/matrix-org/matrix-react-sdk/pull/8549)). Fixes vector-im/element-web#22109.
+ * Set color-scheme property in themes ([\#8547](/~https://github.com/matrix-org/matrix-react-sdk/pull/8547)). Fixes vector-im/element-web#22124.
+ * Improve the styling of error messages during search initialization. ([\#6899](/~https://github.com/matrix-org/matrix-react-sdk/pull/6899)). Fixes vector-im/element-web#19245 and vector-im/element-web#18164. Contributed by @KalleStruik.
+ * Don't leave button tooltips open when closing modals ([\#8546](/~https://github.com/matrix-org/matrix-react-sdk/pull/8546)). Fixes vector-im/element-web#22121.
+ * update matrix-analytics-events ([\#8543](/~https://github.com/matrix-org/matrix-react-sdk/pull/8543)).
+ * Handle Jitsi Meet crashes more gracefully ([\#8541](/~https://github.com/matrix-org/matrix-react-sdk/pull/8541)).
+ * Fix regression around pasting links ([\#8537](/~https://github.com/matrix-org/matrix-react-sdk/pull/8537)). Fixes vector-im/element-web#22117.
+ * Fixes suggested room not ellipsized on shrinking ([\#8536](/~https://github.com/matrix-org/matrix-react-sdk/pull/8536)). Contributed by @yaya-usman.
+ * Add global spacing between display name and location body ([\#8523](/~https://github.com/matrix-org/matrix-react-sdk/pull/8523)). Fixes vector-im/element-web#22111. Contributed by @luixxiul.
+ * Add box-shadow to the reply preview on the main (left) panel only ([\#8397](/~https://github.com/matrix-org/matrix-react-sdk/pull/8397)). Fixes vector-im/element-web#21894. Contributed by @luixxiul.
+ * Set line-height: 1 to RedactedBody inside GenericEventListSummary for IRC/modern layout ([\#8529](/~https://github.com/matrix-org/matrix-react-sdk/pull/8529)). Fixes vector-im/element-web#22112. Contributed by @luixxiul.
+ * Fix position of timestamp on the chat panel in IRC layout and message edits history modal window ([\#8464](/~https://github.com/matrix-org/matrix-react-sdk/pull/8464)). Fixes vector-im/element-web#22011 and vector-im/element-web#22014. Contributed by @luixxiul.
+ * Fix unexpected and inconsistent inheritance of line-height property for mx_TextualEvent ([\#8485](/~https://github.com/matrix-org/matrix-react-sdk/pull/8485)). Fixes vector-im/element-web#22041. Contributed by @luixxiul.
+ * Set the same margin to the right side of NewRoomIntro on TimelineCard ([\#8453](/~https://github.com/matrix-org/matrix-react-sdk/pull/8453)). Contributed by @luixxiul.
+ * Remove duplicate tooltip from user pills ([\#8512](/~https://github.com/matrix-org/matrix-react-sdk/pull/8512)).
+ * Set max-width for MLocationBody and MLocationBody_map by default ([\#8519](/~https://github.com/matrix-org/matrix-react-sdk/pull/8519)). Fixes vector-im/element-web#21983. Contributed by @luixxiul.
+ * Simplify ReplyPreview UI implementation ([\#8516](/~https://github.com/matrix-org/matrix-react-sdk/pull/8516)). Fixes vector-im/element-web#22091. Contributed by @luixxiul.
+ * Fix thread summary overflow on narrow message panel on bubble message layout ([\#8520](/~https://github.com/matrix-org/matrix-react-sdk/pull/8520)). Fixes vector-im/element-web#22097. Contributed by @luixxiul.
+ * Live location sharing - refresh beacon timers on tab becoming active ([\#8515](/~https://github.com/matrix-org/matrix-react-sdk/pull/8515)).
+ * Enlarge emoji again ([\#8509](/~https://github.com/matrix-org/matrix-react-sdk/pull/8509)). Fixes vector-im/element-web#22086.
+ * Order receipts with the most recent on the right ([\#8506](/~https://github.com/matrix-org/matrix-react-sdk/pull/8506)). Fixes vector-im/element-web#22044.
+ * Disconnect from video rooms when leaving ([\#8500](/~https://github.com/matrix-org/matrix-react-sdk/pull/8500)).
+ * Fix soft crash around threads when room isn't yet in store ([\#8496](/~https://github.com/matrix-org/matrix-react-sdk/pull/8496)). Fixes vector-im/element-web#22047.
+ * Fix reading of cached room device setting values ([\#8491](/~https://github.com/matrix-org/matrix-react-sdk/pull/8491)).
+ * Add loading spinners to threads panels ([\#8490](/~https://github.com/matrix-org/matrix-react-sdk/pull/8490)). Fixes vector-im/element-web#21335.
+ * Fix forwarding UI papercuts ([\#8482](/~https://github.com/matrix-org/matrix-react-sdk/pull/8482)). Fixes vector-im/element-web#17616.
+
Changes in [3.44.0](/~https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.44.0) (2022-05-10)
=====================================================================================================
diff --git a/__mocks__/browser-request.js b/__mocks__/browser-request.js
index aa9c7102998..7029f1c1909 100644
--- a/__mocks__/browser-request.js
+++ b/__mocks__/browser-request.js
@@ -1,3 +1,19 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
const en = require("../src/i18n/strings/en_EN");
const de = require("../src/i18n/strings/de_DE");
const lv = {
@@ -5,6 +21,32 @@ const lv = {
"Uploading %(filename)s and %(count)s others|one": "Качване на %(filename)s и %(count)s друг",
};
+function weblateToCounterpart(inTrs) {
+ const outTrs = {};
+
+ for (const key of Object.keys(inTrs)) {
+ const keyParts = key.split('|', 2);
+ if (keyParts.length === 2) {
+ let obj = outTrs[keyParts[0]];
+ if (obj === undefined) {
+ obj = outTrs[keyParts[0]] = {};
+ } else if (typeof obj === "string") {
+ // This is a transitional edge case if a string went from singular to pluralised and both still remain
+ // in the translation json file. Use the singular translation as `other` and merge pluralisation atop.
+ obj = outTrs[keyParts[0]] = {
+ "other": inTrs[key],
+ };
+ console.warn("Found entry in i18n file in both singular and pluralised form", keyParts[0]);
+ }
+ obj[keyParts[1]] = inTrs[key];
+ } else {
+ outTrs[key] = inTrs[key];
+ }
+ }
+
+ return outTrs;
+}
+
// Mock the browser-request for the languageHandler tests to return
// Fake languages.json containing references to en_EN, de_DE and lv
// en_EN.json
@@ -13,7 +55,7 @@ const lv = {
module.exports = jest.fn((opts, cb) => {
const url = opts.url || opts.uri;
if (url && url.endsWith("languages.json")) {
- cb(undefined, {status: 200}, JSON.stringify({
+ cb(undefined, { status: 200 }, JSON.stringify({
"en": {
"fileName": "en_EN.json",
"label": "English",
@@ -24,16 +66,16 @@ module.exports = jest.fn((opts, cb) => {
},
"lv": {
"fileName": "lv.json",
- "label": "Latvian"
- }
+ "label": "Latvian",
+ },
}));
} else if (url && url.endsWith("en_EN.json")) {
- cb(undefined, {status: 200}, JSON.stringify(en));
+ cb(undefined, { status: 200 }, JSON.stringify(weblateToCounterpart(en)));
} else if (url && url.endsWith("de_DE.json")) {
- cb(undefined, {status: 200}, JSON.stringify(de));
+ cb(undefined, { status: 200 }, JSON.stringify(weblateToCounterpart(de)));
} else if (url && url.endsWith("lv.json")) {
- cb(undefined, {status: 200}, JSON.stringify(lv));
+ cb(undefined, { status: 200 }, JSON.stringify(weblateToCounterpart(lv)));
} else {
- cb(true, {status: 404}, "");
+ cb(true, { status: 404 }, "");
}
});
diff --git a/cypress.json b/cypress.json
index 2c39bb411fe..d41cc70dd00 100644
--- a/cypress.json
+++ b/cypress.json
@@ -1,5 +1,7 @@
{
"baseUrl": "http://localhost:8080",
"videoUploadOnPasses": false,
- "projectId": "ppvnzg"
+ "projectId": "ppvnzg",
+ "experimentalSessionAndOrigin": true,
+ "experimentalInteractiveRunEvents": true
}
diff --git a/cypress/global.d.ts b/cypress/global.d.ts
new file mode 100644
index 00000000000..efbb255b081
--- /dev/null
+++ b/cypress/global.d.ts
@@ -0,0 +1,51 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import "matrix-js-sdk/src/@types/global";
+import type { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client";
+import type { RoomMemberEvent } from "matrix-js-sdk/src/models/room-member";
+import type { MatrixDispatcher } from "../src/dispatcher/dispatcher";
+import type PerformanceMonitor from "../src/performance";
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace Cypress {
+ interface ApplicationWindow {
+ mxMatrixClientPeg: {
+ matrixClient?: MatrixClient;
+ };
+ mxDispatcher: MatrixDispatcher;
+ mxPerformanceMonitor: PerformanceMonitor;
+ beforeReload?: boolean; // for detecting reloads
+ // Partial type for the matrix-js-sdk module, exported by browser-matrix
+ matrixcs: {
+ MatrixClient: typeof MatrixClient;
+ ClientEvent: typeof ClientEvent;
+ RoomMemberEvent: typeof RoomMemberEvent;
+ };
+ }
+ }
+
+ interface Window {
+ // to appease the MatrixDispatcher import
+ mxDispatcher: MatrixDispatcher;
+ // to appease the PerformanceMonitor import
+ mxPerformanceMonitor: PerformanceMonitor;
+ mxPerformanceEntryNames: any;
+ }
+}
+
+export { MatrixClient };
diff --git a/cypress/integration/1-register/register.spec.ts b/cypress/integration/1-register/register.spec.ts
index f719da55477..3dba78c4904 100644
--- a/cypress/integration/1-register/register.spec.ts
+++ b/cypress/integration/1-register/register.spec.ts
@@ -16,37 +16,56 @@ limitations under the License.
///
-import { SynapseInstance } from "../../plugins/synapsedocker/index";
+import { SynapseInstance } from "../../plugins/synapsedocker";
describe("Registration", () => {
- let synapseId;
- let synapsePort;
+ let synapse: SynapseInstance;
beforeEach(() => {
- cy.task("synapseStart", "consent").then(result => {
- synapseId = result.synapseId;
- synapsePort = result.port;
- });
cy.visit("/#/register");
+ cy.startSynapse("consent").then(data => {
+ synapse = data;
+ });
});
afterEach(() => {
- cy.task("synapseStop", synapseId);
+ cy.stopSynapse(synapse);
});
it("registers an account and lands on the home screen", () => {
cy.get(".mx_ServerPicker_change", { timeout: 15000 }).click();
- cy.get(".mx_ServerPickerDialog_otherHomeserver").type(`http://localhost:${synapsePort}`);
+ cy.get(".mx_ServerPickerDialog_continue").should("be.visible");
+ cy.percySnapshot("Server Picker");
+
+ cy.get(".mx_ServerPickerDialog_otherHomeserver").type(synapse.baseUrl);
cy.get(".mx_ServerPickerDialog_continue").click();
// wait for the dialog to go away
cy.get('.mx_ServerPickerDialog').should('not.exist');
+
+ cy.get("#mx_RegistrationForm_username").should("be.visible");
+ // Hide the server text as it contains the randomly allocated Synapse port
+ const percyCSS = ".mx_ServerPicker_server { visibility: hidden !important; }";
+ cy.percySnapshot("Registration", { percyCSS });
+
cy.get("#mx_RegistrationForm_username").type("alice");
cy.get("#mx_RegistrationForm_password").type("totally a great password");
cy.get("#mx_RegistrationForm_passwordConfirm").type("totally a great password");
+ cy.startMeasuring("create-account");
cy.get(".mx_Login_submit").click();
+
+ cy.get(".mx_RegistrationEmailPromptDialog").should("be.visible");
+ cy.percySnapshot("Registration email prompt", { percyCSS });
cy.get(".mx_RegistrationEmailPromptDialog button.mx_Dialog_primary").click();
+
+ cy.stopMeasuring("create-account");
+ cy.get(".mx_InteractiveAuthEntryComponents_termsPolicy").should("be.visible");
+ cy.percySnapshot("Registration terms prompt", { percyCSS });
+
cy.get(".mx_InteractiveAuthEntryComponents_termsPolicy input").click();
+ cy.startMeasuring("from-submit-to-home");
cy.get(".mx_InteractiveAuthEntryComponents_termsSubmit").click();
+
cy.url().should('contain', '/#/home');
+ cy.stopMeasuring("from-submit-to-home");
});
});
diff --git a/cypress/integration/2-login/consent.spec.ts b/cypress/integration/2-login/consent.spec.ts
new file mode 100644
index 00000000000..a4cd31bd26c
--- /dev/null
+++ b/cypress/integration/2-login/consent.spec.ts
@@ -0,0 +1,73 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+///
+
+import { SinonStub } from "cypress/types/sinon";
+
+import { SynapseInstance } from "../../plugins/synapsedocker";
+
+describe("Consent", () => {
+ let synapse: SynapseInstance;
+
+ beforeEach(() => {
+ cy.startSynapse("consent").then(data => {
+ synapse = data;
+
+ cy.initTestUser(synapse, "Bob");
+ });
+ });
+
+ afterEach(() => {
+ cy.stopSynapse(synapse);
+ });
+
+ it("should prompt the user to consent to terms when server deems it necessary", () => {
+ // Attempt to create a room using the js-sdk which should return an error with `M_CONSENT_NOT_GIVEN`
+ cy.window().then(win => {
+ win.mxMatrixClientPeg.matrixClient.createRoom({}).catch(() => {});
+
+ // Stub `window.open` - clicking the primary button below will call it
+ cy.stub(win, "open").as("windowOpen").returns({});
+ });
+
+ // Accept terms & conditions
+ cy.get(".mx_QuestionDialog").within(() => {
+ cy.get("#mx_BaseDialog_title").contains("Terms and Conditions");
+ cy.get(".mx_Dialog_primary").click();
+ });
+
+ cy.get("@windowOpen").then(stub => {
+ const url = stub.getCall(0).args[0];
+
+ // Go to Synapse's consent page and accept it
+ cy.origin(synapse.baseUrl, { args: { url } }, ({ url }) => {
+ cy.visit(url);
+
+ cy.get('[type="submit"]').click();
+ cy.get("p").contains("Danke schon");
+ });
+ });
+
+ // go back to the app
+ cy.visit("/");
+ // wait for the app to re-load
+ cy.get(".mx_MatrixChat", { timeout: 15000 });
+
+ // attempt to perform the same action again and expect it to not fail
+ cy.createRoom({});
+ });
+});
diff --git a/cypress/integration/2-login/login.spec.ts b/cypress/integration/2-login/login.spec.ts
new file mode 100644
index 00000000000..521eb66f25a
--- /dev/null
+++ b/cypress/integration/2-login/login.spec.ts
@@ -0,0 +1,62 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+///
+
+import { SynapseInstance } from "../../plugins/synapsedocker";
+
+describe("Login", () => {
+ let synapse: SynapseInstance;
+
+ beforeEach(() => {
+ cy.visit("/#/login");
+ cy.startSynapse("consent").then(data => {
+ synapse = data;
+ });
+ });
+
+ afterEach(() => {
+ cy.stopSynapse(synapse);
+ });
+
+ describe("m.login.password", () => {
+ const username = "user1234";
+ const password = "p4s5W0rD";
+
+ beforeEach(() => {
+ cy.registerUser(synapse, username, password);
+ });
+
+ it("logs in with an existing account and lands on the home screen", () => {
+ cy.get("#mx_LoginForm_username", { timeout: 15000 }).should("be.visible");
+ cy.percySnapshot("Login");
+
+ cy.get(".mx_ServerPicker_change").click();
+ cy.get(".mx_ServerPickerDialog_otherHomeserver").type(synapse.baseUrl);
+ cy.get(".mx_ServerPickerDialog_continue").click();
+ // wait for the dialog to go away
+ cy.get('.mx_ServerPickerDialog').should('not.exist');
+
+ cy.get("#mx_LoginForm_username").type(username);
+ cy.get("#mx_LoginForm_password").type(password);
+ cy.startMeasuring("from-submit-to-home");
+ cy.get(".mx_Login_submit").click();
+
+ cy.url().should('contain', '/#/home');
+ cy.stopMeasuring("from-submit-to-home");
+ });
+ });
+});
diff --git a/cypress/integration/3-user-menu/user-menu.spec.ts b/cypress/integration/3-user-menu/user-menu.spec.ts
new file mode 100644
index 00000000000..671fd4eacf3
--- /dev/null
+++ b/cypress/integration/3-user-menu/user-menu.spec.ts
@@ -0,0 +1,47 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+///
+
+import { SynapseInstance } from "../../plugins/synapsedocker";
+import type { UserCredentials } from "../../support/login";
+
+describe("User Menu", () => {
+ let synapse: SynapseInstance;
+ let user: UserCredentials;
+
+ beforeEach(() => {
+ cy.startSynapse("default").then(data => {
+ synapse = data;
+
+ cy.initTestUser(synapse, "Jeff").then(credentials => {
+ user = credentials;
+ });
+ });
+ });
+
+ afterEach(() => {
+ cy.stopSynapse(synapse);
+ });
+
+ it("should contain our name & userId", () => {
+ cy.get('[aria-label="User menu"]').click();
+ cy.get(".mx_ContextualMenu").within(() => {
+ cy.get(".mx_UserMenu_contextMenu_displayName").should("contain", "Jeff");
+ cy.get(".mx_UserMenu_contextMenu_userId").should("contain", user.userId);
+ });
+ });
+});
diff --git a/cypress/integration/4-create-room/create-room.spec.ts b/cypress/integration/4-create-room/create-room.spec.ts
new file mode 100644
index 00000000000..9bf38194d92
--- /dev/null
+++ b/cypress/integration/4-create-room/create-room.spec.ts
@@ -0,0 +1,66 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+///
+
+import { SynapseInstance } from "../../plugins/synapsedocker";
+import Chainable = Cypress.Chainable;
+
+function openCreateRoomDialog(): Chainable> {
+ cy.get('[aria-label="Add room"]').click();
+ cy.get('.mx_ContextualMenu [aria-label="New room"]').click();
+ return cy.get(".mx_CreateRoomDialog");
+}
+
+describe("Create Room", () => {
+ let synapse: SynapseInstance;
+
+ beforeEach(() => {
+ cy.startSynapse("default").then(data => {
+ synapse = data;
+
+ cy.initTestUser(synapse, "Jim");
+ });
+ });
+
+ afterEach(() => {
+ cy.stopSynapse(synapse);
+ });
+
+ it("should allow us to create a public room with name, topic & address set", () => {
+ const name = "Test room 1";
+ const topic = "This room is dedicated to this test and this test only!";
+
+ openCreateRoomDialog().within(() => {
+ // Fill name & topic
+ cy.get('[label="Name"]').type(name);
+ cy.get('[label="Topic (optional)"]').type(topic);
+ // Change room to public
+ cy.get('[aria-label="Room visibility"]').click();
+ cy.get("#mx_JoinRuleDropdown__public").click();
+ // Fill room address
+ cy.get('[label="Room address"]').type("test-room-1");
+ // Submit
+ cy.startMeasuring("from-submit-to-room");
+ cy.get(".mx_Dialog_primary").click();
+ });
+
+ cy.url().should("contain", "/#/room/#test-room-1:localhost");
+ cy.stopMeasuring("from-submit-to-room");
+ cy.get(".mx_RoomHeader_nametext").contains(name);
+ cy.get(".mx_RoomHeader_topic").contains(topic);
+ });
+});
diff --git a/cypress/integration/5-threads/threads.spec.ts b/cypress/integration/5-threads/threads.spec.ts
new file mode 100644
index 00000000000..43b0058bb11
--- /dev/null
+++ b/cypress/integration/5-threads/threads.spec.ts
@@ -0,0 +1,214 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+///
+
+import { SynapseInstance } from "../../plugins/synapsedocker";
+import { MatrixClient } from "../../global";
+
+function markWindowBeforeReload(): void {
+ // mark our window object to "know" when it gets reloaded
+ cy.window().then(w => w.beforeReload = true);
+}
+
+describe("Threads", () => {
+ let synapse: SynapseInstance;
+
+ beforeEach(() => {
+ cy.window().then(win => {
+ win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
+ win.localStorage.setItem("mx_labs_feature_feature_thread", "true"); // Default threads to ON for this spec
+ });
+ cy.startSynapse("default").then(data => {
+ synapse = data;
+
+ cy.initTestUser(synapse, "Tom");
+ });
+ });
+
+ afterEach(() => {
+ cy.stopSynapse(synapse);
+ });
+
+ it("should reload when enabling threads beta", () => {
+ markWindowBeforeReload();
+
+ // Turn off
+ cy.openUserSettings("Labs").within(() => {
+ // initially the new property is there
+ cy.window().should("have.prop", "beforeReload", true);
+
+ cy.leaveBeta("Threads");
+ // after reload the property should be gone
+ cy.window().should("not.have.prop", "beforeReload");
+ });
+
+ cy.get(".mx_MatrixChat", { timeout: 15000 }); // wait for the app
+ markWindowBeforeReload();
+
+ // Turn on
+ cy.openUserSettings("Labs").within(() => {
+ // initially the new property is there
+ cy.window().should("have.prop", "beforeReload", true);
+
+ cy.joinBeta("Threads");
+ // after reload the property should be gone
+ cy.window().should("not.have.prop", "beforeReload");
+ });
+ });
+
+ it("should be usable for a conversation", () => {
+ let bot: MatrixClient;
+ cy.getBot(synapse, "BotBob").then(_bot => {
+ bot = _bot;
+ });
+
+ let roomId: string;
+ cy.createRoom({}).then(_roomId => {
+ roomId = _roomId;
+ cy.inviteUser(roomId, bot.getUserId());
+ cy.visit("/#/room/" + roomId);
+ });
+
+ // User sends message
+ cy.get(".mx_RoomView_body .mx_BasicMessageComposer_input").type("Hello Mr. Bot{enter}");
+
+ // Wait for message to send, get its ID and save as @threadId
+ cy.get(".mx_RoomView_body .mx_EventTile").contains("Hello Mr. Bot")
+ .closest(".mx_EventTile[data-scroll-tokens]").invoke("attr", "data-scroll-tokens").as("threadId");
+
+ // Bot starts thread
+ cy.get("@threadId").then(threadId => {
+ bot.sendMessage(roomId, threadId, {
+ body: "Hello there",
+ msgtype: "m.text",
+ });
+ });
+
+ // User asserts timeline thread summary visible & clicks it
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "BotBob");
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "Hello there");
+ cy.get(".mx_RoomView_body .mx_ThreadSummary").click();
+
+ // User responds in thread
+ cy.get(".mx_ThreadView .mx_BasicMessageComposer_input").type("Test{enter}");
+
+ // User asserts summary was updated correctly
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "Tom");
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "Test");
+
+ // User reacts to message instead
+ cy.get(".mx_ThreadView .mx_EventTile").contains("Hello there").closest(".mx_EventTile_line")
+ .find('[aria-label="React"]').click({ force: true }); // Cypress has no ability to hover
+ cy.get(".mx_EmojiPicker").within(() => {
+ cy.get('input[type="text"]').type("wave");
+ cy.get('[role="menuitem"]').contains("👋").click();
+ });
+
+ // User redacts their prior response
+ cy.get(".mx_ThreadView .mx_EventTile").contains("Test").closest(".mx_EventTile_line")
+ .find('[aria-label="Options"]').click({ force: true }); // Cypress has no ability to hover
+ cy.get(".mx_IconizedContextMenu").within(() => {
+ cy.get('[role="menuitem"]').contains("Remove").click();
+ });
+ cy.get(".mx_TextInputDialog").within(() => {
+ cy.get(".mx_Dialog_primary").contains("Remove").click();
+ });
+
+ // User asserts summary was updated correctly
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "BotBob");
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "Hello there");
+
+ // User closes right panel after clicking back to thread list
+ cy.get(".mx_ThreadView .mx_BaseCard_back").click();
+ cy.get(".mx_ThreadPanel .mx_BaseCard_close").click();
+
+ // Bot responds to thread
+ cy.get("@threadId").then(threadId => {
+ bot.sendMessage(roomId, threadId, {
+ body: "How are things?",
+ msgtype: "m.text",
+ });
+ });
+
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "BotBob");
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "How are things?");
+ // User asserts thread list unread indicator
+ cy.get('.mx_HeaderButtons [aria-label="Threads"]').should("have.class", "mx_RightPanel_headerButton_unread");
+
+ // User opens thread list
+ cy.get('.mx_HeaderButtons [aria-label="Threads"]').click();
+
+ // User asserts thread with correct root & latest events & unread dot
+ cy.get(".mx_ThreadPanel .mx_EventTile_last").within(() => {
+ cy.get(".mx_EventTile_body").should("contain", "Hello Mr. Bot");
+ cy.get(".mx_ThreadSummary_content").should("contain", "How are things?");
+ // User opens thread via threads list
+ cy.get(".mx_EventTile_line").click();
+ });
+
+ // User responds & asserts
+ cy.get(".mx_ThreadView .mx_BasicMessageComposer_input").type("Great!{enter}");
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "Tom");
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should("contain", "Great!");
+
+ // User edits & asserts
+ cy.get(".mx_ThreadView .mx_EventTile_last").contains("Great!").closest(".mx_EventTile_line").within(() => {
+ cy.get('[aria-label="Edit"]').click({ force: true }); // Cypress has no ability to hover
+ cy.get(".mx_BasicMessageComposer_input").type(" How about yourself?{enter}");
+ });
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "Tom");
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content")
+ .should("contain", "Great! How about yourself?");
+
+ // User closes right panel
+ cy.get(".mx_ThreadView .mx_BaseCard_close").click();
+
+ // Bot responds to thread and saves the id of their message to @eventId
+ cy.get("@threadId").then(threadId => {
+ cy.wrap(bot.sendMessage(roomId, threadId, {
+ body: "I'm very good thanks",
+ msgtype: "m.text",
+ }).then(res => res.event_id)).as("eventId");
+ });
+
+ // User asserts
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "BotBob");
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content")
+ .should("contain", "I'm very good thanks");
+
+ // Bot edits their latest event
+ cy.get("@eventId").then(eventId => {
+ bot.sendMessage(roomId, {
+ "body": "* I'm very good thanks :)",
+ "msgtype": "m.text",
+ "m.new_content": {
+ "body": "I'm very good thanks :)",
+ "msgtype": "m.text",
+ },
+ "m.relates_to": {
+ "rel_type": "m.replace",
+ "event_id": eventId,
+ },
+ });
+ });
+
+ // User asserts
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "BotBob");
+ cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content")
+ .should("contain", "I'm very good thanks :)");
+ });
+});
diff --git a/cypress/plugins/index.ts b/cypress/plugins/index.ts
index db01ceceb4f..eab5441c203 100644
--- a/cypress/plugins/index.ts
+++ b/cypress/plugins/index.ts
@@ -16,8 +16,15 @@ limitations under the License.
///
-import { synapseDocker } from "./synapsedocker/index";
+import PluginEvents = Cypress.PluginEvents;
+import PluginConfigOptions = Cypress.PluginConfigOptions;
+import { performance } from "./performance";
+import { synapseDocker } from "./synapsedocker";
-export default function(on, config) {
+/**
+ * @type {Cypress.PluginConfig}
+ */
+export default function(on: PluginEvents, config: PluginConfigOptions) {
+ performance(on, config);
synapseDocker(on, config);
}
diff --git a/cypress/plugins/performance.ts b/cypress/plugins/performance.ts
new file mode 100644
index 00000000000..c6bd3e4ce9f
--- /dev/null
+++ b/cypress/plugins/performance.ts
@@ -0,0 +1,47 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+///
+
+import * as path from "path";
+import * as fse from "fs-extra";
+
+import PluginEvents = Cypress.PluginEvents;
+import PluginConfigOptions = Cypress.PluginConfigOptions;
+
+// This holds all the performance measurements throughout the run
+let bufferedMeasurements: PerformanceEntry[] = [];
+
+function addMeasurements(measurements: PerformanceEntry[]): void {
+ bufferedMeasurements = bufferedMeasurements.concat(measurements);
+ return null;
+}
+
+async function writeMeasurementsFile() {
+ try {
+ const measurementsPath = path.join("cypress", "performance", "measurements.json");
+ await fse.outputJSON(measurementsPath, bufferedMeasurements, {
+ spaces: 4,
+ });
+ } finally {
+ bufferedMeasurements = [];
+ }
+}
+
+export function performance(on: PluginEvents, config: PluginConfigOptions) {
+ on("task", { addMeasurements });
+ on("after:run", writeMeasurementsFile);
+}
diff --git a/cypress/plugins/synapsedocker/index.ts b/cypress/plugins/synapsedocker/index.ts
index 0f029e7b2ed..292c74ee670 100644
--- a/cypress/plugins/synapsedocker/index.ts
+++ b/cypress/plugins/synapsedocker/index.ts
@@ -21,6 +21,10 @@ import * as os from "os";
import * as crypto from "crypto";
import * as childProcess from "child_process";
import * as fse from "fs-extra";
+import * as net from "net";
+
+import PluginEvents = Cypress.PluginEvents;
+import PluginConfigOptions = Cypress.PluginConfigOptions;
// A cypress plugins to add command to start & stop synapses in
// docker with preset templates.
@@ -28,11 +32,13 @@ import * as fse from "fs-extra";
interface SynapseConfig {
configDir: string;
registrationSecret: string;
+ // Synapse must be configured with its public_baseurl so we have to allocate a port & url at this stage
+ baseUrl: string;
+ port: number;
}
export interface SynapseInstance extends SynapseConfig {
synapseId: string;
- port: number;
}
const synapses = new Map();
@@ -41,6 +47,16 @@ function randB64Bytes(numBytes: number): string {
return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, "");
}
+async function getFreePort(): Promise {
+ return new Promise(resolve => {
+ const srv = net.createServer();
+ srv.listen(0, () => {
+ const port = (srv.address()).port;
+ srv.close(() => resolve(port));
+ });
+ });
+}
+
async function cfgDirFromTemplate(template: string): Promise {
const templateDir = path.join(__dirname, "templates", template);
@@ -61,12 +77,16 @@ async function cfgDirFromTemplate(template: string): Promise {
const macaroonSecret = randB64Bytes(16);
const formSecret = randB64Bytes(16);
- // now copy homeserver.yaml, applying sustitutions
+ const port = await getFreePort();
+ const baseUrl = `http://localhost:${port}`;
+
+ // now copy homeserver.yaml, applying substitutions
console.log(`Gen ${path.join(templateDir, "homeserver.yaml")}`);
let hsYaml = await fse.readFile(path.join(templateDir, "homeserver.yaml"), "utf8");
hsYaml = hsYaml.replace(/{{REGISTRATION_SECRET}}/g, registrationSecret);
hsYaml = hsYaml.replace(/{{MACAROON_SECRET_KEY}}/g, macaroonSecret);
hsYaml = hsYaml.replace(/{{FORM_SECRET}}/g, formSecret);
+ hsYaml = hsYaml.replace(/{{PUBLIC_BASEURL}}/g, baseUrl);
await fse.writeFile(path.join(tempDir, "homeserver.yaml"), hsYaml);
// now generate a signing key (we could use synapse's config generation for
@@ -77,6 +97,8 @@ async function cfgDirFromTemplate(template: string): Promise {
await fse.writeFile(path.join(tempDir, "localhost.signing.key"), `ed25519 x ${signingKey}`);
return {
+ port,
+ baseUrl,
configDir: tempDir,
registrationSecret,
};
@@ -98,7 +120,7 @@ async function synapseStart(template: string): Promise {
"--name", containerName,
"-d",
"-v", `${synCfg.configDir}:/data`,
- "-p", "8008/tcp",
+ "-p", `${synCfg.port}:8008/tcp`,
"matrixdotorg/synapse:develop",
"run",
], (err, stdout) => {
@@ -107,30 +129,31 @@ async function synapseStart(template: string): Promise {
});
});
- // Get the port that docker allocated: specifying only one
- // port above leaves docker to just grab a free one, although
- // in hindsight we need to put the port in public_baseurl in the
- // config really, so this will probably need changing to use a fixed
- // / configured port.
- const port = await new Promise((resolve, reject) => {
- childProcess.execFile('docker', [
- "port", synapseId, "8008",
+ synapses.set(synapseId, { synapseId, ...synCfg });
+
+ console.log(`Started synapse with id ${synapseId} on port ${synCfg.port}.`);
+
+ // Await Synapse healthcheck
+ await new Promise((resolve, reject) => {
+ childProcess.execFile("docker", [
+ "exec", synapseId,
+ "curl",
+ "--connect-timeout", "30",
+ "--retry", "30",
+ "--retry-delay", "1",
+ "--retry-all-errors",
+ "--silent",
+ "http://localhost:8008/health",
], { encoding: 'utf8' }, (err, stdout) => {
if (err) reject(err);
- resolve(Number(stdout.trim().split(":")[1]));
+ else resolve();
});
});
- synapses.set(synapseId, Object.assign({
- port,
- synapseId,
- }, synCfg));
-
- console.log(`Started synapse with id ${synapseId} on port ${port}.`);
return synapses.get(synapseId);
}
-async function synapseStop(id) {
+async function synapseStop(id: string): Promise {
const synCfg = synapses.get(id);
if (!synCfg) throw new Error("Unknown synapse ID");
@@ -178,7 +201,7 @@ async function synapseStop(id) {
synapses.delete(id);
console.log(`Stopped synapse id ${id}.`);
- // cypres deliberately fails if you return 'undefined', so
+ // cypress deliberately fails if you return 'undefined', so
// return null to signal all is well and we've handled the task.
return null;
}
@@ -186,10 +209,10 @@ async function synapseStop(id) {
/**
* @type {Cypress.PluginConfig}
*/
-// eslint-disable-next-line no-unused-vars
-export function synapseDocker(on, config) {
+export function synapseDocker(on: PluginEvents, config: PluginConfigOptions) {
on("task", {
- synapseStart, synapseStop,
+ synapseStart,
+ synapseStop,
});
on("after:spec", async (spec) => {
@@ -197,7 +220,7 @@ export function synapseDocker(on, config) {
// This is on the theory that we should avoid re-using synapse
// instances between spec runs: they should be cheap enough to
// start that we can have a separate one for each spec run or even
- // test. If we accidentally re-use synapses, we could inadvertantly
+ // test. If we accidentally re-use synapses, we could inadvertently
// make our tests depend on each other.
for (const synId of synapses.keys()) {
console.warn(`Cleaning up synapse ID ${synId} after ${spec.name}`);
diff --git a/cypress/plugins/synapsedocker/templates/consent/homeserver.yaml b/cypress/plugins/synapsedocker/templates/consent/homeserver.yaml
index e26133f6d11..6decaeb5a0b 100644
--- a/cypress/plugins/synapsedocker/templates/consent/homeserver.yaml
+++ b/cypress/plugins/synapsedocker/templates/consent/homeserver.yaml
@@ -1,6 +1,6 @@
server_name: "localhost"
pid_file: /data/homeserver.pid
-public_baseurl: http://localhost:5005/
+public_baseurl: "{{PUBLIC_BASEURL}}"
listeners:
- port: 8008
tls: false
diff --git a/cypress/plugins/synapsedocker/templates/default/README.md b/cypress/plugins/synapsedocker/templates/default/README.md
new file mode 100644
index 00000000000..8f6b11f999b
--- /dev/null
+++ b/cypress/plugins/synapsedocker/templates/default/README.md
@@ -0,0 +1 @@
+A synapse configured with user privacy consent disabled
diff --git a/cypress/plugins/synapsedocker/templates/default/homeserver.yaml b/cypress/plugins/synapsedocker/templates/default/homeserver.yaml
new file mode 100644
index 00000000000..7839c69c463
--- /dev/null
+++ b/cypress/plugins/synapsedocker/templates/default/homeserver.yaml
@@ -0,0 +1,52 @@
+server_name: "localhost"
+pid_file: /data/homeserver.pid
+public_baseurl: "{{PUBLIC_BASEURL}}"
+listeners:
+ - port: 8008
+ tls: false
+ bind_addresses: ['::']
+ type: http
+ x_forwarded: true
+
+ resources:
+ - names: [client]
+ compress: false
+
+database:
+ name: "sqlite3"
+ args:
+ database: ":memory:"
+
+log_config: "/data/log.config"
+
+rc_messages_per_second: 10000
+rc_message_burst_count: 10000
+rc_registration:
+ per_second: 10000
+ burst_count: 10000
+
+rc_login:
+ address:
+ per_second: 10000
+ burst_count: 10000
+ account:
+ per_second: 10000
+ burst_count: 10000
+ failed_attempts:
+ per_second: 10000
+ burst_count: 10000
+
+media_store_path: "/data/media_store"
+uploads_path: "/data/uploads"
+enable_registration: true
+enable_registration_without_verification: true
+disable_msisdn_registration: false
+registration_shared_secret: "{{REGISTRATION_SECRET}}"
+report_stats: false
+macaroon_secret_key: "{{MACAROON_SECRET_KEY}}"
+form_secret: "{{FORM_SECRET}}"
+signing_key_path: "/data/localhost.signing.key"
+
+trusted_key_servers:
+ - server_name: "matrix.org"
+suppress_key_server_warning: true
diff --git a/cypress/plugins/synapsedocker/templates/default/log.config b/cypress/plugins/synapsedocker/templates/default/log.config
new file mode 100644
index 00000000000..ac232762da3
--- /dev/null
+++ b/cypress/plugins/synapsedocker/templates/default/log.config
@@ -0,0 +1,50 @@
+# Log configuration for Synapse.
+#
+# This is a YAML file containing a standard Python logging configuration
+# dictionary. See [1] for details on the valid settings.
+#
+# Synapse also supports structured logging for machine readable logs which can
+# be ingested by ELK stacks. See [2] for details.
+#
+# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema
+# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html
+
+version: 1
+
+formatters:
+ precise:
+ format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
+
+handlers:
+ # A handler that writes logs to stderr. Unused by default, but can be used
+ # instead of "buffer" and "file" in the logger handlers.
+ console:
+ class: logging.StreamHandler
+ formatter: precise
+
+loggers:
+ synapse.storage.SQL:
+ # beware: increasing this to DEBUG will make synapse log sensitive
+ # information such as access tokens.
+ level: INFO
+
+ twisted:
+ # We send the twisted logging directly to the file handler,
+ # to work around /~https://github.com/matrix-org/synapse/issues/3471
+ # when using "buffer" logger. Use "console" to log to stderr instead.
+ handlers: [console]
+ propagate: false
+
+root:
+ level: INFO
+
+ # Write logs to the `buffer` handler, which will buffer them together in memory,
+ # then write them to a file.
+ #
+ # Replace "buffer" with "console" to log to stderr instead. (Note that you'll
+ # also need to update the configuration for the `twisted` logger above, in
+ # this case.)
+ #
+ handlers: [console]
+
+disable_existing_loggers: false
diff --git a/cypress/support/bot.ts b/cypress/support/bot.ts
new file mode 100644
index 00000000000..a2488c0081e
--- /dev/null
+++ b/cypress/support/bot.ts
@@ -0,0 +1,63 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+///
+
+import request from "browser-request";
+
+import type { MatrixClient } from "matrix-js-sdk/src/client";
+import { SynapseInstance } from "../plugins/synapsedocker";
+import Chainable = Cypress.Chainable;
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace Cypress {
+ interface Chainable {
+ /**
+ * Returns a new Bot instance
+ * @param synapse the instance on which to register the bot user
+ * @param displayName the display name to give to the bot user
+ */
+ getBot(synapse: SynapseInstance, displayName?: string): Chainable;
+ }
+ }
+}
+
+Cypress.Commands.add("getBot", (synapse: SynapseInstance, displayName?: string): Chainable => {
+ const username = Cypress._.uniqueId("userId_");
+ const password = Cypress._.uniqueId("password_");
+ return cy.registerUser(synapse, username, password, displayName).then(credentials => {
+ return cy.window({ log: false }).then(win => {
+ const cli = new win.matrixcs.MatrixClient({
+ baseUrl: synapse.baseUrl,
+ userId: credentials.userId,
+ deviceId: credentials.deviceId,
+ accessToken: credentials.accessToken,
+ request,
+ });
+
+ cli.on(win.matrixcs.RoomMemberEvent.Membership, (event, member) => {
+ if (member.membership === "invite" && member.userId === cli.getUserId()) {
+ cli.joinRoom(member.roomId);
+ }
+ });
+
+ cli.startClient();
+
+ return cli;
+ });
+ });
+});
diff --git a/cypress/support/client.ts b/cypress/support/client.ts
new file mode 100644
index 00000000000..682f3ee426c
--- /dev/null
+++ b/cypress/support/client.ts
@@ -0,0 +1,78 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+///
+
+import type { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
+import type { MatrixClient } from "matrix-js-sdk/src/client";
+import type { Room } from "matrix-js-sdk/src/models/room";
+import Chainable = Cypress.Chainable;
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace Cypress {
+ interface Chainable {
+ /**
+ * Returns the MatrixClient from the MatrixClientPeg
+ */
+ getClient(): Chainable;
+ /**
+ * Create a room with given options.
+ * @param options the options to apply when creating the room
+ * @return the ID of the newly created room
+ */
+ createRoom(options: ICreateRoomOpts): Chainable;
+ /**
+ * Invites the given user to the given room.
+ * @param roomId the id of the room to invite to
+ * @param userId the id of the user to invite
+ */
+ inviteUser(roomId: string, userId: string): Chainable<{}>;
+ }
+ }
+}
+
+Cypress.Commands.add("getClient", (): Chainable => {
+ return cy.window({ log: false }).then(win => win.mxMatrixClientPeg.matrixClient);
+});
+
+Cypress.Commands.add("createRoom", (options: ICreateRoomOpts): Chainable => {
+ return cy.window({ log: false }).then(async win => {
+ const cli = win.mxMatrixClientPeg.matrixClient;
+ const resp = await cli.createRoom(options);
+ const roomId = resp.room_id;
+
+ if (!cli.getRoom(roomId)) {
+ await new Promise(resolve => {
+ const onRoom = (room: Room) => {
+ if (room.roomId === roomId) {
+ cli.off(win.matrixcs.ClientEvent.Room, onRoom);
+ resolve();
+ }
+ };
+ cli.on(win.matrixcs.ClientEvent.Room, onRoom);
+ });
+ }
+
+ return roomId;
+ });
+});
+
+Cypress.Commands.add("inviteUser", (roomId: string, userId: string): Chainable<{}> => {
+ return cy.getClient().then(async (cli: MatrixClient) => {
+ return cli.invite(roomId, userId);
+ });
+});
diff --git a/cypress/support/index.ts b/cypress/support/index.ts
index 9901ef4cb80..dd8e5cab991 100644
--- a/cypress/support/index.ts
+++ b/cypress/support/index.ts
@@ -1,3 +1,26 @@
-// Empty file to prevent cypress from recreating a helpful example
-// file on every run (their example file doesn't use semicolons and
-// so fails our lint rules).
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+///
+
+import "@percy/cypress";
+
+import "./performance";
+import "./synapse";
+import "./login";
+import "./client";
+import "./settings";
+import "./bot";
diff --git a/cypress/support/login.ts b/cypress/support/login.ts
new file mode 100644
index 00000000000..50be88ae670
--- /dev/null
+++ b/cypress/support/login.ts
@@ -0,0 +1,98 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+///
+
+import Chainable = Cypress.Chainable;
+import { SynapseInstance } from "../plugins/synapsedocker";
+
+export interface UserCredentials {
+ accessToken: string;
+ userId: string;
+ deviceId: string;
+ password: string;
+ homeServer: string;
+}
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace Cypress {
+ interface Chainable {
+ /**
+ * Generates a test user and instantiates an Element session with that user.
+ * @param synapse the synapse returned by startSynapse
+ * @param displayName the displayName to give the test user
+ */
+ initTestUser(synapse: SynapseInstance, displayName: string): Chainable;
+ }
+ }
+}
+
+Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: string): Chainable => {
+ // XXX: work around Cypress not clearing IDB between tests
+ cy.window({ log: false }).then(win => {
+ win.indexedDB.databases().then(databases => {
+ databases.forEach(database => {
+ win.indexedDB.deleteDatabase(database.name);
+ });
+ });
+ });
+
+ const username = Cypress._.uniqueId("userId_");
+ const password = Cypress._.uniqueId("password_");
+ return cy.registerUser(synapse, username, password, displayName).then(() => {
+ const url = `${synapse.baseUrl}/_matrix/client/r0/login`;
+ return cy.request<{
+ access_token: string;
+ user_id: string;
+ device_id: string;
+ home_server: string;
+ }>({
+ url,
+ method: "POST",
+ body: {
+ "type": "m.login.password",
+ "identifier": {
+ "type": "m.id.user",
+ "user": username,
+ },
+ "password": password,
+ },
+ });
+ }).then(response => {
+ cy.window({ log: false }).then(win => {
+ // Seed the localStorage with the required credentials
+ win.localStorage.setItem("mx_hs_url", synapse.baseUrl);
+ win.localStorage.setItem("mx_user_id", response.body.user_id);
+ win.localStorage.setItem("mx_access_token", response.body.access_token);
+ win.localStorage.setItem("mx_device_id", response.body.device_id);
+ win.localStorage.setItem("mx_is_guest", "false");
+ win.localStorage.setItem("mx_has_pickle_key", "false");
+ win.localStorage.setItem("mx_has_access_token", "true");
+ });
+
+ return cy.visit("/").then(() => {
+ // wait for the app to load
+ return cy.get(".mx_MatrixChat", { timeout: 15000 });
+ }).then(() => ({
+ password,
+ accessToken: response.body.access_token,
+ userId: response.body.user_id,
+ deviceId: response.body.device_id,
+ homeServer: response.body.home_server,
+ }));
+ });
+});
diff --git a/cypress/support/performance.ts b/cypress/support/performance.ts
new file mode 100644
index 00000000000..bbd1fe217d4
--- /dev/null
+++ b/cypress/support/performance.ts
@@ -0,0 +1,74 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+///
+
+import Chainable = Cypress.Chainable;
+import AUTWindow = Cypress.AUTWindow;
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace Cypress {
+ interface Chainable {
+ /**
+ * Start measuring the duration of some task.
+ * @param task The task name.
+ */
+ startMeasuring(task: string): Chainable;
+ /**
+ * Stop measuring the duration of some task.
+ * The duration is reported in the Cypress log.
+ * @param task The task name.
+ */
+ stopMeasuring(task: string): Chainable;
+ }
+ }
+}
+
+function getPrefix(task: string): string {
+ return `cy:${Cypress.spec.name.split(".")[0]}:${task}`;
+}
+
+function startMeasuring(task: string): Chainable {
+ return cy.window({ log: false }).then((win) => {
+ win.mxPerformanceMonitor.start(getPrefix(task));
+ });
+}
+
+function stopMeasuring(task: string): Chainable {
+ return cy.window({ log: false }).then((win) => {
+ const measure = win.mxPerformanceMonitor.stop(getPrefix(task));
+ cy.log(`**${task}** ${measure.duration} ms`);
+ });
+}
+
+Cypress.Commands.add("startMeasuring", startMeasuring);
+Cypress.Commands.add("stopMeasuring", stopMeasuring);
+
+Cypress.on("window:before:unload", (event: BeforeUnloadEvent) => {
+ const doc = event.target as Document;
+ if (doc.location.href === "about:blank") return;
+ const win = doc.defaultView as AUTWindow;
+ if (!win.mxPerformanceMonitor) return;
+ const entries = win.mxPerformanceMonitor.getEntries().filter(entry => {
+ return entry.name.startsWith("cy:");
+ });
+ if (!entries || entries.length === 0) return;
+ cy.task("addMeasurements", entries);
+});
+
+// Needed to make this file a module
+export { };
diff --git a/cypress/support/settings.ts b/cypress/support/settings.ts
new file mode 100644
index 00000000000..11f48c2db26
--- /dev/null
+++ b/cypress/support/settings.ts
@@ -0,0 +1,101 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+///
+
+import "./client"; // XXX: without an (any) import here, types break down
+import Chainable = Cypress.Chainable;
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace Cypress {
+ interface Chainable {
+ /**
+ * Open the top left user menu, returning a handle to the resulting context menu.
+ */
+ openUserMenu(): Chainable>;
+
+ /**
+ * Open user settings (via user menu), returning a handle to the resulting dialog.
+ * @param tab the name of the tab to switch to after opening, optional.
+ */
+ openUserSettings(tab?: string): Chainable>;
+
+ /**
+ * Switch settings tab to the one by the given name, ideally call this in the context of the dialog.
+ * @param tab the name of the tab to switch to.
+ */
+ switchTabUserSettings(tab: string): Chainable>;
+
+ /**
+ * Close user settings, ideally call this in the context of the dialog.
+ */
+ closeUserSettings(): Chainable>;
+
+ /**
+ * Join the given beta, the `Labs` tab must already be opened,
+ * ideally call this in the context of the dialog.
+ * @param name the name of the beta to join.
+ */
+ joinBeta(name: string): Chainable>;
+
+ /**
+ * Leave the given beta, the `Labs` tab must already be opened,
+ * ideally call this in the context of the dialog.
+ * @param name the name of the beta to leave.
+ */
+ leaveBeta(name: string): Chainable>;
+ }
+ }
+}
+
+Cypress.Commands.add("openUserMenu", (): Chainable> => {
+ cy.get('[aria-label="User menu"]').click();
+ return cy.get(".mx_ContextualMenu");
+});
+
+Cypress.Commands.add("openUserSettings", (tab?: string): Chainable> => {
+ cy.openUserMenu().within(() => {
+ cy.get('[aria-label="All settings"]').click();
+ });
+ return cy.get(".mx_UserSettingsDialog").within(() => {
+ if (tab) {
+ cy.switchTabUserSettings(tab);
+ }
+ });
+});
+
+Cypress.Commands.add("switchTabUserSettings", (tab: string): Chainable> => {
+ return cy.get(".mx_TabbedView_tabLabels").within(() => {
+ cy.get(".mx_TabbedView_tabLabel").contains(tab).click();
+ });
+});
+
+Cypress.Commands.add("closeUserSettings", (): Chainable> => {
+ return cy.get('[aria-label="Close dialog"]').click();
+});
+
+Cypress.Commands.add("joinBeta", (name: string): Chainable> => {
+ return cy.get(".mx_BetaCard_title").contains(name).closest(".mx_BetaCard").within(() => {
+ return cy.get(".mx_BetaCard_buttons").contains("Join the beta").click();
+ });
+});
+
+Cypress.Commands.add("leaveBeta", (name: string): Chainable> => {
+ return cy.get(".mx_BetaCard_title").contains(name).closest(".mx_BetaCard").within(() => {
+ return cy.get(".mx_BetaCard_buttons").contains("Leave the beta").click();
+ });
+});
diff --git a/cypress/support/synapse.ts b/cypress/support/synapse.ts
new file mode 100644
index 00000000000..5696e8c015f
--- /dev/null
+++ b/cypress/support/synapse.ts
@@ -0,0 +1,122 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+///
+
+import * as crypto from 'crypto';
+
+import Chainable = Cypress.Chainable;
+import AUTWindow = Cypress.AUTWindow;
+import { SynapseInstance } from "../plugins/synapsedocker";
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace Cypress {
+ interface Chainable {
+ /**
+ * Start a synapse instance with a given config template.
+ * @param template path to template within cypress/plugins/synapsedocker/template/ directory.
+ */
+ startSynapse(template: string): Chainable;
+
+ /**
+ * Custom command wrapping task:synapseStop whilst preventing uncaught exceptions
+ * for if Synapse stopping races with the app's background sync loop.
+ * @param synapse the synapse instance returned by startSynapse
+ */
+ stopSynapse(synapse: SynapseInstance): Chainable;
+
+ /**
+ * Register a user on the given Synapse using the shared registration secret.
+ * @param synapse the synapse instance returned by startSynapse
+ * @param username the username of the user to register
+ * @param password the password of the user to register
+ * @param displayName optional display name to set on the newly registered user
+ */
+ registerUser(
+ synapse: SynapseInstance,
+ username: string,
+ password: string,
+ displayName?: string,
+ ): Chainable;
+ }
+ }
+}
+
+function startSynapse(template: string): Chainable {
+ return cy.task("synapseStart", template);
+}
+
+function stopSynapse(synapse?: SynapseInstance): Chainable {
+ if (!synapse) return;
+ // Navigate away from app to stop the background network requests which will race with Synapse shutting down
+ return cy.window({ log: false }).then((win) => {
+ win.location.href = 'about:blank';
+ cy.task("synapseStop", synapse.synapseId);
+ });
+}
+
+interface Credentials {
+ accessToken: string;
+ userId: string;
+ deviceId: string;
+ homeServer: string;
+}
+
+function registerUser(
+ synapse: SynapseInstance,
+ username: string,
+ password: string,
+ displayName?: string,
+): Chainable {
+ const url = `${synapse.baseUrl}/_synapse/admin/v1/register`;
+ return cy.then(() => {
+ // get a nonce
+ return cy.request<{ nonce: string }>({ url });
+ }).then(response => {
+ const { nonce } = response.body;
+ const mac = crypto.createHmac('sha1', synapse.registrationSecret).update(
+ `${nonce}\0${username}\0${password}\0notadmin`,
+ ).digest('hex');
+
+ return cy.request<{
+ access_token: string;
+ user_id: string;
+ home_server: string;
+ device_id: string;
+ }>({
+ url,
+ method: "POST",
+ body: {
+ nonce,
+ username,
+ password,
+ mac,
+ admin: false,
+ displayname: displayName,
+ },
+ });
+ }).then(response => ({
+ homeServer: response.body.home_server,
+ accessToken: response.body.access_token,
+ userId: response.body.user_id,
+ deviceId: response.body.device_id,
+ }));
+}
+
+Cypress.Commands.add("startSynapse", startSynapse);
+Cypress.Commands.add("stopSynapse", stopSynapse);
+Cypress.Commands.add("registerUser", registerUser);
diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json
index 85239e1a2a7..e8db14a01f5 100644
--- a/cypress/tsconfig.json
+++ b/cypress/tsconfig.json
@@ -2,7 +2,7 @@
"compilerOptions": {
"target": "es2016",
"lib": ["es2020", "dom"],
- "types": ["cypress"],
+ "types": ["cypress", "@percy/cypress"],
"moduleResolution": "node"
},
"include": ["**/*.ts"]
diff --git a/docs/cypress.md b/docs/cypress.md
index a6436e4b99a..021f10b215e 100644
--- a/docs/cypress.md
+++ b/docs/cypress.md
@@ -6,10 +6,11 @@ It aims to cover:
* How to run the tests yourself
* How the tests work
* How to write great Cypress tests
+ * Visual testing
## Running the Tests
Our Cypress tests run automatically as part of our CI along with our other tests,
-on every pull request and on every merge to develop.
+on every pull request and on every merge to develop & master.
However the Cypress tests are run, an element-web must be running on
http://localhost:8080 (this is configured in `cypress.json`) - this is what will
@@ -31,7 +32,7 @@ This will run the Cypress tests once, non-interactively.
You can also run individual tests this way too, as you'd expect:
```
-yarn run test:cypress cypress/integration/1-register/register.spec.ts
+yarn run test:cypress --spec cypress/integration/1-register/register.spec.ts
```
Cypress also has its own UI that you can use to run and debug the tests.
@@ -53,17 +54,17 @@ Synapse can be launched with different configurations in order to test element
in different configurations. `cypress/plugins/synapsedocker/templates` contains
template configuration files for each different configuration.
-Each test suite can then launch whatever Syanpse instances it needs it whatever
+Each test suite can then launch whatever Synapse instances it needs it whatever
configurations.
Note that although tests should stop the Synapse instances after running and the
plugin also stop any remaining instances after all tests have run, it is possible
to be left with some stray containers if, for example, you terminate a test such
that the `after()` does not run and also exit Cypress uncleanly. All the containers
-it starts are prefixed so they are easy to recognise. They can be removed safely.
+it starts are prefixed, so they are easy to recognise. They can be removed safely.
-After each test run, logs from the Syanpse instances are saved in `cypress/synapselogs`
-with each instance in a separate directory named after it's ID. These logs are removed
+After each test run, logs from the Synapse instances are saved in `cypress/synapselogs`
+with each instance in a separate directory named after its ID. These logs are removed
at the start of each test run.
## Writing Tests
@@ -73,23 +74,29 @@ https://docs.cypress.io/guides/references/best-practices .
### Getting a Synapse
The key difference is in starting Synapse instances. Tests use this plugin via
-`cy.task()` to provide a Synapse instance to log into:
+`cy.startSynapse()` to provide a Synapse instance to log into:
-```
-cy.task("synapseStart", "consent").then(result => {
- synapseId = result.synapseId;
- synapsePort = result.port;
+```javascript
+cy.startSynapse("consent").then(result => {
+ synapse = result;
});
```
This returns an object with information about the Synapse instance, including what port
it was started on and the ID that needs to be passed to shut it down again. It also
returns the registration shared secret (`registrationSecret`) that can be used to
-register users via the REST API.
+register users via the REST API. The Synapse has been ensured ready to go by awaiting
+its internal health-check.
Synapse instances should be reasonably cheap to start (you may see the first one take a
while as it pulls the Docker image), so it's generally expected that tests will start a
-Synapse instance for each test suite, ie. in `before()`, and then tear it down in `after()`.
+Synapse instance for each test suite, i.e. in `before()`, and then tear it down in `after()`.
+
+To later destroy your Synapse you should call `stopSynapse`, passing the SynapseInstance
+object you received when starting it.
+```javascript
+cy.stopSynapse(synapse);
+```
### Synapse Config Templates
When a Synapse instance is started, it's given a config generated from one of the config
@@ -100,6 +107,7 @@ in these templates:
* `REGISTRATION_SECRET`: The secret used to register users via the REST API.
* `MACAROON_SECRET_KEY`: Generated each time for security
* `FORM_SECRET`: Generated each time for security
+ * `PUBLIC_BASEURL`: The localhost url + port combination the synapse is accessible at
* `localhost.signing.key`: A signing key is auto-generated and saved to this file.
Config templates should not contain a signing key and instead assume that one will exist
in this file.
@@ -108,42 +116,41 @@ All other files in the template are copied recursively to `/data/`, so the file
in a template can be referenced in the config as `/data/foo.html`.
### Logging In
-This doesn't quite exist yet. Most tests will just want to start with the client in a 'logged in'
-state, so we should provide an easy way to start a test with element in this state. The
-`registrationSecret` provided when starting a Synapse can be used to create a user (porting
-the code from /~https://github.com/matrix-org/matrix-react-sdk/blob/develop/test/end-to-end-tests/src/rest/creator.ts#L49).
-We'd then need to log in as this user. Ways of doing this would be:
-
-1. Fill in the login form. This isn't ideal as it's effectively testing the login process in each
- test, and will just be slower.
-1. Mint an access token using https://matrix-org.github.io/synapse/develop/admin_api/user_admin_api.html#login-as-a-user
- then inject this into element-web. This would probably be fastest, although also relies on correctly
- setting up localstorage
-1. Mint a login token, inject the Homeserver URL into localstorage and then load element, passing the login
- token as a URL parameter. This is a supported way of logging in to element-web, but there's no API
- on Synapse to make such a token currently. It would be fairly easy to add a synapse-specific admin API
- to do so. We should write tests for token login (and the rest of SSO) at some point anyway though.
-
-If we make this as a convenience API, it can easily be swapped out later: we could start with option 1
-and then switch later.
+There exists a basic utility to start the app with a random user already logged in:
+```javascript
+cy.initTestUser(synapse, "Jeff");
+```
+It takes the SynapseInstance you received from `startSynapse` and a display name for your test user.
+This custom command will register a random userId using the registrationSecret with a random password
+and the given display name. The returned Chainable will contain details about the credentials for if
+they are needed for User-Interactive Auth or similar but localStorage will already be seeded with them
+and the app loaded (path `/`).
+
+The internals of how this custom command run may be swapped out later,
+but the signature can be maintained for simpler maintenance.
### Joining a Room
Many tests will also want to start with the client in a room, ready to send & receive messages. Best
way to do this may be to get an access token for the user and use this to create a room with the REST
-API before logging the user in.
+API before logging the user in. You can make use of `cy.getBot(synapse)` and `cy.getClient()` to do this.
### Convenience APIs
We should probably end up with convenience APIs that wrap the synapse creation, logging in and room
creation that can be called to set up tests.
+### Using matrix-js-sdk
+Due to the way we run the Cypress tests in CI, at this time you can only use the matrix-js-sdk module
+exposed on `window.matrixcs`. This has the limitation that it is only accessible with the app loaded.
+This may be revisited in the future.
+
## Good Test Hygiene
This section mostly summarises general good Cypress testing practice, and should not be news to anyone
already familiar with Cypress.
1. Test a well-isolated unit of functionality. The more specific, the easier it will be to tell what's
- wrong when they fail.
+ wrong when they fail.
1. Don't depend on state from other tests: any given test should be able to run in isolation.
-1. Try to avoid driving the UI for anything other than the UI you're trying to test. eg. if you're
+1. Try to avoid driving the UI for anything other than the UI you're trying to test. e.g. if you're
testing that the user can send a reaction to a message, it's best to send a message using a REST
API, then react to it using the UI, rather than using the element-web UI to send the message.
1. Avoid explicit waits. `cy.get()` will implicitly wait for the specified element to appear and
@@ -160,3 +167,14 @@ already familiar with Cypress.
This is a small selection - the Cypress best practices guide, linked above, has more good advice, and we
should generally try to adhere to them.
+
+## Percy Visual Testing
+We also support visual testing via Percy, this extracts the DOM from Cypress and renders it using custom renderers
+for Safari, Firefox, Chrome & Edge, allowing us to spot visual regressions before they become release regressions.
+Right now we run it as part of the standard Pull Request CI automation but due to only having 25k screenshots/month,
+and each `cy.percySnapshot()` call results in 8 screenshots (4 browsers, 2 sizes) this could quickly be exhausted and
+at that point we would likely run it on a CRON interval or before releases.
+
+To record a snapshot use `cy.percySnapshot()`, you may have to pass `percyCSS` into the 2nd argument to hide certain
+elements which contain dynamic/generated data to avoid them cause false positives in the Percy screenshot diffs.
+
diff --git a/package.json b/package.json
index 40c455b57ed..2745cf91fef 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
- "version": "3.44.0",
+ "version": "3.45.0",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@@ -83,15 +83,15 @@
"is-ip": "^3.1.0",
"jszip": "^3.7.0",
"katex": "^0.12.0",
- "linkify-element": "^4.0.0-beta.4",
- "linkify-string": "^4.0.0-beta.4",
- "linkifyjs": "^4.0.0-beta.4",
+ "linkify-element": "4.0.0-beta.4",
+ "linkify-string": "4.0.0-beta.4",
+ "linkifyjs": "4.0.0-beta.4",
"lodash": "^4.17.20",
"maplibre-gl": "^1.15.2",
- "matrix-analytics-events": "github:matrix-org/matrix-analytics-events.git#4aef17b56798639906f26a8739043a3c5c5fde7e",
+ "matrix-analytics-events": "github:matrix-org/matrix-analytics-events.git#a0687ca6fbdb7258543d49b99fb88b9201e900b0",
"matrix-encrypt-attachment": "^1.0.3",
"matrix-events-sdk": "^0.0.1-beta.7",
- "matrix-js-sdk": "17.2.0",
+ "matrix-js-sdk": "18.0.0",
"matrix-widget-api": "^0.1.0-beta.18",
"minimist": "^1.2.5",
"opus-recorder": "^8.0.3",
@@ -134,6 +134,8 @@
"@babel/traverse": "^7.12.12",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
"@peculiar/webcrypto": "^1.1.4",
+ "@percy/cli": "^1.1.4",
+ "@percy/cypress": "^3.1.1",
"@sentry/types": "^6.10.0",
"@sinonjs/fake-timers": "^9.1.2",
"@types/classnames": "^2.2.11",
@@ -167,14 +169,14 @@
"babel-jest": "^26.6.3",
"blob-polyfill": "^6.0.20211015",
"chokidar": "^3.5.1",
- "cypress": "^9.5.4",
+ "cypress": "^9.6.1",
"enzyme": "^3.11.0",
"enzyme-to-json": "^3.6.2",
"eslint": "8.9.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jsx-a11y": "^6.5.1",
- "eslint-plugin-matrix-org": "^0.4.0",
+ "eslint-plugin-matrix-org": "^0.5.2",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"fs-extra": "^10.0.1",
diff --git a/res/css/_common.scss b/res/css/_common.scss
index 8fbbf5428b9..a0c7bbbd9ac 100644
--- a/res/css/_common.scss
+++ b/res/css/_common.scss
@@ -21,6 +21,7 @@ limitations under the License.
@import "./_font-weights.scss";
@import "./_border-radii.scss";
@import "./_animations.scss";
+@import "./_spacing.scss";
@import url("maplibre-gl/dist/maplibre-gl.css");
$hover-transition: 0.08s cubic-bezier(.46, .03, .52, .96); // quadratic
@@ -386,15 +387,22 @@ legend {
color: $alert;
}
-.mx_Dialog_cancelButton {
+@define-mixin customisedCancelButton {
mask: url('$(res)/img/feather-customised/cancel.svg');
mask-repeat: no-repeat;
mask-position: center;
mask-size: cover;
- width: 14px;
- height: 14px;
background-color: $dialog-close-fg-color;
cursor: pointer;
+ position: unset;
+ width: unset;
+ height: unset;
+}
+
+.mx_Dialog_cancelButton {
+ @mixin customisedCancelButton;
+ width: 14px;
+ height: 14px;
position: absolute;
top: 10px;
right: 0;
@@ -408,7 +416,8 @@ legend {
}
.mx_Dialog_buttons {
- margin-top: 20px;
+ margin-top: $spacing-20;
+ margin-inline-start: auto;
text-align: right;
.mx_Dialog_buttons_additive {
@@ -417,6 +426,22 @@ legend {
}
}
+.mx_Dialog_buttons_row {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ text-align: initial;
+ margin-inline-start: auto;
+
+ // default gap among elements
+ column-gap: $spacing-8; // See margin-right below inside the button style
+ row-gap: 5px; // See margin-bottom below inside the button style
+
+ button {
+ margin: 0 !important; // override the margin settings
+ }
+}
+
/* XXX: Our button style are a mess: buttons that happen to appear in dialogs get special styles applied
* to them that no button anywhere else in the app gets by default. In practice, buttons in other places
* in the app look the same by being AccessibleButtons, or possibly by having explict button classes.
diff --git a/res/css/_components.scss b/res/css/_components.scss
index d2836fdb76c..1b1b87b39d4 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -6,6 +6,7 @@
@import "./_spacing.scss";
@import "./components/views/beacon/_BeaconListItem.scss";
@import "./components/views/beacon/_BeaconStatus.scss";
+@import "./components/views/beacon/_BeaconStatusTooltip.scss";
@import "./components/views/beacon/_BeaconViewDialog.scss";
@import "./components/views/beacon/_DialogOwnBeaconStatus.scss";
@import "./components/views/beacon/_DialogSidebar.scss";
@@ -57,6 +58,7 @@
@import "./structures/_ViewSource.scss";
@import "./structures/auth/_CompleteSecurity.scss";
@import "./structures/auth/_Login.scss";
+@import "./structures/auth/_Registration.scss";
@import "./structures/auth/_SetupEncryptionBody.scss";
@import "./views/audio_messages/_AudioPlayer.scss";
@import "./views/audio_messages/_PlayPauseButton.scss";
@@ -80,6 +82,7 @@
@import "./views/avatars/_WidgetAvatar.scss";
@import "./views/beta/_BetaCard.scss";
@import "./views/context_menus/_CallContextMenu.scss";
+@import "./views/context_menus/_DeviceContextMenu.scss";
@import "./views/context_menus/_IconizedContextMenu.scss";
@import "./views/context_menus/_MessageContextMenu.scss";
@import "./views/dialogs/_AddExistingToSpaceDialog.scss";
@@ -125,7 +128,6 @@
@import "./views/dialogs/_SpacePreferencesDialog.scss";
@import "./views/dialogs/_SpaceSettingsDialog.scss";
@import "./views/dialogs/_SpotlightDialog.scss";
-@import "./views/dialogs/_TabbedIntegrationManagerDialog.scss";
@import "./views/dialogs/_TermsDialog.scss";
@import "./views/dialogs/_UntrustedDeviceDialog.scss";
@import "./views/dialogs/_UploadConfirmDialog.scss";
@@ -143,7 +145,6 @@
@import "./views/elements/_AddressSelector.scss";
@import "./views/elements/_AddressTile.scss";
@import "./views/elements/_CopyableText.scss";
-@import "./views/elements/_DesktopBuildsNotice.scss";
@import "./views/elements/_DesktopCapturerSourcePicker.scss";
@import "./views/elements/_DialPadBackspaceButton.scss";
@import "./views/elements/_DirectorySearchBox.scss";
@@ -162,6 +163,7 @@
@import "./views/elements/_InviteReason.scss";
@import "./views/elements/_ManageIntegsButton.scss";
@import "./views/elements/_MiniAvatarUploader.scss";
+@import "./views/elements/_Pill.scss";
@import "./views/elements/_PowerSelector.scss";
@import "./views/elements/_ProgressBar.scss";
@import "./views/elements/_QRCode.scss";
@@ -171,6 +173,7 @@
@import "./views/elements/_RoleButton.scss";
@import "./views/elements/_RoomAliasField.scss";
@import "./views/elements/_SSOButtons.scss";
+@import "./views/elements/_SearchWarning.scss";
@import "./views/elements/_ServerPicker.scss";
@import "./views/elements/_SettingsFlag.scss";
@import "./views/elements/_Slider.scss";
@@ -255,9 +258,11 @@
@import "./views/rooms/_ReplyTile.scss";
@import "./views/rooms/_RoomBreadcrumbs.scss";
@import "./views/rooms/_RoomHeader.scss";
+@import "./views/rooms/_RoomInfoLine.scss";
@import "./views/rooms/_RoomList.scss";
@import "./views/rooms/_RoomListHeader.scss";
@import "./views/rooms/_RoomPreviewBar.scss";
+@import "./views/rooms/_RoomPreviewCard.scss";
@import "./views/rooms/_RoomSublist.scss";
@import "./views/rooms/_RoomTile.scss";
@import "./views/rooms/_RoomTileSc.scss";
diff --git a/res/css/components/views/beacon/_BeaconListItem.scss b/res/css/components/views/beacon/_BeaconListItem.scss
index 60311a4466f..dd99192cf56 100644
--- a/res/css/components/views/beacon/_BeaconListItem.scss
+++ b/res/css/components/views/beacon/_BeaconListItem.scss
@@ -40,6 +40,7 @@ limitations under the License.
.mx_BeaconListItem_info {
flex: 1 1 0;
+ width: 0;
display: flex;
flex-direction: column;
align-items: stretch;
diff --git a/res/css/components/views/beacon/_BeaconStatus.scss b/res/css/components/views/beacon/_BeaconStatus.scss
index 4dd3d325475..95c41749111 100644
--- a/res/css/components/views/beacon/_BeaconStatus.scss
+++ b/res/css/components/views/beacon/_BeaconStatus.scss
@@ -46,14 +46,15 @@ limitations under the License.
}
.mx_BeaconStatus_description {
- flex: 1;
+ flex: 1 1 0;
display: flex;
flex-direction: column;
line-height: $font-14px;
padding-right: $spacing-8;
- // TODO handle text-overflow
+ white-space: nowrap;
+ overflow: hidden;
}
.mx_BeaconStatus_expiryTime {
@@ -62,4 +63,6 @@ limitations under the License.
.mx_BeaconStatus_label {
margin-bottom: 2px;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
diff --git a/res/css/components/views/beacon/_BeaconStatusTooltip.scss b/res/css/components/views/beacon/_BeaconStatusTooltip.scss
new file mode 100644
index 00000000000..07b3a43cc01
--- /dev/null
+++ b/res/css/components/views/beacon/_BeaconStatusTooltip.scss
@@ -0,0 +1,37 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.mx_BeaconStatusTooltip {
+ position: absolute;
+ top: 42px;
+ max-width: 150px;
+ height: 38px;
+ box-sizing: content-box;
+ padding-top: $spacing-8;
+
+ // override copyable text style to make compact
+ .mx_CopyableText_copyButton {
+ margin-left: 0 !important;
+ }
+}
+
+.mx_BeaconStatusTooltip_inner {
+ position: relative;
+ height: 100%;
+ border-radius: 4px;
+ background: $menu-bg-color;
+ box-shadow: 4px 4px 12px 0 $menu-box-shadow-color;
+}
diff --git a/res/css/structures/_RoomDirectory.scss b/res/css/structures/_RoomDirectory.scss
index 9473e18f783..62d12965e41 100644
--- a/res/css/structures/_RoomDirectory.scss
+++ b/res/css/structures/_RoomDirectory.scss
@@ -155,7 +155,7 @@ limitations under the License.
line-height: $font-20px;
padding: 0 5px;
color: $accent-fg-color;
- background-color: $rte-room-pill-color;
+ background-color: $pill-bg-color;
}
.mx_RoomDirectory_topic {
diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss
index eed3d8830f6..f4d37e0e246 100644
--- a/res/css/structures/_SpaceRoomView.scss
+++ b/res/css/structures/_SpaceRoomView.scss
@@ -137,124 +137,6 @@ $SpaceRoomViewInnerWidth: 428px;
}
}
- .mx_SpaceRoomView_preview,
- .mx_SpaceRoomView_landing {
- .mx_SpaceRoomView_info_memberCount {
- color: inherit;
- position: relative;
- padding: 0 0 0 16px;
- font-size: $font-15px;
- display: inline; // cancel inline-flex
-
- &::before {
- content: "·"; // visual separator
- position: absolute;
- left: 6px;
- }
- }
- }
-
- .mx_SpaceRoomView_preview {
- padding: 32px 24px !important; // override default padding from above
- margin: auto;
- max-width: 480px;
- box-sizing: border-box;
- box-shadow: 2px 15px 30px $dialog-shadow-color;
- border-radius: 8px;
- position: relative;
-
- // XXX remove this when spaces leaves Beta
- .mx_BetaCard_betaPill {
- position: absolute;
- right: 24px;
- top: 32px;
- }
-
- // XXX remove this when spaces leaves Beta
- .mx_SpaceRoomView_preview_spaceBetaPrompt {
- font-weight: $font-semi-bold;
- font-size: $font-14px;
- line-height: $font-24px;
- color: $primary-content;
- margin-top: 24px;
- position: relative;
- padding-left: 24px;
-
- .mx_AccessibleButton_kind_link {
- display: inline;
- padding: 0;
- font-size: inherit;
- line-height: inherit;
- }
-
- &::before {
- content: "";
- position: absolute;
- height: $font-24px;
- width: 20px;
- left: 0;
- mask-repeat: no-repeat;
- mask-position: center;
- mask-size: contain;
- mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
- background-color: $secondary-content;
- }
- }
-
- .mx_SpaceRoomView_preview_inviter {
- display: flex;
- align-items: center;
- margin-bottom: 20px;
- font-size: $font-15px;
-
- > div {
- margin-left: 8px;
-
- .mx_SpaceRoomView_preview_inviter_name {
- line-height: $font-18px;
- }
-
- .mx_SpaceRoomView_preview_inviter_mxid {
- line-height: $font-24px;
- color: $secondary-content;
- }
- }
- }
-
- > .mx_RoomAvatar_isSpaceRoom {
- &.mx_BaseAvatar_image, .mx_BaseAvatar_image {
- border-radius: 12px;
- }
- }
-
- h1.mx_SpaceRoomView_preview_name {
- margin: 20px 0 !important; // override default margin from above
- }
-
- .mx_SpaceRoomView_preview_topic {
- font-size: $font-14px;
- line-height: $font-22px;
- color: $secondary-content;
- margin: 20px 0;
- max-height: 160px;
- overflow-y: auto;
- }
-
- .mx_SpaceRoomView_preview_joinButtons {
- margin-top: 20px;
-
- .mx_AccessibleButton {
- width: 200px;
- box-sizing: border-box;
- padding: 14px 0;
-
- & + .mx_AccessibleButton {
- margin-left: 20px;
- }
- }
- }
- }
-
.mx_SpaceRoomView_landing {
display: flex;
flex-direction: column;
@@ -314,40 +196,6 @@ $SpaceRoomViewInnerWidth: 428px;
flex-wrap: wrap;
line-height: $font-24px;
- .mx_SpaceRoomView_info {
- color: $secondary-content;
- font-size: $font-15px;
- display: inline-block;
-
- .mx_SpaceRoomView_info_public,
- .mx_SpaceRoomView_info_private {
- padding-left: 20px;
- position: relative;
-
- &::before {
- position: absolute;
- content: "";
- width: 20px;
- height: 20px;
- top: 0;
- left: -2px;
- mask-position: center;
- mask-repeat: no-repeat;
- background-color: $tertiary-content;
- }
- }
-
- .mx_SpaceRoomView_info_public::before {
- mask-size: 12px;
- mask-image: url("$(res)/img/globe.svg");
- }
-
- .mx_SpaceRoomView_info_private::before {
- mask-size: 14px;
- mask-image: url("$(res)/img/element-icons/lock.svg");
- }
- }
-
.mx_SpaceRoomView_landing_infoBar_interactive {
display: flex;
flex-wrap: wrap;
diff --git a/res/css/structures/_VideoRoomView.scss b/res/css/structures/_VideoRoomView.scss
index d99b3f5894b..3577e7b73e1 100644
--- a/res/css/structures/_VideoRoomView.scss
+++ b/res/css/structures/_VideoRoomView.scss
@@ -24,8 +24,7 @@ limitations under the License.
margin-right: calc($container-gap-width / 2);
background-color: $header-panel-bg-color;
- padding-top: 33px; // to match the right panel chat heading
- border: 8px solid $header-panel-bg-color;
+ padding: 8px;
border-radius: 8px;
.mx_AppTile {
diff --git a/res/css/structures/auth/_CompleteSecurity.scss b/res/css/structures/auth/_CompleteSecurity.scss
index bf5aeb15f56..4c3602ac264 100644
--- a/res/css/structures/auth/_CompleteSecurity.scss
+++ b/res/css/structures/auth/_CompleteSecurity.scss
@@ -34,14 +34,9 @@ limitations under the License.
}
.mx_CompleteSecurity_skip {
- mask: url('$(res)/img/feather-customised/cancel.svg');
- mask-repeat: no-repeat;
- mask-position: center;
- mask-size: cover;
+ @mixin customisedCancelButton;
width: 18px;
height: 18px;
- background-color: $dialog-close-fg-color;
- cursor: pointer;
position: absolute;
right: 24px;
}
diff --git a/res/css/structures/auth/_Registration.scss b/res/css/structures/auth/_Registration.scss
new file mode 100644
index 00000000000..b415e78f107
--- /dev/null
+++ b/res/css/structures/auth/_Registration.scss
@@ -0,0 +1,53 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.mx_Register_mainContent {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ min-height: 270px;
+
+ p {
+ font-size: $font-14px;
+ color: $authpage-primary-color;
+
+ &.secondary {
+ color: $authpage-secondary-color;
+ }
+ }
+
+ > img:first-child {
+ margin-bottom: 16px;
+ width: max-content;
+ }
+
+ .mx_Login_submit {
+ margin-bottom: 0;
+ }
+}
+
+.mx_Register_footerActions {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ padding-top: 16px;
+ margin-top: 16px;
+ border-top: 1px solid rgba(141, 151, 165, 0.2);
+
+ > * {
+ flex-basis: content;
+ }
+}
diff --git a/res/css/views/audio_messages/_AudioPlayer.scss b/res/css/views/audio_messages/_AudioPlayer.scss
index 3c2551e36a5..6b8d0ca4383 100644
--- a/res/css/views/audio_messages/_AudioPlayer.scss
+++ b/res/css/views/audio_messages/_AudioPlayer.scss
@@ -58,10 +58,10 @@ limitations under the License.
}
.mx_Clock {
- width: $font-42px; // we're not using a monospace font, so fake it
min-width: $font-42px; // for flexbox
- padding-left: 4px; // isolate from seek bar
- text-align: right;
+ padding-left: $spacing-4; // isolate from seek bar
+ text-align: justify;
+ white-space: nowrap;
}
}
}
diff --git a/res/css/views/auth/_AuthBody.scss b/res/css/views/auth/_AuthBody.scss
index 33adeff9960..516f3f22de2 100644
--- a/res/css/views/auth/_AuthBody.scss
+++ b/res/css/views/auth/_AuthBody.scss
@@ -24,6 +24,11 @@ limitations under the License.
padding: 25px 60px;
box-sizing: border-box;
+ &.mx_AuthBody_flex {
+ display: flex;
+ flex-direction: column;
+ }
+
h2 {
font-size: $font-24px;
font-weight: 600;
@@ -139,7 +144,6 @@ limitations under the License.
.mx_AuthBody_changeFlow {
display: block;
text-align: center;
- width: 100%;
> a {
font-weight: $font-semi-bold;
diff --git a/res/css/views/auth/_AuthPage.scss b/res/css/views/auth/_AuthPage.scss
index 816293e5db2..100d98095b1 100644
--- a/res/css/views/auth/_AuthPage.scss
+++ b/res/css/views/auth/_AuthPage.scss
@@ -28,10 +28,12 @@ limitations under the License.
border-radius: $border-radius-4px;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.33);
background-color: $authpage-modal-bg-color;
-}
-@media only screen and (max-width: 480px) {
- .mx_AuthPage_modal {
+ @media only screen and (max-height: 768px) {
+ margin-top: 50px;
+ }
+
+ @media only screen and (max-width: 480px) {
margin-top: 0;
}
}
diff --git a/res/css/views/auth/_InteractiveAuthEntryComponents.scss b/res/css/views/auth/_InteractiveAuthEntryComponents.scss
index b65e733d141..4bdac745f27 100644
--- a/res/css/views/auth/_InteractiveAuthEntryComponents.scss
+++ b/res/css/views/auth/_InteractiveAuthEntryComponents.scss
@@ -14,35 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-.mx_InteractiveAuthEntryComponents_emailWrapper {
- padding-right: 100px;
- position: relative;
- margin-top: 32px;
- margin-bottom: 32px;
-
- &::before, &::after {
- position: absolute;
- width: 116px;
- height: 116px;
- content: "";
- right: -10px;
- }
-
- &::before {
- background-color: rgba(244, 246, 250, 0.91);
- border-radius: 50%;
- top: -20px;
- }
-
- &::after {
- background-image: url('$(res)/img/element-icons/email-prompt.svg');
- background-repeat: no-repeat;
- background-position: center;
- background-size: contain;
- top: -25px;
- }
-}
-
.mx_InteractiveAuthEntryComponents_msisdnWrapper {
text-align: center;
}
@@ -103,3 +74,21 @@ limitations under the License.
margin-left: 5px;
}
}
+
+.mx_InteractiveAuthEntryComponents_emailWrapper {
+ // "Resend" button/link
+ .mx_AccessibleButton_kind_link_inline {
+ // We need this to be an inline-block so positioning works correctly
+ display: inline-block !important;
+
+ // Spinner as end adornment of the "resend" button/link
+ .mx_Spinner {
+ // Spinners are usually block elements, but we need it as inline element
+ display: inline-flex !important;
+ // Spinners by default fill all available width, but we don't want that
+ width: auto !important;
+ // We need to center the spinner relative to the button/link
+ vertical-align: middle !important;
+ }
+ }
+}
diff --git a/res/css/views/avatars/_BaseAvatar.scss b/res/css/views/avatars/_BaseAvatar.scss
index 964e8156261..16261f000e3 100644
--- a/res/css/views/avatars/_BaseAvatar.scss
+++ b/res/css/views/avatars/_BaseAvatar.scss
@@ -47,6 +47,7 @@ limitations under the License.
.mx_BaseAvatar_image {
object-fit: cover;
+ aspect-ratio: 1;
border-radius: 125px;
vertical-align: top;
background-color: $background;
diff --git a/res/css/views/beta/_BetaCard.scss b/res/css/views/beta/_BetaCard.scss
index 658e43f051b..d54dd4a4c82 100644
--- a/res/css/views/beta/_BetaCard.scss
+++ b/res/css/views/beta/_BetaCard.scss
@@ -119,7 +119,7 @@ limitations under the License.
font-size: 12px;
font-weight: $font-semi-bold;
line-height: 15px;
- color: #FFFFFF;
+ color: $button-primary-fg-color;
display: inline-block;
vertical-align: text-bottom;
word-break: keep-all; // avoid multiple lines on CJK language
diff --git a/res/css/views/context_menus/_DeviceContextMenu.scss b/res/css/views/context_menus/_DeviceContextMenu.scss
new file mode 100644
index 00000000000..4b886279d7d
--- /dev/null
+++ b/res/css/views/context_menus/_DeviceContextMenu.scss
@@ -0,0 +1,27 @@
+/*
+Copyright 2021 Šimon Brandner
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.mx_DeviceContextMenu {
+ max-width: 252px;
+
+ .mx_DeviceContextMenu_device_icon {
+ display: none;
+ }
+
+ .mx_IconizedContextMenu_label {
+ padding-left: 0 !important;
+ }
+}
diff --git a/res/css/views/context_menus/_IconizedContextMenu.scss b/res/css/views/context_menus/_IconizedContextMenu.scss
index 430970d7cd2..fa744ce732f 100644
--- a/res/css/views/context_menus/_IconizedContextMenu.scss
+++ b/res/css/views/context_menus/_IconizedContextMenu.scss
@@ -25,6 +25,11 @@ limitations under the License.
padding-right: 20px;
}
+ .mx_IconizedContextMenu_optionList_label {
+ font-size: $font-15px;
+ font-weight: $font-semi-bold;
+ }
+
// the notFirst class is for cases where the optionList might be under a header of sorts.
&:nth-child(n + 2), .mx_IconizedContextMenu_optionList_notFirst {
// This is a bit of a hack when we could just use a simple border-top property,
diff --git a/res/css/views/dialogs/_CompoundDialog.scss b/res/css/views/dialogs/_CompoundDialog.scss
index d90c7e0f8e6..28e7388e0ea 100644
--- a/res/css/views/dialogs/_CompoundDialog.scss
+++ b/res/css/views/dialogs/_CompoundDialog.scss
@@ -38,14 +38,9 @@ limitations under the License.
}
.mx_CompoundDialog_cancelButton {
- mask: url('$(res)/img/feather-customised/cancel.svg');
- mask-repeat: no-repeat;
- mask-position: center;
- mask-size: cover;
+ @mixin customisedCancelButton;
width: 20px;
height: 20px;
- background-color: $dialog-close-fg-color;
- cursor: pointer;
// Align with middle of title, 34px from right edge
position: absolute;
diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss
index ad7bf9a8167..2cdec19ebfb 100644
--- a/res/css/views/dialogs/_ForwardDialog.scss
+++ b/res/css/views/dialogs/_ForwardDialog.scss
@@ -85,6 +85,10 @@ limitations under the License.
margin-top: 24px;
}
+ .mx_ForwardList_resultsList {
+ padding-right: 8px;
+ }
+
.mx_ForwardList_entry {
display: flex;
justify-content: space-between;
diff --git a/res/css/views/dialogs/_MessageEditHistoryDialog.scss b/res/css/views/dialogs/_MessageEditHistoryDialog.scss
index 5838939d9bf..1d7759fe2bb 100644
--- a/res/css/views/dialogs/_MessageEditHistoryDialog.scss
+++ b/res/css/views/dialogs/_MessageEditHistoryDialog.scss
@@ -55,6 +55,12 @@ limitations under the License.
text-decoration: underline;
}
+ .mx_EventTile {
+ .mx_MessageTimestamp {
+ position: absolute;
+ }
+ }
+
.mx_EventTile_line, .mx_EventTile_content {
margin-right: 0px;
}
diff --git a/res/css/views/dialogs/_TabbedIntegrationManagerDialog.scss b/res/css/views/dialogs/_TabbedIntegrationManagerDialog.scss
deleted file mode 100644
index c096ec2a7ce..00000000000
--- a/res/css/views/dialogs/_TabbedIntegrationManagerDialog.scss
+++ /dev/null
@@ -1,62 +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_TabbedIntegrationManagerDialog .mx_Dialog {
- width: 60%;
- height: 70%;
- overflow: hidden;
- padding: 0;
- max-width: initial;
- max-height: initial;
- position: relative;
-}
-
-.mx_TabbedIntegrationManagerDialog_container {
- // Full size of the dialog, whatever it is
- position: absolute;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
-
- .mx_TabbedIntegrationManagerDialog_currentManager {
- width: 100%;
- height: 100%;
- border-top: 1px solid $accent;
-
- iframe {
- background-color: #fff;
- border: 0;
- width: 100%;
- height: 100%;
- }
- }
-}
-
-.mx_TabbedIntegrationManagerDialog_tab {
- display: inline-block;
- border: 1px solid $accent;
- border-bottom: 0;
- border-top-left-radius: $border-radius-3px;
- border-top-right-radius: $border-radius-3px;
- padding: 10px 8px;
- margin-right: 5px;
-}
-
-.mx_TabbedIntegrationManagerDialog_currentTab {
- background-color: $accent;
- color: $accent-fg-color;
-}
diff --git a/res/css/views/dialogs/security/_AccessSecretStorageDialog.scss b/res/css/views/dialogs/security/_AccessSecretStorageDialog.scss
index 3d716800508..f3558212cca 100644
--- a/res/css/views/dialogs/security/_AccessSecretStorageDialog.scss
+++ b/res/css/views/dialogs/security/_AccessSecretStorageDialog.scss
@@ -14,38 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-.mx_AccessSecretStorageDialog_reset {
- position: relative;
- padding-left: 24px; // 16px icon + 8px padding
- margin-top: 7px; // vertical alignment to buttons
- margin-bottom: 7px; // space between the buttons and the text when float is activated
- text-align: left;
-
- &::before {
- content: "";
- display: inline-block;
- position: absolute;
- height: 16px;
- width: 16px;
- left: 0;
- top: 2px; // alignment
- background-image: url("$(res)/img/element-icons/warning-badge.svg");
- background-size: contain;
- }
-
- .mx_AccessSecretStorageDialog_reset_link {
- color: $alert;
- }
-}
-
.mx_AccessSecretStorageDialog_titleWithIcon::before {
content: '';
display: inline-block;
width: 24px;
height: 24px;
- margin-right: 8px;
+ margin-inline-end: $spacing-8;
position: relative;
- top: 5px;
+ top: 5px; // TODO: spacing variable
background-color: $primary-content;
}
@@ -84,7 +60,7 @@ limitations under the License.
}
.mx_AccessSecretStorageDialog_recoveryKeyEntry_entryControlSeparatorText {
- margin: 16px;
+ margin: $spacing-16;
}
.mx_AccessSecretStorageDialog_recoveryKeyFeedback {
@@ -97,7 +73,7 @@ limitations under the License.
mask-repeat: no-repeat;
mask-position: center;
mask-size: 20px;
- margin-right: 5px;
+ margin-inline-end: 5px; // TODO: spacing variable
}
}
@@ -120,3 +96,44 @@ limitations under the License.
.mx_AccessSecretStorageDialog_recoveryKeyEntry_fileInput {
display: none;
}
+
+.mx_AccessSecretStorageDialog_primaryContainer {
+ .mx_Dialog_buttons {
+ $spacingStart: $spacing-24; // 16px icon + 8px padding
+
+ text-align: initial;
+ display: flex;
+ flex-flow: column;
+ gap: 14px; // TODO: spacing variable
+
+ .mx_Dialog_buttons_additive {
+ float: none;
+
+ .mx_AccessSecretStorageDialog_reset {
+ position: relative;
+ padding-inline-start: $spacingStart;
+
+ &::before {
+ content: "";
+ display: inline-block;
+ position: absolute;
+ height: 16px;
+ width: 16px;
+ left: 0;
+ top: 2px; // alignment
+ background-image: url("$(res)/img/element-icons/warning-badge.svg");
+ background-size: contain;
+ }
+
+ .mx_AccessSecretStorageDialog_reset_link {
+ color: $alert;
+ }
+ }
+ }
+
+ .mx_Dialog_buttons_row {
+ gap: $spacing-16; // TODO: needs normalization
+ padding-inline-start: $spacingStart;
+ }
+ }
+}
diff --git a/res/css/views/elements/_EditableItemList.scss b/res/css/views/elements/_EditableItemList.scss
index 91ef20539cf..87824562490 100644
--- a/res/css/views/elements/_EditableItemList.scss
+++ b/res/css/views/elements/_EditableItemList.scss
@@ -25,14 +25,12 @@ limitations under the License.
}
.mx_EditableItem_delete {
+ @mixin customisedCancelButton;
order: 3;
margin-right: 5px;
- cursor: pointer;
vertical-align: middle;
width: 14px;
height: 14px;
- mask-image: url('$(res)/img/feather-customised/cancel.svg');
- mask-repeat: no-repeat;
background-color: $alert;
mask-size: 100%;
}
diff --git a/res/css/views/elements/_FacePile.scss b/res/css/views/elements/_FacePile.scss
index 90f1c590a14..e40695fcf14 100644
--- a/res/css/views/elements/_FacePile.scss
+++ b/res/css/views/elements/_FacePile.scss
@@ -15,6 +15,9 @@ limitations under the License.
*/
.mx_FacePile {
+ display: flex;
+ align-items: center;
+
.mx_FacePile_faces {
display: inline-flex;
flex-direction: row-reverse;
diff --git a/res/css/views/elements/_ImageView.scss b/res/css/views/elements/_ImageView.scss
index 787d33ddc22..e0fb9144471 100644
--- a/res/css/views/elements/_ImageView.scss
+++ b/res/css/views/elements/_ImageView.scss
@@ -79,6 +79,11 @@ $button-gap: 24px;
font-weight: bold;
}
+.mx_ImageView_title {
+ color: $lightbox-fg-color;
+ font-size: $font-12px;
+}
+
.mx_ImageView_toolbar {
padding-right: 16px;
pointer-events: initial;
diff --git a/res/css/views/elements/_InteractiveTooltip.scss b/res/css/views/elements/_InteractiveTooltip.scss
index 8196441b6d7..2240fbc3ac8 100644
--- a/res/css/views/elements/_InteractiveTooltip.scss
+++ b/res/css/views/elements/_InteractiveTooltip.scss
@@ -16,7 +16,7 @@ limitations under the License.
.mx_InteractiveTooltip_wrapper {
position: fixed;
- z-index: 5000;
+ z-index: 3999;
}
.mx_InteractiveTooltip {
diff --git a/res/css/views/elements/_MiniAvatarUploader.scss b/res/css/views/elements/_MiniAvatarUploader.scss
index 46ffd9a01cd..577cf40727a 100644
--- a/res/css/views/elements/_MiniAvatarUploader.scss
+++ b/res/css/views/elements/_MiniAvatarUploader.scss
@@ -25,7 +25,9 @@ limitations under the License.
z-index: unset;
width: max-content;
left: 72px;
- top: 0;
+ // top edge starting at 50 % of parent - 50 % of itself -> centered vertically
+ top: 50%;
+ transform: translateY(-50%);
}
.mx_MiniAvatarUploader_indicator {
diff --git a/res/css/views/elements/_Pill.scss b/res/css/views/elements/_Pill.scss
new file mode 100644
index 00000000000..e9ccef666a6
--- /dev/null
+++ b/res/css/views/elements/_Pill.scss
@@ -0,0 +1,61 @@
+/*
+Copyright 2021 Šimon Brandner
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.mx_Pill {
+ padding: $font-1px 0.4em $font-1px 0.4em;
+ line-height: $font-17px;
+ border-radius: $font-16px;
+ vertical-align: text-top;
+ display: inline-flex;
+ align-items: center;
+
+ cursor: pointer;
+
+ color: $accent-fg-color !important; // To override .markdown-body
+ background-color: $pill-bg-color !important; // To override .markdown-body
+
+ &.mx_UserPill_me,
+ &.mx_AtRoomPill {
+ background-color: $alert !important; // To override .markdown-body
+ }
+
+ &:hover {
+ background-color: $pill-hover-bg-color !important; // To override .markdown-body
+ }
+
+ &.mx_UserPill_me:hover {
+ background-color: #ff6b75 !important; // To override .markdown-body | same on both themes
+ }
+
+ // We don't want to indicate clickability
+ &.mx_AtRoomPill:hover {
+ background-color: $alert !important; // To override .markdown-body
+ cursor: unset;
+ }
+
+ &::before,
+ .mx_BaseAvatar {
+ margin-left: -0.3em; // Otherwise the gap is too large
+ margin-right: 0.2em;
+ }
+
+ a& {
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ text-decoration: none !important; // To override .markdown-body
+ }
+}
diff --git a/res/css/views/elements/_RichText.scss b/res/css/views/elements/_RichText.scss
index d349947a5de..69df80b166d 100644
--- a/res/css/views/elements/_RichText.scss
+++ b/res/css/views/elements/_RichText.scss
@@ -2,104 +2,6 @@
// naming scheme; it's completely unclear where or how they're being used
// --Matthew
-.mx_UserPill,
-.mx_RoomPill,
-.mx_AtRoomPill {
- display: inline-flex;
- align-items: center;
- vertical-align: middle;
- border-radius: $font-16px;
- line-height: $font-15px;
- padding-left: 0;
-}
-
-.mx_CustomEmojiPill {
- display: inline-flex;
- align-items: center;
- vertical-align: middle;
- padding-left: 1px;
- font-size: 0;
-}
-
-a.mx_Pill {
- text-overflow: ellipsis;
- white-space: nowrap;
- overflow: hidden;
- max-width: 100%;
-}
-
-.mx_Pill {
- padding: $font-1px;
- padding-right: 0.4em;
- vertical-align: text-top;
- line-height: $font-17px;
-}
-
-/* More specific to override `.markdown-body a` text-decoration */
-.mx_EventTile_content .markdown-body a.mx_Pill {
- text-decoration: none;
-}
-
-/* More specific to override `.markdown-body a` color */
-.mx_EventTile_content .markdown-body a.mx_UserPill,
-.mx_UserPill {
- color: $primary-content;
- background-color: $other-user-pill-bg-color;
-}
-
-.mx_UserPill_selected {
- background-color: $accent !important;
-}
-
-/* More specific to override `.markdown-body a` color */
-.mx_EventTile_highlight .mx_EventTile_content .markdown-body a.mx_UserPill_me,
-.mx_EventTile_content .markdown-body a.mx_AtRoomPill,
-.mx_EventTile_content .mx_AtRoomPill,
-.mx_MessageComposer_input .mx_AtRoomPill {
- color: $accent-fg-color;
- background-color: $alert;
-}
-
-/* More specific to override `.markdown-body a` color */
-.mx_EventTile_content .markdown-body a.mx_RoomPill,
-.mx_RoomPill {
- color: $primary-content;
- background-color: $rte-room-pill-color;
-}
-
-.mx_EventTile_body .mx_UserPill,
-.mx_EventTile_body .mx_RoomPill {
- cursor: pointer;
-}
-
-.mx_UserPill .mx_BaseAvatar,
-.mx_RoomPill .mx_BaseAvatar,
-.mx_AtRoomPill .mx_BaseAvatar {
- position: relative;
- display: inline-flex;
- align-items: center;
- border-radius: 10rem;
- margin-right: 0.24rem;
- pointer-events: none;
-}
-
-.mx_Emoji {
- // Should be 1.8rem for our default 1.4rem message bodies,
- // and scale with the size of the surrounding text
- font-size: calc(18 / 14 * 1em);
- vertical-align: bottom;
-}
-
-// same for custom emojis
-img[data-mx-emoticon] {
- // Should be 1.8rem for our default 1.4rem message bodies,
- // and scale with the size of the surrounding text
- max-height: unset !important;
- height: calc(18 / 14 * 1em) !important;
- vertical-align: bottom;
- object-position: center;
-}
-
.mx_Markdown_BOLD {
font-weight: bold;
}
@@ -133,3 +35,10 @@ img[data-mx-emoticon] {
.mx_Markdown_STRIKETHROUGH {
text-decoration: line-through;
}
+
+.mx_Emoji {
+ // Should be 1.8rem for our default message bodies, and scale with the
+ // surrounding text
+ font-size: max($font-18px, 1em);
+ vertical-align: bottom;
+}
diff --git a/res/css/views/elements/_DesktopBuildsNotice.scss b/res/css/views/elements/_SearchWarning.scss
similarity index 96%
rename from res/css/views/elements/_DesktopBuildsNotice.scss
rename to res/css/views/elements/_SearchWarning.scss
index 3672595bf1e..c69cfd561b9 100644
--- a/res/css/views/elements/_DesktopBuildsNotice.scss
+++ b/res/css/views/elements/_SearchWarning.scss
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-.mx_DesktopBuildsNotice {
+.mx_SearchWarning {
text-align: center;
padding: 0 16px;
diff --git a/res/css/views/elements/_Tooltip.scss b/res/css/views/elements/_Tooltip.scss
index 48b36377f7f..7fd568e2e70 100644
--- a/res/css/views/elements/_Tooltip.scss
+++ b/res/css/views/elements/_Tooltip.scss
@@ -70,14 +70,17 @@ limitations under the License.
font-weight: 500;
max-width: 300px;
word-break: break-word;
- margin-left: 6px;
- margin-right: 6px;
background-color: #21262C; // Same on both themes
color: $accent-fg-color;
border: 0;
text-align: center;
+ &:not(.mx_Tooltip_noMargin) {
+ margin-left: 6px;
+ margin-right: 6px;
+ }
+
.mx_Tooltip_chevron {
display: none;
}
diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss
index 8fbe5eb13d6..0cbcbd46582 100644
--- a/res/css/views/messages/_MImageBody.scss
+++ b/res/css/views/messages/_MImageBody.scss
@@ -17,6 +17,28 @@ limitations under the License.
$timeline-image-border-radius: $border-radius-8px;
+.mx_MImageBody_banner {
+ position: absolute;
+ bottom: 4px;
+ left: 4px;
+ padding: 4px;
+ border-radius: $timeline-image-border-radius;
+ font-size: $font-15px;
+
+ pointer-events: none; // let the cursor go through to the media underneath
+
+ // Trying to match the width of the image is surprisingly difficult, so arbitrarily break it off early.
+ max-width: min(100%, 350px);
+
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+
+ // Hardcoded colours because it's the same on all themes
+ background-color: rgba(0, 0, 0, 0.6);
+ color: #ffffff;
+}
+
.mx_MImageBody_placeholder {
// Position the placeholder on top of the thumbnail, so that the reveal animation can work
position: absolute;
diff --git a/res/css/views/messages/_MImageReplyBody.scss b/res/css/views/messages/_MImageReplyBody.scss
index 3207443d65b..2bdf571f0d1 100644
--- a/res/css/views/messages/_MImageReplyBody.scss
+++ b/res/css/views/messages/_MImageReplyBody.scss
@@ -20,6 +20,10 @@ limitations under the License.
.mx_MImageBody_thumbnail_container {
flex: 1;
margin-right: 4px;
+
+ .mx_MImageBody_banner {
+ display: none;
+ }
}
.mx_MImageReplyBody_info {
diff --git a/res/css/views/messages/_MLocationBody.scss b/res/css/views/messages/_MLocationBody.scss
index 72202ca6e1b..cbbd34526db 100644
--- a/res/css/views/messages/_MLocationBody.scss
+++ b/res/css/views/messages/_MLocationBody.scss
@@ -15,7 +15,10 @@ limitations under the License.
*/
.mx_MLocationBody {
+ max-width: 100%;
+
.mx_MLocationBody_map {
+ max-width: 100%;
width: 450px;
height: 300px;
z-index: 0; // keeps the entire map under the message action bar
@@ -27,15 +30,15 @@ limitations under the License.
/* In the timeline, we fit the width of the container */
.mx_EventTile_line .mx_MLocationBody .mx_MLocationBody_map {
- width: 100%;
max-width: 450px;
+ width: 100%;
}
-.mx_EventTile[data-layout="bubble"] .mx_EventTile_line .mx_MLocationBody {
+.mx_EventTile[data-layout="bubble"] .mx_EventTile_line .mx_MLocationBody .mx_MLocationBody_map {
max-width: 100%;
+ width: 450px;
+}
- .mx_MLocationBody_map {
- max-width: 100%;
- width: 450px;
- }
+.mx_DisambiguatedProfile ~ .mx_MLocationBody {
+ margin-top: 6px; // See: /~https://github.com/matrix-org/matrix-react-sdk/pull/8442
}
diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss
index 593e1f4bd95..c00e47a93f6 100644
--- a/res/css/views/messages/_MessageActionBar.scss
+++ b/res/css/views/messages/_MessageActionBar.scss
@@ -16,11 +16,14 @@ limitations under the License.
*/
.mx_MessageActionBar {
+ --MessageActionBar-size-button: 28px;
+ --MessageActionBar-size-box: 32px; // 28px + 2px (margin) * 2
+
position: absolute;
visibility: hidden;
cursor: pointer;
display: flex;
- height: 32px;
+ height: var(--MessageActionBar-size-box);
line-height: $font-24px;
border-radius: $border-radius-8px;
background: $background;
@@ -28,8 +31,9 @@ limitations under the License.
top: -32px;
right: 8px;
user-select: none;
- // Ensure the action bar appears above over things, like the read marker.
- z-index: 1;
+ // Ensure the action bar appears above other things like the read marker
+ // and sender avatar (for small screens)
+ z-index: 10;
// Adds a previous event safe area so that you can't accidentally hover the
// previous event while trying to mouse into the action bar or from the
@@ -63,8 +67,8 @@ limitations under the License.
}
.mx_MessageActionBar_maskButton {
- width: 28px;
- height: 28px;
+ width: var(--MessageActionBar-size-button);
+ height: var(--MessageActionBar-size-button);
&:disabled,
&[disabled] {
diff --git a/res/css/views/messages/_MessageTimestamp.scss b/res/css/views/messages/_MessageTimestamp.scss
index 85c910296af..d496ff82c8c 100644
--- a/res/css/views/messages/_MessageTimestamp.scss
+++ b/res/css/views/messages/_MessageTimestamp.scss
@@ -18,4 +18,5 @@ limitations under the License.
color: $event-timestamp-color;
font-size: $font-10px;
font-variant-numeric: tabular-nums;
+ width: $MessageTimestamp_width;
}
diff --git a/res/css/views/messages/_TextualEvent.scss b/res/css/views/messages/_TextualEvent.scss
index 607ed9cecba..530ad50587b 100644
--- a/res/css/views/messages/_TextualEvent.scss
+++ b/res/css/views/messages/_TextualEvent.scss
@@ -17,6 +17,7 @@ limitations under the License.
.mx_TextualEvent {
opacity: 0.5;
overflow-y: hidden;
+ line-height: normal;
a {
color: $accent;
diff --git a/res/css/views/messages/_ViewSourceEvent.scss b/res/css/views/messages/_ViewSourceEvent.scss
index 5e288eb19ac..c0803eafd05 100644
--- a/res/css/views/messages/_ViewSourceEvent.scss
+++ b/res/css/views/messages/_ViewSourceEvent.scss
@@ -19,8 +19,11 @@ limitations under the License.
opacity: 0.6;
font-size: $font-12px;
width: 100%;
+ overflow-x: auto; // Cancel overflow setting of .mx_EventTile_content
+ line-height: normal; // Align with avatar and E2E icon
- pre, code {
+ pre,
+ code {
flex: 1;
}
diff --git a/res/css/views/right_panel/_BaseCard.scss b/res/css/views/right_panel/_BaseCard.scss
index f933b317b9b..a615f5a8814 100644
--- a/res/css/views/right_panel/_BaseCard.scss
+++ b/res/css/views/right_panel/_BaseCard.scss
@@ -15,6 +15,8 @@ limitations under the License.
*/
.mx_BaseCard {
+ --BaseCard_EventTile_line-padding-block: 2px;
+
padding: 0 8px;
overflow: hidden;
display: flex;
diff --git a/res/css/views/right_panel/_ThreadPanel.scss b/res/css/views/right_panel/_ThreadPanel.scss
index 9e9c59d2cbb..bab7c2e608c 100644
--- a/res/css/views/right_panel/_ThreadPanel.scss
+++ b/res/css/views/right_panel/_ThreadPanel.scss
@@ -104,11 +104,13 @@ limitations under the License.
}
}
- .mx_AutoHideScrollbar {
+ .mx_AutoHideScrollbar,
+ .mx_RoomView_messagePanelSpinner {
background-color: $background;
border-radius: 8px;
padding-inline-end: 0;
overflow-y: scroll; // set gap between the thread tile and the right border
+ height: 100%;
}
// Override _GroupLayout.scss for the thread panel
@@ -189,17 +191,13 @@ limitations under the License.
}
}
- .mx_GenericEventListSummary > .mx_EventTile_line {
- padding-left: 30px !important; // Override main timeline styling - align summary text with message text
- }
-
- .mx_EventTile:not([data-layout=bubble]) {
- .mx_EventTile_e2eIcon {
- left: 8px;
+ .mx_GenericEventListSummary {
+ &[data-layout=bubble] > .mx_EventTile_line {
+ padding-left: 30px !important; // Override main timeline styling - align summary text with message text
}
- &:hover .mx_EventTile_line {
- box-shadow: unset !important; // don't show the verification left stroke in the thread list
+ &:not([data-layout=bubble]) > .mx_EventTile_line {
+ padding-inline-start: var(--ThreadView_group_spacing-start); // align summary text with message text
}
}
@@ -239,27 +237,64 @@ limitations under the License.
color: $secondary-content;
}
- // handling for hidden events (e.g reactions) in the thread view
- &.mx_ThreadView .mx_EventTile_info {
- padding-top: 0 !important; // override main timeline padding
-
- .mx_EventTile_line {
- padding-left: 0 !important; // override main timeline padding
+ &.mx_ThreadView .mx_EventTile {
+ // handling for hidden events (e.g reactions) in the thread view
- .mx_EventTile_content {
- margin-left: 48px; // align with text
- width: calc(100% - 48px - 8px); // match width of parent
+ &:not([data-layout=bubble]) {
+ &:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line,
+ &:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line,
+ &:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line {
+ padding-inline-start: 0; // Override
}
}
- .mx_EventTile_avatar {
- position: absolute;
- left: 30px !important; // override main timeline positioning
- z-index: 9; // position above the hover styling
- }
+ &.mx_EventTile_info {
+ padding-top: 0;
+
+ &.mx_EventTile_selected .mx_EventTile_line,
+ .mx_EventTile_line {
+ $line-height: $font-12px;
- .mx_ViewSourceEvent_toggle {
- display: none; // hide the hidden event expand button, not enough space, view source can still be used
+ padding-inline-start: 0;
+ line-height: $line-height;
+
+ .mx_EventTile_content,
+ .mx_RedactedBody {
+ width: auto;
+ margin-inline-start: calc(var(--ThreadView_group_spacing-start) + 14px + 6px); // 14px: avatar width, 6px: 20px - 14px
+ font-size: $line-height;
+ }
+ }
+
+ &:not([data-layout=bubble]) {
+ .mx_MessageTimestamp {
+ top: 2px; // Align with avatar
+ }
+
+ .mx_EventTile_avatar {
+ left: calc($MessageTimestamp_width + 14px - 4px); // 14px: avatar width, 4px: align with text
+ z-index: 9; // position above the hover styling
+ }
+ }
+
+ &[data-layout=bubble] {
+ .mx_EventTile_avatar {
+ inset-inline-start: 0;
+ }
+ }
+
+ .mx_EventTile_avatar {
+ position: absolute;
+ top: 1.5px; // Align with hidden event content
+ margin-top: 0;
+ margin-bottom: 0;
+ width: 14px; // avatar img size
+ height: 14px; // avatar img size
+ }
+
+ .mx_ViewSourceEvent_toggle {
+ display: none; // hide the hidden event expand button, not enough space, view source can still be used
+ }
}
}
diff --git a/res/css/views/right_panel/_TimelineCard.scss b/res/css/views/right_panel/_TimelineCard.scss
index 5a121a8f61c..6b4c0d23610 100644
--- a/res/css/views/right_panel/_TimelineCard.scss
+++ b/res/css/views/right_panel/_TimelineCard.scss
@@ -43,7 +43,8 @@ limitations under the License.
}
.mx_NewRoomIntro {
- margin-left: 36px;
+ margin-inline-start: 36px; // TODO: Use a variable
+ margin-inline-end: 36px; // TODO: Use a variable
}
.mx_EventTile_content {
@@ -51,31 +52,45 @@ limitations under the License.
}
.mx_EventTile:not([data-layout="bubble"]) {
+ $left-gutter: 36px;
+
.mx_EventTile_line {
- padding-left: 36px;
- padding-right: 36px;
+ padding-inline-start: $left-gutter;
+ padding-inline-end: 36px;
+ padding-top: var(--BaseCard_EventTile_line-padding-block);
+ padding-bottom: var(--BaseCard_EventTile_line-padding-block);
+
+ .mx_EventTile_e2eIcon {
+ inset-inline-start: 8px;
+ }
+ }
+
+ .mx_DisambiguatedProfile,
+ .mx_ReactionsRow,
+ .mx_ThreadSummary {
+ margin-inline-start: $left-gutter;
}
.mx_ReactionsRow {
padding: 0;
// See margin setting of ReactionsRow on _EventTile.scss
- margin-left: 36px;
margin-right: 8px;
}
.mx_ThreadSummary {
- margin-left: 36px;
margin-right: 0;
max-width: min(calc(100% - 36px), 600px);
}
.mx_EventTile_avatar {
+ position: absolute; // for IRC layout
top: 12px;
left: -3px;
}
.mx_MessageTimestamp {
+ position: absolute; // for modern layout and IRC layout
right: -4px;
left: auto;
}
@@ -86,7 +101,7 @@ limitations under the License.
&.mx_EventTile_info {
.mx_EventTile_line {
- padding-left: 36px;
+ padding-left: $left-gutter;
}
.mx_EventTile_avatar {
@@ -95,18 +110,6 @@ limitations under the License.
}
}
- .mx_GroupLayout {
- .mx_EventTile {
- > .mx_DisambiguatedProfile {
- margin-left: 36px;
- }
-
- .mx_EventTile_line {
- padding-bottom: 8px;
- }
- }
- }
-
.mx_CallEvent_wrapper {
justify-content: center;
margin: auto 5px;
diff --git a/res/css/views/right_panel/_VerificationPanel.scss b/res/css/views/right_panel/_VerificationPanel.scss
index d971ba31733..e416cd8234d 100644
--- a/res/css/views/right_panel/_VerificationPanel.scss
+++ b/res/css/views/right_panel/_VerificationPanel.scss
@@ -23,16 +23,36 @@ limitations under the License.
}
}
-.mx_UserInfo {
+.mx_UserInfo.mx_BaseCard {
+ .mx_UserInfo_container:not(.mx_UserInfo_separator) {
+
+ > div > p {
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+
+ .mx_VerificationPanel_verifyByEmojiButton,
+ .mx_UserInfo_wideButton {
+ width: fit-content;
+ }
+
+ .mx_EncryptionInfo_spinner,
+ .mx_VerificationShowSas {
+ margin-inline-start: auto;
+ margin-inline-end: auto;
+ }
+
+ .mx_Spinner,
+ .mx_VerificationShowSas {
+ align-items: center;
+ }
+ }
+
.mx_EncryptionPanel_cancel {
- mask: url('$(res)/img/feather-customised/cancel.svg');
- mask-repeat: no-repeat;
- mask-position: center;
- mask-size: cover;
+ @mixin customisedCancelButton;
width: 14px;
height: 14px;
background-color: $settings-subsection-fg-color;
- cursor: pointer;
position: absolute;
z-index: 100;
top: 14px;
diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss
index 051a74b7ca1..8bb8fc0aacb 100644
--- a/res/css/views/rooms/_BasicMessageComposer.scss
+++ b/res/css/views/rooms/_BasicMessageComposer.scss
@@ -51,9 +51,15 @@ limitations under the License.
}
&.mx_BasicMessageComposer_input_shouldShowPillAvatar {
- span.mx_UserPill, span.mx_RoomPill {
- position: relative;
+ span.mx_UserPill, span.mx_RoomPill, span.mx_SpacePill {
user-select: all;
+ position: relative;
+ cursor: unset; // We don't want indicate clickability
+
+ &:hover {
+ // We don't want indicate clickability | To override the overriding of .markdown-body
+ background-color: $pill-bg-color !important;
+ }
// avatar psuedo element
&::before {
@@ -90,14 +96,6 @@ limitations under the License.
font-weight: normal;
}
}
-
- span.mx_UserPill {
- cursor: pointer;
- }
-
- span.mx_RoomPill {
- cursor: default;
- }
}
&.mx_BasicMessageComposer_input_disabled {
diff --git a/res/css/views/rooms/_EditMessageComposer.scss b/res/css/views/rooms/_EditMessageComposer.scss
index 452d94a8987..f4c15dac5ef 100644
--- a/res/css/views/rooms/_EditMessageComposer.scss
+++ b/res/css/views/rooms/_EditMessageComposer.scss
@@ -16,13 +16,13 @@ limitations under the License.
*/
.mx_EditMessageComposer {
-
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
padding: 3px;
- // this is to try not make the text move but still have some
- // padding around and in the editor.
- // Actual values from fiddling around in inspector
- margin: -7px -10px -5px -10px;
- overflow: visible !important; // override mx_EventTile_content
+
+ // Make sure the formatting bar is visible
+ overflow: visible !important; // override mx_EventTile_content
.mx_BasicMessageComposer_input {
border-radius: $border-radius-4px;
@@ -40,23 +40,10 @@ limitations under the License.
display: flex;
flex-direction: row;
justify-content: flex-end;
- padding: 5px;
- position: absolute;
- left: 0;
- background: $header-panel-bg-color;
- z-index: 100;
- right: 0;
- margin: 0 -110px 0 0;
- padding-right: 147px;
+ gap: 5px;
.mx_AccessibleButton {
- margin-left: 5px;
padding: 5px 40px;
}
}
}
-
-.mx_EventTile_last .mx_EditMessageComposer_buttons {
- position: static;
- margin-right: -147px;
-}
diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss
index 29888908fa8..bbc4d3c53fa 100644
--- a/res/css/views/rooms/_EventBubbleTile.scss
+++ b/res/css/views/rooms/_EventBubbleTile.scss
@@ -31,6 +31,27 @@ limitations under the License.
margin-right: 60px;
}
+.mx_RoomView_searchResultsPanel {
+ .mx_EventTile[data-layout=bubble] {
+ .mx_SenderProfile {
+ // Group layout adds a 64px left margin, which we really don't want on search results
+ margin-left: 0;
+ }
+
+ &[data-self=true] {
+ // The avatars end up overlapping, so just hide them
+ .mx_EventTile_avatar {
+ display: none;
+ }
+ }
+
+ // Mirror rough designs for "greyed out" text
+ &.mx_EventTile_contextual .mx_EventTile_line {
+ opacity: 0.4;
+ }
+ }
+}
+
.mx_EventTile[data-layout=bubble] {
position: relative;
margin-top: var(--gutterSize);
@@ -109,7 +130,6 @@ limitations under the License.
.mx_MessageActionBar {
top: -28px;
- right: 0;
z-index: 9; // above the avatar
}
@@ -150,7 +170,13 @@ limitations under the License.
}
.mx_MessageActionBar {
- right: -100px; // to make sure it doesn't overflow to the left or cover sender profile
+ inset-inline-start: calc(100% - var(--MessageActionBar-size-box));
+ right: initial; // Reset the default value
+ }
+
+ .mx_ThreadSummary {
+ margin-inline-start: calc(-1 * var(--gutterSize));
+ margin-inline-end: auto;
}
--backgroundColor: $eventbubble-others-bg;
@@ -177,8 +203,8 @@ limitations under the License.
}
.mx_ThreadSummary {
- float: right;
- margin-right: calc(-1 * var(--gutterSize));
+ margin-inline-start: auto;
+ margin-inline-end: calc(-1 * var(--gutterSize));
}
.mx_DisambiguatedProfile {
@@ -199,6 +225,7 @@ limitations under the License.
order: -1;
}
}
+
.mx_EventTile_avatar {
top: -19px; // height of the sender block
right: -35px;
@@ -208,6 +235,10 @@ limitations under the License.
background: $eventbubble-self-bg;
}
+ .mx_MessageActionBar {
+ inset-inline-end: 0;
+ }
+
--backgroundColor: $eventbubble-self-bg;
}
@@ -418,13 +449,6 @@ limitations under the License.
}
}
- .mx_EditMessageComposer_buttons {
- position: static;
- padding: 0;
- margin: 8px 0 0;
- background: transparent;
- }
-
.mx_ReactionsRow {
margin-right: -18px;
margin-left: -9px;
@@ -601,6 +625,7 @@ limitations under the License.
margin-right: 0;
.mx_MessageActionBar {
+ inset-inline-start: initial; // Reset .mx_EventTile[data-layout="bubble"][data-self="false"] .mx_MessageActionBar
right: 48px; // align with that of right-column bubbles
}
diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss
index 417f9404cc4..1ec22179271 100644
--- a/res/css/views/rooms/_EventTile.scss
+++ b/res/css/views/rooms/_EventTile.scss
@@ -50,6 +50,12 @@ $threadInfoLineHeight: calc(2 * $font-12px); // See: _commons.scss
.mx_EventTile_receiptSending::before {
mask-image: url('$(res)/img/element-icons/circle-sending.svg');
}
+
+ &[data-layout=group] {
+ .mx_EventTile_line {
+ line-height: var(--GroupLayout-EventTile-line-height);
+ }
+ }
}
.mx_EventTile:not([data-layout=bubble]) {
@@ -83,10 +89,6 @@ $threadInfoLineHeight: calc(2 * $font-12px); // See: _commons.scss
margin-left: 64px;
}
- &.mx_EventTile_info {
- padding-top: 1px;
- }
-
.mx_EventTile_avatar {
top: 14px;
left: 8px;
@@ -94,22 +96,35 @@ $threadInfoLineHeight: calc(2 * $font-12px); // See: _commons.scss
user-select: none;
}
- &.mx_EventTile_info .mx_EventTile_avatar {
- top: $font-6px;
- left: $left-gutter;
- }
+ &.mx_EventTile_info {
+ padding-top: 0;
- &.mx_EventTile_continuation {
- padding-top: 0px !important;
+ .mx_EventTile_avatar,
+ .mx_EventTile_e2eIcon {
+ margin: 3px 0 2px; // Align with mx_EventTile_line
+ }
- &.mx_EventTile_isEditing {
- padding-top: 5px !important;
- margin-top: -5px;
+ .mx_EventTile_e2eIcon {
+ top: 0;
+ }
+
+ .mx_EventTile_avatar {
+ top: initial;
+ inset-inline-start: $left-gutter;
+ height: 14px;
+ }
+
+ .mx_EventTile_line {
+ padding: 3px 0 2px; // Align with mx_EventTile_avatar and mx_EventTile_e2eIcon
+
+ .mx_MessageTimestamp {
+ top: 0;
+ }
}
}
- &.mx_EventTile_isEditing {
- background-color: $header-panel-bg-color;
+ &.mx_EventTile_continuation {
+ padding-top: 0px !important;
}
.mx_DisambiguatedProfile {
@@ -157,6 +172,7 @@ $threadInfoLineHeight: calc(2 * $font-12px); // See: _commons.scss
/* this is used for the tile for the event which is selected via the URL.
* TODO: ultimately we probably want some transition on here.
*/
+ &.mx_EventTile_isEditing > .mx_EventTile_line,
&.mx_EventTile_selected > .mx_EventTile_line {
box-shadow: inset calc(50px + $selected-message-border-width) 0 0 -50px $accent;
background-color: $event-selected-color;
@@ -263,8 +279,14 @@ $threadInfoLineHeight: calc(2 * $font-12px); // See: _commons.scss
}
}
-.mx_GenericEventListSummary:not([data-layout=bubble]) .mx_EventTile_line {
- padding-left: $left-gutter;
+.mx_GenericEventListSummary:not([data-layout=bubble]) {
+ .mx_EventTile_line {
+ padding-left: $left-gutter;
+
+ .mx_RedactedBody {
+ line-height: 1; // remove spacing between lines
+ }
+ }
}
.mx_EventTile:not([data-layout=bubble]).mx_EventTile_info .mx_EventTile_line,
@@ -710,7 +732,7 @@ $threadInfoLineHeight: calc(2 * $font-12px); // See: _commons.scss
.mx_MessagePanel_narrow .mx_ThreadSummary {
min-width: initial;
- max-width: initial;
+ max-width: 100%; // prevent overflow
width: initial;
}
@@ -730,11 +752,15 @@ $threadInfoLineHeight: calc(2 * $font-12px); // See: _commons.scss
align-items: center;
&:hover,
- // To cancel "&.mx_EventTile:hover .mx_EventTile_line"
+ // Override .mx_EventTile:not([data-layout=bubble]).mx_EventTile:hover .mx_EventTile_line
&:not([data-layout=bubble]):hover .mx_EventTile_line {
background-color: $system;
}
+ &:not([data-layout=bubble]):hover .mx_EventTile_line {
+ box-shadow: none; // don't show the verification left stroke in the thread list
+ }
+
&::after {
content: "";
position: absolute;
@@ -814,6 +840,8 @@ $threadInfoLineHeight: calc(2 * $font-12px); // See: _commons.scss
}
.mx_ThreadView {
+ --ThreadView_group_spacing-start: 56px; // 56px: 64px - 8px (padding)
+
display: flex;
flex-direction: column;
max-height: 100%;
@@ -848,14 +876,13 @@ $threadInfoLineHeight: calc(2 * $font-12px); // See: _commons.scss
padding-right: 0;
}
- .mx_ReplyChain {
- .mx_MLocationBody {
- margin-top: 6px; // See: /~https://github.com/matrix-org/matrix-react-sdk/pull/8442
- }
- }
-
&:not([data-layout=bubble]) {
padding-top: $spacing-16;
+
+ .mx_EventTile_line {
+ padding-top: var(--BaseCard_EventTile_line-padding-block);
+ padding-bottom: var(--BaseCard_EventTile_line-padding-block);
+ }
}
}
@@ -879,16 +906,9 @@ $threadInfoLineHeight: calc(2 * $font-12px); // See: _commons.scss
margin: 0 -13px 0 0; // align with normal messages
}
}
-
- &[data-self=false] {
- .mx_MessageActionBar {
- right: -60px; // smaller overlap, otherwise it'll overflow on the right
- }
- }
}
.mx_EventTile[data-layout=group] {
- $spacing-start: 56px; // 56px: 64px - 8px (padding)
width: 100%;
.mx_EventTile_content,
@@ -899,7 +919,7 @@ $threadInfoLineHeight: calc(2 * $font-12px); // See: _commons.scss
.mx_MLocationBody,
.mx_ReplyChain_wrapper,
.mx_ReactionsRow {
- margin-left: $spacing-start;
+ margin-inline-start: var(--ThreadView_group_spacing-start);
margin-right: 8px;
.mx_EventTile_content,
@@ -942,7 +962,7 @@ $threadInfoLineHeight: calc(2 * $font-12px); // See: _commons.scss
}
.mx_EventTile_mediaLine {
- padding-inline-start: $spacing-start;
+ padding-inline-start: var(--ThreadView_group_spacing-start);
}
}
@@ -968,13 +988,4 @@ $threadInfoLineHeight: calc(2 * $font-12px); // See: _commons.scss
padding-right: 11px; // align with right edge of input
margin-right: 0; // align with right edge of background
}
-
- .mx_GroupLayout {
- .mx_EventTile {
- .mx_EventTile_line {
- padding-top: 2px;
- padding-bottom: 2px;
- }
- }
- }
}
diff --git a/res/css/views/rooms/_GroupLayout.scss b/res/css/views/rooms/_GroupLayout.scss
index a9005cfc525..28ba80b3895 100644
--- a/res/css/views/rooms/_GroupLayout.scss
+++ b/res/css/views/rooms/_GroupLayout.scss
@@ -18,6 +18,8 @@ limitations under the License.
$left-gutter: 64px;
.mx_GroupLayout {
+ --GroupLayout-EventTile-line-height: $font-22px;
+
.mx_EventTile {
> .mx_DisambiguatedProfile {
line-height: $font-20px;
@@ -30,14 +32,17 @@ $left-gutter: 64px;
}
.mx_MessageTimestamp {
- position: absolute;
- width: $MessageTimestamp_width;
+ position: absolute; // for modern layout
}
- .mx_EventTile_line, .mx_EventTile_reply {
+ .mx_EventTile_line,
+ .mx_EventTile_reply {
padding-top: 1px;
padding-bottom: 3px;
- line-height: $font-22px;
+ }
+
+ .mx_EventTile_reply {
+ line-height: var(--GroupLayout-EventTile-line-height);
}
}
@@ -52,7 +57,8 @@ $left-gutter: 64px;
.mx_EventTile {
padding-top: 4px;
- .mx_EventTile_line, .mx_EventTile_reply {
+ .mx_EventTile_line,
+ .mx_EventTile_reply {
padding-top: 0;
padding-bottom: 0;
}
@@ -61,9 +67,12 @@ $left-gutter: 64px;
// same as the padding for non-compact .mx_EventTile.mx_EventTile_info
padding-top: 0px;
font-size: $font-13px;
- .mx_EventTile_line, .mx_EventTile_reply {
+
+ .mx_EventTile_line,
+ .mx_EventTile_reply {
line-height: $font-20px;
}
+
.mx_EventTile_avatar {
top: 4px;
}
@@ -76,10 +85,13 @@ $left-gutter: 64px;
&.mx_EventTile_emote {
// add a bit more space for emotes so that avatars don't collide
padding-top: 8px;
+
.mx_EventTile_avatar {
top: 2px;
}
- .mx_EventTile_line, .mx_EventTile_reply {
+
+ .mx_EventTile_line,
+ .mx_EventTile_reply {
padding-top: 0px;
padding-bottom: 1px;
}
@@ -87,7 +99,9 @@ $left-gutter: 64px;
&.mx_EventTile_emote.mx_EventTile_continuation {
padding-top: 0;
- .mx_EventTile_line, .mx_EventTile_reply {
+
+ .mx_EventTile_line,
+ .mx_EventTile_reply {
padding-top: 0px;
padding-bottom: 0px;
}
diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss
index c09ee476767..d689d152348 100644
--- a/res/css/views/rooms/_IRCLayout.scss
+++ b/res/css/views/rooms/_IRCLayout.scss
@@ -15,7 +15,6 @@ limitations under the License.
*/
$icon-width: 14px;
-$timestamp-width: 45px;
$right-padding: 5px;
$irc-line-height: $font-18px;
@@ -28,7 +27,7 @@ $irc-line-height: $font-18px;
// timestamps are links which shouldn't be underlined
> a {
text-decoration: none;
- min-width: 45px;
+ min-width: $MessageTimestamp_width;
}
display: flex;
@@ -85,7 +84,6 @@ $irc-line-height: $font-18px;
.mx_MessageTimestamp {
font-size: $font-10px;
- width: $timestamp-width;
text-align: right;
}
@@ -141,7 +139,7 @@ $irc-line-height: $font-18px;
.mx_GenericEventListSummary {
> .mx_EventTile_line {
- padding-left: calc(var(--name-width) + $icon-width + $timestamp-width + 3 * $right-padding); // 15 px of padding
+ padding-left: calc(var(--name-width) + $icon-width + $MessageTimestamp_width + 3 * $right-padding); // 15 px of padding
}
.mx_GenericEventListSummary_avatars {
diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss
index fe1a2b4dd41..45c86b72474 100644
--- a/res/css/views/rooms/_MessageComposer.scss
+++ b/res/css/views/rooms/_MessageComposer.scss
@@ -196,12 +196,6 @@ limitations under the License.
}
}
-.mx_ContextualMenu {
- .mx_MessageComposer_button {
- padding-left: calc(var(--size) + 6px);
- }
-}
-
.mx_MessageComposer_button {
--size: 26px;
position: relative;
@@ -210,20 +204,16 @@ limitations under the License.
line-height: var(--size);
width: auto;
padding-left: var(--size);
+ border-radius: 50%;
+ margin-right: 6px;
- &:not(.mx_CallContextMenu_item) {
- border-radius: 50%;
- margin-right: 6px;
-
- &:last-child {
- margin-right: auto;
- }
+ &:last-child {
+ margin-right: auto;
}
&::before {
content: '';
position: absolute;
-
top: 3px;
left: 3px;
height: 20px;
@@ -425,18 +415,3 @@ limitations under the License.
left: 0;
}
}
-
-.mx_MessageComposer_Menu .mx_CallContextMenu_item {
- display: flex;
- align-items: center;
- max-width: unset;
- margin: 7px 7px 7px 16px; // space out the buttons
-}
-
-.mx_MessageComposer_Menu .mx_ContextualMenu {
- min-width: 150px;
- width: max-content;
- padding: 5px 10px 5px 0;
- box-shadow: 0px 2px 9px rgba(0, 0, 0, 0.25);
- border-radius: 8px;
-}
diff --git a/res/css/views/rooms/_ReplyPreview.scss b/res/css/views/rooms/_ReplyPreview.scss
index 8205f2ec1e1..e32686fc219 100644
--- a/res/css/views/rooms/_ReplyPreview.scss
+++ b/res/css/views/rooms/_ReplyPreview.scss
@@ -16,39 +16,47 @@ limitations under the License.
.mx_ReplyPreview {
border: 1px solid $primary-hairline-color;
- background: $background;
border-bottom: none;
- border-radius: $border-radius-8px $border-radius-8px 0 0;
+ background: $background;
max-height: 50vh;
overflow: auto;
- box-shadow: 0px -16px 32px $composer-shadow-color;
.mx_ReplyPreview_section {
border-bottom: 1px solid $primary-hairline-color;
+ display: flex;
+ flex-flow: column;
+ row-gap: $spacing-8;
+ padding: $spacing-8 $spacing-8 0 $spacing-8;
.mx_ReplyPreview_header {
- margin: 8px;
+ display: flex;
+ justify-content: space-between;
+ column-gap: 8px;
+
color: $primary-content;
font-weight: 400;
opacity: 0.4;
- }
-
- .mx_ReplyPreview_tile {
- margin: 0 8px;
- }
- .mx_ReplyPreview_title {
- float: left;
- }
-
- .mx_ReplyPreview_cancel {
- float: right;
- cursor: pointer;
- display: flex;
+ .mx_ReplyPreview_header_cancel {
+ background-color: $primary-content;
+ mask: url('$(res)/img/cancel.svg');
+ mask-repeat: no-repeat;
+ mask-position: center;
+ mask-size: 18px;
+ width: 18px;
+ height: 18px;
+ min-width: 18px;
+ min-height: 18px;
+ }
}
+ }
+}
- .mx_ReplyPreview_clear {
- clear: both;
- }
+.mx_RoomView_body {
+ .mx_ReplyPreview {
+ // Add box-shadow to the reply preview on the main (left) panel only.
+ // It is not added to the preview on the (right) panel for threads and a chat with a maximized widget.
+ box-shadow: 0px -16px 32px $composer-shadow-color;
+ border-radius: $border-radius-8px $border-radius-8px 0 0;
}
}
diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss
index e1ec8261923..fbb65bbecb7 100644
--- a/res/css/views/rooms/_RoomHeader.scss
+++ b/res/css/views/rooms/_RoomHeader.scss
@@ -140,6 +140,11 @@ limitations under the License.
cursor: pointer;
}
+.mx_RoomTopic {
+ position: relative;
+ cursor: pointer;
+}
+
.mx_RoomHeader_topic {
$lineHeight: $font-16px;
$lines: 2;
@@ -214,6 +219,7 @@ limitations under the License.
.mx_RoomHeader_appsButton::before {
mask-image: url('$(res)/img/element-icons/room/apps.svg');
}
+
.mx_RoomHeader_appsButton_highlight::before {
background-color: $accent;
}
@@ -244,6 +250,7 @@ limitations under the License.
padding: 0;
margin: 0;
}
+
.mx_RoomHeader {
overflow: hidden;
}
diff --git a/res/css/views/rooms/_RoomInfoLine.scss b/res/css/views/rooms/_RoomInfoLine.scss
new file mode 100644
index 00000000000..5c0aea7c0bd
--- /dev/null
+++ b/res/css/views/rooms/_RoomInfoLine.scss
@@ -0,0 +1,58 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.mx_RoomInfoLine {
+ color: $secondary-content;
+ display: inline-block;
+
+ &::before {
+ content: "";
+ display: inline-block;
+ height: 1.2em;
+ mask-position-y: center;
+ mask-repeat: no-repeat;
+ background-color: $tertiary-content;
+ vertical-align: text-bottom;
+ margin-right: 6px;
+ }
+
+ &.mx_RoomInfoLine_public::before {
+ width: 12px;
+ mask-size: 12px;
+ mask-image: url("$(res)/img/globe.svg");
+ }
+
+ &.mx_RoomInfoLine_private::before {
+ width: 14px;
+ mask-size: 14px;
+ mask-image: url("$(res)/img/element-icons/lock.svg");
+ }
+
+ &.mx_RoomInfoLine_video::before {
+ width: 16px;
+ mask-size: 16px;
+ mask-image: url("$(res)/img/element-icons/call/video-call.svg");
+ }
+
+ .mx_RoomInfoLine_members {
+ color: inherit;
+
+ &::before {
+ content: "·"; // visual separator
+ margin: 0 6px;
+ }
+ }
+}
diff --git a/res/css/views/rooms/_RoomPreviewCard.scss b/res/css/views/rooms/_RoomPreviewCard.scss
new file mode 100644
index 00000000000..b561bf666df
--- /dev/null
+++ b/res/css/views/rooms/_RoomPreviewCard.scss
@@ -0,0 +1,136 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.mx_RoomPreviewCard {
+ padding: $spacing-32 $spacing-24 !important; // Override SpaceRoomView's default padding
+ margin: auto;
+ flex-grow: 1;
+ max-width: 480px;
+ box-sizing: border-box;
+ background-color: $system;
+ border-radius: 8px;
+ position: relative;
+ font-size: $font-14px;
+
+ .mx_RoomPreviewCard_notice {
+ font-weight: $font-semi-bold;
+ line-height: $font-24px;
+ color: $primary-content;
+ margin-top: $spacing-24;
+ position: relative;
+ padding-left: calc(20px + $spacing-8);
+
+ .mx_AccessibleButton_kind_link {
+ display: inline;
+ padding: 0;
+ font-size: inherit;
+ line-height: inherit;
+ }
+
+ &::before {
+ content: "";
+ position: absolute;
+ height: $font-24px;
+ width: 20px;
+ left: 0;
+ mask-repeat: no-repeat;
+ mask-position: center;
+ mask-size: contain;
+ mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
+ background-color: $secondary-content;
+ }
+ }
+
+ .mx_RoomPreviewCard_inviter {
+ display: flex;
+ align-items: center;
+ margin-bottom: $spacing-20;
+ font-size: $font-15px;
+
+ > div {
+ margin-left: $spacing-8;
+
+ .mx_RoomPreviewCard_inviter_name {
+ line-height: $font-18px;
+ }
+
+ .mx_RoomPreviewCard_inviter_mxid {
+ color: $secondary-content;
+ }
+ }
+ }
+
+ .mx_RoomPreviewCard_avatar {
+ display: flex;
+ align-items: center;
+
+ .mx_RoomAvatar_isSpaceRoom {
+ &.mx_BaseAvatar_image, .mx_BaseAvatar_image {
+ border-radius: 12px;
+ }
+ }
+
+ .mx_RoomPreviewCard_video {
+ width: 50px;
+ height: 50px;
+ border-radius: calc((50px + 2 * 3px) / 2);
+ background-color: $accent;
+ border: 3px solid $system;
+
+ position: relative;
+ left: calc(-50px / 4 - 3px);
+
+ &::before {
+ content: "";
+ background-color: $button-primary-fg-color;
+ position: absolute;
+ width: 50px;
+ height: 50px;
+ mask-size: 22px;
+ mask-position: center;
+ mask-repeat: no-repeat;
+ mask-image: url('$(res)/img/element-icons/call/video-call.svg');
+ }
+ }
+ }
+
+ h1.mx_RoomPreviewCard_name {
+ margin: $spacing-16 0 !important; // Override SpaceRoomView's default margins
+ }
+
+ .mx_RoomPreviewCard_topic {
+ line-height: $font-22px;
+ margin-top: $spacing-16;
+ max-height: 160px;
+ overflow-y: auto;
+ }
+
+ .mx_FacePile {
+ margin-top: $spacing-20;
+ }
+
+ .mx_RoomPreviewCard_joinButtons {
+ margin-top: $spacing-20;
+ display: flex;
+ gap: $spacing-20;
+
+ .mx_AccessibleButton {
+ max-width: 200px;
+ padding: 14px 0;
+ flex-grow: 1;
+ }
+ }
+}
diff --git a/res/css/views/rooms/_RoomSublist.scss b/res/css/views/rooms/_RoomSublist.scss
index 5cf477dfef5..48e659275a3 100644
--- a/res/css/views/rooms/_RoomSublist.scss
+++ b/res/css/views/rooms/_RoomSublist.scss
@@ -420,10 +420,6 @@ limitations under the License.
}
}
-.mx_RoomSublist_addRoomTooltip {
- margin-top: -3px;
-}
-
.mx_RoomSublist_skeletonUI {
position: relative;
margin-left: 4px;
diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss
index deaeae4505c..7d43155625e 100644
--- a/res/css/views/rooms/_RoomTile.scss
+++ b/res/css/views/rooms/_RoomTile.scss
@@ -50,6 +50,10 @@ limitations under the License.
}
}
+ .mx_RoomTile_details {
+ min-width: 0;
+ }
+
.mx_RoomTile_titleContainer {
min-width: 0;
flex-basis: 0;
diff --git a/res/css/views/rooms/_SendMessageComposer.scss b/res/css/views/rooms/_SendMessageComposer.scss
index 1e2b060096a..3e2cf68f1db 100644
--- a/res/css/views/rooms/_SendMessageComposer.scss
+++ b/res/css/views/rooms/_SendMessageComposer.scss
@@ -20,7 +20,7 @@ limitations under the License.
flex-direction: column;
font-size: $font-14px;
// fixed line height to prevent emoji from being taller than text
- line-height: calc(1.2 * $font-14px);
+ line-height: $font-18px;
justify-content: center;
margin-right: 6px;
// don't grow wider than available space
diff --git a/res/css/views/settings/_ProfileSettings.scss b/res/css/views/settings/_ProfileSettings.scss
index ae77a432e0a..6ab13058d40 100644
--- a/res/css/views/settings/_ProfileSettings.scss
+++ b/res/css/views/settings/_ProfileSettings.scss
@@ -28,12 +28,6 @@ limitations under the License.
.mx_ProfileSettings_controls {
flex-grow: 1;
margin-right: 54px;
-
- // We put the header under the controls with some minor styling to cheat
- // alignment of the field with the avatar
- .mx_SettingsTab_subheading {
- margin-top: 0;
- }
}
.mx_ProfileSettings_controls .mx_Field #profileTopic {
diff --git a/res/css/views/settings/tabs/_SettingsTab.scss b/res/css/views/settings/tabs/_SettingsTab.scss
index 5f6109cea42..1eb4868e557 100644
--- a/res/css/views/settings/tabs/_SettingsTab.scss
+++ b/res/css/views/settings/tabs/_SettingsTab.scss
@@ -27,6 +27,7 @@ limitations under the License.
font-weight: 600;
color: $primary-content;
margin-bottom: 10px;
+ margin-top: 10px;
}
.mx_SettingsTab_heading:nth-child(n + 2) {
diff --git a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss
index 57cca73edaf..58443216e67 100644
--- a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss
+++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss
@@ -26,6 +26,7 @@ limitations under the License.
.mx_AppearanceUserSettingsTab {
> .mx_SettingsTab_SubHeading {
margin-bottom: 32px;
+ margin-top: 12px;
}
}
diff --git a/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.scss b/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.scss
index 80cb72f2547..2f64cde2714 100644
--- a/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.scss
+++ b/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.scss
@@ -27,6 +27,9 @@ limitations under the License.
}
.mx_SecurityUserSettingsTab {
+ .mx_SettingsTab_heading {
+ margin-bottom: 22px;
+ }
.mx_SettingsTab_section {
.mx_AccessibleButton_kind_link {
padding: 0;
diff --git a/res/css/views/settings/tabs/user/_SidebarUserSettingsTab.scss b/res/css/views/settings/tabs/user/_SidebarUserSettingsTab.scss
index 42a8f1aaafb..5000f3e9a69 100644
--- a/res/css/views/settings/tabs/user/_SidebarUserSettingsTab.scss
+++ b/res/css/views/settings/tabs/user/_SidebarUserSettingsTab.scss
@@ -16,7 +16,7 @@ limitations under the License.
.mx_SidebarUserSettingsTab {
.mx_SettingsTab_section {
- margin-top: 10px;
+ margin-top: 12px;
}
.mx_SidebarUserSettingsTab_subheading {
diff --git a/res/css/views/voip/CallView/_CallViewButtons.scss b/res/css/views/voip/CallView/_CallViewButtons.scss
index 9305d07f3b7..4c375ee2222 100644
--- a/res/css/views/voip/CallView/_CallViewButtons.scss
+++ b/res/css/views/voip/CallView/_CallViewButtons.scss
@@ -1,7 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
-Copyright 2021 Šimon Brandner
+Copyright 2021 - 2022 Šimon Brandner
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -27,7 +27,7 @@ limitations under the License.
position: absolute;
display: flex;
justify-content: center;
- bottom: 24px;
+ bottom: 32px;
opacity: 1;
transition: opacity 0.5s;
z-index: 200; // To be above _all_ feeds
@@ -46,6 +46,10 @@ limitations under the License.
justify-content: center;
align-items: center;
+ position: relative;
+
+ box-shadow: 0px 4px 4px 0px #00000026; // Same on both themes
+
&::before {
content: '';
display: inline-block;
@@ -60,6 +64,25 @@ limitations under the License.
width: 24px;
}
+ &.mx_CallViewButtons_dropdownButton {
+ width: 16px;
+ height: 16px;
+
+ position: absolute;
+ right: 0;
+ bottom: 0;
+
+ &::before {
+ width: 14px;
+ height: 14px;
+ mask-image: url('$(res)/img/element-icons/message/chevron-up.svg');
+ }
+
+ &.mx_CallViewButtons_dropdownButton_collapsed::before {
+ transform: rotate(180deg);
+ }
+ }
+
// State buttons
&.mx_CallViewButtons_button_on {
background-color: $call-view-button-on-background;
diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss
index da80d0102c2..e31f42727a0 100644
--- a/res/css/views/voip/_CallView.scss
+++ b/res/css/views/voip/_CallView.scss
@@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2020 The Matrix.org Foundation C.I.C.
+Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
+Copyright 2021 - 2022 Šimon Brandner
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -22,202 +23,176 @@ border-radius: $border-radius-8px;
padding-right: 8px;
// XXX: PiPContainer sets pointer-events: none - should probably be set back in a better place
pointer-events: initial;
-}
-
-.mx_CallView_large {
- padding-bottom: 10px;
- margin: $container-gap-width;
- // The left side gap is fully handled by this margin. To prohibit bleeding on webkit browser.
- margin-right: calc($container-gap-width / 2);
- margin-bottom: 10px;
- display: flex;
- flex-direction: column;
- flex: 1;
-
- .mx_CallView_voice {
- flex: 1;
- }
-
- &.mx_CallView_belowWidget {
- margin-top: 0;
- }
-}
-.mx_CallView_pip {
- width: 320px;
- padding-bottom: 8px;
- background-color: $system;
- box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.2);
- border-radius: $border-radius-8px;
-
- .mx_CallView_video_hold,
- .mx_CallView_voice {
- height: 180px;
- }
-
- .mx_CallViewButtons {
- bottom: 13px;
- }
+ .mx_CallView_toast {
+ position: absolute;
+ top: 74px;
- .mx_CallViewButtons_button {
- width: 34px;
- height: 34px;
+ padding: 4px 8px;
- &::before {
- width: 22px;
- height: 22px;
- }
- }
+ border-radius: 4px;
+ z-index: 50;
- .mx_CallView_holdTransferContent {
- padding-top: 10px;
- padding-bottom: 25px;
+ // Same on both themes
+ color: white;
+ background-color: #17191c;
}
-}
-.mx_CallView_content {
- position: relative;
- display: flex;
- justify-content: center;
- border-radius: 8px;
+ .mx_CallView_content_wrapper {
+ display: flex;
+ justify-content: center;
- > .mx_VideoFeed {
width: 100%;
height: 100%;
- &.mx_VideoFeed_voice {
+ overflow: hidden;
+
+ .mx_CallView_content {
+ position: relative;
+
display: flex;
+ flex-direction: column;
justify-content: center;
align-items: center;
- }
- .mx_VideoFeed_video {
- height: 100%;
- background-color: #000;
+ flex: 1;
+ overflow: hidden;
+
+ border-radius: 10px;
+
+ padding: 10px;
+ padding-right: calc(20% + 20px); // Space for the sidebar
+
+ background-color: $call-view-content-background;
+
+ .mx_CallView_status {
+ z-index: 50;
+ color: $accent-fg-color;
+ }
+
+ .mx_CallView_avatarsContainer {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+
+ div {
+ margin-left: 12px;
+ margin-right: 12px;
+ }
+ }
+
+ .mx_CallView_holdBackground {
+ position: absolute;
+ left: 0;
+ right: 0;
+
+ width: 100%;
+ height: 100%;
+
+ background-repeat: no-repeat;
+ background-size: cover;
+ background-position: center;
+ filter: blur(20px);
+
+ &::after {
+ content: "";
+ display: block;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ left: 0;
+ right: 0;
+ background-color: rgba(0, 0, 0, 0.6);
+ }
+ }
+
+ &.mx_CallView_content_hold .mx_CallView_status {
+ font-weight: bold;
+ text-align: center;
+
+ &::before {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+ content: "";
+ width: 40px;
+ height: 40px;
+ background-image: url("$(res)/img/voip/paused.svg");
+ background-position: center;
+ background-size: cover;
+ }
+
+ .mx_CallView_pip &::before {
+ width: 30px;
+ height: 30px;
+ }
+
+ .mx_AccessibleButton_hasKind {
+ padding: 0px;
+ }
+ }
}
+ }
- .mx_VideoFeed_mic {
- left: 10px;
- bottom: 10px;
+ &:not(.mx_CallView_sidebar) .mx_CallView_content {
+ padding: 0;
+ width: 100%;
+ height: 100%;
+
+ .mx_VideoFeed_primary {
+ aspect-ratio: unset;
+ border: 0;
+
+ width: 100%;
+ height: 100%;
}
}
-}
-.mx_CallView_voice {
- align-items: center;
- justify-content: center;
- flex-direction: column;
- background-color: $inverted-bg-color;
-}
+ &.mx_CallView_pip {
+ width: 320px;
+ padding-bottom: 8px;
-.mx_CallView_voice_avatarsContainer {
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: center;
- div {
- margin-left: 12px;
- margin-right: 12px;
- }
-}
+ border-radius: 8px;
-.mx_CallView_voice .mx_CallView_holdTransferContent {
- // This masks the avatar image so when it's blurred, the edge is still crisp
- .mx_CallView_voice_avatarContainer {
- border-radius: 2000px;
- overflow: hidden;
- position: relative;
- }
-}
+ background-color: $system;
+ box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.2);
-.mx_CallView_holdTransferContent {
- height: 20px;
- padding-top: 20px;
- padding-bottom: 15px;
- color: $accent-fg-color;
- user-select: none;
+ .mx_CallViewButtons {
+ bottom: 13px;
- .mx_AccessibleButton_hasKind {
- padding: 0px;
- font-weight: bold;
+ .mx_CallViewButtons_button {
+ width: 34px;
+ height: 34px;
+
+ &::before {
+ width: 22px;
+ height: 22px;
+ }
+ }
+ }
+
+ .mx_CallView_content {
+ min-height: 180px;
+ }
}
-}
-.mx_CallView_video {
- width: 100%;
- height: 100%;
- z-index: 30;
- overflow: hidden;
-}
+ &.mx_CallView_large {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
-.mx_CallView_video_hold {
- overflow: hidden;
+ flex: 1;
- // we keep these around in the DOM: it saved wiring them up again when the call
- // is resumed and keeps the container the right size
- .mx_VideoFeed {
- visibility: hidden;
- }
-}
+ padding-bottom: 10px;
-.mx_CallView_video_holdBackground {
- position: absolute;
- width: 100%;
- height: 100%;
- left: 0;
- right: 0;
- background-repeat: no-repeat;
- background-size: cover;
- background-position: center;
- filter: blur(20px);
- &::after {
- content: "";
- display: block;
- position: absolute;
- width: 100%;
- height: 100%;
- left: 0;
- right: 0;
- background-color: rgba(0, 0, 0, 0.6);
+ margin: $container-gap-width;
+ // The left side gap is fully handled by this margin. To prohibit bleeding on webkit browser.
+ margin-right: calc($container-gap-width / 2);
+ margin-bottom: 10px;
}
-}
-.mx_CallView_video .mx_CallView_holdTransferContent {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- font-weight: bold;
- color: $accent-fg-color;
- text-align: center;
-
- &::before {
- display: block;
- margin-left: auto;
- margin-right: auto;
- content: "";
- width: 40px;
- height: 40px;
- background-image: url("$(res)/img/voip/paused.svg");
- background-position: center;
- background-size: cover;
- }
- .mx_CallView_pip &::before {
- width: 30px;
- height: 30px;
- }
- .mx_AccessibleButton_hasKind {
- padding: 0px;
+ &.mx_CallView_belowWidget {
+ margin-top: 0;
}
}
-
-.mx_CallView_presenting {
- position: absolute;
- margin-top: 18px;
- padding: 4px 8px;
- border-radius: 4px;
-
- // Same on both themes
- color: white;
- background-color: #17191c;
-}
diff --git a/res/css/views/voip/_CallViewHeader.scss b/res/css/views/voip/_CallViewHeader.scss
index 358357f1343..6280da8cbb7 100644
--- a/res/css/views/voip/_CallViewHeader.scss
+++ b/res/css/views/voip/_CallViewHeader.scss
@@ -1,5 +1,6 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
+Copyright 2021 - 2022 Šimon Brandner
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -19,8 +20,9 @@ limitations under the License.
display: flex;
flex-direction: row;
align-items: center;
- justify-content: left;
+ justify-content: space-between;
flex-shrink: 0;
+ width: 100%;
&.mx_CallViewHeader_pip {
cursor: pointer;
@@ -43,6 +45,8 @@ limitations under the License.
.mx_CallViewHeader_controls {
margin-left: auto;
+ display: flex;
+ gap: 5px;
}
.mx_CallViewHeader_button {
@@ -61,17 +65,23 @@ limitations under the License.
mask-size: contain;
mask-position: center;
}
-}
-.mx_CallViewHeader_button_fullscreen {
- &::before {
- mask-image: url('$(res)/img/element-icons/call/fullscreen.svg');
+ &.mx_CallViewHeader_button_fullscreen {
+ &::before {
+ mask-image: url('$(res)/img/element-icons/call/fullscreen.svg');
+ }
}
-}
-.mx_CallViewHeader_button_expand {
- &::before {
- mask-image: url('$(res)/img/element-icons/call/expand.svg');
+ &.mx_CallViewHeader_button_pin {
+ &::before {
+ mask-image: url('$(res)/img/element-icons/room/pin-upright.svg');
+ }
+ }
+
+ &.mx_CallViewHeader_button_expand {
+ &::before {
+ mask-image: url('$(res)/img/element-icons/call/expand.svg');
+ }
}
}
diff --git a/res/css/views/voip/_CallViewSidebar.scss b/res/css/views/voip/_CallViewSidebar.scss
index 4871ccfe65e..351f4061f4b 100644
--- a/res/css/views/voip/_CallViewSidebar.scss
+++ b/res/css/views/voip/_CallViewSidebar.scss
@@ -1,5 +1,5 @@
/*
-Copyright 2021 Šimon Brandner
+Copyright 2021 - 2022 Šimon Brandner
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,18 +16,15 @@ limitations under the License.
.mx_CallViewSidebar {
position: absolute;
- right: 16px;
- bottom: 16px;
- z-index: 100; // To be above the primary feed
+ right: 10px;
- overflow: auto;
-
- height: calc(100% - 32px); // Subtract the top and bottom padding
width: 20%;
+ height: 100%;
+ overflow: auto;
display: flex;
- flex-direction: column-reverse;
- justify-content: flex-start;
+ flex-direction: column;
+ justify-content: center;
align-items: flex-end;
gap: 12px;
@@ -42,15 +39,6 @@ limitations under the License.
background-color: $video-feed-secondary-background;
}
-
- .mx_VideoFeed_video {
- border-radius: 4px;
- }
-
- .mx_VideoFeed_mic {
- left: 6px;
- bottom: 6px;
- }
}
&.mx_CallViewSidebar_pipMode {
diff --git a/res/css/views/voip/_DialPadContextMenu.scss b/res/css/views/voip/_DialPadContextMenu.scss
index 905486de8a3..046db3133e9 100644
--- a/res/css/views/voip/_DialPadContextMenu.scss
+++ b/res/css/views/voip/_DialPadContextMenu.scss
@@ -35,15 +35,10 @@ limitations under the License.
}
.mx_DialPadContextMenu_cancel {
+ @mixin customisedCancelButton;
float: right;
- mask: url('$(res)/img/feather-customised/cancel.svg');
- mask-repeat: no-repeat;
- mask-position: center;
- mask-size: cover;
width: 14px;
height: 14px;
- background-color: $dialog-close-fg-color;
- cursor: pointer;
}
.mx_DialPadContextMenu_header:focus-within {
diff --git a/res/css/views/voip/_DialPadModal.scss b/res/css/views/voip/_DialPadModal.scss
index ff1ded029c3..75ad8a19029 100644
--- a/res/css/views/voip/_DialPadModal.scss
+++ b/res/css/views/voip/_DialPadModal.scss
@@ -45,15 +45,10 @@ limitations under the License.
}
.mx_DialPadModal_cancel {
+ @mixin customisedCancelButton;
float: right;
- mask: url('$(res)/img/feather-customised/cancel.svg');
- mask-repeat: no-repeat;
- mask-position: center;
- mask-size: cover;
width: 14px;
height: 14px;
- background-color: $dialog-close-fg-color;
- cursor: pointer;
margin-right: 16px;
}
diff --git a/res/css/views/voip/_VideoFeed.scss b/res/css/views/voip/_VideoFeed.scss
index 29dcb5cba3c..a0ab8269c0a 100644
--- a/res/css/views/voip/_VideoFeed.scss
+++ b/res/css/views/voip/_VideoFeed.scss
@@ -1,5 +1,6 @@
/*
-Copyright 2015, 2016, 2020 The Matrix.org Foundation C.I.C.
+Copyright 2015, 2016, 2020, 2021 The Matrix.org Foundation C.I.C.
+Copyright 2021 - 2022 Šimon Brandner
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -20,15 +21,32 @@ limitations under the License.
box-sizing: border-box;
border: transparent 2px solid;
display: flex;
+ border-radius: 4px;
+
+ &.mx_VideoFeed_secondary {
+ position: absolute;
+ right: 24px;
+ bottom: 72px;
+ width: 20%;
+ }
&.mx_VideoFeed_voice {
background-color: $inverted-bg-color;
- aspect-ratio: 16 / 9;
+
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ &:not(.mx_VideoFeed_primary) {
+ aspect-ratio: 16 / 9;
+ }
}
.mx_VideoFeed_video {
+ height: 100%;
width: 100%;
- background-color: transparent;
+ border-radius: 4px;
+ background-color: #000000;
&.mx_VideoFeed_video_mirror {
transform: scale(-1, 1);
@@ -37,6 +55,8 @@ limitations under the License.
.mx_VideoFeed_mic {
position: absolute;
+ left: 6px;
+ bottom: 6px;
display: flex;
align-items: center;
justify-content: center;
diff --git a/res/img/element-icons/email-prompt.svg b/res/img/element-icons/email-prompt.svg
index 19b8f824498..126fff6dd3c 100644
--- a/res/img/element-icons/email-prompt.svg
+++ b/res/img/element-icons/email-prompt.svg
@@ -1,13 +1,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss
index 2fb3dcb0cc3..00c1c2ceab7 100644
--- a/res/themes/dark/css/_dark.scss
+++ b/res/themes/dark/css/_dark.scss
@@ -94,8 +94,8 @@ $roomheader-addroom-fg-color: $primary-content;
// Rich-text-editor
// ********************
-$rte-room-pill-color: rgba(255, 255, 255, 0.15);
-$other-user-pill-bg-color: rgba(255, 255, 255, 0.15);
+$pill-bg-color: $room-highlight-color;
+$pill-hover-bg-color: #545a66;
// ********************
// Inputs
@@ -184,6 +184,7 @@ $call-view-button-on-foreground: $primary-content;
$call-view-button-on-background: $system;
$call-view-button-off-foreground: $system;
$call-view-button-off-background: $primary-content;
+$call-view-content-background: $quinary-content;
$video-feed-secondary-background: $system;
@@ -267,6 +268,10 @@ $selected-color: $room-highlight-color;
}
// ********************
+body {
+ color-scheme: dark;
+}
+
// Nasty hacks to apply a filter to arbitrary monochrome artwork to make it
// better match the theme. Typically applied to dark grey 'off' buttons or
// light grey 'on' buttons.
diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss
index 81bfbb01039..fc9e2cf6417 100644
--- a/res/themes/legacy-dark/css/_legacy-dark.scss
+++ b/res/themes/legacy-dark/css/_legacy-dark.scss
@@ -27,8 +27,8 @@ $light-fg-color: $header-panel-text-secondary-color;
// used for focusing form controls
$focus-bg-color: $room-highlight-color;
-$other-user-pill-bg-color: rgba(255, 255, 255, 0.15);
-$rte-room-pill-color: rgba(255, 255, 255, 0.15);
+$pill-bg-color: $room-highlight-color;
+$pill-hover-bg-color: #545a66;
// informational plinth
$info-plinth-bg-color: $header-panel-bg-color;
@@ -117,6 +117,7 @@ $call-view-button-on-foreground: $primary-content;
$call-view-button-on-background: $system;
$call-view-button-off-foreground: $system;
$call-view-button-off-background: $primary-content;
+$call-view-content-background: $quinary-content;
$video-feed-secondary-background: $system;
@@ -241,6 +242,10 @@ $location-live-secondary-color: #e0e0e0;
text-decoration: none;
}
+body {
+ color-scheme: dark;
+}
+
// Nasty hacks to apply a filter to arbitrary monochrome artwork to make it
// better match the theme. Typically applied to dark grey 'off' buttons or
// light grey 'on' buttons.
diff --git a/res/themes/legacy-light/css/_fonts.scss b/res/themes/legacy-light/css/_fonts.scss
index 68d9496276c..e8a397bcf8a 100644
--- a/res/themes/legacy-light/css/_fonts.scss
+++ b/res/themes/legacy-light/css/_fonts.scss
@@ -1,122 +1,3 @@
-/* the 'src' links are relative to the bundle.css, which is in a subdirectory.
- */
-
-/* Inter unexpectedly contains various codepoints which collide with emoji, even
- when variation-16 is applied to request the emoji variant. From eyeballing
- the emoji picker, these are: 20e3, 23cf, 24c2, 25a0-25c1, 2665, 2764, 2b06, 2b1c.
- Therefore we define a unicode-range to load which excludes the glyphs
- (to avoid having to maintain a fork of Inter). */
-
-$inter-unicode-range: U+0000-20e2,U+20e4-23ce,U+23d0-24c1,U+24c3-259f,U+25c2-2664,U+2666-2763,U+2765-2b05,U+2b07-2b1b,U+2b1d-10FFFF;
-
-@font-face {
- font-family: 'Inter';
- font-style: normal;
- font-weight: 400;
- font-display: swap;
- unicode-range: $inter-unicode-range;
- src: url("$(res)/fonts/Inter/Inter-Regular.woff2?v=3.18") format("woff2"),
- url("$(res)/fonts/Inter/Inter-Regular.woff?v=3.18") format("woff");
-}
-@font-face {
- font-family: 'Inter';
- font-style: italic;
- font-weight: 400;
- font-display: swap;
- unicode-range: $inter-unicode-range;
- src: url("$(res)/fonts/Inter/Inter-Italic.woff2?v=3.18") format("woff2"),
- url("$(res)/fonts/Inter/Inter-Italic.woff?v=3.18") format("woff");
-}
-
-@font-face {
- font-family: 'Inter';
- font-style: normal;
- font-weight: 500;
- font-display: swap;
- unicode-range: $inter-unicode-range;
- src: url("$(res)/fonts/Inter/Inter-Medium.woff2?v=3.18") format("woff2"),
- url("$(res)/fonts/Inter/Inter-Medium.woff?v=3.18") format("woff");
-}
-@font-face {
- font-family: 'Inter';
- font-style: italic;
- font-weight: 500;
- font-display: swap;
- unicode-range: $inter-unicode-range;
- src: url("$(res)/fonts/Inter/Inter-MediumItalic.woff2?v=3.18") format("woff2"),
- url("$(res)/fonts/Inter/Inter-MediumItalic.woff?v=3.18") format("woff");
-}
-
-@font-face {
- font-family: 'Inter';
- font-style: normal;
- font-weight: 600;
- font-display: swap;
- unicode-range: $inter-unicode-range;
- src: url("$(res)/fonts/Inter/Inter-SemiBold.woff2?v=3.18") format("woff2"),
- url("$(res)/fonts/Inter/Inter-SemiBold.woff?v=3.18") format("woff");
-}
-@font-face {
- font-family: 'Inter';
- font-style: italic;
- font-weight: 600;
- font-display: swap;
- unicode-range: $inter-unicode-range;
- src: url("$(res)/fonts/Inter/Inter-SemiBoldItalic.woff2?v=3.18") format("woff2"),
- url("$(res)/fonts/Inter/Inter-SemiBoldItalic.woff?v=3.18") format("woff");
-}
-
-@font-face {
- font-family: 'Inter';
- font-style: normal;
- font-weight: 700;
- font-display: swap;
- unicode-range: $inter-unicode-range;
- src: url("$(res)/fonts/Inter/Inter-Bold.woff2?v=3.18") format("woff2"),
- url("$(res)/fonts/Inter/Inter-Bold.woff?v=3.18") format("woff");
-}
-@font-face {
- font-family: 'Inter';
- font-style: italic;
- font-weight: 700;
- font-display: swap;
- unicode-range: $inter-unicode-range;
- src: url("$(res)/fonts/Inter/Inter-BoldItalic.woff2?v=3.18") format("woff2"),
- url("$(res)/fonts/Inter/Inter-BoldItalic.woff?v=3.18") format("woff");
-}
-
-/* latin-ext */
-@font-face {
- font-family: 'Inconsolata';
- font-style: normal;
- font-weight: 400;
- src: local('Inconsolata Regular'), local('Inconsolata-Regular'), url('$(res)/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlX5qhExfHwNJU.woff2') format('woff2');
- unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
-}
-/* latin */
-@font-face {
- font-family: 'Inconsolata';
- font-style: normal;
- font-weight: 400;
- font-display: swap;
- src: local('Inconsolata Regular'), local('Inconsolata-Regular'), url('$(res)/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlZ5qhExfHw.woff2') format('woff2');
- unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
-}
-/* latin-ext */
-@font-face {
- font-family: 'Inconsolata';
- font-style: normal;
- font-weight: 700;
- font-display: swap;
- src: local('Inconsolata Bold'), local('Inconsolata-Bold'), url('$(res)/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71n5_zaDpwm80E.woff2') format('woff2');
- unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
-}
-/* latin */
-@font-face {
- font-family: 'Inconsolata';
- font-style: normal;
- font-weight: 700;
- font-display: swap;
- src: local('Inconsolata Bold'), local('Inconsolata-Bold'), url('$(res)/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71p5_zaDpwm.woff2') format('woff2');
- unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
-}
+// Grab the other fonts from the current theme, so we can override to Inter
+// in custom fonts if needed.
+@import "../../light/css/_fonts.scss";
diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss
index a5c35f312f0..4b59630d392 100644
--- a/res/themes/legacy-light/css/_legacy-light.scss
+++ b/res/themes/legacy-light/css/_legacy-light.scss
@@ -37,8 +37,6 @@ $selection-fg-color: $primary-bg-color;
$focus-brightness: 105%;
-$other-user-pill-bg-color: rgba(0, 0, 0, 0.13);
-
// informational plinth
$info-plinth-bg-color: #f7f7f7;
$info-plinth-fg-color: #888;
@@ -117,10 +115,12 @@ $settings-subsection-fg-color: #616161;
$rte-bg-color: #e0e0e0;
$rte-code-bg-color: rgba(0, 0, 0, 0.04);
-$rte-room-pill-color: rgba(0, 0, 0, 0.13);
$header-panel-text-primary-color: #757575;
+$pill-bg-color: #aaa;
+$pill-hover-bg-color: #ccc;
+
$topleftmenu-color: #212121;
$roomheader-bg-color: $primary-bg-color;
$roomheader-addroom-bg-color: #757575;
@@ -175,6 +175,7 @@ $call-view-button-on-foreground: $secondary-content;
$call-view-button-on-background: $background;
$call-view-button-off-foreground: $background;
$call-view-button-off-background: $secondary-content;
+$call-view-content-background: #21262C;
$video-feed-secondary-background: #424242; // XXX: Color from dark theme
@@ -353,6 +354,10 @@ $location-live-secondary-color: #e0e0e0;
text-decoration: none;
}
+body {
+ color-scheme: light;
+}
+
// diff highlight colors
.hljs-addition {
background: #dfd;
diff --git a/res/themes/light-custom/css/_custom.scss b/res/themes/light-custom/css/_custom.scss
index b85f7c5f457..38995139d02 100644
--- a/res/themes/light-custom/css/_custom.scss
+++ b/res/themes/light-custom/css/_custom.scss
@@ -142,6 +142,6 @@ $message-bubble-background-selected: var(--eventbubble-selected-bg, $message-bub
$reaction-row-button-selected-bg-color: var(--reaction-row-button-selected-bg-color, $reaction-row-button-selected-bg-color);
$menu-selected-color: var(--menu-selected-color, $menu-selected-color);
-$rte-room-pill-color: var(--rte-room-pill-color, $rte-room-pill-color);
-$other-user-pill-bg-color: var(--other-user-pill-bg-color, $other-user-pill-bg-color);
+$pill-bg-color: var(--other-user-pill-bg-color, $pill-bg-color);
+$pill-hover-bg-color: var(--other-user-pill-bg-color, $pill-hover-bg-color);
$icon-button-color: var(--icon-button-color, $icon-button-color);
diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss
index 9b627f31d3e..8ab2e4e4fcf 100644
--- a/res/themes/light/css/_light.scss
+++ b/res/themes/light/css/_light.scss
@@ -155,9 +155,9 @@ $roomheader-addroom-fg-color: #616161;
// Rich-text-editor
// ********************
-$rte-room-pill-color: rgba(0, 0, 0, 0.13);
-$other-user-pill-bg-color: rgba(0, 0, 0, 0.13);
-$rte-bg-color: #e0e0e0;
+$pill-bg-color: #aaa;
+$pill-hover-bg-color: #ccc;
+$rte-bg-color: #e9e9e9;
$rte-code-bg-color: rgba(0, 0, 0, 0.04);
// ********************
@@ -283,6 +283,7 @@ $call-view-button-on-foreground: $secondary-content;
$call-view-button-on-background: $background;
$call-view-button-off-foreground: $background;
$call-view-button-off-background: $secondary-content;
+$call-view-content-background: #21262C;
$video-feed-secondary-background: #424242; // XXX: Color from dark theme
$voipcall-plinth-color: $system;
@@ -383,6 +384,10 @@ $location-live-secondary-color: #e0e0e0;
text-decoration: none;
}
+body {
+ color-scheme: light;
+}
+
// ********************
// diff highlight colors
diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh
index 737e87844f5..81f0784ff96 100755
--- a/scripts/fetchdep.sh
+++ b/scripts/fetchdep.sh
@@ -10,6 +10,9 @@ defbranch="$3"
rm -r "$defrepo" || true
+PR_ORG=${PR_ORG:-"matrix-org"}
+PR_REPO=${PR_REPO:-"matrix-react-sdk"}
+
# A function that clones a branch of a repo based on the org, repo and branch
clone() {
org=$1
@@ -29,8 +32,7 @@ getPRInfo() {
if [ -n "$number" ]; then
echo "Getting info about a PR with number $number"
- apiEndpoint="https://api.github.com/repos/${REPOSITORY:-"matrix-org/matrix-react-sdk"}/pulls/"
- apiEndpoint+=$number
+ apiEndpoint="https://api.github.com/repos/$PR_ORG/$PR_REPO/pulls/$number"
head=$(curl $apiEndpoint | jq -r '.head.label')
fi
@@ -58,7 +60,7 @@ TRY_ORG=$deforg
TRY_BRANCH=${BRANCH_ARRAY[0]}
if [[ "$head" == *":"* ]]; then
# ... but only match that fork if it's a real fork
- if [ "${BRANCH_ARRAY[0]}" != "matrix-org" ]; then
+ if [ "${BRANCH_ARRAY[0]}" != "$PR_ORG" ]; then
TRY_ORG=${BRANCH_ARRAY[0]}
fi
TRY_BRANCH=${BRANCH_ARRAY[1]}
diff --git a/sonar-project.properties b/sonar-project.properties
index b6516cb92ac..47814f9d418 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -1,13 +1,6 @@
sonar.projectKey=matrix-react-sdk
sonar.organization=matrix-org
-# This is the name and version displayed in the SonarCloud UI.
-#sonar.projectName=matrix-react-sdk
-#sonar.projectVersion=1.0
-
-# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
-#sonar.sources=.
-
# Encoding of the source code. Default is default system encoding
#sonar.sourceEncoding=UTF-8
@@ -17,5 +10,5 @@ sonar.exclusions=__mocks__,docs
sonar.typescript.tsconfigPath=./tsconfig.json
sonar.javascript.lcov.reportPaths=coverage/lcov.info
-sonar.coverage.exclusions=spec/*.ts
+sonar.coverage.exclusions=test/**/*,cypress/**/*
sonar.testExecutionReportPaths=coverage/test-report.xml
diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts
index d0f266470c9..4d87e0a2f02 100644
--- a/src/@types/global.d.ts
+++ b/src/@types/global.d.ts
@@ -51,6 +51,7 @@ import { ConsoleLogger, IndexedDBLogStore } from "../rageshake/rageshake";
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
import AutoRageshakeStore from "../stores/AutoRageshakeStore";
import { IConfigOptions } from "../IConfigOptions";
+import { MatrixDispatcher } from "../dispatcher/dispatcher";
/* eslint-disable @typescript-eslint/naming-convention */
@@ -109,6 +110,7 @@ declare global {
mxSendSentryReport: (userText: string, issueUrl: string, error: Error) => Promise;
mxLoginWithAccessToken: (hsUrl: string, accessToken: string) => Promise;
mxAutoRageshakeStore?: AutoRageshakeStore;
+ mxDispatcher: MatrixDispatcher;
}
interface Electron {
diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts
index b7f52d38952..95f34597635 100644
--- a/src/BasePlatform.ts
+++ b/src/BasePlatform.ts
@@ -237,7 +237,7 @@ export default abstract class BasePlatform {
}
/**
- * Restarts the application, without neccessarily reloading
+ * Restarts the application, without necessarily reloading
* any application code
*/
abstract reload();
diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index 2b934251b72..787f602fb77 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -948,7 +948,7 @@ export default class CallHandler extends EventEmitter {
): Promise {
if (consultFirst) {
// if we're consulting, we just start by placing a call to the transfer
- // target (passing the transferee so the actual tranfer can happen later)
+ // target (passing the transferee so the actual transfer can happen later)
this.dialNumber(destination, call);
return;
}
diff --git a/src/ContentMessages.ts b/src/ContentMessages.ts
index 1d54b1adc3a..7cb0ad1db9c 100644
--- a/src/ContentMessages.ts
+++ b/src/ContentMessages.ts
@@ -380,11 +380,11 @@ export default class ContentMessages {
const tooBigFiles = [];
const okFiles = [];
- for (let i = 0; i < files.length; ++i) {
- if (this.isFileSizeAcceptable(files[i])) {
- okFiles.push(files[i]);
+ for (const file of files) {
+ if (this.isFileSizeAcceptable(file)) {
+ okFiles.push(file);
} else {
- tooBigFiles.push(files[i]);
+ tooBigFiles.push(file);
}
}
@@ -450,13 +450,7 @@ export default class ContentMessages {
}
public cancelUpload(promise: Promise, matrixClient: MatrixClient): void {
- let upload: IUpload;
- for (let i = 0; i < this.inprogress.length; ++i) {
- if (this.inprogress[i].promise === promise) {
- upload = this.inprogress[i];
- break;
- }
- }
+ const upload = this.inprogress.find(item => item.promise === promise);
if (upload) {
upload.canceled = true;
matrixClient.cancelUpload(upload.promise);
diff --git a/src/DecryptionFailureTracker.ts b/src/DecryptionFailureTracker.ts
index 2bb522e7fe9..c56b245f259 100644
--- a/src/DecryptionFailureTracker.ts
+++ b/src/DecryptionFailureTracker.ts
@@ -31,18 +31,19 @@ export class DecryptionFailure {
type ErrorCode = "OlmKeysNotSentError" | "OlmIndexError" | "UnknownError" | "OlmUnspecifiedError";
-type TrackingFn = (count: number, trackedErrCode: ErrorCode) => void;
+type TrackingFn = (count: number, trackedErrCode: ErrorCode, rawError: string) => void;
export type ErrCodeMapFn = (errcode: string) => ErrorCode;
export class DecryptionFailureTracker {
- private static internalInstance = new DecryptionFailureTracker((total, errorCode) => {
+ private static internalInstance = new DecryptionFailureTracker((total, errorCode, rawError) => {
Analytics.trackEvent('E2E', 'Decryption failure', errorCode, String(total));
for (let i = 0; i < total; i++) {
PosthogAnalytics.instance.trackEvent({
eventName: "Error",
domain: "E2EE",
name: errorCode,
+ context: `mxc_crypto_error_type_${rawError}`,
});
}
}, (errorCode) => {
@@ -236,7 +237,7 @@ export class DecryptionFailureTracker {
if (this.failureCounts[errorCode] > 0) {
const trackedErrorCode = this.errorCodeMapFn(errorCode);
- this.fn(this.failureCounts[errorCode], trackedErrorCode);
+ this.fn(this.failureCounts[errorCode], trackedErrorCode, errorCode);
this.failureCounts[errorCode] = 0;
}
}
diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index ba3b4e6004c..09ec78121bb 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -27,13 +27,17 @@ import katex from 'katex';
import { AllHtmlEntities } from 'html-entities';
import { IContent } from 'matrix-js-sdk/src/models/event';
-import { _linkifyElement, _linkifyString } from './linkify-matrix';
+import {
+ _linkifyElement,
+ _linkifyString,
+ ELEMENT_URL_PATTERN,
+ options as linkifyMatrixOptions,
+} from './linkify-matrix';
import { IExtendedSanitizeOptions } from './@types/sanitize-html';
import SettingsStore from './settings/SettingsStore';
import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks";
import { getEmojiFromUnicode } from "./emoji";
import { mediaFromMxc } from "./customisations/Media";
-import { ELEMENT_URL_PATTERN, options as linkifyMatrixOptions } from './linkify-matrix';
import { stripHTMLReply, stripPlainReply } from './utils/Reply';
// Anything outside the basic multilingual plane will be a surrogate pair
@@ -45,10 +49,10 @@ const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/;
const SYMBOL_PATTERN = /([\u2100-\u2bff])/;
// Regex pattern for Zero-Width joiner unicode characters
-const ZWJ_REGEX = new RegExp("\u200D|\u2003", "g");
+const ZWJ_REGEX = /[\u200D\u2003]/g;
// Regex pattern for whitespace characters
-const WHITESPACE_REGEX = new RegExp("\\s", "g");
+const WHITESPACE_REGEX = /\s/g;
const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i');
@@ -183,7 +187,7 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
delete attribs.target;
}
} else {
- // Delete the href attrib if it is falsey
+ // Delete the href attrib if it is falsy
delete attribs.href;
}
diff --git a/src/ImageUtils.ts b/src/ImageUtils.ts
index 9bfab371936..acf8daa607a 100644
--- a/src/ImageUtils.ts
+++ b/src/ImageUtils.ts
@@ -25,7 +25,7 @@ limitations under the License.
* reflect the actual height the scaled thumbnail occupies.
*
* This is very useful for calculating how much height a thumbnail will actually
- * consume in the timeline, when performing scroll offset calcuations
+ * consume in the timeline, when performing scroll offset calculations
* (e.g. scroll locking)
*/
export function thumbHeight(fullWidth: number, fullHeight: number, thumbWidth: number, thumbHeight: number) {
diff --git a/src/KeyBindingsDefaults.ts b/src/KeyBindingsDefaults.ts
index d4f4ffc6811..8ce30252f92 100644
--- a/src/KeyBindingsDefaults.ts
+++ b/src/KeyBindingsDefaults.ts
@@ -15,14 +15,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { isMac, Key } from "./Keyboard";
+import { IS_MAC, Key } from "./Keyboard";
import SettingsStore from "./settings/SettingsStore";
import SdkConfig from "./SdkConfig";
-import {
- IKeyBindingsProvider,
- KeyBinding,
- KeyCombo,
-} from "./KeyBindingsManager";
+import { IKeyBindingsProvider, KeyBinding } from "./KeyBindingsManager";
import {
CATEGORIES,
CategoryName,
@@ -31,13 +27,10 @@ import {
import { getKeyboardShortcuts } from "./accessibility/KeyboardShortcutUtils";
export const getBindingsByCategory = (category: CategoryName): KeyBinding[] => {
- return CATEGORIES[category].settingNames.reduce((bindings, name) => {
- const value = getKeyboardShortcuts()[name]?.default;
- if (value) {
- bindings.push({
- action: name as KeyBindingAction,
- keyCombo: value as KeyCombo,
- });
+ return CATEGORIES[category].settingNames.reduce((bindings, action) => {
+ const keyCombo = getKeyboardShortcuts()[action]?.default;
+ if (keyCombo) {
+ bindings.push({ action, keyCombo });
}
return bindings;
}, []);
@@ -81,7 +74,7 @@ const messageComposerBindings = (): KeyBinding[] => {
shiftKey: true,
},
});
- if (isMac) {
+ if (IS_MAC) {
bindings.push({
action: KeyBindingAction.NewLine,
keyCombo: {
diff --git a/src/KeyBindingsManager.ts b/src/KeyBindingsManager.ts
index 7a79a69ce87..aee403e31d1 100644
--- a/src/KeyBindingsManager.ts
+++ b/src/KeyBindingsManager.ts
@@ -17,7 +17,7 @@ limitations under the License.
import { KeyBindingAction } from "./accessibility/KeyboardShortcuts";
import { defaultBindingsProvider } from './KeyBindingsDefaults';
-import { isMac } from './Keyboard';
+import { IS_MAC } from './Keyboard';
/**
* Represent a key combination.
@@ -127,7 +127,7 @@ export class KeyBindingsManager {
): KeyBindingAction | undefined {
for (const getter of getters) {
const bindings = getter();
- const binding = bindings.find(it => isKeyComboMatch(ev, it.keyCombo, isMac));
+ const binding = bindings.find(it => isKeyComboMatch(ev, it.keyCombo, IS_MAC));
if (binding) {
return binding.action;
}
diff --git a/src/Keyboard.ts b/src/Keyboard.ts
index 8d7d39fc190..efecd791fd8 100644
--- a/src/Keyboard.ts
+++ b/src/Keyboard.ts
@@ -74,10 +74,10 @@ export const Key = {
Z: "z",
};
-export const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
+export const IS_MAC = navigator.platform.toUpperCase().includes('MAC');
export function isOnlyCtrlOrCmdKeyEvent(ev) {
- if (isMac) {
+ if (IS_MAC) {
return ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey;
} else {
return ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
@@ -85,7 +85,7 @@ export function isOnlyCtrlOrCmdKeyEvent(ev) {
}
export function isOnlyCtrlOrCmdIgnoreShiftKeyEvent(ev) {
- if (isMac) {
+ if (IS_MAC) {
return ev.metaKey && !ev.altKey && !ev.ctrlKey;
} else {
return ev.ctrlKey && !ev.altKey && !ev.metaKey;
diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts
index 6b7268d57ed..5d864cc1cc7 100644
--- a/src/Lifecycle.ts
+++ b/src/Lifecycle.ts
@@ -60,6 +60,8 @@ import StorageEvictedDialog from "./components/views/dialogs/StorageEvictedDialo
import { setSentryUser } from "./sentry";
import SdkConfig from "./SdkConfig";
import { DialogOpener } from "./utils/DialogOpener";
+import VideoChannelStore from "./stores/VideoChannelStore";
+import { fixStuckDevices } from "./utils/VideoChannelUtils";
import { Action } from "./dispatcher/actions";
import AbstractLocalStorageSettingsHandler from "./settings/handlers/AbstractLocalStorageSettingsHandler";
@@ -665,7 +667,7 @@ async function persistCredentials(credentials: IMatrixClientCreds): Promise {
}
// Now that we have a MatrixClientPeg, update the Jitsi info
- await Jitsi.getInstance().start();
+ Jitsi.getInstance().start();
+
+ // In case we disconnected uncleanly from a video room, clean up the stuck device
+ if (VideoChannelStore.instance.roomId) {
+ fixStuckDevices(MatrixClientPeg.get().getRoom(VideoChannelStore.instance.roomId), false);
+ }
// dispatch that we finished starting up to wire up any other bits
// of the matrix client that cannot be set prior to starting up.
diff --git a/src/Login.ts b/src/Login.ts
index f7b188c64ac..a16f570fa90 100644
--- a/src/Login.ts
+++ b/src/Login.ts
@@ -51,7 +51,7 @@ export interface IIdentityProvider {
export interface ISSOFlow {
type: "m.login.sso" | "m.login.cas";
// eslint-disable-next-line camelcase
- identity_providers: IIdentityProvider[];
+ identity_providers?: IIdentityProvider[];
}
export type LoginFlow = ISSOFlow | IPasswordFlow;
diff --git a/src/Markdown.ts b/src/Markdown.ts
index 53841c809ef..9480f19ffb4 100644
--- a/src/Markdown.ts
+++ b/src/Markdown.ts
@@ -308,7 +308,6 @@ export default class Markdown {
renderer.html_inline = function(node: commonmark.Node) {
if (isAllowedHtmlTag(node)) {
this.lit(node.literal);
- return;
} else {
this.lit(escape(node.literal));
}
diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts
index 46d599b156d..a281ba65625 100644
--- a/src/MatrixClientPeg.ts
+++ b/src/MatrixClientPeg.ts
@@ -49,6 +49,12 @@ export interface IMatrixClientCreds {
freshLogin?: boolean;
}
+/**
+ * Holds the current instance of the `MatrixClient` to use across the codebase.
+ * Looking for an `MatrixClient`? Just look for the `MatrixClientPeg` on the peg
+ * board. "Peg" is the literal meaning of something you hang something on. So
+ * you'll find a `MatrixClient` hanging on the `MatrixClientPeg`.
+ */
export interface IMatrixClientPeg {
opts: IStartClientOpts;
@@ -123,9 +129,6 @@ class MatrixClientPegClass implements IMatrixClientPeg {
// used if we tear it down & recreate it with a different store
private currentClientCreds: IMatrixClientCreds;
- constructor() {
- }
-
public get(): MatrixClient {
return this.matrixClient;
}
@@ -314,6 +317,10 @@ class MatrixClientPegClass implements IMatrixClientPeg {
}
}
+/**
+ * Note: You should be using a React context with access to a client rather than
+ * using this, as in a multi-account world this will not exist!
+ */
export const MatrixClientPeg: IMatrixClientPeg = new MatrixClientPegClass();
if (!window.mxMatrixClientPeg) {
diff --git a/src/MediaDeviceHandler.ts b/src/MediaDeviceHandler.ts
index ddf1977bf04..59f624f0808 100644
--- a/src/MediaDeviceHandler.ts
+++ b/src/MediaDeviceHandler.ts
@@ -72,12 +72,12 @@ export default class MediaDeviceHandler extends EventEmitter {
/**
* Retrieves devices from the SettingsStore and tells the js-sdk to use them
*/
- public static loadDevices(): void {
+ public static async loadDevices(): Promise {
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
- MatrixClientPeg.get().getMediaHandler().setAudioInput(audioDeviceId);
- MatrixClientPeg.get().getMediaHandler().setVideoInput(videoDeviceId);
+ await MatrixClientPeg.get().getMediaHandler().setAudioInput(audioDeviceId);
+ await MatrixClientPeg.get().getMediaHandler().setVideoInput(videoDeviceId);
}
public setAudioOutput(deviceId: string): void {
@@ -90,9 +90,9 @@ export default class MediaDeviceHandler extends EventEmitter {
* need to be ended and started again for this change to take effect
* @param {string} deviceId
*/
- public setAudioInput(deviceId: string): void {
+ public async setAudioInput(deviceId: string): Promise {
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
- MatrixClientPeg.get().getMediaHandler().setAudioInput(deviceId);
+ return MatrixClientPeg.get().getMediaHandler().setAudioInput(deviceId);
}
/**
@@ -100,16 +100,16 @@ export default class MediaDeviceHandler extends EventEmitter {
* need to be ended and started again for this change to take effect
* @param {string} deviceId
*/
- public setVideoInput(deviceId: string): void {
+ public async setVideoInput(deviceId: string): Promise {
SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
- MatrixClientPeg.get().getMediaHandler().setVideoInput(deviceId);
+ return MatrixClientPeg.get().getMediaHandler().setVideoInput(deviceId);
}
- public setDevice(deviceId: string, kind: MediaDeviceKindEnum): void {
+ public async setDevice(deviceId: string, kind: MediaDeviceKindEnum): Promise {
switch (kind) {
case MediaDeviceKindEnum.AudioOutput: this.setAudioOutput(deviceId); break;
- case MediaDeviceKindEnum.AudioInput: this.setAudioInput(deviceId); break;
- case MediaDeviceKindEnum.VideoInput: this.setVideoInput(deviceId); break;
+ case MediaDeviceKindEnum.AudioInput: await this.setAudioInput(deviceId); break;
+ case MediaDeviceKindEnum.VideoInput: await this.setVideoInput(deviceId); break;
}
}
@@ -124,4 +124,17 @@ export default class MediaDeviceHandler extends EventEmitter {
public static getVideoInput(): string {
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_videoinput");
}
+
+ /**
+ * Returns the current set deviceId for a device kind
+ * @param {MediaDeviceKindEnum} kind of the device that will be returned
+ * @returns {string} the deviceId
+ */
+ public static getDevice(kind: MediaDeviceKindEnum): string {
+ switch (kind) {
+ case MediaDeviceKindEnum.AudioOutput: return this.getAudioOutput();
+ case MediaDeviceKindEnum.AudioInput: return this.getAudioInput();
+ case MediaDeviceKindEnum.VideoInput: return this.getVideoInput();
+ }
+ }
}
diff --git a/src/NodeAnimator.tsx b/src/NodeAnimator.tsx
index 1a8942f5f53..2bb79542404 100644
--- a/src/NodeAnimator.tsx
+++ b/src/NodeAnimator.tsx
@@ -1,3 +1,19 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
import React from "react";
import ReactDom from "react-dom";
diff --git a/src/Notifier.ts b/src/Notifier.ts
index 62e2f093703..892d5bc19cc 100644
--- a/src/Notifier.ts
+++ b/src/Notifier.ts
@@ -408,10 +408,6 @@ export const Notifier = {
// don't bother notifying as user was recently active in this room
return;
}
- if (SettingsStore.getValue("doNotDisturb")) {
- // Don't bother the user if they didn't ask to be bothered
- return;
- }
if (this.isEnabled()) {
this._displayPopupNotification(ev, room);
diff --git a/src/PlatformPeg.ts b/src/PlatformPeg.ts
index 1d2b813ebc6..14714a1d349 100644
--- a/src/PlatformPeg.ts
+++ b/src/PlatformPeg.ts
@@ -18,11 +18,15 @@ limitations under the License.
import BasePlatform from "./BasePlatform";
/*
- * Holds the current Platform object used by the code to do anything
- * specific to the platform we're running on (eg. web, electron)
- * Platforms are provided by the app layer.
- * This allows the app layer to set a Platform without necessarily
- * having to have a MatrixChat object
+ * Holds the current instance of the `Platform` to use across the codebase.
+ * Looking for an `Platform`? Just look for the `PlatformPeg` on the peg board.
+ * "Peg" is the literal meaning of something you hang something on. So you'll
+ * find a `Platform` hanging on the `PlatformPeg`.
+ *
+ * Used by the code to do anything specific to the platform we're running on
+ * (eg. web, electron). Platforms are provided by the app layer. This allows the
+ * app layer to set a Platform without necessarily having to have a MatrixChat
+ * object.
*/
export class PlatformPeg {
platform: BasePlatform = null;
diff --git a/src/RoomInvite.tsx b/src/RoomInvite.tsx
index 200da2f7cf8..e9204996ed2 100644
--- a/src/RoomInvite.tsx
+++ b/src/RoomInvite.tsx
@@ -19,6 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { User } from "matrix-js-sdk/src/models/user";
import { logger } from "matrix-js-sdk/src/logger";
+import { EventType } from "matrix-js-sdk/src/@types/event";
import { MatrixClientPeg } from './MatrixClientPeg';
import MultiInviter, { CompletionStates } from './utils/MultiInviter';
@@ -84,12 +85,12 @@ export function showRoomInviteDialog(roomId: string, initialText = ""): void {
* @returns {boolean} True if valid, false otherwise
*/
export function isValid3pidInvite(event: MatrixEvent): boolean {
- if (!event || event.getType() !== "m.room.third_party_invite") return false;
+ if (!event || event.getType() !== EventType.RoomThirdPartyInvite) return false;
// any events without these keys are not valid 3pid invites, so we ignore them
const requiredKeys = ['key_validity_url', 'public_key', 'display_name'];
- for (let i = 0; i < requiredKeys.length; ++i) {
- if (!event.getContent()[requiredKeys[i]]) return false;
+ if (requiredKeys.some(key => !event.getContent()[key])) {
+ return false;
}
// Valid enough by our standards
diff --git a/src/RoomNotifs.ts b/src/RoomNotifs.ts
index 39d1d5d4aa7..97e4785104b 100644
--- a/src/RoomNotifs.ts
+++ b/src/RoomNotifs.ts
@@ -17,7 +17,13 @@ limitations under the License.
import { PushProcessor } from 'matrix-js-sdk/src/pushprocessor';
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
-import { ConditionKind, IPushRule, PushRuleActionName, PushRuleKind } from "matrix-js-sdk/src/@types/PushRules";
+import {
+ ConditionKind,
+ IPushRule,
+ PushRuleActionName,
+ PushRuleKind,
+ TweakName,
+} from "matrix-js-sdk/src/@types/PushRules";
import { EventType } from 'matrix-js-sdk/src/@types/event';
import { MatrixClientPeg } from './MatrixClientPeg';
@@ -144,13 +150,13 @@ function setRoomNotifsStateMuted(roomId: string): Promise {
promises.push(cli.addPushRule('global', PushRuleKind.Override, roomId, {
conditions: [
{
- kind: 'event_match',
+ kind: ConditionKind.EventMatch,
key: 'room_id',
pattern: roomId,
},
],
actions: [
- 'dont_notify',
+ PushRuleActionName.DontNotify,
],
}));
@@ -174,7 +180,7 @@ function setRoomNotifsStateUnmuted(roomId: string, newState: RoomNotifState): Pr
} else if (newState === RoomNotifState.MentionsOnly) {
promises.push(cli.addPushRule('global', PushRuleKind.RoomSpecific, roomId, {
actions: [
- 'dont_notify',
+ PushRuleActionName.DontNotify,
],
}));
// https://matrix.org/jira/browse/SPEC-400
@@ -182,9 +188,9 @@ function setRoomNotifsStateUnmuted(roomId: string, newState: RoomNotifState): Pr
} else if (newState === RoomNotifState.AllMessagesLoud) {
promises.push(cli.addPushRule('global', PushRuleKind.RoomSpecific, roomId, {
actions: [
- 'notify',
+ PushRuleActionName.Notify,
{
- set_tweak: 'sound',
+ set_tweak: TweakName.Sound,
value: 'default',
},
],
diff --git a/src/Rooms.ts b/src/Rooms.ts
index f6afbfb3af2..a7b562c06a6 100644
--- a/src/Rooms.ts
+++ b/src/Rooms.ts
@@ -53,7 +53,7 @@ export function looksLikeDirectMessageRoom(room: Room, myUserId: string): boolea
// Used to split rooms via tags
const tagNames = Object.keys(room.tags);
// Used for 1:1 direct chats
- // Show 1:1 chats in seperate "Direct Messages" section as long as they haven't
+ // Show 1:1 chats in separate "Direct Messages" section as long as they haven't
// been moved to a different tag section
const totalMemberCount = room.currentState.getJoinedMemberCount() +
room.currentState.getInvitedMemberCount();
diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts
index f3d254d0590..c67e8ec8d96 100644
--- a/src/SecurityManager.ts
+++ b/src/SecurityManager.ts
@@ -83,9 +83,11 @@ async function confirmToDismiss(): Promise {
return !sure;
}
+type KeyParams = { passphrase: string, recoveryKey: string };
+
function makeInputToKey(
keyInfo: ISecretStorageKeyInfo,
-): (keyParams: { passphrase: string, recoveryKey: string }) => Promise {
+): (keyParams: KeyParams) => Promise {
return async ({ passphrase, recoveryKey }) => {
if (passphrase) {
return deriveKey(
@@ -101,11 +103,10 @@ function makeInputToKey(
async function getSecretStorageKey(
{ keys: keyInfos }: { keys: Record },
- ssssItemName,
): Promise<[string, Uint8Array]> {
const cli = MatrixClientPeg.get();
let keyId = await cli.getDefaultSecretStorageKeyId();
- let keyInfo;
+ let keyInfo: ISecretStorageKeyInfo;
if (keyId) {
// use the default SSSS key if set
keyInfo = keyInfos[keyId];
@@ -154,9 +155,9 @@ async function getSecretStorageKey(
/* props= */
{
keyInfo,
- checkPrivateKey: async (input) => {
+ checkPrivateKey: async (input: KeyParams) => {
const key = await inputToKey(input);
- return await MatrixClientPeg.get().checkSecretStorageKey(key, keyInfo);
+ return MatrixClientPeg.get().checkSecretStorageKey(key, keyInfo);
},
},
/* className= */ null,
@@ -171,11 +172,11 @@ async function getSecretStorageKey(
},
},
);
- const [input] = await finished;
- if (!input) {
+ const [keyParams] = await finished;
+ if (!keyParams) {
throw new AccessCancelledError();
}
- const key = await inputToKey(input);
+ const key = await inputToKey(keyParams);
// Save to cache to avoid future prompts in the current session
cacheSecretStorageKey(keyId, keyInfo, key);
diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx
index bb6d9eab3c7..10530d7c9b4 100644
--- a/src/TextForEvent.tsx
+++ b/src/TextForEvent.tsx
@@ -29,7 +29,6 @@ import {
M_POLL_END,
PollStartEvent,
} from "matrix-events-sdk";
-import { M_LOCATION } from "matrix-js-sdk/src/@types/location";
import { _t } from './languageHandler';
import * as Roles from './Roles';
@@ -46,6 +45,7 @@ import AccessibleButton from './components/views/elements/AccessibleButton';
import RightPanelStore from './stores/right-panel/RightPanelStore';
import UserIdentifierCustomisations from './customisations/UserIdentifier';
import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
+import { isLocationEvent } from './utils/EventUtils';
export function getSenderName(event: MatrixEvent): string {
return event.sender?.name ?? event.getSender() ?? _t("Someone");
@@ -224,7 +224,7 @@ const onViewJoinRuleSettingsClick = () => {
});
};
-function textForJoinRulesEvent(ev: MatrixEvent, allowJSX: boolean): () => string | JSX.Element | null {
+function textForJoinRulesEvent(ev: MatrixEvent, allowJSX: boolean): () => Renderable {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
switch (ev.getContent().join_rule) {
case JoinRule.Public:
@@ -281,7 +281,7 @@ function textForServerACLEvent(ev: MatrixEvent): () => string | null {
const prev = {
deny: Array.isArray(prevContent.deny) ? prevContent.deny : [],
allow: Array.isArray(prevContent.allow) ? prevContent.allow : [],
- allow_ip_literals: !(prevContent.allow_ip_literals === false),
+ allow_ip_literals: prevContent.allow_ip_literals !== false,
};
let getText = null;
@@ -305,11 +305,7 @@ function textForServerACLEvent(ev: MatrixEvent): () => string | null {
}
function textForMessageEvent(ev: MatrixEvent): () => string | null {
- const type = ev.getType();
- const content = ev.getContent();
- const msgtype = content.msgtype;
-
- if (M_LOCATION.matches(type) || M_LOCATION.matches(msgtype)) {
+ if (isLocationEvent(ev)) {
return textForLocationEvent(ev);
}
@@ -372,13 +368,15 @@ function textForCanonicalAliasEvent(ev: MatrixEvent): () => string | null {
addresses: addedAltAliases.join(", "),
count: addedAltAliases.length,
});
- } if (removedAltAliases.length && !addedAltAliases.length) {
+ }
+ if (removedAltAliases.length && !addedAltAliases.length) {
return () => _t('%(senderName)s removed the alternative addresses %(addresses)s for this room.', {
senderName,
addresses: removedAltAliases.join(", "),
count: removedAltAliases.length,
});
- } if (removedAltAliases.length && addedAltAliases.length) {
+ }
+ if (removedAltAliases.length && addedAltAliases.length) {
return () => _t('%(senderName)s changed the alternative addresses for this room.', {
senderName,
});
@@ -504,7 +502,7 @@ const onPinnedMessagesClick = (): void => {
RightPanelStore.instance.setCard({ phase: RightPanelPhases.PinnedMessages }, false);
};
-function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => string | JSX.Element | null {
+function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => Renderable {
if (!SettingsStore.getValue("feature_pinning")) return null;
const senderName = getSenderName(event);
const roomId = event.getRoomId();
@@ -758,10 +756,12 @@ function textForPollEndEvent(event: MatrixEvent): () => string | null {
});
}
+type Renderable = string | JSX.Element | null;
+
interface IHandlers {
[type: string]:
(ev: MatrixEvent, allowJSX: boolean, showHiddenEvents?: boolean) =>
- (() => string | JSX.Element | null);
+ (() => Renderable);
}
const handlers: IHandlers = {
diff --git a/src/accessibility/KeyboardShortcutUtils.ts b/src/accessibility/KeyboardShortcutUtils.ts
index 434116d4303..1dff38cde34 100644
--- a/src/accessibility/KeyboardShortcutUtils.ts
+++ b/src/accessibility/KeyboardShortcutUtils.ts
@@ -15,7 +15,7 @@ limitations under the License.
*/
import { KeyCombo } from "../KeyBindingsManager";
-import { isMac, Key } from "../Keyboard";
+import { IS_MAC, Key } from "../Keyboard";
import { _t, _td } from "../languageHandler";
import PlatformPeg from "../PlatformPeg";
import SettingsStore from "../settings/SettingsStore";
@@ -96,7 +96,7 @@ export const getKeyboardShortcuts = (): IKeyboardShortcuts => {
return Object.keys(KEYBOARD_SHORTCUTS).filter((k: KeyBindingAction) => {
if (KEYBOARD_SHORTCUTS[k]?.controller?.settingDisabled) return false;
- if (MAC_ONLY_SHORTCUTS.includes(k) && !isMac) return false;
+ if (MAC_ONLY_SHORTCUTS.includes(k) && !IS_MAC) return false;
if (DESKTOP_SHORTCUTS.includes(k) && !overrideBrowserShortcuts) return false;
return true;
diff --git a/src/accessibility/KeyboardShortcuts.ts b/src/accessibility/KeyboardShortcuts.ts
index 97e428d2a0f..50992eb299a 100644
--- a/src/accessibility/KeyboardShortcuts.ts
+++ b/src/accessibility/KeyboardShortcuts.ts
@@ -16,7 +16,7 @@ limitations under the License.
*/
import { _td } from "../languageHandler";
-import { isMac, Key } from "../Keyboard";
+import { IS_MAC, Key } from "../Keyboard";
import { IBaseSetting } from "../settings/Settings";
import IncompatibleController from "../settings/controllers/IncompatibleController";
import { KeyCombo } from "../KeyBindingsManager";
@@ -200,7 +200,7 @@ export const KEY_ICON: Record = {
[Key.ARROW_LEFT]: "←",
[Key.ARROW_RIGHT]: "→",
};
-if (isMac) {
+if (IS_MAC) {
KEY_ICON[Key.META] = "⌘";
KEY_ICON[Key.ALT] = "⌥";
}
@@ -528,8 +528,8 @@ export const KEYBOARD_SHORTCUTS: IKeyboardShortcuts = {
[KeyBindingAction.GoToHome]: {
default: {
ctrlOrCmdKey: true,
- altKey: !isMac,
- shiftKey: isMac,
+ altKey: !IS_MAC,
+ shiftKey: IS_MAC,
key: Key.H,
},
displayName: _td("Go to Home View"),
@@ -621,25 +621,25 @@ export const KEYBOARD_SHORTCUTS: IKeyboardShortcuts = {
},
[KeyBindingAction.EditRedo]: {
default: {
- key: isMac ? Key.Z : Key.Y,
+ key: IS_MAC ? Key.Z : Key.Y,
ctrlOrCmdKey: true,
- shiftKey: isMac,
+ shiftKey: IS_MAC,
},
displayName: _td("Redo edit"),
},
[KeyBindingAction.PreviousVisitedRoomOrSpace]: {
default: {
- metaKey: isMac,
- altKey: !isMac,
- key: isMac ? Key.SQUARE_BRACKET_LEFT : Key.ARROW_LEFT,
+ metaKey: IS_MAC,
+ altKey: !IS_MAC,
+ key: IS_MAC ? Key.SQUARE_BRACKET_LEFT : Key.ARROW_LEFT,
},
displayName: _td("Previous recently visited room or space"),
},
[KeyBindingAction.NextVisitedRoomOrSpace]: {
default: {
- metaKey: isMac,
- altKey: !isMac,
- key: isMac ? Key.SQUARE_BRACKET_RIGHT : Key.ARROW_RIGHT,
+ metaKey: IS_MAC,
+ altKey: !IS_MAC,
+ key: IS_MAC ? Key.SQUARE_BRACKET_RIGHT : Key.ARROW_RIGHT,
},
displayName: _td("Next recently visited room or space"),
},
diff --git a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx
index 560eaadab1d..707e3cbaf7e 100644
--- a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx
+++ b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx
@@ -291,7 +291,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent
{ _t("Unable to create key backup") }
-
-
-
+
;
} else {
switch (this.state.phase) {
diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx
index 53df137f6d6..f58a8b2003d 100644
--- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx
+++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx
@@ -276,7 +276,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent void): Promise => {
if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
- await makeRequest({
+ makeRequest({
type: 'm.login.password',
identifier: {
type: 'm.id.user',
@@ -649,7 +649,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
- return await timeout(
+ return timeout(
provider.getCompletions(query, selection, force, limit),
null,
PROVIDER_COMPLETION_TIMEOUT,
diff --git a/src/boundThreepids.ts b/src/boundThreepids.ts
index a703d10fd78..6421c1309aa 100644
--- a/src/boundThreepids.ts
+++ b/src/boundThreepids.ts
@@ -53,7 +53,7 @@ export async function getThreepidsWithBindStatus(
}
} catch (e) {
// Ignore terms errors here and assume other flows handle this
- if (!(e.errcode === "M_TERMS_NOT_SIGNED")) {
+ if (e.errcode !== "M_TERMS_NOT_SIGNED") {
throw e;
}
}
diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx
index 187e55cc392..695d6ec2a7b 100644
--- a/src/components/structures/ContextMenu.tsx
+++ b/src/components/structures/ContextMenu.tsx
@@ -157,12 +157,14 @@ export default class ContextMenu extends React.PureComponent {
// XXX: This isn't pretty but the only way to allow opening a different context menu on right click whilst
// a context menu and its click-guard are up without completely rewriting how the context menus work.
setImmediate(() => {
- const clickEvent = document.createEvent('MouseEvents');
- clickEvent.initMouseEvent(
- 'contextmenu', true, true, window, 0,
- 0, 0, x, y, false, false,
- false, false, 0, null,
- );
+ const clickEvent = new MouseEvent("contextmenu", {
+ clientX: x,
+ clientY: y,
+ screenX: 0,
+ screenY: 0,
+ button: 0, // Left
+ relatedTarget: null,
+ });
document.elementFromPoint(x, y).dispatchEvent(clickEvent);
});
}
@@ -417,8 +419,8 @@ export type ToRightOf = {
// Placement method for to position context menu to right of elementRect with chevronOffset
export const toRightOf = (elementRect: Pick, chevronOffset = 12): ToRightOf => {
- const left = elementRect.right + window.pageXOffset + 3;
- let top = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
+ const left = elementRect.right + window.scrollX + 3;
+ let top = elementRect.top + (elementRect.height / 2) + window.scrollY;
top -= chevronOffset + 8; // where 8 is half the height of the chevron
return { left, top, chevronOffset };
};
@@ -436,9 +438,9 @@ export const aboveLeftOf = (
): AboveLeftOf => {
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
- const buttonRight = elementRect.right + window.pageXOffset;
- const buttonBottom = elementRect.bottom + window.pageYOffset;
- const buttonTop = elementRect.top + window.pageYOffset;
+ const buttonRight = elementRect.right + window.scrollX;
+ const buttonBottom = elementRect.bottom + window.scrollY;
+ const buttonTop = elementRect.top + window.scrollY;
// Align the right edge of the menu to the right edge of the button
menuOptions.right = UIStore.instance.windowWidth - buttonRight;
// Align the menu vertically on whichever side of the button has more space available.
@@ -460,9 +462,9 @@ export const aboveRightOf = (
): AboveLeftOf => {
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
- const buttonLeft = elementRect.left + window.pageXOffset;
- const buttonBottom = elementRect.bottom + window.pageYOffset;
- const buttonTop = elementRect.top + window.pageYOffset;
+ const buttonLeft = elementRect.left + window.scrollX;
+ const buttonBottom = elementRect.bottom + window.scrollY;
+ const buttonTop = elementRect.top + window.scrollY;
// Align the left edge of the menu to the left edge of the button
menuOptions.left = buttonLeft;
// Align the menu vertically on whichever side of the button has more space available.
@@ -484,9 +486,9 @@ export const alwaysAboveLeftOf = (
) => {
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
- const buttonRight = elementRect.right + window.pageXOffset;
- const buttonBottom = elementRect.bottom + window.pageYOffset;
- const buttonTop = elementRect.top + window.pageYOffset;
+ const buttonRight = elementRect.right + window.scrollX;
+ const buttonBottom = elementRect.bottom + window.scrollY;
+ const buttonTop = elementRect.top + window.scrollY;
// Align the right edge of the menu to the right edge of the button
menuOptions.right = UIStore.instance.windowWidth - buttonRight;
// Align the menu vertically on whichever side of the button has more space available.
@@ -508,8 +510,8 @@ export const alwaysAboveRightOf = (
) => {
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
- const buttonLeft = elementRect.left + window.pageXOffset;
- const buttonTop = elementRect.top + window.pageYOffset;
+ const buttonLeft = elementRect.left + window.scrollX;
+ const buttonTop = elementRect.top + window.scrollY;
// Align the left edge of the menu to the left edge of the button
menuOptions.left = buttonLeft;
// Align the menu vertically above the menu
diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx
index 986a2f80553..29b67706a45 100644
--- a/src/components/structures/FilePanel.tsx
+++ b/src/components/structures/FilePanel.tsx
@@ -27,7 +27,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from '../../MatrixClientPeg';
import EventIndexPeg from "../../indexing/EventIndexPeg";
import { _t } from '../../languageHandler';
-import DesktopBuildsNotice, { WarningKind } from "../views/elements/DesktopBuildsNotice";
+import SearchWarning, { WarningKind } from "../views/elements/SearchWarning";
import BaseCard from "../views/right_panel/BaseCard";
import ResizeNotifier from '../../utils/ResizeNotifier';
import TimelinePanel from "./TimelinePanel";
@@ -277,7 +277,7 @@ class FilePanel extends React.Component {
sensor={this.card.current}
onMeasurement={this.onMeasurement}
/>
-
+
{
hasAvatarLabel={_tDom("Great, that'll help people know it's you")}
noAvatarLabel={_tDom("Add a photo so people know it's you.")}
setAvatarUrl={url => cli.setAvatarUrl(url)}
+ isUserAvatar
+ onClick={ev => PosthogTrackers.trackInteraction("WebHomeMiniAvatarUploadButton", ev)}
>
= ({ justRegistered = false }) => {
}
let introSection;
- if (justRegistered) {
+ if (justRegistered || !!OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_SIZE)) {
introSection = ;
} else {
const brandingConfig = SdkConfig.getObject("branding");
diff --git a/src/components/structures/InteractiveAuth.tsx b/src/components/structures/InteractiveAuth.tsx
index 94c54b32e32..b42e65d57fb 100644
--- a/src/components/structures/InteractiveAuth.tsx
+++ b/src/components/structures/InteractiveAuth.tsx
@@ -269,6 +269,7 @@ export default class InteractiveAuthComponent extends React.Component {
showNotificationsToast(false);
}
- if (!localStorage.getItem("mx_seen_feature_thread_experimental")) {
- setTimeout(() => {
- if (SettingsStore.getValue("feature_thread") && SdkConfig.get("show_labs_settings")) {
- Modal.createDialog(InfoDialog, {
- title: _t("Threads Approaching Beta 🎉"),
- description: <>
-
- { _t("We're getting closer to releasing a public Beta for Threads.") }
-
-
- { _t("As we prepare for it, we need to make some changes: threads created "
- + "before this point will be displayed as regular replies .",
- {}, {
- "strong": sub => { sub } ,
- }) }
-
-
- { _t("This will be a one-off transition, as threads are now part "
- + "of the Matrix specification.") }
-
- >,
- button: _t("Got it"),
- onFinished: () => {
- localStorage.setItem("mx_seen_feature_thread_experimental", "true");
- },
- });
- }
- }, 1 * 60 * 1000); // show after 1 minute to not overload user on launch
- }
-
// SC: No search beta toast
// eslint-disable-next-line no-constant-condition
if (!localStorage.getItem("mx_seen_feature_spotlight_toast") && false) {
diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx
index 2b0618e9809..3a61c0cdec4 100644
--- a/src/components/structures/MessagePanel.tsx
+++ b/src/components/structures/MessagePanel.tsx
@@ -23,6 +23,7 @@ import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { Relations } from "matrix-js-sdk/src/models/relations";
import { logger } from 'matrix-js-sdk/src/logger';
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
+import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts";
import { M_BEACON_INFO } from 'matrix-js-sdk/src/@types/beacon';
import shouldHideEvent from '../../shouldHideEvent';
@@ -56,6 +57,7 @@ import { getEventDisplayInfo } from "../../utils/EventRenderingUtils";
import { IReadReceiptInfo } from "../views/rooms/ReadReceiptMarker";
import { haveRendererForEvent } from "../../events/EventTileFactory";
import { editorRoomKey } from "../../Editing";
+import { hasThreadSummary } from "../../utils/EventUtils";
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
@@ -96,7 +98,7 @@ export function shouldFormContinuation(
// Thread summaries in the main timeline should break up a continuation on both sides
if (threadsEnabled &&
- (mxEvent.isThreadRoot || prevEvent.isThreadRoot) &&
+ (hasThreadSummary(mxEvent) || hasThreadSummary(prevEvent)) &&
timelineRenderingType !== TimelineRenderingType.Thread
) {
return false;
@@ -860,7 +862,7 @@ export default class MessagePanel extends React.Component {
}
const receipts: IReadReceiptProps[] = [];
room.getReceiptsForEvent(event).forEach((r) => {
- if (!r.userId || r.type !== "m.read" || r.userId === myUserId) {
+ if (!r.userId || ![ReceiptType.Read, ReceiptType.ReadPrivate].includes(r.type) || r.userId === myUserId) {
return; // ignore non-read receipts and receipts from self.
}
if (MatrixClientPeg.get().isUserIgnored(r.userId)) {
diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx
index 99aeb6f5478..aa8f38556a7 100644
--- a/src/components/structures/RoomDirectory.tsx
+++ b/src/components/structures/RoomDirectory.tsx
@@ -26,7 +26,7 @@ import dis from "../../dispatcher/dispatcher";
import Modal from "../../Modal";
import { _t } from '../../languageHandler';
import SdkConfig from '../../SdkConfig';
-import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
+import { instanceForInstanceId, protocolNameForInstanceId, ALL_ROOMS, Protocols } from '../../utils/DirectoryUtils';
import Analytics from '../../Analytics';
import NetworkDropdown from "../views/directory/NetworkDropdown";
import SettingsStore from "../../settings/SettingsStore";
@@ -43,7 +43,6 @@ import PosthogTrackers from "../../PosthogTrackers";
import { PublicRoomTile } from "../views/rooms/PublicRoomTile";
import { getFieldsForThirdPartyLocation, joinRoomByAlias, showRoom } from "../../utils/rooms";
import { GenericError } from "../../utils/error";
-import { ALL_ROOMS, Protocols } from "../../utils/DirectoryUtils";
const LAST_SERVER_KEY = "mx_last_room_directory_server";
const LAST_INSTANCE_KEY = "mx_last_room_directory_instance";
diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx
index 94212927641..77faf0f9298 100644
--- a/src/components/structures/RoomSearch.tsx
+++ b/src/components/structures/RoomSearch.tsx
@@ -28,7 +28,7 @@ import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCo
import { getKeyBindingsManager } from "../../KeyBindingsManager";
import SpaceStore from "../../stores/spaces/SpaceStore";
import { UPDATE_SELECTED_SPACE } from "../../stores/spaces";
-import { isMac, Key } from "../../Keyboard";
+import { IS_MAC, Key } from "../../Keyboard";
import SettingsStore from "../../settings/SettingsStore";
import Modal from "../../Modal";
import SpotlightDialog from "../views/dialogs/SpotlightDialog";
@@ -206,7 +206,7 @@ export default class RoomSearch extends React.PureComponent {
);
let shortcutPrompt =
- { isMac ? "⌘ K" : _t(ALTERNATE_KEY_NAME[Key.CONTROL]) + " K" }
+ { IS_MAC ? "⌘ K" : _t(ALTERNATE_KEY_NAME[Key.CONTROL]) + " K" }
;
if (this.props.isMinimized) {
diff --git a/src/components/structures/RoomStatusBar.tsx b/src/components/structures/RoomStatusBar.tsx
index 94b9905becc..a89f205a88e 100644
--- a/src/components/structures/RoomStatusBar.tsx
+++ b/src/components/structures/RoomStatusBar.tsx
@@ -223,7 +223,7 @@ export default class RoomStatusBar extends React.PureComponent {
"Please contact your service administrator to continue using the service.",
),
'hs_disabled': _td(
- "Your message wasn't sent because this homeserver has been blocked by it's administrator. " +
+ "Your message wasn't sent because this homeserver has been blocked by its administrator. " +
"Please contact your service administrator to continue using the service.",
),
'': _td(
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index c05250492c3..c87e168456f 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -36,6 +36,7 @@ import { MatrixError } from 'matrix-js-sdk/src/http-api';
import { ClientEvent } from "matrix-js-sdk/src/client";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import { THREAD_RELATION_TYPE } from 'matrix-js-sdk/src/models/thread';
+import { HistoryVisibility } from 'matrix-js-sdk/src/@types/partials';
import shouldHideEvent from '../../shouldHideEvent';
import { _t } from '../../languageHandler';
@@ -65,6 +66,7 @@ import ScrollPanel from "./ScrollPanel";
import TimelinePanel from "./TimelinePanel";
import ErrorBoundary from "../views/elements/ErrorBoundary";
import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
+import RoomPreviewCard from "../views/rooms/RoomPreviewCard";
import SearchBar, { SearchScope } from "../views/rooms/SearchBar";
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
import AuxPanel from "../views/rooms/AuxPanel";
@@ -167,7 +169,6 @@ export interface IRoomState {
searchHighlights?: string[];
searchInProgress?: boolean;
callState?: CallState;
- guestsCanJoin: boolean;
canPeek: boolean;
showApps: boolean;
isPeeking: boolean;
@@ -255,7 +256,6 @@ export class RoomView extends React.Component {
numUnreadMessages: 0,
searchResults: null,
callState: null,
- guestsCanJoin: false,
canPeek: false,
showApps: false,
isPeeking: false,
@@ -293,11 +293,9 @@ export class RoomView extends React.Component {
context.on(ClientEvent.Room, this.onRoom);
context.on(RoomEvent.Timeline, this.onRoomTimeline);
context.on(RoomEvent.Name, this.onRoomName);
- context.on(RoomEvent.AccountData, this.onRoomAccountData);
context.on(RoomStateEvent.Events, this.onRoomStateEvents);
context.on(RoomStateEvent.Update, this.onRoomStateUpdate);
context.on(RoomEvent.MyMembership, this.onMyMembership);
- context.on(ClientEvent.AccountData, this.onAccountData);
context.on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus);
context.on(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged);
context.on(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged);
@@ -356,6 +354,8 @@ export class RoomView extends React.Component {
SettingsStore.watchSetting("showHiddenEventsInTimeline", null, (...[,,, value]) =>
this.setState({ showHiddenEvents: value as boolean }),
),
+ SettingsStore.watchSetting("urlPreviewsEnabled", null, this.onUrlPreviewsEnabledChange),
+ SettingsStore.watchSetting("urlPreviewsEnabled_e2ee", null, this.onUrlPreviewsEnabledChange),
];
}
@@ -762,11 +762,9 @@ export class RoomView extends React.Component {
this.context.removeListener(ClientEvent.Room, this.onRoom);
this.context.removeListener(RoomEvent.Timeline, this.onRoomTimeline);
this.context.removeListener(RoomEvent.Name, this.onRoomName);
- this.context.removeListener(RoomEvent.AccountData, this.onRoomAccountData);
this.context.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
this.context.removeListener(RoomEvent.MyMembership, this.onMyMembership);
this.context.removeListener(RoomStateEvent.Update, this.onRoomStateUpdate);
- this.context.removeListener(ClientEvent.AccountData, this.onAccountData);
this.context.removeListener(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus);
this.context.removeListener(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged);
this.context.removeListener(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged);
@@ -1130,19 +1128,10 @@ export class RoomView extends React.Component {
}
private calculatePeekRules(room: Room) {
- const guestAccessEvent = room.currentState.getStateEvents("m.room.guest_access", "");
- if (guestAccessEvent && guestAccessEvent.getContent().guest_access === "can_join") {
- this.setState({
- guestsCanJoin: true,
- });
- }
-
- const historyVisibility = room.currentState.getStateEvents("m.room.history_visibility", "");
- if (historyVisibility && historyVisibility.getContent().history_visibility === "world_readable") {
- this.setState({
- canPeek: true,
- });
- }
+ const historyVisibility = room.currentState.getStateEvents(EventType.RoomHistoryVisibility, "");
+ this.setState({
+ canPeek: historyVisibility?.getContent().history_visibility === HistoryVisibility.WorldReadable,
+ });
}
private updatePreviewUrlVisibility({ roomId }: Room) {
@@ -1212,10 +1201,8 @@ export class RoomView extends React.Component {
this.setState({ e2eStatus });
}
- private onAccountData = (event: MatrixEvent) => {
- const type = event.getType();
- if ((type === "org.matrix.preview_urls" || type === "im.vector.web.settings") && this.state.room) {
- // non-e2ee url previews are stored in legacy event type `org.matrix.room.preview_urls`
+ private onUrlPreviewsEnabledChange = () => {
+ if (this.state.room) {
this.updatePreviewUrlVisibility(this.state.room);
}
@@ -1225,16 +1212,6 @@ export class RoomView extends React.Component {
}
};
- private onRoomAccountData = (event: MatrixEvent, room: Room) => {
- if (room.roomId == this.state.roomId) {
- const type = event.getType();
- if (type === "org.matrix.room.preview_urls" || type === "im.vector.web.settings") {
- // non-e2ee url previews are stored in legacy event type `org.matrix.room.preview_urls`
- this.updatePreviewUrlVisibility(room);
- }
- }
- };
-
private onRoomStateEvents = (ev: MatrixEvent, state: RoomState) => {
// ignore if we don't have a room yet
if (!this.state.room || this.state.room.roomId !== state.roomId) return;
@@ -1937,6 +1914,21 @@ export class RoomView extends React.Component {
}
const myMembership = this.state.room.getMyMembership();
+ if (
+ this.state.room.isElementVideoRoom() &&
+ !(SettingsStore.getValue("feature_video_rooms") && myMembership === "join")
+ ) {
+ return
+
+
+
;
+ ;
+ }
+
// SpaceRoomView handles invites itself
if (myMembership === "invite" && !this.state.room.isSpaceRoom()) {
if (this.state.joining || this.state.rejecting) {
diff --git a/src/components/structures/ScrollPanel.tsx b/src/components/structures/ScrollPanel.tsx
index f2595471c96..5db5e6daad9 100644
--- a/src/components/structures/ScrollPanel.tsx
+++ b/src/components/structures/ScrollPanel.tsx
@@ -17,14 +17,13 @@ limitations under the License.
import React, { createRef, CSSProperties, ReactNode, KeyboardEvent } from "react";
import { logger } from "matrix-js-sdk/src/logger";
+import SettingsStore from '../../settings/SettingsStore';
import Timer from '../../utils/Timer';
import AutoHideScrollbar from "./AutoHideScrollbar";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
import ResizeNotifier from "../../utils/ResizeNotifier";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
-const DEBUG_SCROLL = false;
-
// The amount of extra scroll distance to allow prior to unfilling.
// See getExcessHeight.
const UNPAGINATION_PADDING = 6000;
@@ -36,13 +35,11 @@ const UNFILL_REQUEST_DEBOUNCE_MS = 200;
// much while the content loads.
const PAGE_SIZE = 400;
-let debuglog;
-if (DEBUG_SCROLL) {
- // using bind means that we get to keep useful line numbers in the console
- debuglog = logger.log.bind(console, "ScrollPanel debuglog:");
-} else {
- debuglog = function() {};
-}
+const debuglog = (...args: any[]) => {
+ if (SettingsStore.getValue("debug_scroll_panel")) {
+ logger.log.call(console, "ScrollPanel debuglog:", ...args);
+ }
+};
interface IProps {
/* stickyBottom: if set to true, then once the user hits the bottom of
diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx
index 607b2e4f93e..9d3bf54730b 100644
--- a/src/components/structures/SpaceHierarchy.tsx
+++ b/src/components/structures/SpaceHierarchy.tsx
@@ -36,7 +36,6 @@ import classNames from "classnames";
import { sortBy, uniqBy } from "lodash";
import { GuestAccess, HistoryVisibility } from "matrix-js-sdk/src/@types/partials";
-import dis from "../../dispatcher/dispatcher";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { _t } from "../../languageHandler";
import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
@@ -65,6 +64,7 @@ import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { JoinRoomReadyPayload } from "../../dispatcher/payloads/JoinRoomReadyPayload";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
+import { Alignment } from "../views/elements/Tooltip";
interface IProps {
space: Room;
@@ -330,13 +330,13 @@ export const showRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: st
// fail earlier so they don't have to click back to the directory.
if (cli.isGuest()) {
if (!room.world_readable && !room.guest_can_join) {
- dis.dispatch({ action: "require_registration" });
+ defaultDispatcher.dispatch({ action: "require_registration" });
return;
}
}
const roomAlias = getDisplayAliasForRoom(room) || undefined;
- dis.dispatch({
+ defaultDispatcher.dispatch({
action: Action.ViewRoom,
should_peek: true,
room_alias: roomAlias,
@@ -356,7 +356,7 @@ export const joinRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: st
// Don't let the user view a room they won't be able to either peek or join:
// fail earlier so they don't have to click back to the directory.
if (cli.isGuest()) {
- dis.dispatch({ action: "require_registration" });
+ defaultDispatcher.dispatch({ action: "require_registration" });
return;
}
@@ -365,7 +365,7 @@ export const joinRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: st
});
prom.then(() => {
- dis.dispatch({
+ defaultDispatcher.dispatch({
action: Action.JoinRoomReady,
roomId,
metricsTrigger: "SpaceHierarchy",
@@ -569,7 +569,7 @@ const ManageButtons = ({ hierarchy, selected, setSelected, setError }: IManageBu
const selectedRelations = Array.from(selected.keys()).flatMap(parentId => {
return [
...selected.get(parentId).values(),
- ].map(childId => [parentId, childId]) as [string, string][];
+ ].map(childId => [parentId, childId]);
});
const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => {
@@ -584,7 +584,7 @@ const ManageButtons = ({ hierarchy, selected, setSelected, setError }: IManageBu
Button = AccessibleTooltipButton;
props = {
tooltip: _t("Select a room below first"),
- yOffset: -40,
+ alignment: Alignment.Top,
};
}
diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx
index 1e9d5caa0cf..ff77789802f 100644
--- a/src/components/structures/SpaceRoomView.tsx
+++ b/src/components/structures/SpaceRoomView.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2021 The Matrix.org Foundation C.I.C.
+Copyright 2021-2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,39 +14,32 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { RefObject, useContext, useRef, useState } from "react";
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
import { JoinRule, Preset } from "matrix-js-sdk/src/@types/partials";
-import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { logger } from "matrix-js-sdk/src/logger";
+import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
+import React, { RefObject, useCallback, useContext, useRef, useState } from "react";
import MatrixClientContext from "../../contexts/MatrixClientContext";
-import RoomAvatar from "../views/avatars/RoomAvatar";
-import { _t } from "../../languageHandler";
-import AccessibleButton from "../views/elements/AccessibleButton";
-import RoomName from "../views/elements/RoomName";
-import RoomTopic from "../views/elements/RoomTopic";
-import InlineSpinner from "../views/elements/InlineSpinner";
-import { inviteMultipleToRoom, showRoomInviteDialog } from "../../RoomInvite";
-import { useRoomMembers } from "../../hooks/useRoomMembers";
-import { useFeatureEnabled } from "../../hooks/useSettings";
import createRoom, { IOpts } from "../../createRoom";
-import Field from "../views/elements/Field";
-import { useTypedEventEmitter } from "../../hooks/useEventEmitter";
-import withValidation from "../views/elements/Validation";
-import * as Email from "../../email";
-import defaultDispatcher from "../../dispatcher/dispatcher";
-import dis from "../../dispatcher/dispatcher";
+import { shouldShowComponent } from "../../customisations/helpers/UIComponents";
import { Action } from "../../dispatcher/actions";
-import ResizeNotifier from "../../utils/ResizeNotifier";
-import MainSplit from './MainSplit';
-import ErrorBoundary from "../views/elements/ErrorBoundary";
+import defaultDispatcher from "../../dispatcher/dispatcher";
import { ActionPayload } from "../../dispatcher/payloads";
-import RightPanel from "./RightPanel";
+import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
+import * as Email from "../../email";
+import { useEventEmitterState } from "../../hooks/useEventEmitter";
+import { useMyRoomMembership } from "../../hooks/useRoomMembers";
+import { useFeatureEnabled } from "../../hooks/useSettings";
+import { useStateArray } from "../../hooks/useStateArray";
+import { _t } from "../../languageHandler";
+import PosthogTrackers from "../../PosthogTrackers";
+import { inviteMultipleToRoom, showRoomInviteDialog } from "../../RoomInvite";
+import { UIComponent } from "../../settings/UIFeature";
+import { UPDATE_EVENT } from "../../stores/AsyncStore";
import RightPanelStore from "../../stores/right-panel/RightPanelStore";
import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases";
-import { useStateArray } from "../../hooks/useStateArray";
-import SpacePublicShare from "../views/spaces/SpacePublicShare";
+import ResizeNotifier from "../../utils/ResizeNotifier";
import {
shouldShowSpaceInvite,
shouldShowSpaceSettings,
@@ -56,31 +49,33 @@ import {
showSpaceInvite,
showSpaceSettings,
} from "../../utils/space";
-import SpaceHierarchy, { showRoom } from "./SpaceHierarchy";
-import MemberAvatar from "../views/avatars/MemberAvatar";
-import RoomFacePile from "../views/elements/RoomFacePile";
+import RoomAvatar from "../views/avatars/RoomAvatar";
+import { BetaPill } from "../views/beta/BetaCard";
+import IconizedContextMenu, {
+ IconizedContextMenuOption,
+ IconizedContextMenuOptionList,
+} from "../views/context_menus/IconizedContextMenu";
import {
AddExistingToSpace,
defaultDmsRenderer,
defaultRoomsRenderer,
} from "../views/dialogs/AddExistingToSpaceDialog";
-import { ChevronFace, ContextMenuButton, useContextMenu } from "./ContextMenu";
-import IconizedContextMenu, {
- IconizedContextMenuOption,
- IconizedContextMenuOptionList,
-} from "../views/context_menus/IconizedContextMenu";
+import AccessibleButton from "../views/elements/AccessibleButton";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
-import { BetaPill } from "../views/beta/BetaCard";
-import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
+import ErrorBoundary from "../views/elements/ErrorBoundary";
+import Field from "../views/elements/Field";
+import RoomFacePile from "../views/elements/RoomFacePile";
+import RoomName from "../views/elements/RoomName";
+import RoomTopic from "../views/elements/RoomTopic";
+import withValidation from "../views/elements/Validation";
+import RoomInfoLine from "../views/rooms/RoomInfoLine";
+import RoomPreviewCard from "../views/rooms/RoomPreviewCard";
import { SpaceFeedbackPrompt } from "../views/spaces/SpaceCreateMenu";
-import { useAsyncMemo } from "../../hooks/useAsyncMemo";
-import { useDispatcher } from "../../hooks/useDispatcher";
-import { useRoomState } from "../../hooks/useRoomState";
-import { shouldShowComponent } from "../../customisations/helpers/UIComponents";
-import { UIComponent } from "../../settings/UIFeature";
-import { UPDATE_EVENT } from "../../stores/AsyncStore";
-import PosthogTrackers from "../../PosthogTrackers";
-import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
+import SpacePublicShare from "../views/spaces/SpacePublicShare";
+import { ChevronFace, ContextMenuButton, useContextMenu } from "./ContextMenu";
+import MainSplit from './MainSplit';
+import RightPanel from "./RightPanel";
+import SpaceHierarchy, { showRoom } from "./SpaceHierarchy";
interface IProps {
space: Room;
@@ -107,205 +102,6 @@ enum Phase {
PrivateExistingRooms,
}
-const RoomMemberCount = ({ room, children }) => {
- const members = useRoomMembers(room);
- const count = members.length;
-
- if (children) return children(count);
- return count;
-};
-
-const useMyRoomMembership = (room: Room) => {
- const [membership, setMembership] = useState(room.getMyMembership());
- useTypedEventEmitter(room, RoomEvent.MyMembership, () => {
- setMembership(room.getMyMembership());
- });
- return membership;
-};
-
-const SpaceInfo = ({ space }: { space: Room }) => {
- // summary will begin as undefined whilst loading and go null if it fails to load or we are not invited.
- const summary = useAsyncMemo(async () => {
- if (space.getMyMembership() !== "invite") return null;
- try {
- return space.client.getRoomSummary(space.roomId);
- } catch (e) {
- return null;
- }
- }, [space]);
- const joinRule = useRoomState(space, state => state.getJoinRule());
- const membership = useMyRoomMembership(space);
-
- let visibilitySection;
- if (joinRule === JoinRule.Public) {
- visibilitySection =
- { _t("Public space") }
- ;
- } else {
- visibilitySection =
- { _t("Private space") }
- ;
- }
-
- let memberSection;
- if (membership === "invite" && summary) {
- // Don't trust local state and instead use the summary API
- memberSection =
- { _t("%(count)s members", { count: summary.num_joined_members }) }
- ;
- } else if (summary !== undefined) { // summary is not still loading
- memberSection =
- { (count) => count > 0 ? (
- {
- RightPanelStore.instance.setCard({ phase: RightPanelPhases.SpaceMemberList });
- }}
- >
- { _t("%(count)s members", { count }) }
-
- ) : null }
- ;
- }
-
- return
- { visibilitySection }
- { memberSection }
-
;
-};
-
-interface ISpacePreviewProps {
- space: Room;
- onJoinButtonClicked(): void;
- onRejectButtonClicked(): void;
-}
-
-const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }: ISpacePreviewProps) => {
- const cli = useContext(MatrixClientContext);
- const myMembership = useMyRoomMembership(space);
- useDispatcher(defaultDispatcher, payload => {
- if (payload.action === Action.JoinRoomError && payload.roomId === space.roomId) {
- setBusy(false); // stop the spinner, join failed
- }
- });
-
- const [busy, setBusy] = useState(false);
-
- const joinRule = useRoomState(space, state => state.getJoinRule());
- const cannotJoin = getEffectiveMembership(myMembership) === EffectiveMembership.Leave
- && joinRule !== JoinRule.Public;
-
- let inviterSection;
- let joinButtons;
- if (myMembership === "join") {
- // XXX remove this when spaces leaves Beta
- joinButtons = (
- {
- dis.dispatch({
- action: "leave_room",
- room_id: space.roomId,
- });
- }}
- >
- { _t("Leave") }
-
- );
- } else if (myMembership === "invite") {
- const inviteSender = space.getMember(cli.getUserId())?.events.member?.getSender();
- const inviter = inviteSender && space.getMember(inviteSender);
-
- if (inviteSender) {
- inviterSection =
-
-
-
- { _t(" invites you", {}, {
- inviter: () => { inviter?.name || inviteSender } ,
- }) }
-
- { inviter ?
- { inviteSender }
-
: null }
-
-
;
- }
-
- joinButtons = <>
- {
- setBusy(true);
- onRejectButtonClicked();
- }}
- >
- { _t("Reject") }
-
- {
- setBusy(true);
- onJoinButtonClicked();
- }}
- >
- { _t("Accept") }
-
- >;
- } else {
- joinButtons = (
- {
- onJoinButtonClicked();
- if (!cli.isGuest()) {
- // user will be shown a modal that won't fire a room join error
- setBusy(true);
- }
- }}
- disabled={cannotJoin}
- >
- { _t("Join") }
-
- );
- }
-
- if (busy) {
- joinButtons = ;
- }
-
- let footer;
- if (cannotJoin) {
- footer =
- { _t("To view %(spaceName)s, you need an invite", {
- spaceName: space.name,
- }) }
-
;
- }
-
- return
- { inviterSection }
-
-
-
-
-
-
- { (topic, ref) =>
-
- { topic }
-
- }
-
- { space.getJoinRule() === "public" &&
}
-
- { joinButtons }
-
- { footer }
-
;
-};
-
const SpaceLandingAddButton = ({ space }) => {
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
const canCreateRoom = shouldShowComponent(UIComponent.CreateRooms);
@@ -316,8 +112,8 @@ const SpaceLandingAddButton = ({ space }) => {
if (menuDisplayed) {
const rect = handle.current.getBoundingClientRect();
contextMenu = {
const myMembership = useMyRoomMembership(space);
const userId = cli.getUserId();
+ const storeIsShowingSpaceMembers = useCallback(
+ () => RightPanelStore.instance.isOpenForRoom(space.roomId)
+ && RightPanelStore.instance.currentCardForRoom(space.roomId)?.phase === RightPanelPhases.SpaceMemberList,
+ [space.roomId],
+ );
+ const isShowingMembers = useEventEmitterState(RightPanelStore.instance, UPDATE_EVENT, storeIsShowingSpaceMembers);
+
let inviteButton;
if (shouldShowSpaceInvite(space) && shouldShowComponent(UIComponent.InviteUsers)) {
inviteButton = (
@@ -452,20 +255,19 @@ const SpaceLanding = ({ space }: { space: Room }) => {
-
+
-
+
{ inviteButton }
{ settingsButton }
-
- { (topic, ref) => (
-
- { topic }
-
- ) }
-
+
;
@@ -847,8 +649,8 @@ export default class SpaceRoomView extends React.PureComponent {
if (this.state.myMembership === "join") {
return ;
} else {
- return ;
diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx
index 3729cfaeaf1..a84013bb221 100644
--- a/src/components/structures/ThreadPanel.tsx
+++ b/src/components/structures/ThreadPanel.tsx
@@ -39,6 +39,7 @@ import BetaFeedbackDialog from '../views/dialogs/BetaFeedbackDialog';
import { Action } from '../../dispatcher/actions';
import { UserTab } from '../views/dialogs/UserTab';
import dis from '../../dispatcher/dispatcher';
+import Spinner from "../views/elements/Spinner";
interface IProps {
roomId: string;
@@ -301,7 +302,9 @@ const ThreadPanel: React.FC = ({
permalinkCreator={permalinkCreator}
disableGrouping={true}
/>
- :
+ :
+
+
}
diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx
index 8a75b69a6bc..55dd32bd529 100644
--- a/src/components/structures/ThreadView.tsx
+++ b/src/components/structures/ThreadView.tsx
@@ -22,6 +22,7 @@ import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window';
import { Direction } from 'matrix-js-sdk/src/models/event-timeline';
import { IRelationsRequestOpts } from 'matrix-js-sdk/src/@types/requests';
import classNames from "classnames";
+import { logger } from 'matrix-js-sdk/src/logger';
import BaseCard from "../views/right_panel/BaseCard";
import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases";
@@ -51,6 +52,7 @@ import Measured from '../views/elements/Measured';
import PosthogTrackers from "../../PosthogTrackers";
import { ButtonEvent } from "../views/elements/AccessibleButton";
import { RoomViewStore } from '../../stores/RoomViewStore';
+import Spinner from "../views/elements/Spinner";
interface IProps {
room: Room;
@@ -298,11 +300,53 @@ export default class ThreadView extends React.Component {
const threadRelation = this.threadRelation;
- const messagePanelClassNames = classNames(
- "mx_RoomView_messagePanel",
- {
- "mx_GroupLayout": this.state.layout === Layout.Group,
- });
+ const messagePanelClassNames = classNames("mx_RoomView_messagePanel", {
+ "mx_GroupLayout": this.state.layout === Layout.Group,
+ });
+
+ let timeline: JSX.Element;
+ if (this.state.thread) {
+ if (this.props.initialEvent && this.props.initialEvent.getRoomId() !== this.state.thread.roomId) {
+ logger.warn("ThreadView attempting to render TimelinePanel with mismatched initialEvent",
+ this.state.thread.roomId,
+ this.props.initialEvent.getRoomId(),
+ this.props.initialEvent.getId(),
+ );
+ }
+
+ timeline = <>
+
+
+ >;
+ } else {
+ timeline =
+
+
;
+ }
return (
{
sensor={this.card.current}
onMeasurement={this.onMeasurement}
/>
- { this.state.thread &&
-
-
-
}
+
+ { timeline }
+
{ ContentMessages.sharedInstance().getCurrentUploads(threadRelation).length > 0 && (
) }
- { this.state?.thread?.timelineSet && ( ) }
diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx
index 3e8986a881c..21a418897a1 100644
--- a/src/components/structures/TimelinePanel.tsx
+++ b/src/components/structures/TimelinePanel.tsx
@@ -24,10 +24,12 @@ import { TimelineWindow } from "matrix-js-sdk/src/timeline-window";
import { EventType, RelationType } from 'matrix-js-sdk/src/@types/event';
import { SyncState } from 'matrix-js-sdk/src/sync';
import { RoomMember, RoomMemberEvent } from 'matrix-js-sdk/src/models/room-member';
-import { debounce } from 'lodash';
+import { debounce, throttle } from 'lodash';
import { logger } from "matrix-js-sdk/src/logger";
import { ClientEvent } from "matrix-js-sdk/src/client";
import { Thread } from 'matrix-js-sdk/src/models/thread';
+import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts";
+import { MatrixError } from 'matrix-js-sdk/src/http-api';
import SettingsStore from "../../settings/SettingsStore";
import { Layout } from "../../settings/enums/Layout";
@@ -63,13 +65,11 @@ const READ_RECEIPT_INTERVAL_MS = 500;
const READ_MARKER_DEBOUNCE_MS = 100;
-const DEBUG = false;
-
-let debuglog = function(...s: any[]) {};
-if (DEBUG) {
- // using bind means that we get to keep useful line numbers in the console
- debuglog = logger.log.bind(console, "TimelinePanel debuglog:");
-}
+const debuglog = (...args: any[]) => {
+ if (SettingsStore.getValue("debug_timeline_panel")) {
+ logger.log.call(console, "TimelinePanel debuglog:", ...args);
+ }
+};
interface IProps {
// The js-sdk EventTimelineSet object for the timeline sequence we are
@@ -225,6 +225,7 @@ interface IEventIndexOpts {
*/
class TimelinePanel extends React.Component {
static contextType = RoomContext;
+ public context!: React.ContextType;
// a map from room id to read marker event timestamp
static roomReadMarkerTsMap: Record = {};
@@ -254,6 +255,7 @@ class TimelinePanel extends React.Component {
constructor(props, context) {
super(props, context);
+ this.context = context;
debuglog("mounting");
@@ -383,6 +385,72 @@ class TimelinePanel extends React.Component {
}
}
+ /**
+ * Logs out debug info to describe the state of the TimelinePanel and the
+ * events in the room according to the matrix-js-sdk. This is useful when
+ * debugging problems like messages out of order, or messages that should
+ * not be showing up in a thread, etc.
+ *
+ * It's too expensive and cumbersome to do all of these calculations for
+ * every message change so instead we only log it out when asked.
+ */
+ private onDumpDebugLogs = (): void => {
+ const room = this.props.timelineSet.room;
+ // Get a list of the event IDs used in this TimelinePanel.
+ // This includes state and hidden events which we don't render
+ const eventIdList = this.state.events.map((ev) => ev.getId());
+
+ // Get the list of actually rendered events seen in the DOM.
+ // This is useful to know for sure what's being shown on screen.
+ // And we can suss out any corrupted React `key` problems.
+ let renderedEventIds: string[];
+ const messagePanel = this.messagePanel.current;
+ if (messagePanel) {
+ const messagePanelNode = ReactDOM.findDOMNode(messagePanel) as Element;
+ if (messagePanelNode) {
+ const actuallyRenderedEvents = messagePanelNode.querySelectorAll('[data-event-id]');
+ renderedEventIds = [...actuallyRenderedEvents].map((renderedEvent) => {
+ return renderedEvent.getAttribute('data-event-id');
+ });
+ }
+ }
+
+ // Get the list of events and threads for the room as seen by the
+ // matrix-js-sdk.
+ let serializedEventIdsFromTimelineSets: { [key: string]: string[] }[];
+ let serializedEventIdsFromThreadsTimelineSets: { [key: string]: string[] }[];
+ const serializedThreadsMap: { [key: string]: string[] } = {};
+ if (room) {
+ const timelineSets = room.getTimelineSets();
+ const threadsTimelineSets = room.threadsTimelineSets;
+
+ // Serialize all of the timelineSets and timelines in each set to their event IDs
+ serializedEventIdsFromTimelineSets = serializeEventIdsFromTimelineSets(timelineSets);
+ serializedEventIdsFromThreadsTimelineSets = serializeEventIdsFromTimelineSets(threadsTimelineSets);
+
+ // Serialize all threads in the room from theadId -> event IDs in the thread
+ room.getThreads().forEach((thread) => {
+ serializedThreadsMap[thread.id] = thread.events.map(ev => ev.getId());
+ });
+ }
+
+ const timelineWindowEventIds = this.timelineWindow.getEvents().map(ev => ev.getId());
+ const pendingEvents = this.props.timelineSet.getPendingEvents().map(ev => ev.getId());
+
+ logger.debug(
+ `TimelinePanel(${this.context.timelineRenderingType}): Debugging info for ${room?.roomId}\n` +
+ `\tevents(${eventIdList.length})=${JSON.stringify(eventIdList)}\n` +
+ `\trenderedEventIds(${renderedEventIds?.length ?? 0})=` +
+ `${JSON.stringify(renderedEventIds)}\n` +
+ `\tserializedEventIdsFromTimelineSets=${JSON.stringify(serializedEventIdsFromTimelineSets)}\n` +
+ `\tserializedEventIdsFromThreadsTimelineSets=` +
+ `${JSON.stringify(serializedEventIdsFromThreadsTimelineSets)}\n` +
+ `\tserializedThreadsMap=${JSON.stringify(serializedThreadsMap)}\n` +
+ `\ttimelineWindowEventIds(${timelineWindowEventIds.length})=${JSON.stringify(timelineWindowEventIds)}\n` +
+ `\tpendingEvents(${pendingEvents.length})=${JSON.stringify(pendingEvents)}`,
+ );
+ };
+
private onMessageListUnfillRequest = (backwards: boolean, scrollToken: string): void => {
// If backwards, unpaginate from the back (i.e. the start of the timeline)
const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
@@ -540,6 +608,9 @@ class TimelinePanel extends React.Component {
case "ignore_state_changed":
this.forceUpdate();
break;
+ case Action.DumpDebugLogs:
+ this.onDumpDebugLogs();
+ break;
}
};
@@ -752,16 +823,20 @@ class TimelinePanel extends React.Component {
// Can be null for the notification timeline, etc.
if (!this.props.timelineSet.room) return;
+ if (ev.getRoomId() !== this.props.timelineSet.room.roomId) return;
+
+ if (!this.state.events.includes(ev)) return;
+
+ this.recheckFirstVisibleEventIndex();
+
// Need to update as we don't display event tiles for events that
// haven't yet been decrypted. The event will have just been updated
// in place so we just need to re-render.
// TODO: We should restrict this to only events in our timeline,
// but possibly the event tile itself should just update when this
// happens to save us re-rendering the whole timeline.
- if (ev.getRoomId() === this.props.timelineSet.room.roomId) {
- this.buildCallEventGroupers(this.state.events);
- this.forceUpdate();
- }
+ this.buildCallEventGroupers(this.state.events);
+ this.forceUpdate();
};
private onSync = (clientSyncState: SyncState, prevState: SyncState, data: object): void => {
@@ -769,6 +844,13 @@ class TimelinePanel extends React.Component {
this.setState({ clientSyncState });
};
+ private recheckFirstVisibleEventIndex = throttle((): void => {
+ const firstVisibleEventIndex = this.checkForPreJoinUISI(this.state.events);
+ if (firstVisibleEventIndex !== this.state.firstVisibleEventIndex) {
+ this.setState({ firstVisibleEventIndex });
+ }
+ }, 500, { leading: true, trailing: true });
+
private readMarkerTimeout(readMarkerPosition: number): number {
return readMarkerPosition === 0 ?
this.context?.readMarkerInViewThresholdMs ?? this.state.readMarkerInViewThresholdMs :
@@ -875,14 +957,14 @@ class TimelinePanel extends React.Component {
MatrixClientPeg.get().setRoomReadMarkers(
roomId,
this.state.readMarkerEventId,
- lastReadEvent, // Could be null, in which case no RR is sent
- { hidden: hiddenRR },
+ hiddenRR ? null : lastReadEvent, // Could be null, in which case no RR is sent
+ lastReadEvent, // Could be null, in which case no private RR is sent
).catch((e) => {
// /read_markers API is not implemented on this HS, fallback to just RR
if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) {
return MatrixClientPeg.get().sendReadReceipt(
lastReadEvent,
- {},
+ hiddenRR ? ReceiptType.ReadPrivate : ReceiptType.Read,
).catch((e) => {
logger.error(e);
this.lastRRSentEventId = undefined;
@@ -1157,6 +1239,7 @@ class TimelinePanel extends React.Component {
private scrollIntoView(eventId?: string, pixelOffset?: number, offsetBase?: number): void {
const doScroll = () => {
+ if (!this.messagePanel.current) return;
if (eventId) {
debuglog("TimelinePanel scrolling to eventId " + eventId +
" at position " + (offsetBase * 100) + "% + " + pixelOffset);
@@ -1206,9 +1289,8 @@ class TimelinePanel extends React.Component {
* @param {boolean?} scrollIntoView whether to scroll the event into view.
*/
private loadTimeline(eventId?: string, pixelOffset?: number, offsetBase?: number, scrollIntoView = true): void {
- this.timelineWindow = new TimelineWindow(
- MatrixClientPeg.get(), this.props.timelineSet,
- { windowLimit: this.props.timelineCap });
+ const cli = MatrixClientPeg.get();
+ this.timelineWindow = new TimelineWindow(cli, this.props.timelineSet, { windowLimit: this.props.timelineCap });
const onLoaded = () => {
if (this.unmounted) return;
@@ -1233,8 +1315,7 @@ class TimelinePanel extends React.Component {
// we're in a setState callback, and we know
// timelineLoading is now false, so render() should have
// mounted the message panel.
- logger.log("can't initialise scroll state because " +
- "messagePanel didn't load");
+ logger.log("can't initialise scroll state because messagePanel didn't load");
return;
}
@@ -1249,15 +1330,13 @@ class TimelinePanel extends React.Component {
});
};
- const onError = (error) => {
+ const onError = (error: MatrixError) => {
if (this.unmounted) return;
this.setState({ timelineLoading: false });
- logger.error(
- `Error loading timeline panel at ${eventId}: ${error}`,
- );
+ logger.error(`Error loading timeline panel at ${this.props.timelineSet.room?.roomId}/${eventId}: ${error}`);
- let onFinished;
+ let onFinished: () => void;
// if we were given an event ID, then when the user closes the
// dialog, let's jump to the end of the timeline. If we weren't,
@@ -1273,22 +1352,24 @@ class TimelinePanel extends React.Component {
});
};
}
- let message;
+
+ let description: string;
if (error.errcode == 'M_FORBIDDEN') {
- message = _t(
+ description = _t(
"Tried to load a specific point in this room's timeline, but you " +
"do not have permission to view the message in question.",
);
} else {
- message = _t(
+ description = _t(
"Tried to load a specific point in this room's timeline, but was " +
"unable to find it.",
);
}
+
Modal.createTrackedDialog('Failed to load timeline position', '', ErrorDialog, {
title: _t("Failed to load timeline position"),
- description: message,
- onFinished: onFinished,
+ description,
+ onFinished,
});
};
@@ -1403,25 +1484,24 @@ class TimelinePanel extends React.Component {
* such events were found, then it returns 0.
*/
private checkForPreJoinUISI(events: MatrixEvent[]): number {
+ const cli = MatrixClientPeg.get();
const room = this.props.timelineSet.room;
const isThreadTimeline = [TimelineRenderingType.Thread, TimelineRenderingType.ThreadsList]
.includes(this.context.timelineRenderingType);
- if (events.length === 0
- || !room
- || !MatrixClientPeg.get().isRoomEncrypted(room.roomId)
- || isThreadTimeline) {
+ if (events.length === 0 || !room || !cli.isRoomEncrypted(room.roomId) || isThreadTimeline) {
+ logger.info("checkForPreJoinUISI: showing all messages, skipping check");
return 0;
}
- const userId = MatrixClientPeg.get().credentials.userId;
+ const userId = cli.credentials.userId;
// get the user's membership at the last event by getting the timeline
// that the event belongs to, and traversing the timeline looking for
// that event, while keeping track of the user's membership
- let i;
+ let i = events.length - 1;
let userMembership = "leave";
- for (i = events.length - 1; i >= 0; i--) {
+ for (; i >= 0; i--) {
const timeline = room.getTimelineForEvent(events[i].getId());
if (!timeline) {
// Somehow, it seems to be possible for live events to not have
@@ -1433,18 +1513,15 @@ class TimelinePanel extends React.Component {
);
continue;
}
- const userMembershipEvent =
- timeline.getState(EventTimeline.FORWARDS).getMember(userId);
- userMembership = userMembershipEvent ? userMembershipEvent.membership : "leave";
+
+ userMembership = timeline.getState(EventTimeline.FORWARDS).getMember(userId)?.membership ?? "leave";
const timelineEvents = timeline.getEvents();
for (let j = timelineEvents.length - 1; j >= 0; j--) {
const event = timelineEvents[j];
if (event.getId() === events[i].getId()) {
break;
- } else if (event.getStateKey() === userId
- && event.getType() === "m.room.member") {
- const prevContent = event.getPrevContent();
- userMembership = prevContent.membership || "leave";
+ } else if (event.getStateKey() === userId && event.getType() === EventType.RoomMember) {
+ userMembership = event.getPrevContent().membership || "leave";
}
}
break;
@@ -1454,19 +1531,18 @@ class TimelinePanel extends React.Component {
// one that was sent when the user wasn't in the room
for (; i >= 0; i--) {
const event = events[i];
- if (event.getStateKey() === userId
- && event.getType() === "m.room.member") {
- const prevContent = event.getPrevContent();
- userMembership = prevContent.membership || "leave";
- } else if (userMembership === "leave" &&
- (event.isDecryptionFailure() || event.isBeingDecrypted())) {
- // reached an undecryptable message when the user wasn't in
- // the room -- don't try to load any more
+ if (event.getStateKey() === userId && event.getType() === EventType.RoomMember) {
+ userMembership = event.getPrevContent().membership || "leave";
+ } else if (userMembership === "leave" && (event.isDecryptionFailure() || event.isBeingDecrypted())) {
+ // reached an undecryptable message when the user wasn't in the room -- don't try to load any more
// Note: for now, we assume that events that are being decrypted are
- // not decryptable
+ // not decryptable - we will be called once more when it is decrypted.
+ logger.info("checkForPreJoinUISI: reached a pre-join UISI at index ", i);
return i + 1;
}
}
+
+ logger.info("checkForPreJoinUISI: did not find pre-join UISI");
return 0;
}
@@ -1491,7 +1567,7 @@ class TimelinePanel extends React.Component {
const messagePanel = this.messagePanel.current;
if (!messagePanel) return null;
- const messagePanelNode = ReactDOM.findDOMNode(messagePanel) as HTMLElement;
+ const messagePanelNode = ReactDOM.findDOMNode(messagePanel) as Element;
if (!messagePanelNode) return null; // sometimes this happens for fresh rooms/post-sync
const wrapperRect = messagePanelNode.getBoundingClientRect();
const myUserId = MatrixClientPeg.get().credentials.userId;
@@ -1716,4 +1792,30 @@ class TimelinePanel extends React.Component {
}
}
+/**
+ * Iterate across all of the timelineSets and timelines inside to expose all of
+ * the event IDs contained inside.
+ *
+ * @return An event ID list for every timeline in every timelineSet
+ */
+function serializeEventIdsFromTimelineSets(timelineSets): { [key: string]: string[] }[] {
+ const serializedEventIdsInTimelineSet = timelineSets.map((timelineSet) => {
+ const timelineMap = {};
+
+ const timelines = timelineSet.getTimelines();
+ const liveTimeline = timelineSet.getLiveTimeline();
+
+ timelines.forEach((timeline, index) => {
+ // Add a special label when it is the live timeline so we can tell
+ // it apart from the others
+ const isLiveTimeline = timeline === liveTimeline;
+ timelineMap[isLiveTimeline ? 'liveTimeline' : `${index}`] = timeline.getEvents().map(ev => ev.getId());
+ });
+
+ return timelineMap;
+ });
+
+ return serializedEventIdsInTimelineSet;
+}
+
export default TimelinePanel;
diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx
index b4eb00c71bc..81439497490 100644
--- a/src/components/structures/UserMenu.tsx
+++ b/src/components/structures/UserMenu.tsx
@@ -14,13 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { createRef, useContext, useRef, useState } from "react";
+import React, { createRef } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
-import classNames from "classnames";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import defaultDispatcher from "../../dispatcher/dispatcher";
-import dis from "../../dispatcher/dispatcher";
import { ActionPayload } from "../../dispatcher/payloads";
import { Action } from "../../dispatcher/actions";
import { _t } from "../../languageHandler";
@@ -32,9 +30,7 @@ import Modal from "../../Modal";
import LogoutDialog from "../views/dialogs/LogoutDialog";
import SettingsStore from "../../settings/SettingsStore";
import {
- RovingAccessibleButton,
RovingAccessibleTooltipButton,
- useRovingTabIndex,
} from "../../accessibility/RovingTabIndex";
import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
import SdkConfig from "../../SdkConfig";
@@ -44,7 +40,6 @@ import { UPDATE_EVENT } from "../../stores/AsyncStore";
import BaseAvatar from '../views/avatars/BaseAvatar';
import { SettingLevel } from "../../settings/SettingLevel";
import IconizedContextMenu, {
- IconizedContextMenuCheckbox,
IconizedContextMenuOption,
IconizedContextMenuOptionList,
} from "../views/context_menus/IconizedContextMenu";
@@ -52,66 +47,11 @@ import { UIFeature } from "../../settings/UIFeature";
import HostSignupAction from "./HostSignupAction";
import SpaceStore from "../../stores/spaces/SpaceStore";
import { UPDATE_SELECTED_SPACE } from "../../stores/spaces";
-import MatrixClientContext from "../../contexts/MatrixClientContext";
-import { SettingUpdatedPayload } from "../../dispatcher/payloads/SettingUpdatedPayload";
import { Theme } from "../../settings/enums/Theme";
import UserIdentifierCustomisations from "../../customisations/UserIdentifier";
import PosthogTrackers from "../../PosthogTrackers";
import { ViewHomePagePayload } from "../../dispatcher/payloads/ViewHomePagePayload";
-const CustomStatusSection = () => {
- const cli = useContext(MatrixClientContext);
- const setStatus = cli.getUser(cli.getUserId()).unstable_statusMessage || "";
- const [value, setValue] = useState(setStatus);
-
- const ref = useRef(null);
- const [onFocus, isActive] = useRovingTabIndex(ref);
-
- const classes = classNames({
- 'mx_UserMenu_CustomStatusSection_field': true,
- 'mx_UserMenu_CustomStatusSection_field_hasQuery': value,
- });
-
- let details: JSX.Element;
- if (value !== setStatus) {
- details = <>
- { _t("Your status will be shown to people you have a DM with.") }
-
- cli._unstable_setStatusMessage(value)}
- kind="primary_outline"
- >
- { value ? _t("Set status") : _t("Clear status") }
-
- >;
- }
-
- return ;
-};
-
interface IProps {
isPanelCollapsed: boolean;
}
@@ -122,7 +62,6 @@ interface IState {
contextMenuPosition: PartialDOMRect;
themeInUse: Theme;
selectedSpace?: Room;
- dndEnabled: boolean;
}
const toRightOf = (rect: PartialDOMRect) => {
@@ -153,19 +92,11 @@ export default class UserMenu extends React.Component {
this.state = {
contextMenuPosition: null,
themeInUse: SettingsStore.getValue("theme_in_use"),
- dndEnabled: this.doNotDisturb,
selectedSpace: SpaceStore.instance.activeSpaceRoom,
};
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
-
- SettingsStore.monitorSetting("feature_dnd", null);
- SettingsStore.monitorSetting("doNotDisturb", null);
- }
-
- private get doNotDisturb(): boolean {
- return SettingsStore.getValue("doNotDisturb");
}
private get hasHomePage(): boolean {
@@ -210,20 +141,6 @@ export default class UserMenu extends React.Component {
if (this.buttonRef.current) this.buttonRef.current.click();
}
break;
-
- case Action.SettingUpdated: {
- const settingUpdatedPayload = payload as SettingUpdatedPayload;
- switch (settingUpdatedPayload.settingName) {
- case "feature_dnd":
- case "doNotDisturb": {
- const dndEnabled = this.doNotDisturb;
- if (this.state.dndEnabled !== dndEnabled) {
- this.setState({ dndEnabled });
- }
- break;
- }
- }
- }
}
};
@@ -283,7 +200,7 @@ export default class UserMenu extends React.Component {
const cli = MatrixClientPeg.get();
if (!cli || !cli.isCryptoEnabled() || !(await cli.exportRoomKeys())?.length) {
// log out without user prompt if they have no local megolm sessions
- dis.dispatch({ action: 'logout' });
+ defaultDispatcher.dispatch({ action: 'logout' });
} else {
Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog);
}
@@ -292,12 +209,12 @@ export default class UserMenu extends React.Component {
};
private onSignInClick = () => {
- dis.dispatch({ action: 'start_login' });
+ defaultDispatcher.dispatch({ action: 'start_login' });
this.setState({ contextMenuPosition: null }); // also close the menu
};
private onRegisterClick = () => {
- dis.dispatch({ action: 'start_registration' });
+ defaultDispatcher.dispatch({ action: 'start_registration' });
this.setState({ contextMenuPosition: null }); // also close the menu
};
@@ -309,12 +226,6 @@ export default class UserMenu extends React.Component {
this.setState({ contextMenuPosition: null }); // also close the menu
};
- private onDndToggle = (ev: ButtonEvent) => {
- ev.stopPropagation();
- const current = SettingsStore.getValue("doNotDisturb");
- SettingsStore.setValue("doNotDisturb", null, SettingLevel.DEVICE, !current);
- };
-
private renderContextMenu = (): React.ReactNode => {
if (!this.state.contextMenuPosition) return null;
@@ -361,24 +272,6 @@ export default class UserMenu extends React.Component {
);
}
- let customStatusSection: JSX.Element;
- if (SettingsStore.getValue("feature_custom_status")) {
- customStatusSection = ;
- }
-
- let dndButton: JSX.Element;
- if (SettingsStore.getValue("feature_dnd")) {
- dndButton = (
-
- );
- }
-
let feedbackButton;
if (SettingsStore.getValue(UIFeature.Feedback)) {
feedbackButton = {
let primaryOptionList = (
{ homeButton }
- { dndButton }
{
: null
}
- { customStatusSection }
{ topSection }
{ primaryOptionList }
;
@@ -479,11 +370,6 @@ export default class UserMenu extends React.Component {
const displayName = OwnProfileStore.instance.displayName || userId;
const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
- let badge: JSX.Element;
- if (this.state.dndEnabled) {
- badge =
;
- }
-
let name: JSX.Element;
if (!this.props.isPanelCollapsed) {
name =
@@ -498,9 +384,6 @@ export default class UserMenu extends React.Component
{
label={_t("User menu")}
isExpanded={!!this.state.contextMenuPosition}
onContextMenu={this.onContextMenu}
- className={classNames({
- mx_UserMenu_cutout: badge,
- })}
>
{
resizeMethod="crop"
className="mx_UserMenu_userAvatar_BaseAvatar"
/>
- { badge }
{ name }
diff --git a/src/components/structures/VideoRoomView.tsx b/src/components/structures/VideoRoomView.tsx
index 2695dafa798..5535c5c14f8 100644
--- a/src/components/structures/VideoRoomView.tsx
+++ b/src/components/structures/VideoRoomView.tsx
@@ -14,34 +14,59 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { FC, useContext, useState, useMemo } from "react";
+import React, { FC, useContext, useState, useMemo, useEffect } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { Room } from "matrix-js-sdk/src/models/room";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import { useEventEmitter } from "../../hooks/useEventEmitter";
-import { getVideoChannel } from "../../utils/VideoChannelUtils";
-import WidgetStore from "../../stores/WidgetStore";
+import WidgetUtils from "../../utils/WidgetUtils";
+import { addVideoChannel, getVideoChannel, fixStuckDevices } from "../../utils/VideoChannelUtils";
+import WidgetStore, { IApp } from "../../stores/WidgetStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
import VideoChannelStore, { VideoChannelEvent } from "../../stores/VideoChannelStore";
import AppTile from "../views/elements/AppTile";
import VideoLobby from "../views/voip/VideoLobby";
-const VideoRoomView: FC<{ room: Room, resizing: boolean }> = ({ room, resizing }) => {
+interface IProps {
+ room: Room;
+ resizing: boolean;
+}
+
+const VideoRoomView: FC = ({ room, resizing }) => {
const cli = useContext(MatrixClientContext);
const store = VideoChannelStore.instance;
// In case we mount before the WidgetStore knows about our Jitsi widget
+ const [widgetStoreReady, setWidgetStoreReady] = useState(Boolean(WidgetStore.instance.matrixClient));
const [widgetLoaded, setWidgetLoaded] = useState(false);
useEventEmitter(WidgetStore.instance, UPDATE_EVENT, (roomId: string) => {
- if (roomId === null || roomId === room.roomId) setWidgetLoaded(true);
+ if (roomId === null) setWidgetStoreReady(true);
+ if (roomId === null || roomId === room.roomId) {
+ setWidgetLoaded(Boolean(getVideoChannel(room.roomId)));
+ }
});
- const app = useMemo(() => {
- const app = getVideoChannel(room.roomId);
- if (!app) logger.warn(`No video channel for room ${room.roomId}`);
- return app;
- }, [room, widgetLoaded]); // eslint-disable-line react-hooks/exhaustive-deps
+ const app: IApp = useMemo(() => {
+ if (widgetStoreReady) {
+ const app = getVideoChannel(room.roomId);
+ if (!app) {
+ logger.warn(`No video channel for room ${room.roomId}`);
+ // Since widgets in video rooms are mutable, we'll take this opportunity to
+ // reinstate the Jitsi widget in case another client removed it
+ if (WidgetUtils.canUserModifyWidgets(room.roomId)) {
+ addVideoChannel(room.roomId, room.name);
+ }
+ }
+ return app;
+ }
+ }, [room, widgetStoreReady, widgetLoaded]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ // We'll also take this opportunity to fix any stuck devices.
+ // The linter thinks that store.connected should be a dependency, but we explicitly
+ // *only* want this to happen at mount to avoid racing with normal device updates.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ useEffect(() => { fixStuckDevices(room, store.connected); }, [room]);
const [connected, setConnected] = useState(store.connected && store.roomId === room.roomId);
useEventEmitter(store, VideoChannelEvent.Connect, () => setConnected(store.roomId === room.roomId));
diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx
index 297e233444e..e38fdb4180b 100644
--- a/src/components/structures/auth/Login.tsx
+++ b/src/components/structures/auth/Login.tsx
@@ -222,7 +222,7 @@ export default class LoginComponent extends React.PureComponent
"This homeserver has hit its Monthly Active User limit.",
),
'hs_blocked': _td(
- "This homeserver has been blocked by it's administrator.",
+ "This homeserver has been blocked by its administrator.",
),
'': _td(
"This homeserver has exceeded one of its resource limits.",
diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx
index ff7e41b9d58..7515a4f0d90 100644
--- a/src/components/structures/auth/Registration.tsx
+++ b/src/components/structures/auth/Registration.tsx
@@ -15,7 +15,7 @@ limitations under the License.
*/
import { createClient } from 'matrix-js-sdk/src/matrix';
-import React, { ReactNode } from 'react';
+import React, { Fragment, ReactNode } from 'react';
import { MatrixClient } from "matrix-js-sdk/src/client";
import classNames from "classnames";
import { logger } from "matrix-js-sdk/src/logger";
@@ -36,6 +36,8 @@ import AuthBody from "../../views/auth/AuthBody";
import AuthHeader from "../../views/auth/AuthHeader";
import InteractiveAuth from "../InteractiveAuth";
import Spinner from "../../views/elements/Spinner";
+import { AuthHeaderDisplay } from './header/AuthHeaderDisplay';
+import { AuthHeaderProvider } from './header/AuthHeaderProvider';
interface IProps {
serverConfig: ValidatedServerConfig;
@@ -295,7 +297,7 @@ export default class Registration extends React.Component {
response.data.admin_contact,
{
'monthly_active_user': _td("This homeserver has hit its Monthly Active User limit."),
- 'hs_blocked': _td("This homeserver has been blocked by it's administrator."),
+ 'hs_blocked': _td("This homeserver has been blocked by its administrator."),
'': _td("This homeserver has exceeded one of its resource limits."),
},
);
@@ -619,28 +621,37 @@ export default class Registration extends React.Component {
{ regDoneText }
;
} else {
- body =
-
{ _t('Create account') }
- { errorText }
- { serverDeadSection }
-
- { this.renderRegisterComponent() }
- { goBack }
- { signIn }
-
;
+ body =
+
+
}
+ >
+ { errorText }
+ { serverDeadSection }
+
+ { this.renderRegisterComponent() }
+
+
+ { goBack }
+ { signIn }
+
+ ;
}
return (
-
- { body }
-
+
+
+ { body }
+
+
);
}
diff --git a/src/dispatcher/payloads/OpenTabbedIntegrationManagerDialogPayload.ts b/src/components/structures/auth/header/AuthHeaderContext.tsx
similarity index 58%
rename from src/dispatcher/payloads/OpenTabbedIntegrationManagerDialogPayload.ts
rename to src/components/structures/auth/header/AuthHeaderContext.tsx
index 891d1261694..347b26252dd 100644
--- a/src/dispatcher/payloads/OpenTabbedIntegrationManagerDialogPayload.ts
+++ b/src/components/structures/auth/header/AuthHeaderContext.tsx
@@ -14,16 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { Room } from "matrix-js-sdk/src/models/room";
-import { Optional } from "matrix-events-sdk";
+import { createContext, Dispatch, ReducerAction, ReducerState } from "react";
-import { ActionPayload } from "../payloads";
-import { Action } from "../actions";
+import type { AuthHeaderReducer } from "./AuthHeaderProvider";
-export interface OpenTabbedIntegrationManagerDialogPayload extends ActionPayload {
- action: Action.OpenTabbedIntegrationManagerDialog;
-
- room: Optional;
- screen: Optional;
- integrationId: Optional;
+interface AuthHeaderContextType {
+ state: ReducerState;
+ dispatch: Dispatch>;
}
+
+export const AuthHeaderContext = createContext(undefined);
diff --git a/src/components/structures/auth/header/AuthHeaderDisplay.tsx b/src/components/structures/auth/header/AuthHeaderDisplay.tsx
new file mode 100644
index 00000000000..fd5b65a1ebd
--- /dev/null
+++ b/src/components/structures/auth/header/AuthHeaderDisplay.tsx
@@ -0,0 +1,41 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, { Fragment, PropsWithChildren, ReactNode, useContext } from "react";
+
+import { AuthHeaderContext } from "./AuthHeaderContext";
+
+interface Props {
+ title: ReactNode;
+ icon?: ReactNode;
+ serverPicker: ReactNode;
+}
+
+export function AuthHeaderDisplay({ title, icon, serverPicker, children }: PropsWithChildren) {
+ const context = useContext(AuthHeaderContext);
+ if (!context) {
+ return null;
+ }
+ const current = context.state.length ? context.state[0] : null;
+ return (
+
+ { current?.icon ?? icon }
+ { current?.title ?? title }
+ { children }
+ { current?.hideServerPicker !== true && serverPicker }
+
+ );
+}
diff --git a/src/components/structures/auth/header/AuthHeaderModifier.tsx b/src/components/structures/auth/header/AuthHeaderModifier.tsx
new file mode 100644
index 00000000000..a5646ff4f1f
--- /dev/null
+++ b/src/components/structures/auth/header/AuthHeaderModifier.tsx
@@ -0,0 +1,39 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { ReactNode, useContext, useEffect } from "react";
+
+import { AuthHeaderContext } from "./AuthHeaderContext";
+import { AuthHeaderActionType } from "./AuthHeaderProvider";
+
+interface Props {
+ title: ReactNode;
+ icon?: ReactNode;
+ hideServerPicker?: boolean;
+}
+
+export function AuthHeaderModifier(props: Props) {
+ const context = useContext(AuthHeaderContext);
+ const dispatch = context ? context.dispatch : null;
+ useEffect(() => {
+ if (!dispatch) {
+ return;
+ }
+ dispatch({ type: AuthHeaderActionType.Add, value: props });
+ return () => dispatch({ type: AuthHeaderActionType.Remove, value: props });
+ }, [props, dispatch]);
+ return null;
+}
diff --git a/src/components/structures/auth/header/AuthHeaderProvider.tsx b/src/components/structures/auth/header/AuthHeaderProvider.tsx
new file mode 100644
index 00000000000..6c2bc5a7509
--- /dev/null
+++ b/src/components/structures/auth/header/AuthHeaderProvider.tsx
@@ -0,0 +1,52 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { isEqual } from "lodash";
+import React, { ComponentProps, PropsWithChildren, Reducer, useReducer } from "react";
+
+import { AuthHeaderContext } from "./AuthHeaderContext";
+import { AuthHeaderModifier } from "./AuthHeaderModifier";
+
+export enum AuthHeaderActionType {
+ Add,
+ Remove
+}
+
+interface AuthHeaderAction {
+ type: AuthHeaderActionType;
+ value: ComponentProps;
+}
+
+export type AuthHeaderReducer = Reducer[], AuthHeaderAction>;
+
+export function AuthHeaderProvider({ children }: PropsWithChildren<{}>) {
+ const [state, dispatch] = useReducer(
+ (state: ComponentProps[], action: AuthHeaderAction) => {
+ switch (action.type) {
+ case AuthHeaderActionType.Add:
+ return [action.value, ...state];
+ case AuthHeaderActionType.Remove:
+ return (state.length && isEqual(state[0], action.value)) ? state.slice(1) : state;
+ }
+ },
+ [] as ComponentProps[],
+ );
+ return (
+
+ { children }
+
+ );
+}
diff --git a/src/components/views/auth/AuthBody.tsx b/src/components/views/auth/AuthBody.tsx
index 4532ceeaf44..cacab416f64 100644
--- a/src/components/views/auth/AuthBody.tsx
+++ b/src/components/views/auth/AuthBody.tsx
@@ -14,12 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from 'react';
+import classNames from "classnames";
+import React, { PropsWithChildren } from 'react';
-export default class AuthBody extends React.PureComponent {
- public render(): React.ReactNode {
- return
- { this.props.children }
-
;
- }
+interface Props {
+ flex?: boolean;
+}
+
+export default function AuthBody({ flex, children }: PropsWithChildren) {
+ return
+ { children }
+
;
}
diff --git a/src/components/views/auth/CountryDropdown.tsx b/src/components/views/auth/CountryDropdown.tsx
index 0318c4b95d7..07866bc98e5 100644
--- a/src/components/views/auth/CountryDropdown.tsx
+++ b/src/components/views/auth/CountryDropdown.tsx
@@ -135,7 +135,7 @@ export default class CountryDropdown extends React.Component {
});
// default value here too, otherwise we need to handle null / undefined
- // values between mounting and the initial value propgating
+ // values between mounting and the initial value propagating
const value = this.props.value || this.state.defaultCountry.iso2;
return void;
submitAuthDict: (auth: IAuthDict) => void;
+ requestEmailToken?: () => Promise;
}
interface IPasswordAuthEntryState {
@@ -205,7 +210,9 @@ export class RecaptchaAuthEntry extends React.Component ;
+ return (
+
+ );
}
let errorText = this.props.errorText;
@@ -349,7 +356,9 @@ export class TermsAuthEntry extends React.Component ;
+ return (
+
+ );
}
const checkboxes = [];
@@ -405,9 +414,24 @@ interface IEmailIdentityAuthEntryProps extends IAuthEntryProps {
};
}
-export class EmailIdentityAuthEntry extends React.Component {
+interface IEmailIdentityAuthEntryState {
+ requested: boolean;
+ requesting: boolean;
+}
+
+export class EmailIdentityAuthEntry extends
+ React.Component {
static LOGIN_TYPE = AuthType.Email;
+ constructor(props: IEmailIdentityAuthEntryProps) {
+ super(props);
+
+ this.state = {
+ requested: false,
+ requesting: false,
+ };
+ }
+
componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE);
}
@@ -440,11 +464,51 @@ export class EmailIdentityAuthEntry extends React.Component
- { _t("A confirmation email has been sent to %(emailAddress)s",
+ }
+ hideServerPicker={true}
+ />
+
{ _t("To create your account, open the link in the email we just sent to %(emailAddress)s.",
{ emailAddress: { this.props.inputs.emailAddress } },
- ) }
-
- { _t("Open the link in the email to continue registration.") }
+ ) }
+ { this.state.requesting ? (
+ { _t("Did not receive it? Resend it ", {}, {
+ a: (text: string) =>
+ null}
+ disabled
+ >{ text }
+ ,
+ }) }
+ ) : { _t("Did not receive it? Resend it ", {}, {
+ a: (text: string) => this.setState({ requested: false })
+ : undefined}
+ onClick={async () => {
+ this.setState({ requesting: true });
+ try {
+ await this.props.requestEmailToken?.();
+ } catch (e) {
+ logger.warn("Email token request failed: ", e);
+ } finally {
+ this.setState({ requested: true, requesting: false });
+ }
+ }}
+ >{ text } ,
+ }) }
}
{ errorSection }
);
@@ -560,7 +624,9 @@ export class MsisdnAuthEntry extends React.Component ;
+ return (
+
+ );
} else {
const enableSubmit = Boolean(this.state.token);
const submitClasses = classNames({
@@ -726,13 +792,15 @@ export class SSOAuthEntry extends React.Component
- { errorSection }
-
- { cancelButton }
- { continueButton }
-
- ;
+ return (
+
+ { errorSection }
+
+ { cancelButton }
+ { continueButton }
+
+
+ );
}
}
@@ -817,6 +885,7 @@ export interface IStageComponentProps extends IAuthEntryProps {
fail?(e: Error): void;
setEmailSid?(sid: string): void;
onCancel?(): void;
+ requestEmailToken?(): Promise;
}
export interface IStageComponent extends React.ComponentClass> {
diff --git a/src/components/views/beacon/BeaconMarker.tsx b/src/components/views/beacon/BeaconMarker.tsx
index f7f284b88ed..644482e48b3 100644
--- a/src/components/views/beacon/BeaconMarker.tsx
+++ b/src/components/views/beacon/BeaconMarker.tsx
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { useContext } from 'react';
+import React, { ReactNode, useContext } from 'react';
import maplibregl from 'maplibre-gl';
import {
Beacon,
@@ -29,12 +29,13 @@ import SmartMarker from '../location/SmartMarker';
interface Props {
map: maplibregl.Map;
beacon: Beacon;
+ tooltip?: ReactNode;
}
/**
* Updates a map SmartMarker with latest location from given beacon
*/
-const BeaconMarker: React.FC = ({ map, beacon }) => {
+const BeaconMarker: React.FC = ({ map, beacon, tooltip }) => {
const latestLocationState = useEventEmitterState(
beacon,
BeaconEvent.LocationUpdate,
@@ -58,6 +59,7 @@ const BeaconMarker: React.FC = ({ map, beacon }) => {
id={beacon.identifier}
geoUri={geoUri}
roomMember={markerRoomMember}
+ tooltip={tooltip}
useMemberColor
/>;
};
diff --git a/src/components/views/beacon/BeaconStatusTooltip.tsx b/src/components/views/beacon/BeaconStatusTooltip.tsx
new file mode 100644
index 00000000000..bc9f3609395
--- /dev/null
+++ b/src/components/views/beacon/BeaconStatusTooltip.tsx
@@ -0,0 +1,61 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, { useContext } from 'react';
+import { Beacon } from 'matrix-js-sdk/src/matrix';
+import { LocationAssetType } from 'matrix-js-sdk/src/@types/location';
+
+import MatrixClientContext from '../../../contexts/MatrixClientContext';
+import CopyableText from '../elements/CopyableText';
+import BeaconStatus from './BeaconStatus';
+import { BeaconDisplayStatus } from './displayStatus';
+
+interface Props {
+ beacon: Beacon;
+}
+
+const useBeaconName = (beacon: Beacon): string => {
+ const matrixClient = useContext(MatrixClientContext);
+
+ if (beacon.beaconInfo.assetType !== LocationAssetType.Self) {
+ return beacon.beaconInfo.description;
+ }
+ const room = matrixClient.getRoom(beacon.roomId);
+ const member = room?.getMember(beacon.beaconInfoOwner);
+
+ return member?.rawDisplayName || beacon.beaconInfoOwner;
+};
+
+const BeaconStatusTooltip: React.FC = ({ beacon }) => {
+ const label = useBeaconName(beacon);
+
+ return
+
+ beacon.latestLocationState?.uri}
+ />
+
+
;
+};
+
+export default BeaconStatusTooltip;
diff --git a/src/components/views/beacon/BeaconViewDialog.tsx b/src/components/views/beacon/BeaconViewDialog.tsx
index e6c4a423fe9..a7cdb242d37 100644
--- a/src/components/views/beacon/BeaconViewDialog.tsx
+++ b/src/components/views/beacon/BeaconViewDialog.tsx
@@ -37,6 +37,7 @@ import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
import DialogSidebar from './DialogSidebar';
import DialogOwnBeaconStatus from './DialogOwnBeaconStatus';
+import BeaconStatusTooltip from './BeaconStatusTooltip';
interface IProps extends IDialogProps {
roomId: Room['roomId'];
@@ -103,6 +104,7 @@ const BeaconViewDialog: React.FC = ({
key={beacon.identifier}
map={map}
beacon={beacon}
+ tooltip={ }
/>) }
>
diff --git a/src/components/views/beacon/LeftPanelLiveShareWarning.tsx b/src/components/views/beacon/LeftPanelLiveShareWarning.tsx
index 43b00a8fa76..b2686285925 100644
--- a/src/components/views/beacon/LeftPanelLiveShareWarning.tsx
+++ b/src/components/views/beacon/LeftPanelLiveShareWarning.tsx
@@ -15,8 +15,8 @@ limitations under the License.
*/
import classNames from 'classnames';
-import React from 'react';
-import { BeaconIdentifier, Room } from 'matrix-js-sdk/src/matrix';
+import React, { useEffect } from 'react';
+import { Beacon, BeaconIdentifier, Room } from 'matrix-js-sdk/src/matrix';
import { useEventEmitterState } from '../../../hooks/useEventEmitter';
import { _t } from '../../../languageHandler';
@@ -56,11 +56,30 @@ const getLabel = (hasStoppingErrors: boolean, hasLocationErrors: boolean): strin
return _t('An error occurred while stopping your live location');
}
if (hasLocationErrors) {
- return _t('An error occured whilst sharing your live location');
+ return _t('An error occurred whilst sharing your live location');
}
return _t('You are sharing your live location');
};
+const useLivenessMonitor = (liveBeaconIds: BeaconIdentifier[], beacons: Map): void => {
+ useEffect(() => {
+ // chromium sets the minimum timer interval to 1000ms
+ // for inactive tabs
+ // refresh beacon monitors when the tab becomes active again
+ const onPageVisibilityChanged = () => {
+ if (document.visibilityState === 'visible') {
+ liveBeaconIds.forEach(identifier => beacons.get(identifier)?.monitorLiveness());
+ }
+ };
+ if (liveBeaconIds.length) {
+ document.addEventListener("visibilitychange", onPageVisibilityChanged);
+ }
+ return () => {
+ document.removeEventListener("visibilitychange", onPageVisibilityChanged);
+ };
+ }, [liveBeaconIds, beacons]);
+};
+
const LeftPanelLiveShareWarning: React.FC = ({ isMinimized }) => {
const isMonitoringLiveLocation = useEventEmitterState(
OwnBeaconStore.instance,
@@ -91,6 +110,8 @@ const LeftPanelLiveShareWarning: React.FC = ({ isMinimized }) => {
const hasLocationPublishErrors = !!beaconIdsWithLocationPublishError.length;
const hasStoppingErrors = !!beaconIdsWithStoppingError.length;
+ useLivenessMonitor(liveBeaconIds, OwnBeaconStore.instance.beacons);
+
if (!isMonitoringLiveLocation) {
return null;
}
diff --git a/src/components/views/beacon/RoomLiveShareWarning.tsx b/src/components/views/beacon/RoomLiveShareWarning.tsx
index 2fe76a10e88..ecd200cf96f 100644
--- a/src/components/views/beacon/RoomLiveShareWarning.tsx
+++ b/src/components/views/beacon/RoomLiveShareWarning.tsx
@@ -29,7 +29,7 @@ import LiveTimeRemaining from './LiveTimeRemaining';
const getLabel = (hasLocationPublishError: boolean, hasStopSharingError: boolean): string => {
if (hasLocationPublishError) {
- return _t('An error occured whilst sharing your live location, please try again');
+ return _t('An error occurred whilst sharing your live location, please try again');
}
if (hasStopSharingError) {
return _t('An error occurred while stopping your live location, please try again');
diff --git a/src/components/views/beta/BetaCard.tsx b/src/components/views/beta/BetaCard.tsx
index 4639886b701..c2a0e282953 100644
--- a/src/components/views/beta/BetaCard.tsx
+++ b/src/components/views/beta/BetaCard.tsx
@@ -60,7 +60,6 @@ export const BetaPill = ({
}
onClick={onClick}
- yOffset={-10}
>
{ _t("Beta") }
;
diff --git a/src/components/views/context_menus/DeviceContextMenu.tsx b/src/components/views/context_menus/DeviceContextMenu.tsx
new file mode 100644
index 00000000000..04463e81ff0
--- /dev/null
+++ b/src/components/views/context_menus/DeviceContextMenu.tsx
@@ -0,0 +1,89 @@
+/*
+Copyright 2021 Šimon Brandner
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, { useEffect, useState } from "react";
+
+import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../MediaDeviceHandler";
+import IconizedContextMenu, { IconizedContextMenuOptionList, IconizedContextMenuRadio } from "./IconizedContextMenu";
+import { IProps as IContextMenuProps } from "../../structures/ContextMenu";
+import { _t, _td } from "../../../languageHandler";
+
+const SECTION_NAMES: Record = {
+ [MediaDeviceKindEnum.AudioInput]: _td("Input devices"),
+ [MediaDeviceKindEnum.AudioOutput]: _td("Output devices"),
+ [MediaDeviceKindEnum.VideoInput]: _td("Cameras"),
+};
+
+interface IDeviceContextMenuDeviceProps {
+ label: string;
+ selected: boolean;
+ onClick: () => void;
+}
+
+const DeviceContextMenuDevice: React.FC = ({ label, selected, onClick }) => {
+ return ;
+};
+
+interface IDeviceContextMenuSectionProps {
+ deviceKind: MediaDeviceKindEnum;
+}
+
+const DeviceContextMenuSection: React.FC = ({ deviceKind }) => {
+ const [devices, setDevices] = useState([]);
+ const [selectedDevice, setSelectedDevice] = useState(MediaDeviceHandler.getDevice(deviceKind));
+
+ useEffect(() => {
+ const getDevices = async () => {
+ return setDevices((await MediaDeviceHandler.getDevices())[deviceKind]);
+ };
+ getDevices();
+ }, [deviceKind]);
+
+ const onDeviceClick = (deviceId: string): void => {
+ MediaDeviceHandler.instance.setDevice(deviceId, deviceKind);
+ setSelectedDevice(deviceId);
+ };
+
+ return
+ { devices.map(({ label, deviceId }) => {
+ return onDeviceClick(deviceId)}
+ />;
+ }) }
+ ;
+};
+
+interface IProps extends IContextMenuProps {
+ deviceKinds: MediaDeviceKind[];
+}
+
+const DeviceContextMenu: React.FC = ({ deviceKinds, ...props }) => {
+ return
+ { deviceKinds.map((kind) => {
+ return ;
+ }) }
+ ;
+};
+
+export default DeviceContextMenu;
diff --git a/src/components/views/context_menus/IconizedContextMenu.tsx b/src/components/views/context_menus/IconizedContextMenu.tsx
index 2c6bdb3776b..9b7896790ef 100644
--- a/src/components/views/context_menus/IconizedContextMenu.tsx
+++ b/src/components/views/context_menus/IconizedContextMenu.tsx
@@ -33,6 +33,7 @@ interface IProps extends IContextMenuProps {
interface IOptionListProps {
first?: boolean;
red?: boolean;
+ label?: string;
className?: string;
}
@@ -126,13 +127,20 @@ export const IconizedContextMenuOption: React.FC = ({
;
};
-export const IconizedContextMenuOptionList: React.FC = ({ first, red, className, children }) => {
+export const IconizedContextMenuOptionList: React.FC = ({
+ first,
+ red,
+ className,
+ label,
+ children,
+}) => {
const classes = classNames("mx_IconizedContextMenu_optionList", className, {
mx_IconizedContextMenu_optionList_notFirst: !first,
mx_IconizedContextMenu_optionList_red: red,
});
return
+ { label &&
{ label }
}
{ children }
;
};
diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx
index 917091ece83..86e9d9cc306 100644
--- a/src/components/views/context_menus/MessageContextMenu.tsx
+++ b/src/components/views/context_menus/MessageContextMenu.tsx
@@ -37,12 +37,11 @@ import { Action } from "../../../dispatcher/actions";
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
import { ButtonEvent } from '../elements/AccessibleButton';
import { copyPlaintext, getSelectedText } from '../../../utils/strings';
-import ContextMenu, { toRightOf } from '../../structures/ContextMenu';
+import ContextMenu, { toRightOf, IPosition, ChevronFace } from '../../structures/ContextMenu';
import ReactionPicker from '../emojipicker/ReactionPicker';
import ViewSource from '../../structures/ViewSource';
import { createRedactEventDialog } from '../dialogs/ConfirmRedactDialog';
import ShareDialog from '../dialogs/ShareDialog';
-import { IPosition, ChevronFace } from '../../structures/ContextMenu';
import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext';
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
import EndPollDialog from '../dialogs/EndPollDialog';
@@ -71,8 +70,8 @@ interface IProps extends IPosition {
rightClick?: boolean;
// The Relations model from the JS SDK for reactions to `mxEvent`
reactions?: Relations;
- // A permalink to the event
- showPermalink?: boolean;
+ // A permalink to this event or an href of an anchor element the user has clicked
+ link?: string;
getRelationsForEvent?: GetRelationsForEvent;
}
@@ -228,7 +227,7 @@ export default class MessageContextMenu extends React.Component
this.closeMenu();
};
- private onPermalinkClick = (e: React.MouseEvent): void => {
+ private onShareClick = (e: React.MouseEvent): void => {
e.preventDefault();
Modal.createTrackedDialog('share room message dialog', '', ShareDialog, {
target: this.props.mxEvent,
@@ -237,9 +236,9 @@ export default class MessageContextMenu extends React.Component
this.closeMenu();
};
- private onCopyPermalinkClick = (e: ButtonEvent): void => {
+ private onCopyLinkClick = (e: ButtonEvent): void => {
e.preventDefault(); // So that we don't open the permalink
- copyPlaintext(this.getPermalink());
+ copyPlaintext(this.props.link);
this.closeMenu();
};
@@ -296,11 +295,6 @@ export default class MessageContextMenu extends React.Component
});
}
- private getPermalink(): string {
- if (!this.props.permalinkCreator) return;
- return this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
- }
-
private getUnsentReactions(): MatrixEvent[] {
return this.getReactions(e => e.status === EventStatus.NOT_SENT);
}
@@ -319,11 +313,11 @@ export default class MessageContextMenu extends React.Component
public render(): JSX.Element {
const cli = MatrixClientPeg.get();
const me = cli.getUserId();
- const { mxEvent, rightClick, showPermalink, eventTileOps, reactions, collapseReplyChain } = this.props;
+ const { mxEvent, rightClick, link, eventTileOps, reactions, collapseReplyChain } = this.props;
const eventStatus = mxEvent.status;
const unsentReactionsCount = this.getUnsentReactions().length;
const contentActionable = isContentActionable(mxEvent);
- const permalink = this.getPermalink();
+ const permalink = this.props.permalinkCreator?.forEvent(this.props.mxEvent.getId());
// status is SENT before remote-echo, null after
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
const { timelineRenderingType, canReact, canSendMessages } = this.context;
@@ -421,17 +415,13 @@ export default class MessageContextMenu extends React.Component
if (permalink) {
permalinkButton = (
);
}
+ let copyLinkButton: JSX.Element;
+ if (link) {
+ copyLinkButton = (
+
+ );
+ }
+
let copyButton: JSX.Element;
if (rightClick && getSelectedText()) {
copyButton = (
@@ -567,10 +577,11 @@ export default class MessageContextMenu extends React.Component
}
let nativeItemsList: JSX.Element;
- if (copyButton) {
+ if (copyButton || copyLinkButton) {
nativeItemsList = (
{ copyButton }
+ { copyLinkButton }
);
}
diff --git a/src/components/views/context_menus/SpaceContextMenu.tsx b/src/components/views/context_menus/SpaceContextMenu.tsx
index d9286c618b4..5d045900453 100644
--- a/src/components/views/context_menus/SpaceContextMenu.tsx
+++ b/src/components/views/context_menus/SpaceContextMenu.tsx
@@ -16,7 +16,7 @@ limitations under the License.
import React, { useContext } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
-import { EventType } from "matrix-js-sdk/src/@types/event";
+import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
import { IProps as IContextMenuProps } from "../../structures/ContextMenu";
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu";
@@ -136,6 +136,7 @@ const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) =
const hasPermissionToAddSpaceChild = space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
const canAddRooms = hasPermissionToAddSpaceChild && shouldShowComponent(UIComponent.CreateRooms);
+ const canAddVideoRooms = canAddRooms && SettingsStore.getValue("feature_video_rooms");
const canAddSubSpaces = hasPermissionToAddSpaceChild && shouldShowComponent(UIComponent.CreateSpaces);
let newRoomSection: JSX.Element;
@@ -149,6 +150,14 @@ const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) =
onFinished();
};
+ const onNewVideoRoomClick = (ev: ButtonEvent) => {
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ showCreateNewRoom(space, RoomType.ElementVideo);
+ onFinished();
+ };
+
const onNewSubspaceClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
@@ -169,6 +178,14 @@ const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) =
onClick={onNewRoomClick}
/>
}
+ { canAddVideoRooms &&
+
+ }
{ canAddSubSpaces &&
{
// align the context menu's icons with the icon which opened the context menu
- const left = elementRect.left + window.pageXOffset + elementRect.width;
- const top = elementRect.bottom + window.pageYOffset;
+ const left = elementRect.left + window.scrollX + elementRect.width;
+ const top = elementRect.bottom + window.scrollY;
const chevronFace = ChevronFace.None;
return { left, top, chevronFace };
};
diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx
index 1aeeccc724b..b6a46bd2aca 100644
--- a/src/components/views/context_menus/WidgetContextMenu.tsx
+++ b/src/components/views/context_menus/WidgetContextMenu.tsx
@@ -110,7 +110,8 @@ const WidgetContextMenu: React.FC = ({
}
let snapshotButton;
- if (widgetMessaging?.hasCapability(MatrixCapabilities.Screenshots)) {
+ const screenshotsEnabled = SettingsStore.getValue("enableWidgetScreenshots");
+ if (screenshotsEnabled && widgetMessaging?.hasCapability(MatrixCapabilities.Screenshots)) {
const onSnapshotClick = () => {
widgetMessaging?.takeScreenshot().then(data => {
dis.dispatch({
diff --git a/src/components/views/dialogs/BugReportDialog.tsx b/src/components/views/dialogs/BugReportDialog.tsx
index eb70e44560c..a56427a2ab1 100644
--- a/src/components/views/dialogs/BugReportDialog.tsx
+++ b/src/components/views/dialogs/BugReportDialog.tsx
@@ -30,6 +30,8 @@ import Field from '../elements/Field';
import Spinner from "../elements/Spinner";
import DialogButtons from "../elements/DialogButtons";
import { sendSentryReport } from "../../../sentry";
+import defaultDispatcher from '../../../dispatcher/dispatcher';
+import { Action } from '../../../dispatcher/actions';
interface IProps {
onFinished: (success: boolean) => void;
@@ -65,6 +67,16 @@ export default class BugReportDialog extends React.Component {
downloadProgress: null,
};
this.unmounted = false;
+
+ // Get all of the extra info dumped to the console when someone is about
+ // to send debug logs. Since this is a fire and forget action, we do
+ // this when the bug report dialog is opened instead of when we submit
+ // logs because we have no signal to know when all of the various
+ // components have finished logging. Someone could potentially send logs
+ // before we fully dump everything but it's probably unlikely.
+ defaultDispatcher.dispatch({
+ action: Action.DumpDebugLogs,
+ });
}
public componentWillUnmount() {
diff --git a/src/components/views/dialogs/BulkRedactDialog.tsx b/src/components/views/dialogs/BulkRedactDialog.tsx
index 86c0f4033f1..3d8e6967f18 100644
--- a/src/components/views/dialogs/BulkRedactDialog.tsx
+++ b/src/components/views/dialogs/BulkRedactDialog.tsx
@@ -25,7 +25,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
import { _t } from '../../../languageHandler';
import dis from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
-import { IDialogProps } from "../dialogs/IDialogProps";
+import { IDialogProps } from "./IDialogProps";
import BaseDialog from "../dialogs/BaseDialog";
import InfoDialog from "../dialogs/InfoDialog";
import DialogButtons from "../elements/DialogButtons";
diff --git a/src/components/views/dialogs/DevtoolsDialog.tsx b/src/components/views/dialogs/DevtoolsDialog.tsx
index 14fb69b1563..fb2f7113dd8 100644
--- a/src/components/views/dialogs/DevtoolsDialog.tsx
+++ b/src/components/views/dialogs/DevtoolsDialog.tsx
@@ -101,6 +101,7 @@ const DevtoolsDialog: React.FC = ({ roomId, onFinished }) => {
{ _t("Options") }
+
;
}
diff --git a/src/components/views/dialogs/ExportDialog.tsx b/src/components/views/dialogs/ExportDialog.tsx
index 6881c1c52d8..1a8d1020cd6 100644
--- a/src/components/views/dialogs/ExportDialog.tsx
+++ b/src/components/views/dialogs/ExportDialog.tsx
@@ -200,7 +200,7 @@ const ExportDialog: React.FC = ({ room, onFinished }) => {
}, {
key: "number",
test: ({ value }) => {
- const parsedSize = parseInt(value as string, 10);
+ const parsedSize = parseInt(value, 10);
return validateNumberInRange(1, 2000)(parsedSize);
},
invalid: () => {
@@ -238,7 +238,7 @@ const ExportDialog: React.FC = ({ room, onFinished }) => {
}, {
key: "number",
test: ({ value }) => {
- const parsedSize = parseInt(value as string, 10);
+ const parsedSize = parseInt(value, 10);
return validateNumberInRange(1, 10 ** 8)(parsedSize);
},
invalid: () => {
@@ -263,7 +263,7 @@ const ExportDialog: React.FC = ({ room, onFinished }) => {
else onFinished(false);
};
- const confirmCanel = async () => {
+ const confirmCancel = async () => {
await exporter?.cancelExport();
setExportCancelled(true);
setExporting(false);
@@ -346,7 +346,7 @@ const ExportDialog: React.FC = ({ room, onFinished }) => {
hasCancel={true}
cancelButton={_t("Continue")}
onCancel={() => setCancelWarning(false)}
- onPrimaryButtonClick={confirmCanel}
+ onPrimaryButtonClick={confirmCancel}
/>
);
diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx
index 3c482bf0438..7b01e721692 100644
--- a/src/components/views/dialogs/ForwardDialog.tsx
+++ b/src/components/views/dialogs/ForwardDialog.tsx
@@ -21,6 +21,8 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { EventType } from "matrix-js-sdk/src/@types/event";
+import { ILocationContent, LocationAssetType, M_TIMESTAMP } from "matrix-js-sdk/src/@types/location";
+import { makeLocationContent } from "matrix-js-sdk/src/content-helpers";
import { _t } from "../../../languageHandler";
import dis from "../../../dispatcher/dispatcher";
@@ -47,6 +49,8 @@ import { Action } from "../../../dispatcher/actions";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { ButtonEvent } from "../elements/AccessibleButton";
import { roomContextDetailsText } from "../../../utils/i18n-helpers";
+import { isLocationEvent } from "../../../utils/EventUtils";
+import { isSelfLocation, locationEventGeoUri } from "../../../utils/location";
const AVATAR_SIZE = 30;
@@ -131,8 +135,7 @@ const Entry: React.FC = ({ room, type, content, matrixClient: cli,
@@ -147,7 +150,6 @@ const Entry: React.FC = ({ room, type, content, matrixClient: cli,
onClick={send}
disabled={disabled}
title={title}
- yOffset={-20}
alignment={Alignment.Top}
>
{ _t("Send") }
@@ -156,6 +158,34 @@ const Entry: React.FC = ({ room, type, content, matrixClient: cli,
;
};
+const getStrippedEventContent = (event: MatrixEvent): IContent => {
+ const {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ "m.relates_to": _, // strip relations - in future we will attach a relation pointing at the original event
+ // We're taking a shallow copy here to avoid /~https://github.com/vector-im/element-web/issues/10924
+ ...content
+ } = event.getContent();
+
+ // self location shares should have their description removed
+ // and become 'pin' share type
+ if (isLocationEvent(event) && isSelfLocation(content as ILocationContent)) {
+ const timestamp = M_TIMESTAMP.findIn(content);
+ const geoUri = locationEventGeoUri(event);
+ return {
+ ...content,
+ ...makeLocationContent(
+ undefined, // text
+ geoUri,
+ timestamp || Date.now(),
+ undefined, // description
+ LocationAssetType.Pin,
+ ),
+ };
+ }
+
+ return content;
+};
+
const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCreator, onFinished }) => {
const userId = cli.getUserId();
const [profileInfo, setProfileInfo] = useState({});
@@ -163,12 +193,7 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr
cli.getProfileInfo(userId).then(info => setProfileInfo(info));
}, [cli, userId]);
- const {
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- "m.relates_to": _, // strip relations - in future we will attach a relation pointing at the original event
- // We're taking a shallow copy here to avoid /~https://github.com/vector-im/element-web/issues/10924
- ...content
- } = event.getContent();
+ const content = getStrippedEventContent(event);
// For the message preview we fake the sender as ourselves
const mockEvent = new MatrixEvent({
@@ -262,6 +287,7 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr
{ rooms.length > 0 ? (
rooms.slice(start, end).map(room =>
diff --git a/src/components/views/dialogs/KeySignatureUploadFailedDialog.tsx b/src/components/views/dialogs/KeySignatureUploadFailedDialog.tsx
index 381c96dc660..72ce6bd3ba1 100644
--- a/src/components/views/dialogs/KeySignatureUploadFailedDialog.tsx
+++ b/src/components/views/dialogs/KeySignatureUploadFailedDialog.tsx
@@ -29,7 +29,7 @@ interface IProps extends IDialogProps {
error: string;
}>>;
source: string;
- continuation: () => void;
+ continuation: () => Promise;
}
const KeySignatureUploadFailedDialog: React.FC = ({
diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.tsx b/src/components/views/dialogs/MessageEditHistoryDialog.tsx
index 2dedfb52937..0f51530a128 100644
--- a/src/components/views/dialogs/MessageEditHistoryDialog.tsx
+++ b/src/components/views/dialogs/MessageEditHistoryDialog.tsx
@@ -138,7 +138,8 @@ export default class MessageEditHistoryDialog extends React.PureComponent ));
+ />
+ ));
lastEvent = e;
});
return nodes;
diff --git a/src/components/views/dialogs/ShareDialog.tsx b/src/components/views/dialogs/ShareDialog.tsx
index cb06d09d802..f07964650f3 100644
--- a/src/components/views/dialogs/ShareDialog.tsx
+++ b/src/components/views/dialogs/ShareDialog.tsx
@@ -52,7 +52,7 @@ const socials = [
}, {
name: 'Reddit',
img: require("../../../../res/img/social/reddit.png"),
- url: (url) => `http://www.reddit.com/submit?url=${url}`,
+ url: (url) => `https://www.reddit.com/submit?url=${url}`,
}, {
name: 'email',
img: require("../../../../res/img/social/email-1.png"),
diff --git a/src/components/views/dialogs/SpotlightDialog.tsx b/src/components/views/dialogs/SpotlightDialog.tsx
index f579e262875..f5efc0b8dc8 100644
--- a/src/components/views/dialogs/SpotlightDialog.tsx
+++ b/src/components/views/dialogs/SpotlightDialog.tsx
@@ -74,6 +74,7 @@ import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { PosthogAnalytics } from "../../../PosthogAnalytics";
import { getCachedRoomIDForAlias } from "../../../RoomAliasCache";
import { roomContextDetailsText, spaceContextDetailsText } from "../../../utils/i18n-helpers";
+import { RecentAlgorithm } from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
const MAX_RECENT_SEARCHES = 10;
const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons
@@ -210,6 +211,8 @@ type Result = IRoomResult | IResult;
const isRoomResult = (result: any): result is IRoomResult => !!result?.room;
+const recentAlgorithm = new RecentAlgorithm();
+
export const useWebSearchMetrics = (numResults: number, queryLength: number, viaSpotlight: boolean): void => {
useEffect(() => {
if (!queryLength) return;
@@ -280,6 +283,7 @@ const SpotlightDialog: React.FC = ({ initialText = "", onFinished }) =>
const results: [Result[], Result[], Result[]] = [[], [], []];
+ // Group results in their respective sections
possibleResults.forEach(entry => {
if (isRoomResult(entry)) {
if (!entry.room.normalizedName.includes(normalizedQuery) &&
@@ -295,8 +299,25 @@ const SpotlightDialog: React.FC = ({ initialText = "", onFinished }) =>
results[entry.section].push(entry);
});
+ // Sort results by most recent activity
+
+ const myUserId = cli.getUserId();
+ for (const resultArray of results) {
+ resultArray.sort((a: Result, b: Result) => {
+ // This is not a room result, it should appear at the bottom of
+ // the list
+ if (!(a as IRoomResult).room) return 1;
+ if (!(b as IRoomResult).room) return -1;
+
+ const roomA = (a as IRoomResult).room;
+ const roomB = (b as IRoomResult).room;
+
+ return recentAlgorithm.getLastTs(roomB, myUserId) - recentAlgorithm.getLastTs(roomA, myUserId);
+ });
+ }
+
return results;
- }, [possibleResults, trimmedQuery]);
+ }, [possibleResults, trimmedQuery, cli]);
const numResults = trimmedQuery ? people.length + rooms.length + spaces.length : 0;
useWebSearchMetrics(numResults, query.length, true);
@@ -367,16 +388,16 @@ const SpotlightDialog: React.FC = ({ initialText = "", onFinished }) =>
);
}
- const otherResult = (result as IResult);
+ // IResult case
return (
- { otherResult.avatar }
- { otherResult.name }
- { otherResult.description }
+ { result.avatar }
+ { result.name }
+ { result.description }
);
};
diff --git a/src/components/views/dialogs/TabbedIntegrationManagerDialog.tsx b/src/components/views/dialogs/TabbedIntegrationManagerDialog.tsx
deleted file mode 100644
index 5a5d6e38229..00000000000
--- a/src/components/views/dialogs/TabbedIntegrationManagerDialog.tsx
+++ /dev/null
@@ -1,176 +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 { Room } from "matrix-js-sdk/src/models/room";
-import classNames from 'classnames';
-import { logger } from "matrix-js-sdk/src/logger";
-
-import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
-import { dialogTermsInteractionCallback, TermsNotSignedError } from "../../../Terms";
-import * as ScalarMessaging from "../../../ScalarMessaging";
-import { IntegrationManagerInstance } from "../../../integrations/IntegrationManagerInstance";
-import ScalarAuthClient from "../../../ScalarAuthClient";
-import AccessibleButton from "../elements/AccessibleButton";
-import IntegrationManager from "../settings/IntegrationManager";
-import { IDialogProps } from "./IDialogProps";
-
-interface IProps extends IDialogProps {
- /**
- * Optional room where the integration manager should be open to
- */
- room?: Room;
-
- /**
- * Optional screen to open on the integration manager
- */
- screen?: string;
-
- /**
- * Optional integration ID to open in the integration manager
- */
- integrationId?: string;
-}
-
-interface IState {
- managers: IntegrationManagerInstance[];
- busy: boolean;
- currentIndex: number;
- currentConnected: boolean;
- currentLoading: boolean;
- currentScalarClient: ScalarAuthClient;
-}
-
-export default class TabbedIntegrationManagerDialog extends React.Component {
- constructor(props: IProps) {
- super(props);
-
- this.state = {
- managers: IntegrationManagers.sharedInstance().getOrderedManagers(),
- busy: true,
- currentIndex: 0,
- currentConnected: false,
- currentLoading: true,
- currentScalarClient: null,
- };
- }
-
- public componentDidMount(): void {
- this.openManager(0, true);
- }
-
- private openManager = async (i: number, force = false): Promise => {
- if (i === this.state.currentIndex && !force) return;
-
- const manager = this.state.managers[i];
- const client = manager.getScalarClient();
- this.setState({
- busy: true,
- currentIndex: i,
- currentLoading: true,
- currentConnected: false,
- currentScalarClient: client,
- });
-
- ScalarMessaging.setOpenManagerUrl(manager.uiUrl);
-
- client.setTermsInteractionCallback((policyInfo, agreedUrls) => {
- // To avoid visual glitching of two modals stacking briefly, we customise the
- // terms dialog sizing when it will appear for the integration manager so that
- // it gets the same basic size as the IM's own modal.
- return dialogTermsInteractionCallback(
- policyInfo, agreedUrls, 'mx_TermsDialog_forIntegrationManager',
- );
- });
-
- try {
- await client.connect();
- if (!client.hasCredentials()) {
- this.setState({
- busy: false,
- currentLoading: false,
- currentConnected: false,
- });
- } else {
- this.setState({
- busy: false,
- currentLoading: false,
- currentConnected: true,
- });
- }
- } catch (e) {
- if (e instanceof TermsNotSignedError) {
- return;
- }
-
- logger.error(e);
- this.setState({
- busy: false,
- currentLoading: false,
- currentConnected: false,
- });
- }
- };
-
- private renderTabs(): JSX.Element[] {
- return this.state.managers.map((m, i) => {
- const classes = classNames({
- 'mx_TabbedIntegrationManagerDialog_tab': true,
- 'mx_TabbedIntegrationManagerDialog_currentTab': this.state.currentIndex === i,
- });
- return (
- this.openManager(i)}
- key={`tab_${i}`}
- disabled={this.state.busy}
- >
- { m.name }
-
- );
- });
- }
-
- public renderTab(): JSX.Element {
- let uiUrl = null;
- if (this.state.currentScalarClient) {
- uiUrl = this.state.currentScalarClient.getScalarInterfaceUrlForRoom(
- this.props.room,
- this.props.screen,
- this.props.integrationId,
- );
- }
- return {/* no-op */}}
- />;
- }
-
- public render(): JSX.Element {
- return (
-
-
- { this.renderTabs() }
-
-
- { this.renderTab() }
-
-
- );
- }
-}
diff --git a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx
index ca5b1db9fbc..4b1928c3a73 100644
--- a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx
+++ b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx
@@ -93,7 +93,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent void): Promise => {
if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
- await makeRequest({
+ makeRequest({
type: 'm.login.password',
identifier: {
type: 'm.id.user',
@@ -106,7 +106,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
const action = getKeyBindingsManager().getAccessibilityAction(e);
diff --git a/src/components/views/elements/AccessibleTooltipButton.tsx b/src/components/views/elements/AccessibleTooltipButton.tsx
index f0be8ba12d8..0f52879cc8d 100644
--- a/src/components/views/elements/AccessibleTooltipButton.tsx
+++ b/src/components/views/elements/AccessibleTooltipButton.tsx
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { SyntheticEvent } from 'react';
+import React, { SyntheticEvent, FocusEvent } from 'react';
import AccessibleButton from "./AccessibleButton";
import Tooltip, { Alignment } from './Tooltip';
@@ -26,8 +26,8 @@ interface IProps extends React.ComponentProps {
label?: string;
tooltipClassName?: string;
forceHide?: boolean;
- yOffset?: number;
alignment?: Alignment;
+ onHover?: (hovering: boolean) => void;
onHideTooltip?(ev: SyntheticEvent): void;
}
@@ -52,6 +52,7 @@ export default class AccessibleTooltipButton extends React.PureComponent {
+ if (this.props.onHover) this.props.onHover(true);
if (this.props.forceHide) return;
this.setState({
hover: true,
@@ -59,21 +60,27 @@ export default class AccessibleTooltipButton extends React.PureComponent {
+ if (this.props.onHover) this.props.onHover(false);
this.setState({
hover: false,
});
this.props.onHideTooltip?.(ev);
};
+ private onFocus = (ev: FocusEvent) => {
+ // We only show the tooltip if focus arrived here from some other
+ // element, to avoid leaving tooltips hanging around when a modal closes
+ if (ev.relatedTarget) this.showTooltip();
+ };
+
render() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
- const { title, tooltip, children, tooltipClassName, forceHide, yOffset, alignment, onHideTooltip,
+ const { title, tooltip, children, tooltipClassName, forceHide, alignment, onHideTooltip,
...props } = this.props;
const tip = this.state.hover && ;
return (
@@ -81,7 +88,7 @@ export default class AccessibleTooltipButton extends React.PureComponent
diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx
index 1eba26a3d4e..f202a1e5705 100644
--- a/src/components/views/elements/AppTile.tsx
+++ b/src/components/views/elements/AppTile.tsx
@@ -57,7 +57,7 @@ interface IProps {
// which bypasses permission prompts as it was added explicitly by that user
room?: Room;
threadId?: string | null;
- // Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
+ // Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer container.
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
fullWidth?: boolean;
// Optional. If set, renders a smaller view of the widget
@@ -288,7 +288,7 @@ export default class AppTile extends React.Component {
private setupSgListeners() {
this.sgWidget.on("preparing", this.onWidgetPreparing);
this.sgWidget.on("ready", this.onWidgetReady);
- // emits when the capabilites have been setup or changed
+ // emits when the capabilities have been set up or changed
this.sgWidget.on("capabilitiesNotified", this.onWidgetCapabilitiesNotified);
}
@@ -543,7 +543,7 @@ export default class AppTile extends React.Component {
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox " +
"allow-same-origin allow-scripts allow-presentation allow-downloads";
- // Additional iframe feature pemissions
+ // Additional iframe feature permissions
// (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/)
const iframeFeatures = "microphone; camera; encrypted-media; autoplay; display-capture; clipboard-write;";
diff --git a/src/components/views/elements/AppWarning.tsx b/src/components/views/elements/AppWarning.tsx
index 352c5990680..b3dfae99118 100644
--- a/src/components/views/elements/AppWarning.tsx
+++ b/src/components/views/elements/AppWarning.tsx
@@ -1,3 +1,19 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
import React from 'react';
interface IProps {
diff --git a/src/components/views/elements/DialogButtons.tsx b/src/components/views/elements/DialogButtons.tsx
index 2a3554e8763..bf018e14f49 100644
--- a/src/components/views/elements/DialogButtons.tsx
+++ b/src/components/views/elements/DialogButtons.tsx
@@ -100,17 +100,19 @@ export default class DialogButtons extends React.Component {
return (
{ additive }
- { cancelButton }
- { this.props.children }
-
- { this.props.primaryButton }
-
+
+ { cancelButton }
+ { this.props.children }
+
+ { this.props.primaryButton }
+
+
);
}
diff --git a/src/components/views/elements/ErrorBoundary.tsx b/src/components/views/elements/ErrorBoundary.tsx
index 89e351bb797..825ff604c7f 100644
--- a/src/components/views/elements/ErrorBoundary.tsx
+++ b/src/components/views/elements/ErrorBoundary.tsx
@@ -53,7 +53,7 @@ export default class ErrorBoundary extends React.PureComponent<{}, IState> {
// in their own `console.error` invocation.
logger.error(error);
logger.error(
- "The above error occured while React was rendering the following components:",
+ "The above error occurred while React was rendering the following components:",
componentStack,
);
}
diff --git a/src/components/views/elements/FacePile.tsx b/src/components/views/elements/FacePile.tsx
index 566eddbe07e..92a175a1cb4 100644
--- a/src/components/views/elements/FacePile.tsx
+++ b/src/components/views/elements/FacePile.tsx
@@ -31,10 +31,16 @@ interface IProps extends HTMLAttributes {
const FacePile: FC = ({ members, faceSize, overflow, tooltip, children, ...props }) => {
const faces = members.map(
- tooltip ?
- m => :
- m =>
-
+ tooltip
+ ? m =>
+ : m =>
+
,
);
@@ -45,7 +51,7 @@ const FacePile: FC = ({ members, faceSize, overflow, tooltip, children,
return
{ tooltip ? (
-
+
{ pileContents }
) : (
diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx
index 3942a9eb1d4..8e4b896efd6 100644
--- a/src/components/views/elements/Field.tsx
+++ b/src/components/views/elements/Field.tsx
@@ -19,7 +19,6 @@ import classNames from 'classnames';
import { debounce } from "lodash";
import { IFieldState, IValidationResult } from "./Validation";
-import { ComponentClass } from "../../../@types/common";
import Tooltip from "./Tooltip";
// Invoke validation from user input (when typing, etc.) at most once every N ms.
@@ -83,7 +82,6 @@ export interface IInputProps extends IProps, InputHTMLAttributes;
// The element to create. Defaults to "input".
element?: "input";
- componentClass?: undefined;
// The input's value. This is a controlled component, so the value is required.
value: string;
}
@@ -93,7 +91,6 @@ interface ISelectProps extends IProps, SelectHTMLAttributes {
inputRef?: RefObject;
// To define options for a select, use
element: "select";
- componentClass?: undefined;
// The select's value. This is a controlled component, so the value is required.
value: string;
}
@@ -102,7 +99,6 @@ interface ITextareaProps extends IProps, TextareaHTMLAttributes;
element: "textarea";
- componentClass?: undefined;
// The textarea's value. This is a controlled component, so the value is required.
value: string;
}
@@ -111,8 +107,6 @@ export interface INativeOnChangeInputProps extends IProps, InputHTMLAttributes;
element: "input";
- // The custom component to render
- componentClass: ComponentClass;
// The input's value. This is a controlled component, so the value is required.
value: string;
}
@@ -248,7 +242,7 @@ export default class Field extends React.PureComponent {
public render() {
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
- const { element, componentClass, inputRef, prefixComponent, postfixComponent, className, onValidate, children,
+ const { element, inputRef, prefixComponent, postfixComponent, className, onValidate, children,
tooltipContent, forceValidity, tooltipClassName, list, validateOnBlur, validateOnChange, validateOnFocus,
usePlaceholderAsHint, forceTooltipVisible,
...inputProps } = this.props;
@@ -265,7 +259,7 @@ export default class Field extends React.PureComponent {
// Appease typescript's inference
const inputProps_ = { ...inputProps, ref: this.inputRef, list };
- const fieldInput = React.createElement(this.props.componentClass || this.props.element, inputProps_, children);
+ const fieldInput = React.createElement(this.props.element, inputProps_, children);
let prefixContainer = null;
if (prefixComponent) {
diff --git a/src/components/views/elements/IRCTimelineProfileResizer.tsx b/src/components/views/elements/IRCTimelineProfileResizer.tsx
index c0e37d93d74..61fee35bdd7 100644
--- a/src/components/views/elements/IRCTimelineProfileResizer.tsx
+++ b/src/components/views/elements/IRCTimelineProfileResizer.tsx
@@ -44,7 +44,7 @@ export default class IRCTimelineProfileResizer extends React.Component this.updateCSSWidth(this.state.width));
}
diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx
index fd24bc745a9..9bb1b551d59 100644
--- a/src/components/views/elements/ImageView.tsx
+++ b/src/components/views/elements/ImageView.tsx
@@ -38,6 +38,7 @@ import UIStore from '../../../stores/UIStore';
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
+import { presentableTextForFile } from "../../../utils/FileUtils";
// Max scale to keep gaps around the image
const MAX_SCALE = 0.95;
@@ -534,6 +535,15 @@ export default class ImageView extends React.Component {
);
}
+ let title: JSX.Element;
+ if (this.props.mxEvent?.getContent()) {
+ title = (
+
+ { presentableTextForFile(this.props.mxEvent?.getContent(), _t("Image"), true) }
+
+ );
+ }
+
return (
{
>
{ info }
+ { title }
{ zoomOutButton }
{ zoomInButton }
diff --git a/src/components/views/elements/InteractiveTooltip.tsx b/src/components/views/elements/InteractiveTooltip.tsx
index 62d0c43d06a..ca8ae4c8fd5 100644
--- a/src/components/views/elements/InteractiveTooltip.tsx
+++ b/src/components/views/elements/InteractiveTooltip.tsx
@@ -352,10 +352,10 @@ export default class InteractiveTooltip extends React.Component
const targetRect = this.target.getBoundingClientRect();
if (this.props.direction === Direction.Left) {
- const targetLeft = targetRect.left + window.pageXOffset;
+ const targetLeft = targetRect.left + window.scrollX;
return !contentRect || (targetLeft - contentRect.width > MIN_SAFE_DISTANCE_TO_WINDOW_EDGE);
} else {
- const targetRight = targetRect.right + window.pageXOffset;
+ const targetRight = targetRect.right + window.scrollX;
const spaceOnRight = UIStore.instance.windowWidth - targetRight;
return contentRect && (spaceOnRight - contentRect.width < MIN_SAFE_DISTANCE_TO_WINDOW_EDGE);
}
@@ -366,10 +366,10 @@ export default class InteractiveTooltip extends React.Component
const targetRect = this.target.getBoundingClientRect();
if (this.props.direction === Direction.Top) {
- const targetTop = targetRect.top + window.pageYOffset;
+ const targetTop = targetRect.top + window.scrollY;
return !contentRect || (targetTop - contentRect.height > MIN_SAFE_DISTANCE_TO_WINDOW_EDGE);
} else {
- const targetBottom = targetRect.bottom + window.pageYOffset;
+ const targetBottom = targetRect.bottom + window.scrollY;
const spaceBelow = UIStore.instance.windowHeight - targetBottom;
return contentRect && (spaceBelow - contentRect.height < MIN_SAFE_DISTANCE_TO_WINDOW_EDGE);
}
@@ -429,10 +429,10 @@ export default class InteractiveTooltip extends React.Component
const targetRect = this.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page
- const targetLeft = targetRect.left + window.pageXOffset;
- const targetRight = targetRect.right + window.pageXOffset;
- const targetBottom = targetRect.bottom + window.pageYOffset;
- const targetTop = targetRect.top + window.pageYOffset;
+ const targetLeft = targetRect.left + window.scrollX;
+ const targetRight = targetRect.right + window.scrollX;
+ const targetBottom = targetRect.bottom + window.scrollY;
+ const targetTop = targetRect.top + window.scrollY;
// Place the tooltip above the target by default. If we find that the
// tooltip content would extend past the safe area towards the window
diff --git a/src/components/views/elements/LanguageDropdown.tsx b/src/components/views/elements/LanguageDropdown.tsx
index 7d19dbfce18..cf1dfedcce7 100644
--- a/src/components/views/elements/LanguageDropdown.tsx
+++ b/src/components/views/elements/LanguageDropdown.tsx
@@ -99,7 +99,7 @@ export default class LanguageDropdown extends React.Component {
});
// default value here too, otherwise we need to handle null / undefined
- // values between mounting and the initial value propgating
+ // values between mounting and the initial value propagating
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
let value = null;
if (language) {
diff --git a/src/components/views/elements/Linkify.tsx b/src/components/views/elements/Linkify.tsx
new file mode 100644
index 00000000000..4d75fb79218
--- /dev/null
+++ b/src/components/views/elements/Linkify.tsx
@@ -0,0 +1,44 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, { useLayoutEffect, useRef } from "react";
+
+import { linkifyElement } from "../../../HtmlUtils";
+
+interface Props {
+ as?: string;
+ children: React.ReactNode;
+ onClick?: (ev: MouseEvent) => void;
+}
+
+export function Linkify({
+ as = "div",
+ children,
+ onClick,
+}: Props): JSX.Element {
+ const ref = useRef();
+
+ useLayoutEffect(() => {
+ linkifyElement(ref.current);
+ }, [children]);
+
+ return React.createElement(as, {
+ children,
+ ref,
+ onClick,
+ });
+}
+
diff --git a/src/components/views/elements/MiniAvatarUploader.tsx b/src/components/views/elements/MiniAvatarUploader.tsx
index 43e66db09c1..c501c92ac9f 100644
--- a/src/components/views/elements/MiniAvatarUploader.tsx
+++ b/src/components/views/elements/MiniAvatarUploader.tsx
@@ -14,18 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { useContext, useRef, useState } from 'react';
-import { EventType } from 'matrix-js-sdk/src/@types/event';
import classNames from 'classnames';
+import { EventType } from 'matrix-js-sdk/src/@types/event';
+import React, { useContext, useRef, useState, MouseEvent } from 'react';
-import AccessibleButton from "./AccessibleButton";
-import Spinner from "./Spinner";
+import Analytics from "../../../Analytics";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import RoomContext from "../../../contexts/RoomContext";
import { useTimeout } from "../../../hooks/useTimeout";
-import Analytics from "../../../Analytics";
import { TranslatedString } from '../../../languageHandler';
-import RoomContext from "../../../contexts/RoomContext";
import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds";
+import AccessibleButton from "./AccessibleButton";
+import Spinner from "./Spinner";
export const AVATAR_SIZE = 52;
@@ -34,9 +34,13 @@ interface IProps {
noAvatarLabel?: TranslatedString;
hasAvatarLabel?: TranslatedString;
setAvatarUrl(url: string): Promise;
+ isUserAvatar?: boolean;
+ onClick?(ev: MouseEvent): void;
}
-const MiniAvatarUploader: React.FC = ({ hasAvatar, hasAvatarLabel, noAvatarLabel, setAvatarUrl, children }) => {
+const MiniAvatarUploader: React.FC = ({
+ hasAvatar, hasAvatarLabel, noAvatarLabel, setAvatarUrl, isUserAvatar, children, onClick,
+}) => {
const cli = useContext(MatrixClientContext);
const [busy, setBusy] = useState(false);
const [hover, setHover] = useState(false);
@@ -54,7 +58,7 @@ const MiniAvatarUploader: React.FC = ({ hasAvatar, hasAvatarLabel, noAva
const label = (hasAvatar || busy) ? hasAvatarLabel : noAvatarLabel;
const { room } = useContext(RoomContext);
- const canSetAvatar = room?.currentState.maySendStateEvent(EventType.RoomAvatar, cli.getUserId());
+ const canSetAvatar = isUserAvatar || room?.currentState?.maySendStateEvent(EventType.RoomAvatar, cli.getUserId());
if (!canSetAvatar) return { children } ;
const visible = !!label && (hover || show);
@@ -63,7 +67,10 @@ const MiniAvatarUploader: React.FC = ({ hasAvatar, hasAvatarLabel, noAva
type="file"
ref={uploadRef}
className="mx_MiniAvatarUploader_input"
- onClick={chromeFileInputFix}
+ onClick={(ev) => {
+ chromeFileInputFix(ev);
+ onClick?.(ev);
+ }}
onChange={async (ev) => {
if (!ev.target.files?.length) return;
setBusy(true);
diff --git a/src/components/views/elements/NativeOnChangeInput.tsx b/src/components/views/elements/NativeOnChangeInput.tsx
deleted file mode 100644
index 0937fd8de9f..00000000000
--- a/src/components/views/elements/NativeOnChangeInput.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import React from 'react';
-
-import { useCombinedRefs } from "../../../hooks/useCombinedRefs";
-
-interface IProps extends Omit, 'onChange' | 'onInput'> {
- onChange?: (event: Event) => void;
- onInput?: (event: Event) => void;
-}
-
-/**
-* This component restores the native 'onChange' and 'onInput' behavior of
-* JavaScript which have important differences for certain types. This is
-* necessary because in React, the `onChange` handler behaves like the native
-* `oninput` handler and there is no way to tell the difference between an
-* `input` vs `change` event.
-*
-* via https://stackoverflow.com/a/62383569/796832 and
-* /~https://github.com/facebook/react/issues/9657#issuecomment-643970199
-*
-* See:
-* - https://reactjs.org/docs/dom-elements.html#onchange
-* - /~https://github.com/facebook/react/issues/3964
-* - /~https://github.com/facebook/react/issues/9657
-* - /~https://github.com/facebook/react/issues/14857
-*
-* Examples:
-*
-* We use this for the date picker so we can distinguish from
-* a final date picker selection (onChange) vs navigating the months in the date
-* picker (onInput).
-*
-* This is also potentially useful for because the native
-* events behave in such a way that moving the slider around triggers an onInput
-* event and releasing it triggers onChange.
-*/
-const NativeOnChangeInput: React.FC = React.forwardRef((props: IProps, ref) => {
- const registerCallbacks = (input: HTMLInputElement | null) => {
- if (input) {
- input.onchange = props.onChange;
- input.oninput = props.onInput;
- }
- };
-
- return {}}
- onInput={() => {}}
- />;
-});
-
-export default NativeOnChangeInput;
diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.tsx
similarity index 71%
rename from src/components/views/elements/Pill.js
rename to src/components/views/elements/Pill.tsx
index 7d5a9973c7f..f344f894569 100644
--- a/src/components/views/elements/Pill.js
+++ b/src/components/views/elements/Pill.tsx
@@ -13,67 +13,82 @@ 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 classNames from 'classnames';
import { Room } from 'matrix-js-sdk/src/models/room';
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
-import PropTypes from 'prop-types';
import { logger } from "matrix-js-sdk/src/logger";
+import { MatrixClient } from 'matrix-js-sdk/src/client';
import dis from '../../../dispatcher/dispatcher';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { getPrimaryPermalinkEntity, parsePermalink } from "../../../utils/permalinks/Permalinks";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { Action } from "../../../dispatcher/actions";
-import Tooltip from './Tooltip';
-import RoomAvatar from "../avatars/RoomAvatar";
-import MemberAvatar from "../avatars/MemberAvatar";
+import Tooltip, { Alignment } from './Tooltip';
+import RoomAvatar from '../avatars/RoomAvatar';
+import MemberAvatar from '../avatars/MemberAvatar';
+
+export enum PillType {
+ UserMention = 'TYPE_USER_MENTION',
+ RoomMention = 'TYPE_ROOM_MENTION',
+ AtRoomMention = 'TYPE_AT_ROOM_MENTION', // '@room' mention
+}
+
+interface IProps {
+ // The Type of this Pill. If url is given, this is auto-detected.
+ type?: PillType;
+ // The URL to pillify (no validation is done)
+ url?: string;
+ // Whether the pill is in a message
+ inMessage?: boolean;
+ // The room in which this pill is being rendered
+ room?: Room;
+ // Whether to include an avatar in the pill
+ shouldShowPillAvatar?: boolean;
+}
+
+interface IState {
+ // ID/alias of the room/user
+ resourceId: string;
+ // Type of pill
+ pillType: string;
+ // The member related to the user pill
+ member?: RoomMember;
+ // The room related to the room pill
+ room?: Room;
+ // Is the user hovering the pill
+ hover: boolean;
+}
-class Pill extends React.Component {
- static roomNotifPos(text) {
+export default class Pill extends React.Component {
+ private unmounted = true;
+ private matrixClient: MatrixClient;
+
+ public static roomNotifPos(text: string): number {
return text.indexOf("@room");
}
- static roomNotifLen() {
+ public static roomNotifLen(): number {
return "@room".length;
}
- static TYPE_USER_MENTION = 'TYPE_USER_MENTION';
- static TYPE_ROOM_MENTION = 'TYPE_ROOM_MENTION';
- static TYPE_AT_ROOM_MENTION = 'TYPE_AT_ROOM_MENTION'; // '@room' mention
-
- static propTypes = {
- // The Type of this Pill. If url is given, this is auto-detected.
- type: PropTypes.string,
- // The URL to pillify (no validation is done)
- url: PropTypes.string,
- // Whether the pill is in a message
- inMessage: PropTypes.bool,
- // The room in which this pill is being rendered
- room: PropTypes.instanceOf(Room),
- // Whether to include an avatar in the pill
- shouldShowPillAvatar: PropTypes.bool,
- // Whether to render this pill as if it were highlit by a selection
- isSelected: PropTypes.bool,
- };
-
- state = {
- // ID/alias of the room/user
- resourceId: null,
- // Type of pill
- pillType: null,
+ constructor(props: IProps) {
+ super(props);
- // The member related to the user pill
- member: null,
- // The room related to the room pill
- room: null,
- // Is the user hovering the pill
- hover: false,
- };
+ this.state = {
+ resourceId: null,
+ pillType: null,
+ member: null,
+ room: null,
+ hover: false,
+ };
+ }
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
- // eslint-disable-next-line camelcase
- async UNSAFE_componentWillReceiveProps(nextProps) {
+ // eslint-disable-next-line camelcase, @typescript-eslint/naming-convention
+ public async UNSAFE_componentWillReceiveProps(nextProps: IProps): Promise {
let resourceId;
let prefix;
@@ -89,28 +104,28 @@ class Pill extends React.Component {
}
const pillType = this.props.type || {
- '@': Pill.TYPE_USER_MENTION,
- '#': Pill.TYPE_ROOM_MENTION,
- '!': Pill.TYPE_ROOM_MENTION,
+ '@': PillType.UserMention,
+ '#': PillType.RoomMention,
+ '!': PillType.RoomMention,
}[prefix];
let member;
let room;
switch (pillType) {
- case Pill.TYPE_AT_ROOM_MENTION: {
+ case PillType.AtRoomMention: {
room = nextProps.room;
}
break;
- case Pill.TYPE_USER_MENTION: {
+ case PillType.UserMention: {
const localMember = nextProps.room ? nextProps.room.getMember(resourceId) : undefined;
member = localMember;
if (!localMember) {
member = new RoomMember(null, resourceId);
this.doProfileLookup(resourceId, member);
}
- break;
}
- case Pill.TYPE_ROOM_MENTION: {
+ break;
+ case PillType.RoomMention: {
const localRoom = resourceId[0] === '#' ?
MatrixClientPeg.get().getRooms().find((r) => {
return r.getCanonicalAlias() === resourceId ||
@@ -122,39 +137,39 @@ class Pill extends React.Component {
// a room avatar and name.
// this.doRoomProfileLookup(resourceId, member);
}
- break;
}
+ break;
}
this.setState({ resourceId, pillType, member, room });
}
- componentDidMount() {
- this._unmounted = false;
- this._matrixClient = MatrixClientPeg.get();
+ public componentDidMount(): void {
+ this.unmounted = false;
+ this.matrixClient = MatrixClientPeg.get();
// eslint-disable-next-line new-cap
this.UNSAFE_componentWillReceiveProps(this.props); // HACK: We shouldn't be calling lifecycle functions ourselves.
}
- componentWillUnmount() {
- this._unmounted = true;
+ public componentWillUnmount(): void {
+ this.unmounted = true;
}
- onMouseOver = () => {
+ private onMouseOver = (): void => {
this.setState({
hover: true,
});
};
- onMouseLeave = () => {
+ private onMouseLeave = (): void => {
this.setState({
hover: false,
});
};
- doProfileLookup(userId, member) {
+ private doProfileLookup(userId: string, member): void {
MatrixClientPeg.get().getProfileInfo(userId).then((resp) => {
- if (this._unmounted) {
+ if (this.unmounted) {
return;
}
member.name = resp.displayname;
@@ -173,7 +188,7 @@ class Pill extends React.Component {
});
}
- onUserPillClicked = (e) => {
+ private onUserPillClicked = (e): void => {
e.preventDefault();
dis.dispatch({
action: Action.ViewUser,
@@ -181,7 +196,7 @@ class Pill extends React.Component {
});
};
- render() {
+ public render(): JSX.Element {
const resource = this.state.resourceId;
let avatar = null;
@@ -191,7 +206,7 @@ class Pill extends React.Component {
let href = this.props.url;
let onClick;
switch (this.state.pillType) {
- case Pill.TYPE_AT_ROOM_MENTION: {
+ case PillType.AtRoomMention: {
const room = this.props.room;
if (room) {
linkText = "@room";
@@ -200,9 +215,9 @@ class Pill extends React.Component {
}
pillClass = 'mx_AtRoomPill';
}
- break;
}
- case Pill.TYPE_USER_MENTION: {
+ break;
+ case PillType.UserMention: {
// If this user is not a member of this room, default to the empty member
const member = this.state.member;
if (member) {
@@ -210,15 +225,15 @@ class Pill extends React.Component {
member.rawDisplayName = member.rawDisplayName || '';
linkText = member.rawDisplayName;
if (this.props.shouldShowPillAvatar) {
- avatar = ;
+ avatar = ;
}
pillClass = 'mx_UserPill';
href = null;
onClick = this.onUserPillClicked;
}
- break;
}
- case Pill.TYPE_ROOM_MENTION: {
+ break;
+ case PillType.RoomMention: {
const room = this.state.room;
if (room) {
linkText = room.name || resource;
@@ -226,31 +241,27 @@ class Pill extends React.Component {
avatar = ;
}
}
- pillClass = 'mx_RoomPill';
- break;
+ pillClass = room?.isSpaceRoom() ? "mx_SpacePill" : "mx_RoomPill";
}
+ break;
}
const classes = classNames("mx_Pill", pillClass, {
"mx_UserPill_me": userId === MatrixClientPeg.get().getUserId(),
- "mx_UserPill_selected": this.props.isSelected,
});
if (this.state.pillType) {
- const { yOffset } = this.props;
-
let tip;
if (this.state.hover && resource) {
- tip = ;
+ tip = ;
}
- return
+ return
{ this.props.inMessage ?
@@ -260,7 +271,6 @@ class Pill extends React.Component {
:
@@ -275,5 +285,3 @@ class Pill extends React.Component {
}
}
}
-
-export default Pill;
diff --git a/src/components/views/elements/ReplyChain.tsx b/src/components/views/elements/ReplyChain.tsx
index 3897e75d6d6..f6d32dc1a3d 100644
--- a/src/components/views/elements/ReplyChain.tsx
+++ b/src/components/views/elements/ReplyChain.tsx
@@ -31,8 +31,8 @@ import { getUserNameColorClass } from "../../../utils/FormattingUtils";
import { Action } from "../../../dispatcher/actions";
import Spinner from './Spinner';
import ReplyTile from "../rooms/ReplyTile";
-import Pill from './Pill';
import { UserNameColorMode } from '../../../settings/enums/UserNameColorMode';
+import Pill, { PillType } from './Pill';
import { ButtonEvent } from './AccessibleButton';
import { getParentEventId, shouldDisplayReply } from '../../../utils/Reply';
import RoomContext from "../../../contexts/RoomContext";
@@ -170,7 +170,7 @@ export default class ReplyChain extends React.Component {
await this.matrixClient.getEventTimeline(this.room.getUnfilteredTimelineSet(), eventId);
} catch (e) {
// if it fails catch the error and return early, there's no point trying to find the event in this case.
- // Return null as it is falsey and thus should be treated as an error (as the event cannot be resolved).
+ // Return null as it is falsy and thus should be treated as an error (as the event cannot be resolved).
return null;
}
return this.room.findEventById(eventId);
@@ -228,7 +228,7 @@ export default class ReplyChain extends React.Component {
),
'pill': (
= (
// reverse members in tooltip order to make the order between the two match up.
const commaSeparatedMembers = shownMembers.map(m => m.name).reverse().join(", ");
- let tooltip: ReactNode;
- if (props.onClick) {
- let subText: string;
- if (isJoined) {
- subText = _t("Including you, %(commaSeparatedMembers)s", { commaSeparatedMembers });
- } else {
- subText = _t("Including %(commaSeparatedMembers)s", { commaSeparatedMembers });
- }
-
- tooltip =
-
- { _t("View all %(count)s members", { count }) }
-
-
- { subText }
-
-
;
- } else {
- if (isJoined) {
- tooltip = _t("%(count)s members including you, %(commaSeparatedMembers)s", {
- count: count - 1,
- commaSeparatedMembers,
- });
- } else {
- tooltip = _t("%(count)s members including %(commaSeparatedMembers)s", {
- count,
- commaSeparatedMembers,
- });
- }
- }
+ const tooltip =
+
+ { props.onClick
+ ? _t("View all %(count)s members", { count })
+ : _t("%(count)s members", { count }) }
+
+
+ { isJoined
+ ? _t("Including you, %(commaSeparatedMembers)s", { commaSeparatedMembers })
+ : _t("Including %(commaSeparatedMembers)s", { commaSeparatedMembers }) }
+
+
;
return {
room?: Room;
- children?(topic: string, ref: (element: HTMLElement) => void): JSX.Element;
}
-export const getTopic = room => room?.currentState?.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic;
+export default function RoomTopic({
+ room,
+ ...props
+}: IProps) {
+ const client = useContext(MatrixClientContext);
+ const ref = useRef();
+
+ const topic = useTopic(room);
+
+ const onClick = useCallback((e: React.MouseEvent) => {
+ props.onClick?.(e);
+ const target = e.target as HTMLElement;
+ if (target.tagName.toUpperCase() === "A") {
+ return;
+ }
-const RoomTopic = ({ room, children }: IProps): JSX.Element => {
- const [topic, setTopic] = useState(getTopic(room));
- useTypedEventEmitter(room.currentState, RoomStateEvent.Events, (ev: MatrixEvent) => {
- if (ev.getType() !== EventType.RoomTopic) return;
- setTopic(getTopic(room));
+ dis.fire(Action.ShowRoomTopic);
+ }, [props]);
+
+ const ignoreHover = (ev: React.MouseEvent): boolean => {
+ return (ev.target as HTMLElement).tagName.toUpperCase() === "A";
+ };
+
+ useDispatcher(dis, (payload) => {
+ if (payload.action === Action.ShowRoomTopic) {
+ const canSetTopic = room.currentState.maySendStateEvent(EventType.RoomTopic, client.getUserId());
+
+ const modal = Modal.createDialog(InfoDialog, {
+ title: room.name,
+ description:
+
{
+ if ((ev.target as HTMLElement).tagName.toUpperCase() === "A") {
+ modal.close();
+ }
+ }}
+ >
+ { topic }
+
+ { canSetTopic &&
{
+ modal.close();
+ dis.dispatch({ action: "open_room_settings" });
+ }}>
+ { _t("Edit topic") }
+ }
+
,
+ hasCloseButton: true,
+ button: false,
+ });
+ }
});
- useEffect(() => {
- setTopic(getTopic(room));
- }, [room]);
- const ref = e => e && linkifyElement(e);
- if (children) return children(topic, ref);
- return { topic } ;
-};
+ const className = classNames(props.className, "mx_RoomTopic");
-export default RoomTopic;
+ return
+
+
+ { topic }
+
+
+
;
+}
diff --git a/src/components/views/elements/SSOButtons.tsx b/src/components/views/elements/SSOButtons.tsx
index b11e74e1e27..16f860792bb 100644
--- a/src/components/views/elements/SSOButtons.tsx
+++ b/src/components/views/elements/SSOButtons.tsx
@@ -29,7 +29,7 @@ import { mediaFromMxc } from "../../../customisations/Media";
import { PosthogAnalytics } from "../../../PosthogAnalytics";
interface ISSOButtonProps extends Omit {
- idp: IIdentityProvider;
+ idp?: IIdentityProvider;
mini?: boolean;
}
@@ -84,7 +84,7 @@ const SSOButton: React.FC = ({
const label = idp ? _t("Continue with %(provider)s", { provider: idp.name }) : _t("Sign in with single sign-on");
const onClick = () => {
- const authenticationType = getAuthenticationType(idp.brand);
+ const authenticationType = getAuthenticationType(idp?.brand ?? "");
PosthogAnalytics.instance.setAuthenticationType(authenticationType);
PlatformPeg.get().startSingleSignOn(matrixClient, loginType, fragmentAfterLogin, idp?.id);
};
diff --git a/src/components/views/elements/DesktopBuildsNotice.tsx b/src/components/views/elements/SearchWarning.tsx
similarity index 74%
rename from src/components/views/elements/DesktopBuildsNotice.tsx
rename to src/components/views/elements/SearchWarning.tsx
index cb664f02d01..ef2486b0b74 100644
--- a/src/components/views/elements/DesktopBuildsNotice.tsx
+++ b/src/components/views/elements/SearchWarning.tsx
@@ -35,28 +35,30 @@ interface IProps {
kind: WarningKind;
}
-export default function DesktopBuildsNotice({ isRoomEncrypted, kind }: IProps) {
+export default function SearchWarning({ isRoomEncrypted, kind }: IProps) {
if (!isRoomEncrypted) return null;
if (EventIndexPeg.get()) return null;
if (EventIndexPeg.error) {
- 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 }
- ),
- }) }
- >;
+ 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 brand = SdkConfig.get("brand");
@@ -97,7 +99,7 @@ export default function DesktopBuildsNotice({ isRoomEncrypted, kind }: IProps) {
}
return (
-
+
{ logo }
{ text }
diff --git a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
index 126898a4ff1..a269ac00106 100644
--- a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
+++ b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx
@@ -99,7 +99,7 @@ export default class SpellCheckLanguagesDropdown extends React.Component
{
public static readonly defaultProps = {
visible: true,
- yOffset: 0,
alignment: Alignment.Natural,
};
@@ -102,50 +97,49 @@ export default class Tooltip extends React.Component {
// positioned, also taking into account any window zoom
private updatePosition(style: CSSProperties) {
const parentBox = this.parent.getBoundingClientRect();
- let offset = 0;
- if (parentBox.height > MIN_TOOLTIP_HEIGHT) {
- offset = Math.floor((parentBox.height - MIN_TOOLTIP_HEIGHT) / 2);
- } else {
- // The tooltip is larger than the parent height: figure out what offset
- // we need so that we're still centered.
- offset = Math.floor(parentBox.height - MIN_TOOLTIP_HEIGHT);
- }
const width = UIStore.instance.windowWidth;
+ const spacing = 6;
const parentWidth = (
this.props.maxParentWidth
? Math.min(parentBox.width, this.props.maxParentWidth)
: parentBox.width
);
- const baseTop = (parentBox.top - 2 + this.props.yOffset) + window.pageYOffset;
- const top = baseTop + offset;
- const right = width - parentBox.left - window.pageXOffset;
- const left = parentBox.right + window.pageXOffset;
+ const baseTop = parentBox.top + window.scrollY;
+ const centerTop = parentBox.top + window.scrollY + (parentBox.height / 2);
+ const right = width - parentBox.left - window.scrollX;
+ const left = parentBox.right + window.scrollX;
const horizontalCenter = (
- parentBox.left - window.pageXOffset + (parentWidth / 2)
+ parentBox.left - window.scrollX + (parentWidth / 2)
);
+
switch (this.props.alignment) {
case Alignment.Natural:
if (parentBox.right > width / 2) {
- style.right = right;
- style.top = top;
+ style.right = right + spacing;
+ style.top = centerTop;
+ style.transform = "translateY(-50%)";
break;
}
// fall through to Right
case Alignment.Right:
- style.left = left;
- style.top = top;
+ style.left = left + spacing;
+ style.top = centerTop;
+ style.transform = "translateY(-50%)";
break;
case Alignment.Left:
- style.right = right;
- style.top = top;
+ style.right = right + spacing;
+ style.top = centerTop;
+ style.transform = "translateY(-50%)";
break;
case Alignment.Top:
- style.top = baseTop - 16;
+ style.top = baseTop - spacing;
style.left = horizontalCenter;
+ style.transform = "translate(-50%, -100%)";
break;
case Alignment.Bottom:
- style.top = baseTop + parentBox.height;
+ style.top = baseTop + parentBox.height + spacing;
style.left = horizontalCenter;
+ style.transform = "translate(-50%)";
break;
case Alignment.InnerBottom:
style.top = baseTop + parentBox.height - 50;
@@ -153,14 +147,10 @@ export default class Tooltip extends React.Component {
style.transform = "translate(-50%)";
break;
case Alignment.TopRight:
- style.top = baseTop - 5;
- style.right = width - parentBox.right - window.pageXOffset;
- style.transform = "translate(5px, -100%)";
+ style.top = baseTop - spacing;
+ style.right = width - parentBox.right - window.scrollX;
+ style.transform = "translateY(-100%)";
break;
- case Alignment.TopCenter:
- style.top = baseTop - 5;
- style.left = horizontalCenter;
- style.transform = "translate(-50%, -100%)";
}
return style;
diff --git a/src/components/views/elements/TooltipTarget.tsx b/src/components/views/elements/TooltipTarget.tsx
index 5d30bf25396..69b4e98568b 100644
--- a/src/components/views/elements/TooltipTarget.tsx
+++ b/src/components/views/elements/TooltipTarget.tsx
@@ -14,12 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { useState, HTMLAttributes } from 'react';
+import React, { HTMLAttributes } from 'react';
+import useFocus from "../../../hooks/useFocus";
+import useHover from "../../../hooks/useHover";
import Tooltip, { ITooltipProps } from './Tooltip';
interface IProps extends HTMLAttributes, Omit {
tooltipTargetClassName?: string;
+ ignoreHover?: (ev: React.MouseEvent) => boolean;
}
/**
@@ -34,38 +37,33 @@ const TooltipTarget: React.FC = ({
id,
label,
alignment,
- yOffset,
tooltipClassName,
maxParentWidth,
+ ignoreHover,
...rest
}) => {
- const [isVisible, setIsVisible] = useState(false);
-
- const show = () => setIsVisible(true);
- const hide = () => setIsVisible(false);
+ const [isFocused, focusProps] = useFocus();
+ const [isHovering, hoverProps] = useHover(ignoreHover || (() => false));
// No need to fill up the DOM with hidden tooltip elements. Only add the
// tooltip when we're hovering over the item (performance)
- const tooltip = isVisible && ;
return (
{ children }
diff --git a/src/components/views/elements/TruncatedList.tsx b/src/components/views/elements/TruncatedList.tsx
index 56d05515df7..57ae784caac 100644
--- a/src/components/views/elements/TruncatedList.tsx
+++ b/src/components/views/elements/TruncatedList.tsx
@@ -52,7 +52,7 @@ export default class TruncatedList extends React.Component
{
return this.props.getChildren(start, end);
} else {
// XXX: I'm not sure why anything would pass null into this, it seems
- // like a bizzare case to handle, but I'm preserving the behaviour.
+ // like a bizarre case to handle, but I'm preserving the behaviour.
// (see commit 38d5c7d5c5d5a34dc16ef5d46278315f5c57f542)
return React.Children.toArray(this.props.children).filter((c) => {
return c != null;
diff --git a/src/components/views/emojipicker/Category.tsx b/src/components/views/emojipicker/Category.tsx
index 7cd5b96bee8..d4ea4e89ffb 100644
--- a/src/components/views/emojipicker/Category.tsx
+++ b/src/components/views/emojipicker/Category.tsx
@@ -52,7 +52,7 @@ class Category extends React.PureComponent {
const { onClick, onMouseEnter, onMouseLeave, selectedEmojis, emojis } = this.props;
const emojisForRow = emojis.slice(rowIndex * 8, (rowIndex + 1) * 8);
return ({
- emojisForRow.map(emoji => ((
+ emojisForRow.map(emoji => (
{
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
/>
- )))
+ ))
}
);
};
diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx
index 2d45c01003a..7485bae3640 100644
--- a/src/components/views/emojipicker/EmojiPicker.tsx
+++ b/src/components/views/emojipicker/EmojiPicker.tsx
@@ -255,7 +255,7 @@ class EmojiPicker extends React.Component {
>
{ this.categories.map(category => {
const emojis = this.memoizedDataByCategory[category.id];
- const categoryElement = ((
+ const categoryElement = (
{
onMouseLeave={this.onHoverEmojiEnd}
selectedEmojis={this.props.selectedEmojis}
/>
- ));
+ );
const height = EmojiPicker.categoryHeightForEmojiCount(emojis.length);
heightBefore += height;
return categoryElement;
diff --git a/src/components/views/emojipicker/QuickReactions.tsx b/src/components/views/emojipicker/QuickReactions.tsx
index c0336a759de..f53b30f565d 100644
--- a/src/components/views/emojipicker/QuickReactions.tsx
+++ b/src/components/views/emojipicker/QuickReactions.tsx
@@ -72,7 +72,7 @@ class QuickReactions extends React.Component {
}
- { QUICK_REACTIONS.map(emoji => ((
+ { QUICK_REACTIONS.map(emoji => (
{
onMouseLeave={this.onMouseLeave}
selectedEmojis={this.props.selectedEmojis}
/>
- ))) }
+ )) }
);
diff --git a/src/components/views/location/LocationButton.tsx b/src/components/views/location/LocationButton.tsx
index e6a6f8d92e7..b119268588f 100644
--- a/src/components/views/location/LocationButton.tsx
+++ b/src/components/views/location/LocationButton.tsx
@@ -58,7 +58,6 @@ export const LocationButton: React.FC = ({ roomId, sender, menuPosition,
const className = classNames(
"mx_MessageComposer_button",
- "mx_MessageComposer_location",
{
"mx_MessageComposer_button_highlight": menuDisplayed,
},
@@ -67,6 +66,7 @@ export const LocationButton: React.FC = ({ roomId, sender, menuPosition,
return
diff --git a/src/components/views/location/LocationShareMenu.tsx b/src/components/views/location/LocationShareMenu.tsx
index 8be6c070db2..795a7802375 100644
--- a/src/components/views/location/LocationShareMenu.tsx
+++ b/src/components/views/location/LocationShareMenu.tsx
@@ -21,11 +21,10 @@ import { IEventRelation } from 'matrix-js-sdk/src/models/event';
import MatrixClientContext from '../../../contexts/MatrixClientContext';
import ContextMenu, { AboveLeftOf } from '../../structures/ContextMenu';
import LocationPicker, { ILocationPickerProps } from "./LocationPicker";
-import { shareLiveLocation, shareLocation } from './shareLocation';
+import { shareLiveLocation, shareLocation, LocationShareType } from './shareLocation';
import SettingsStore from '../../../settings/SettingsStore';
import ShareDialogButtons from './ShareDialogButtons';
import ShareType from './ShareType';
-import { LocationShareType } from './shareLocation';
import { OwnProfileStore } from '../../../stores/OwnProfileStore';
import { EnableLiveShare } from './EnableLiveShare';
import { useFeatureEnabled } from '../../../hooks/useSettings';
diff --git a/src/components/views/location/Marker.tsx b/src/components/views/location/Marker.tsx
index bacade71cf4..075d2d092ef 100644
--- a/src/components/views/location/Marker.tsx
+++ b/src/components/views/location/Marker.tsx
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from 'react';
+import React, { ReactNode, useState } from 'react';
import classNames from 'classnames';
import { RoomMember } from 'matrix-js-sdk/src/matrix';
@@ -28,12 +28,39 @@ interface Props {
roomMember?: RoomMember;
// use member text color as background
useMemberColor?: boolean;
+ tooltip?: ReactNode;
}
+/**
+ * Wrap with tooltip handlers when
+ * tooltip is truthy
+ */
+const OptionalTooltip: React.FC<{
+ tooltip?: ReactNode; children: ReactNode;
+}> = ({ tooltip, children }) => {
+ const [isVisible, setIsVisible] = useState(false);
+ if (!tooltip) {
+ return <>{ children }>;
+ }
+
+ const show = () => setIsVisible(true);
+ const hide = () => setIsVisible(false);
+ const toggleVisibility = (e: React.MouseEvent) => {
+ // stop map from zooming in on click
+ e.stopPropagation();
+ setIsVisible(!isVisible);
+ };
+
+ return
+ { children }
+ { isVisible && tooltip }
+
;
+};
+
/**
* Generic location marker
*/
-const Marker = React.forwardRef(({ id, roomMember, useMemberColor }, ref) => {
+const Marker = React.forwardRef(({ id, roomMember, useMemberColor, tooltip }, ref) => {
const memberColorClass = useMemberColor && roomMember ? getUserNameColorClass(roomMember.userId) : '';
return (({ id, roomMember, useMem
"mx_Marker_defaultColor": !memberColorClass,
})}
>
-
- { roomMember ?
-
- :
- }
-
+
+
+ { roomMember ?
+
+ :
+ }
+
+
;
});
diff --git a/src/components/views/location/SmartMarker.tsx b/src/components/views/location/SmartMarker.tsx
index c4a7c61e32b..13b7ef093ea 100644
--- a/src/components/views/location/SmartMarker.tsx
+++ b/src/components/views/location/SmartMarker.tsx
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { useCallback, useEffect, useState } from 'react';
+import React, { ReactNode, useCallback, useEffect, useState } from 'react';
import maplibregl from 'maplibre-gl';
import { RoomMember } from 'matrix-js-sdk/src/matrix';
@@ -64,12 +64,13 @@ interface SmartMarkerProps {
roomMember?: RoomMember;
// use member text color as background
useMemberColor?: boolean;
+ tooltip?: ReactNode;
}
/**
* Generic location marker
*/
-const SmartMarker: React.FC = ({ id, map, geoUri, roomMember, useMemberColor }) => {
+const SmartMarker: React.FC = ({ id, map, geoUri, roomMember, useMemberColor, tooltip }) => {
const { onElementRef } = useMapMarker(map, geoUri);
return (
@@ -84,6 +85,7 @@ const SmartMarker: React.FC = ({ id, map, geoUri, roomMember,
id={id}
roomMember={roomMember}
useMemberColor={useMemberColor}
+ tooltip={tooltip}
/>
);
diff --git a/src/components/views/messages/JumpToDatePicker.tsx b/src/components/views/messages/JumpToDatePicker.tsx
index 925a741c188..fc23f82a57e 100644
--- a/src/components/views/messages/JumpToDatePicker.tsx
+++ b/src/components/views/messages/JumpToDatePicker.tsx
@@ -18,7 +18,6 @@ import React, { useState, FormEvent } from 'react';
import { _t } from '../../../languageHandler';
import Field from "../elements/Field";
-import NativeOnChangeInput from "../elements/NativeOnChangeInput";
import { RovingAccessibleButton, useRovingTabIndex } from "../../../accessibility/RovingTabIndex";
interface IProps {
@@ -34,41 +33,11 @@ const JumpToDatePicker: React.FC = ({ ts, onDatePicked }: IProps) => {
const dateDefaultValue = `${year}-${month}-${day}`;
const [dateValue, setDateValue] = useState(dateDefaultValue);
- // Whether or not to automatically navigate to the given date after someone
- // selects a day in the date picker. We want to disable this after someone
- // starts manually typing in the input instead of picking.
- const [navigateOnDatePickerSelection, setNavigateOnDatePickerSelection] = useState(true);
-
- // Since we're using NativeOnChangeInput with native JavaScript behavior, this
- // tracks the date value changes as they come in.
- const onDateValueInput = (e: React.ChangeEvent): void => {
- setDateValue(e.target.value);
- };
-
- // Since we're using NativeOnChangeInput with native JavaScript behavior, the change
- // event listener will trigger when a date is picked from the date picker
- // or when the text is fully filled out. In order to not trigger early
- // as someone is typing out a date, we need to disable when we see keydowns.
- const onDateValueChange = (e: React.ChangeEvent): void => {
- setDateValue(e.target.value);
-
- // Don't auto navigate if they were manually typing out a date
- if (navigateOnDatePickerSelection) {
- onDatePicked(dateValue);
- }
- };
-
const [onFocus, isActive, ref] = useRovingTabIndex();
- const onDateInputKeyDown = (e: React.KeyboardEvent): void => {
- // When we see someone manually typing out a date, disable the auto
- // submit on change.
- setNavigateOnDatePickerSelection(false);
- };
-
+ const onDateValueInput = (ev: React.ChangeEvent) => setDateValue(ev.target.value);
const onJumpToDateSubmit = (ev: FormEvent): void => {
ev.preventDefault();
-
onDatePicked(dateValue);
};
@@ -79,11 +48,9 @@ const JumpToDatePicker: React.FC = ({ ts, onDatePicked }: IProps) => {
>
{ _t("Jump to date") }
{
{ this.context.timelineRenderingType === TimelineRenderingType.File && (
- { this.content.info && this.content.info.size ? filesize(this.content.info.size) : "" }
+ { this.content.info?.size ? filesize(this.content.info.size) : "" }
) }
}
diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx
index 918623deedd..6d8e2d31dd2 100644
--- a/src/components/views/messages/MImageBody.tsx
+++ b/src/components/views/messages/MImageBody.tsx
@@ -37,6 +37,7 @@ import { ImageSize, suggestedSize as suggestedImageSize } from "../../../setting
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
import { blobIsAnimated, mayBeAnimated } from '../../../utils/Image';
+import { presentableTextForFile } from "../../../utils/FileUtils";
enum Placeholder {
NoImage,
@@ -446,6 +447,21 @@ export default class MImageBody extends React.Component {
gifLabel = GIF
;
}
+ let banner: JSX.Element;
+ const isTimeline = [
+ TimelineRenderingType.Room,
+ TimelineRenderingType.Search,
+ TimelineRenderingType.Thread,
+ TimelineRenderingType.Notification,
+ ].includes(this.context.timelineRenderingType);
+ if (this.state.showImage && this.state.hover && isTimeline) {
+ banner = (
+
+ { presentableTextForFile(content, _t("Image"), true, true) }
+
+ );
+ }
+
const classes = classNames({
'mx_MImageBody_placeholder': true,
'mx_MImageBody_placeholder--blurhash': this.props.mxEvent.getContent().info?.[BLURHASH_FIELD],
@@ -473,6 +489,7 @@ export default class MImageBody extends React.Component {
{ img }
{ gifLabel }
+ { banner }
{ /* HACK: This div fills out space while the image loads, to prevent scroll jumps */ }
@@ -485,14 +502,14 @@ export default class MImageBody extends React.Component {
return this.wrapImage(contentUrl, thumbnail);
}
- // Overidden by MStickerBody
+ // Overridden by MStickerBody
protected wrapImage(contentUrl: string, children: JSX.Element): JSX.Element {
return
{ children }
;
}
- // Overidden by MStickerBody
+ // Overridden by MStickerBody
protected getPlaceholder(width: number, height: number): JSX.Element {
const blurhash = this.props.mxEvent.getContent().info?.[BLURHASH_FIELD];
@@ -506,12 +523,12 @@ export default class MImageBody extends React.Component {
return ;
}
- // Overidden by MStickerBody
+ // Overridden by MStickerBody
protected getTooltip(): JSX.Element {
return null;
}
- // Overidden by MStickerBody
+ // Overridden by MStickerBody
protected getFileBody(): string | JSX.Element {
if (this.props.forExport) return null;
/*
diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx
index 0effb252888..85ea8387555 100644
--- a/src/components/views/messages/MessageActionBar.tsx
+++ b/src/components/views/messages/MessageActionBar.tsx
@@ -384,7 +384,7 @@ export default class MessageActionBar extends React.PureComponent ;
- const threadTooltipButton = ;
+ const threadTooltipButton = ;
// We show a different toolbar for failed events, so detect that first.
const mxEvent = this.props.mxEvent;
diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx
index c29074171cf..b7ca3c5872d 100644
--- a/src/components/views/messages/MessageEvent.tsx
+++ b/src/components/views/messages/MessageEvent.tsx
@@ -20,6 +20,7 @@ import { Relations } from 'matrix-js-sdk/src/models/relations';
import { M_BEACON_INFO } from 'matrix-js-sdk/src/@types/beacon';
import { M_LOCATION } from 'matrix-js-sdk/src/@types/location';
import { M_POLL_START } from "matrix-events-sdk";
+import { MatrixEventEvent } from "matrix-js-sdk/src/models/event";
import SettingsStore from "../../../settings/SettingsStore";
import { Mjolnir } from "../../../mjolnir/Mjolnir";
@@ -73,7 +74,12 @@ export default class MessageEvent extends React.Component implements IMe
}
}
+ public componentDidMount(): void {
+ this.props.mxEvent.addListener(MatrixEventEvent.Decrypted, this.onDecrypted);
+ }
+
public componentWillUnmount() {
+ this.props.mxEvent.removeListener(MatrixEventEvent.Decrypted, this.onDecrypted);
this.mediaHelper?.destroy();
}
@@ -118,6 +124,14 @@ export default class MessageEvent extends React.Component implements IMe
return this.mediaHelper;
}
+ private onDecrypted = (): void => {
+ // Recheck MediaEventHelper eligibility as it can change when the event gets decrypted
+ if (MediaEventHelper.isEligible(this.props.mxEvent)) {
+ this.mediaHelper?.destroy();
+ this.mediaHelper = new MediaEventHelper(this.props.mxEvent);
+ }
+ };
+
private onTileUpdate = () => {
this.forceUpdate();
};
diff --git a/src/components/views/messages/ReactionsRow.tsx b/src/components/views/messages/ReactionsRow.tsx
index 0c064746359..2ead0ecbbd5 100644
--- a/src/components/views/messages/ReactionsRow.tsx
+++ b/src/components/views/messages/ReactionsRow.tsx
@@ -165,11 +165,6 @@ export default class ReactionsRow extends React.PureComponent {
return null;
}
- const cli = this.context.room.client;
- const room = cli.getRoom(mxEvent.getRoomId());
- const isPeeking = room.getMyMembership() !== "join";
- const canReact = !isPeeking && this.context.canReact;
-
let items = reactions.getSortedAnnotationsByKey().map(([content, events]) => {
const count = events.size;
if (!count) {
@@ -188,7 +183,7 @@ export default class ReactionsRow extends React.PureComponent {
mxEvent={mxEvent}
reactionEvents={events}
myReactionEvent={myReactionEvent}
- disabled={!canReact}
+ disabled={!this.context.canReact}
/>;
}).filter(item => !!item);
@@ -197,7 +192,7 @@ export default class ReactionsRow extends React.PureComponent {
// Show the first MAX_ITEMS if there are MAX_ITEMS + 1 or more items.
// The "+ 1" ensure that the "show all" reveals something that takes up
// more space than the button itself.
- let showAllButton;
+ let showAllButton: JSX.Element;
if ((items.length > MAX_ITEMS_WHEN_LIMITED + 1) && !showAll) {
items = items.slice(0, MAX_ITEMS_WHEN_LIMITED);
showAllButton = {
;
}
- let addReactionButton;
- if (room.getMyMembership() === "join" && this.context.canReact) {
+ let addReactionButton: JSX.Element;
+ if (this.context.canReact) {
addReactionButton = ;
}
diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx
index 0fc2239cfaa..31cf7e290c5 100644
--- a/src/components/views/right_panel/RoomSummaryCard.tsx
+++ b/src/components/views/right_panel/RoomSummaryCard.tsx
@@ -167,7 +167,6 @@ const AppRow: React.FC = ({ app, room }) => {
title={openTitle}
forceHide={!(isPinned || isMaximised)}
disabled={isPinned || isMaximised}
- yOffset={-48}
>
{ name }
@@ -179,7 +178,6 @@ const AppRow: React.FC = ({ app, room }) => {
isExpanded={menuDisplayed}
onClick={openMenu}
title={_t("Options")}
- yOffset={-24}
/> }
= ({ app, room }) => {
onClick={togglePin}
title={pinTitle}
disabled={cannotPin}
- yOffset={-24}
/>
{ contextMenu }
@@ -208,11 +204,8 @@ const AppsSection: React.FC = ({ room }) => {
if (!managers.hasManager()) {
managers.openNoManagerDialog();
} else {
- if (SettingsStore.getValue("feature_many_integration_managers")) {
- managers.openAll(room);
- } else {
- managers.getPrimaryManager().open(room);
- }
+ // noinspection JSIgnoredPromiseFromCall
+ managers.getPrimaryManager().open(room);
}
};
diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx
index 9d17c5237d1..f1c956bb8f3 100644
--- a/src/components/views/right_panel/UserInfo.tsx
+++ b/src/components/views/right_panel/UserInfo.tsx
@@ -75,7 +75,6 @@ import { UIComponent } from "../../../settings/UIFeature";
import { TimelineRenderingType } from "../../../contexts/RoomContext";
import RightPanelStore from '../../../stores/right-panel/RightPanelStore';
import { IRightPanelCardState } from '../../../stores/right-panel/RightPanelStoreIPanelState';
-import { useUserStatusMessage } from "../../../hooks/useUserStatusMessage";
import UserIdentifierCustomisations from '../../../customisations/UserIdentifier';
import PosthogTrackers from "../../../PosthogTrackers";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
@@ -250,7 +249,7 @@ function DevicesSection({ devices, userId, loading }: { devices: IDevice[], user
return ;
}
if (devices === null) {
- return <>{ _t("Unable to load session list") }>;
+ return { _t("Unable to load session list") }
;
}
const isMe = userId === cli.getUserId();
const deviceTrusts = devices.map(d => cli.checkDeviceTrust(userId, d.deviceId));
@@ -1411,7 +1410,6 @@ const UserInfoHeader: React.FC<{
roomId?: string;
}> = ({ member, e2eStatus, roomId }) => {
const cli = useContext(MatrixClientContext);
- const statusMessage = useUserStatusMessage(member);
const onMemberAvatarClick = useCallback(() => {
const avatarUrl = (member as RoomMember).getMxcAvatarUrl
@@ -1472,11 +1470,6 @@ const UserInfoHeader: React.FC<{
);
}
- let statusLabel = null;
- if (statusMessage) {
- statusLabel = { statusMessage } ;
- }
-
let e2eIcon;
if (e2eStatus) {
e2eIcon = ;
@@ -1499,7 +1492,6 @@ const UserInfoHeader: React.FC<{
{ UserIdentifierCustomisations.getDisplayUserIdentifier(member.userId, { roomId, withDisplayName: true }) }
{ presenceLabel }
- { statusLabel }
diff --git a/src/components/views/rooms/AppsDrawer.tsx b/src/components/views/rooms/AppsDrawer.tsx
index 22afea24902..601cc9ee346 100644
--- a/src/components/views/rooms/AppsDrawer.tsx
+++ b/src/components/views/rooms/AppsDrawer.tsx
@@ -49,7 +49,7 @@ interface IState {
// @ts-ignore - TS wants a string key, but we know better
apps: {[id: Container]: IApp[]};
resizingVertical: boolean; // true when changing the height of the apps drawer
- resizingHorizontal: boolean; // true when chagning the distribution of the width between widgets
+ resizingHorizontal: boolean; // true when changing the distribution of the width between widgets
resizing: boolean;
}
@@ -259,7 +259,7 @@ export default class AppsDrawer extends React.Component
{
mx_AppsDrawer_2apps: apps.length === 2,
mx_AppsDrawer_3apps: apps.length === 3,
});
- const appConatiners =
+ const appContainers =
{ apps.map((app, i) => {
if (i < 1) return app;
@@ -272,7 +272,7 @@ export default class AppsDrawer extends React.Component
{
let drawer;
if (widgetIsMaxmised) {
- drawer = appConatiners;
+ drawer = appContainers;
} else {
drawer = {
handleWrapperClass="mx_AppsContainer_resizerHandleContainer"
className="mx_AppsContainer_resizer"
resizeNotifier={this.props.resizeNotifier}>
- { appConatiners }
+ { appContainers }
;
}
diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx
index 59567a6011a..18f77f342ef 100644
--- a/src/components/views/rooms/AuxPanel.tsx
+++ b/src/components/views/rooms/AuxPanel.tsx
@@ -116,7 +116,7 @@ export default class AuxPanel extends React.Component {
const severity = ev.getContent().severity || "normal";
const stateKey = ev.getStateKey();
- // We want a non-empty title but can accept falsey values (e.g.
+ // We want a non-empty title but can accept falsy values (e.g.
// zero)
if (title && value !== undefined) {
counters.push({
diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx
index e93119643fb..667d5a42a4e 100644
--- a/src/components/views/rooms/BasicMessageComposer.tsx
+++ b/src/components/views/rooms/BasicMessageComposer.tsx
@@ -24,15 +24,16 @@ import { logger } from "matrix-js-sdk/src/logger";
import EditorModel from '../../../editor/model';
import HistoryManager from '../../../editor/history';
import { Caret, setSelection } from '../../../editor/caret';
-import { formatRange, replaceRangeAndMoveCaret, toggleInlineFormat } from '../../../editor/operations';
+import { formatRange, formatRangeAsLink, replaceRangeAndMoveCaret, toggleInlineFormat }
+ from '../../../editor/operations';
import { getCaretOffsetAndText, getRangeForSelection } from '../../../editor/dom';
import Autocomplete, { generateCompletionDomId } from '../rooms/Autocomplete';
-import { getAutoCompleteCreator, Type } from '../../../editor/parts';
+import { getAutoCompleteCreator, Part, Type } 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 { IS_MAC, Key } from "../../../Keyboard";
import { EMOTICON_TO_EMOJI } from "../../../emoji";
import { CommandCategories, CommandMap, parseCommandString } from "../../../SlashCommands";
import Range from "../../../editor/range";
@@ -45,13 +46,12 @@ import { ICompletion } from "../../../autocomplete/Autocompleter";
import { getKeyBindingsManager } from '../../../KeyBindingsManager';
import { ALTERNATE_KEY_NAME, KeyBindingAction } from '../../../accessibility/KeyboardShortcuts';
import { _t } from "../../../languageHandler";
+import { linkify } from '../../../linkify-matrix';
// matches emoticons which follow the start of a line or whitespace
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s|:^$');
export const REGEX_EMOTICON = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')$');
-const IS_MAC = navigator.platform.indexOf("Mac") !== -1;
-
const SURROUND_WITH_CHARACTERS = ["\"", "_", "`", "'", "*", "~", "$"];
const SURROUND_WITH_DOUBLE_CHARACTERS = new Map([
["(", ")"],
@@ -92,7 +92,7 @@ function selectionEquals(a: Partial, b: Selection): boolean {
interface IProps {
model: EditorModel;
room: Room;
- threadId: string;
+ threadId?: string;
placeholder?: string;
label?: string;
initialCaret?: DocumentOffset;
@@ -333,26 +333,32 @@ export default class BasicMessageEditor extends React.Component
private onPaste = (event: ClipboardEvent): boolean => {
event.preventDefault(); // we always handle the paste ourselves
- if (this.props.onPaste && this.props.onPaste(event, this.props.model)) {
+ if (this.props.onPaste?.(event, this.props.model)) {
// to prevent double handling, allow props.onPaste to skip internal onPaste
return true;
}
const { model } = this.props;
const { partCreator } = model;
+ const plainText = event.clipboardData.getData("text/plain");
const partsText = event.clipboardData.getData("application/x-element-composer");
- let parts;
+
+ let parts: Part[];
if (partsText) {
const serializedTextParts = JSON.parse(partsText);
- const deserializedParts = serializedTextParts.map(p => partCreator.deserializePart(p));
- parts = deserializedParts;
+ parts = serializedTextParts.map(p => partCreator.deserializePart(p));
} else {
- const text = event.clipboardData.getData("text/plain");
- parts = parsePlainTextMessage(text, partCreator, { shouldEscape: false });
+ parts = parsePlainTextMessage(plainText, partCreator, { shouldEscape: false });
}
+
this.modifiedFlag = true;
const range = getRangeForSelection(this.editorRef.current, model, document.getSelection());
- replaceRangeAndMoveCaret(range, parts);
+
+ if (plainText && range.length > 0 && linkify.test(plainText)) {
+ formatRangeAsLink(range, plainText);
+ } else {
+ replaceRangeAndMoveCaret(range, parts);
+ }
};
private onInput = (event: Partial): void => {
diff --git a/src/components/views/rooms/CollapsibleButton.tsx b/src/components/views/rooms/CollapsibleButton.tsx
index b9e9f083d0a..d89d277073f 100644
--- a/src/components/views/rooms/CollapsibleButton.tsx
+++ b/src/components/views/rooms/CollapsibleButton.tsx
@@ -20,27 +20,27 @@ import classNames from 'classnames';
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { MenuItem } from "../../structures/ContextMenu";
import { OverflowMenuContext } from './MessageComposerButtons';
+import { IconizedContextMenuOption } from '../context_menus/IconizedContextMenu';
interface ICollapsibleButtonProps extends ComponentProps {
title: string;
+ iconClassName: string;
}
-export const CollapsibleButton = ({ title, children, className, ...props }: ICollapsibleButtonProps) => {
+export const CollapsibleButton = ({ title, children, className, iconClassName, ...props }: ICollapsibleButtonProps) => {
const inOverflowMenu = !!useContext(OverflowMenuContext);
if (inOverflowMenu) {
- return
- { title }
- { children }
- ;
+ iconClassName={iconClassName}
+ label={title}
+ />;
}
return
{ children }
;
diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx
index a801c06a83b..def039fb74f 100644
--- a/src/components/views/rooms/EventTile.tsx
+++ b/src/components/views/rooms/EventTile.tsx
@@ -222,7 +222,7 @@ interface IProps {
// whether or not to display thread info
showThreadInfo?: boolean;
- // if specified and `true`, the message his behing
+ // if specified and `true`, the message is being
// hidden for moderation from other users but is
// displayed to the current user either because they're
// the author or they are a moderator
@@ -244,7 +244,7 @@ interface IState {
// Position of the context menu
contextMenu?: {
position: Pick;
- showPermalink?: boolean;
+ link?: string;
};
isQuoteExpanded?: boolean;
@@ -411,15 +411,14 @@ export class UnwrappedEventTile extends React.Component {
room?.on(ThreadEvent.New, this.onNewThread);
}
- private setupNotificationListener = (thread: Thread): void => {
- const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
- const notifications = RoomNotificationStateStore.instance.getThreadsRoomState(room);
+ private setupNotificationListener(thread: Thread): void {
+ const notifications = RoomNotificationStateStore.instance.getThreadsRoomState(thread.room);
this.threadState = notifications.getThreadRoomState(thread);
this.threadState.on(NotificationStateEvents.Update, this.onThreadStateUpdate);
this.onThreadStateUpdate();
- };
+ }
private onThreadStateUpdate = (): void => {
let threadNotification = null;
@@ -859,26 +858,27 @@ export class UnwrappedEventTile extends React.Component {
};
private onTimestampContextMenu = (ev: React.MouseEvent): void => {
- this.showContextMenu(ev, true);
+ this.showContextMenu(ev, this.props.permalinkCreator?.forEvent(this.props.mxEvent.getId()));
};
- private showContextMenu(ev: React.MouseEvent, showPermalink?: boolean): void {
+ private showContextMenu(ev: React.MouseEvent, permalink?: string): void {
+ const clickTarget = ev.target as HTMLElement;
+
// Return if message right-click context menu isn't enabled
if (!SettingsStore.getValue("feature_message_right_click_context_menu")) return;
- // Return if we're in a browser and click either an a tag or we have
- // selected text, as in those cases we want to use the native browser
- // menu
- const clickTarget = ev.target as HTMLElement;
- if (
- !PlatformPeg.get().allowOverridingNativeContextMenus() &&
- (clickTarget.tagName === "a" || clickTarget.closest("a") || getSelectedText())
- ) return;
+ // Try to find an anchor element
+ const anchorElement = (clickTarget instanceof HTMLAnchorElement) ? clickTarget : clickTarget.closest("a");
// There is no way to copy non-PNG images into clipboard, so we can't
// have our own handling for copying images, so we leave it to the
// Electron layer (webcontents-handler.ts)
- if (ev.target instanceof HTMLImageElement) return;
+ if (clickTarget instanceof HTMLImageElement) return;
+
+ // Return if we're in a browser and click either an a tag or we have
+ // selected text, as in those cases we want to use the native browser
+ // menu
+ if (!PlatformPeg.get().allowOverridingNativeContextMenus() && (getSelectedText() || anchorElement)) return;
// We don't want to show the menu when editing a message
if (this.props.editState) return;
@@ -892,7 +892,7 @@ export class UnwrappedEventTile extends React.Component {
top: ev.clientY,
bottom: ev.clientY,
},
- showPermalink: showPermalink,
+ link: anchorElement?.href || permalink,
},
actionBarFocused: true,
});
@@ -941,7 +941,7 @@ export class UnwrappedEventTile extends React.Component {
onFinished={this.onCloseMenu}
rightClick={true}
reactions={this.state.reactions}
- showPermalink={this.state.contextMenu.showPermalink}
+ link={this.state.contextMenu.link}
/>
);
}
@@ -990,7 +990,8 @@ export class UnwrappedEventTile extends React.Component {
const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted;
const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure();
- const isOwnEvent = this.props.mxEvent?.sender?.userId === MatrixClientPeg.get().getUserId();
+ // Use `getSender()` because searched events might not have a proper `sender`.
+ const isOwnEvent = this.props.mxEvent?.getSender() === MatrixClientPeg.get().getUserId();
const scBubbleEnabled = this.props.layout === Layout.Bubble &&
this.context.timelineRenderingType !== TimelineRenderingType.Notification &&
@@ -1059,6 +1060,11 @@ export class UnwrappedEventTile extends React.Component {
} else if (this.context.timelineRenderingType === TimelineRenderingType.Notification) {
avatarSize = 24;
needsSenderProfile = true;
+ } else if (isInfoMessage) {
+ // a small avatar, with no sender profile, for
+ // joins/parts/etc
+ avatarSize = 14;
+ needsSenderProfile = false;
} else if (this.context.timelineRenderingType === TimelineRenderingType.ThreadsList ||
(this.context.timelineRenderingType === TimelineRenderingType.Thread && !this.props.continuation)
) {
@@ -1067,11 +1073,6 @@ export class UnwrappedEventTile extends React.Component {
} else if (eventType === EventType.RoomCreate || isBubbleMessage) {
avatarSize = 0;
needsSenderProfile = false;
- } else if (isInfoMessage) {
- // a small avatar, with no sender profile, for
- // joins/parts/etc
- avatarSize = 14;
- needsSenderProfile = false;
} else if (this.props.layout == Layout.IRC) {
avatarSize = 14;
needsSenderProfile = true;
@@ -1327,6 +1328,7 @@ export class UnwrappedEventTile extends React.Component {
"data-has-reply": !!replyChain,
"data-layout": this.props.layout,
"data-self": isOwnEvent,
+ "data-event-id": this.props.mxEvent.getId(),
"onMouseEnter": () => this.setState({ hover: true }),
"onMouseLeave": () => this.setState({ hover: false }),
}, [
@@ -1527,6 +1529,7 @@ export class UnwrappedEventTile extends React.Component {
"data-scroll-tokens": scrollToken,
"data-layout": this.props.layout,
"data-self": isOwnEvent,
+ "data-event-id": this.props.mxEvent.getId(),
"data-has-reply": !!replyChain,
"onMouseEnter": () => this.setState({ hover: true }),
"onMouseLeave": () => this.setState({ hover: false }),
@@ -1579,6 +1582,7 @@ export class UnwrappedEventTile extends React.Component {
"data-scroll-tokens": scrollToken,
"data-layout": this.props.layout,
"data-self": isOwnEvent,
+ "data-event-id": this.props.mxEvent.getId(),
"data-has-reply": !!replyChain,
"onMouseEnter": () => this.setState({ hover: true }),
"onMouseLeave": () => this.setState({ hover: false }),
diff --git a/src/components/views/rooms/MemberTile.tsx b/src/components/views/rooms/MemberTile.tsx
index b652771a43e..f292ec3b589 100644
--- a/src/components/views/rooms/MemberTile.tsx
+++ b/src/components/views/rooms/MemberTile.tsx
@@ -20,12 +20,10 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
-import { UserEvent } from "matrix-js-sdk/src/models/user";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { UserTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning';
-import SettingsStore from "../../../settings/SettingsStore";
import dis from "../../../dispatcher/dispatcher";
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from "../../../MatrixClientPeg";
@@ -41,7 +39,6 @@ interface IProps {
}
interface IState {
- statusMessage: string;
isRoomEncrypted: boolean;
e2eStatus: string;
}
@@ -58,7 +55,6 @@ export default class MemberTile extends React.Component {
super(props);
this.state = {
- statusMessage: this.getStatusMessage(),
isRoomEncrypted: false,
e2eStatus: null,
};
@@ -67,13 +63,6 @@ export default class MemberTile extends React.Component {
componentDidMount() {
const cli = MatrixClientPeg.get();
- if (SettingsStore.getValue("feature_custom_status")) {
- const { user } = this.props.member;
- if (user) {
- user.on(UserEvent._UnstableStatusMessage, this.onStatusMessageCommitted);
- }
- }
-
const { roomId } = this.props.member;
if (roomId) {
const isRoomEncrypted = cli.isRoomEncrypted(roomId);
@@ -94,11 +83,6 @@ export default class MemberTile extends React.Component {
componentWillUnmount() {
const cli = MatrixClientPeg.get();
- const { user } = this.props.member;
- if (user) {
- user.removeListener(UserEvent._UnstableStatusMessage, this.onStatusMessageCommitted);
- }
-
if (cli) {
cli.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
cli.removeListener(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged);
@@ -158,21 +142,6 @@ export default class MemberTile extends React.Component {
});
}
- private getStatusMessage(): string {
- const { user } = this.props.member;
- if (!user) {
- return "";
- }
- return user.unstable_statusMessage;
- }
-
- private onStatusMessageCommitted = (): void => {
- // The `User` object has observed a status message change.
- this.setState({
- statusMessage: this.getStatusMessage(),
- });
- };
-
shouldComponentUpdate(nextProps: IProps, nextState: IState): boolean {
if (
this.memberLastModifiedTime === undefined ||
@@ -222,11 +191,6 @@ export default class MemberTile extends React.Component {
const name = this.getDisplayName();
const presenceState = member.user ? member.user.presence : null;
- let statusMessage = null;
- if (member.user && SettingsStore.getValue("feature_custom_status")) {
- statusMessage = this.state.statusMessage;
- }
-
const av = (
);
@@ -277,7 +241,6 @@ export default class MemberTile extends React.Component {
nameJSX={nameJSX}
powerStatus={powerStatus}
showPresence={this.props.showPresence}
- subtextLabel={statusMessage}
e2eStatus={e2eStatus}
onClick={this.onClick}
/>
diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx
index 4c77fd3d2d3..dab14e2ce5d 100644
--- a/src/components/views/rooms/MessageComposer.tsx
+++ b/src/components/views/rooms/MessageComposer.tsx
@@ -85,7 +85,7 @@ interface IProps {
replyToEvent?: MatrixEvent;
relation?: IEventRelation;
e2eStatus?: E2EStatus;
- layout: Layout;
+ layout?: Layout;
userNameColorMode?: UserNameColorMode;
compact?: boolean;
}
@@ -442,7 +442,6 @@ export default class MessageComposer extends React.Component {
recordingTooltip = ;
}
@@ -466,11 +465,9 @@ export default class MessageComposer extends React.Component {
const classes = classNames({
"mx_MessageComposer": true,
- // IRC layout has nothing for message composer so use group layout stuff
- // When IRC layout gets something for the message composer we can use the following
- // "mx_IRCLayout": this.props.layout == Layout.IRC,
- // "mx_GroupLayout": this.props.layout == Layout.Group,
- "mx_GroupLayout": this.props.layout === Layout.IRC || this.props.layout === Layout.Group,
+ // SC: IRC layout has nothing for message composer so use group layout stuff
+ // SC: Also use group layout if layout prop is missing
+ "mx_GroupLayout": this.props.layout !== Layout.Bubble,
"sc_BubbleLayout": this.props.layout === Layout.Bubble,
"mx_MessageComposer--compact": this.props.compact,
diff --git a/src/components/views/rooms/MessageComposerButtons.tsx b/src/components/views/rooms/MessageComposerButtons.tsx
index 2c6e1268648..58ddd2328be 100644
--- a/src/components/views/rooms/MessageComposerButtons.tsx
+++ b/src/components/views/rooms/MessageComposerButtons.tsx
@@ -38,6 +38,7 @@ import MatrixClientContext from '../../../contexts/MatrixClientContext';
import RoomContext from '../../../contexts/RoomContext';
import { useDispatcher } from "../../../hooks/useDispatcher";
import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds";
+import IconizedContextMenu, { IconizedContextMenuOptionList } from '../context_menus/IconizedContextMenu';
interface IProps {
addEmoji: (emoji: string) => boolean;
@@ -119,15 +120,18 @@ const MessageComposerButtons: React.FC = (props: IProps) => {
title={_t("More options")}
/> }
{ props.isMenuOpen && (
-
- { moreButtons }
+
+ { moreButtons }
+
-
+
) }
;
};
@@ -169,7 +173,6 @@ const EmojiButton: React.FC = ({ addEmoji, menuPosition }) =>
const className = classNames(
"mx_MessageComposer_button",
- "mx_MessageComposer_emoji",
{
"mx_MessageComposer_button_highlight": menuDisplayed,
},
@@ -180,6 +183,7 @@ const EmojiButton: React.FC = ({ addEmoji, menuPosition }) =>
return
@@ -265,7 +269,8 @@ const UploadButton = () => {
};
return ;
@@ -277,7 +282,8 @@ function showStickersButton(props: IProps): ReactElement {
? props.setStickerPickerOpen(!props.isStickerPickerOpen)}
title={props.isStickerPickerOpen ? _t("Hide stickers") : _t("Sticker")}
/>
@@ -292,7 +298,8 @@ function voiceRecordingButton(props: IProps, narrow: boolean): ReactElement {
? null
:
@@ -356,7 +363,8 @@ class PollButton extends React.PureComponent {
return (
diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx
index 363e687c9f9..9c9f190210d 100644
--- a/src/components/views/rooms/NewRoomIntro.tsx
+++ b/src/components/views/rooms/NewRoomIntro.tsx
@@ -28,7 +28,6 @@ import AccessibleButton from "../elements/AccessibleButton";
import MiniAvatarUploader, { AVATAR_SIZE } from "../elements/MiniAvatarUploader";
import RoomAvatar from "../avatars/RoomAvatar";
import defaultDispatcher from "../../../dispatcher/dispatcher";
-import dis from "../../../dispatcher/dispatcher";
import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload";
import { Action } from "../../../dispatcher/actions";
import SpaceStore from "../../../stores/spaces/SpaceStore";
@@ -87,7 +86,7 @@ const NewRoomIntro = () => {
const canAddTopic = inRoom && room.currentState.maySendStateEvent(EventType.RoomTopic, cli.getUserId());
const onTopicClick = () => {
- dis.dispatch({
+ defaultDispatcher.dispatch({
action: "open_room_settings",
room_id: roomId,
}, true);
@@ -150,7 +149,7 @@ const NewRoomIntro = () => {
className="mx_NewRoomIntro_inviteButton"
kind="primary_outline"
onClick={() => {
- dis.dispatch({ action: "view_invite", roomId });
+ defaultDispatcher.dispatch({ action: "view_invite", roomId });
}}
>
{ _t("Invite to just this room") }
@@ -162,7 +161,7 @@ const NewRoomIntro = () => {
className="mx_NewRoomIntro_inviteButton"
kind="primary"
onClick={() => {
- dis.dispatch({ action: "view_invite", roomId });
+ defaultDispatcher.dispatch({ action: "view_invite", roomId });
}}
>
{ _t("Invite to this room") }
@@ -192,7 +191,7 @@ const NewRoomIntro = () => {
function openRoomSettings(event) {
event.preventDefault();
- dis.dispatch({
+ defaultDispatcher.dispatch({
action: "open_room_settings",
initial_tab_id: ROOM_SECURITY_TAB,
});
diff --git a/src/components/views/rooms/ReadReceiptGroup.tsx b/src/components/views/rooms/ReadReceiptGroup.tsx
index fa1b7d7424b..fc4c6796faa 100644
--- a/src/components/views/rooms/ReadReceiptGroup.tsx
+++ b/src/components/views/rooms/ReadReceiptGroup.tsx
@@ -55,11 +55,11 @@ interface IAvatarPosition {
position: number;
}
-export function determineAvatarPosition(index: number, count: number, max: number): IAvatarPosition {
+export function determineAvatarPosition(index: number, max: number): IAvatarPosition {
if (index < max) {
return {
hidden: false,
- position: Math.min(count, max) - index - 1,
+ position: index,
};
} else {
return {
@@ -143,7 +143,7 @@ export function ReadReceiptGroup(
}
const avatars = readReceipts.map((receipt, index) => {
- const { hidden, position } = determineAvatarPosition(index, readReceipts.length, maxAvatars);
+ const { hidden, position } = determineAvatarPosition(index, maxAvatars);
const userId = receipt.userId;
let readReceiptInfo: IReadReceiptInfo;
@@ -243,7 +243,7 @@ interface ReadReceiptPersonProps extends IReadReceiptProps {
function ReadReceiptPerson({ userId, roomMember, ts, isTwelveHour, onAfterClick }: ReadReceiptPersonProps) {
const [{ showTooltip, hideTooltip }, tooltip] = useTooltip({
- alignment: Alignment.TopCenter,
+ alignment: Alignment.Top,
tooltipClassName: "mx_ReadReceiptGroup_person--tooltip",
label: (
<>
diff --git a/src/components/views/rooms/ReadReceiptMarker.tsx b/src/components/views/rooms/ReadReceiptMarker.tsx
index 934fd7af7ab..04734256184 100644
--- a/src/components/views/rooms/ReadReceiptMarker.tsx
+++ b/src/components/views/rooms/ReadReceiptMarker.tsx
@@ -129,14 +129,12 @@ export default class ReadReceiptMarker extends React.PureComponent {
const tooltipRef = useRef();
@@ -50,7 +51,10 @@ const RecentlyViewedButton = () => {
tooltipRef.current?.hideTooltip();
}}
>
-
+ { crumb.isSpaceRoom()
+ ?
+ :
+ }
{ crumb.name }
{ contextDetails &&
diff --git a/src/components/views/rooms/ReplyPreview.tsx b/src/components/views/rooms/ReplyPreview.tsx
index 68cd9f23df9..0b30e63c2ab 100644
--- a/src/components/views/rooms/ReplyPreview.tsx
+++ b/src/components/views/rooms/ReplyPreview.tsx
@@ -23,6 +23,7 @@ import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import ReplyTile from './ReplyTile';
import { UserNameColorMode } from '../../../settings/enums/UserNameColorMode';
import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext';
+import AccessibleButton from "../elements/AccessibleButton";
function cancelQuoting(context: TimelineRenderingType) {
dis.dispatch({
@@ -46,26 +47,18 @@ export default class ReplyPreview extends React.Component
{
return
-
- { _t('Replying') }
-
-
-
+
{ _t('Replying') }
+
cancelQuoting(this.context.timelineRenderingType)}
/>
-
-
-
-
+
;
}
diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx
index 9983b6f39c3..ec206b0e369 100644
--- a/src/components/views/rooms/RoomHeader.tsx
+++ b/src/components/views/rooms/RoomHeader.tsx
@@ -186,11 +186,10 @@ export default class RoomHeader extends React.Component {
);
- const topicElement =
- { (topic, ref) =>
- { topic }
-
}
- ;
+ const topicElement = ;
let roomAvatar;
if (this.props.room) {
diff --git a/src/components/views/rooms/RoomInfoLine.tsx b/src/components/views/rooms/RoomInfoLine.tsx
new file mode 100644
index 00000000000..09214043d63
--- /dev/null
+++ b/src/components/views/rooms/RoomInfoLine.tsx
@@ -0,0 +1,86 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, { FC } from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { JoinRule } from "matrix-js-sdk/src/@types/partials";
+
+import { _t } from "../../../languageHandler";
+import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
+import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
+import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
+import { useRoomState } from "../../../hooks/useRoomState";
+import { useRoomMemberCount, useMyRoomMembership } from "../../../hooks/useRoomMembers";
+import AccessibleButton from "../elements/AccessibleButton";
+
+interface IProps {
+ room: Room;
+}
+
+const RoomInfoLine: FC = ({ room }) => {
+ // summary will begin as undefined whilst loading and go null if it fails to load or we are not invited.
+ const summary = useAsyncMemo(async () => {
+ if (room.getMyMembership() !== "invite") return null;
+ try {
+ return room.client.getRoomSummary(room.roomId);
+ } catch (e) {
+ return null;
+ }
+ }, [room]);
+ const joinRule = useRoomState(room, state => state.getJoinRule());
+ const membership = useMyRoomMembership(room);
+ const memberCount = useRoomMemberCount(room);
+
+ let iconClass: string;
+ let roomType: string;
+ if (room.isElementVideoRoom()) {
+ iconClass = "mx_RoomInfoLine_video";
+ roomType = _t("Video room");
+ } else if (joinRule === JoinRule.Public) {
+ iconClass = "mx_RoomInfoLine_public";
+ roomType = room.isSpaceRoom() ? _t("Public space") : _t("Public room");
+ } else {
+ iconClass = "mx_RoomInfoLine_private";
+ roomType = room.isSpaceRoom() ? _t("Private space") : _t("Private room");
+ }
+
+ let members: JSX.Element;
+ if (membership === "invite" && summary) {
+ // Don't trust local state and instead use the summary API
+ members =
+ { _t("%(count)s members", { count: summary.num_joined_members }) }
+ ;
+ } else if (memberCount && summary !== undefined) { // summary is not still loading
+ const viewMembers = () => RightPanelStore.instance.setCard({
+ phase: room.isSpaceRoom() ? RightPanelPhases.SpaceMemberList : RightPanelPhases.RoomMemberList,
+ });
+
+ members =
+ { _t("%(count)s members", { count: memberCount }) }
+ ;
+ }
+
+ return
+ { roomType }
+ { members }
+
;
+};
+
+export default RoomInfoLine;
diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx
index 5beb61144d3..0f7e9c5004b 100644
--- a/src/components/views/rooms/RoomList.tsx
+++ b/src/components/views/rooms/RoomList.tsx
@@ -16,9 +16,8 @@ limitations under the License.
import React, { ComponentType, createRef, ReactComponentElement, RefObject } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
-import { RoomType } from "matrix-js-sdk/src/@types/event";
+import { RoomType, EventType } from "matrix-js-sdk/src/@types/event";
import * as fbEmitter from "fbemitter";
-import { EventType } from "matrix-js-sdk/src/@types/event";
import { _t, _td } from "../../../languageHandler";
import { IState as IRovingTabIndexState, RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
diff --git a/src/components/views/rooms/RoomListHeader.tsx b/src/components/views/rooms/RoomListHeader.tsx
index 1f3e8fcf9fa..530d25fde44 100644
--- a/src/components/views/rooms/RoomListHeader.tsx
+++ b/src/components/views/rooms/RoomListHeader.tsx
@@ -61,8 +61,8 @@ import { UIComponent } from "../../../settings/UIFeature";
const contextMenuBelow = (elementRect: DOMRect) => {
// align the context menu's icons with the icon which opened the context menu
- const left = elementRect.left + window.pageXOffset;
- const top = elementRect.bottom + window.pageYOffset + 12;
+ const left = elementRect.left + window.scrollX;
+ const top = elementRect.bottom + window.scrollY + 12;
const chevronFace = ChevronFace.None;
return { left, top, chevronFace };
};
diff --git a/src/components/views/rooms/RoomPreviewCard.tsx b/src/components/views/rooms/RoomPreviewCard.tsx
new file mode 100644
index 00000000000..71b22765743
--- /dev/null
+++ b/src/components/views/rooms/RoomPreviewCard.tsx
@@ -0,0 +1,196 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, { FC, useContext, useState } from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { JoinRule } from "matrix-js-sdk/src/@types/partials";
+
+import { _t } from "../../../languageHandler";
+import defaultDispatcher from "../../../dispatcher/dispatcher";
+import { Action } from "../../../dispatcher/actions";
+import { UserTab } from "../dialogs/UserTab";
+import { EffectiveMembership, getEffectiveMembership } from "../../../utils/membership";
+import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import { useDispatcher } from "../../../hooks/useDispatcher";
+import { useFeatureEnabled } from "../../../hooks/useSettings";
+import { useRoomState } from "../../../hooks/useRoomState";
+import { useMyRoomMembership } from "../../../hooks/useRoomMembers";
+import AccessibleButton from "../elements/AccessibleButton";
+import InlineSpinner from "../elements/InlineSpinner";
+import RoomName from "../elements/RoomName";
+import RoomTopic from "../elements/RoomTopic";
+import RoomFacePile from "../elements/RoomFacePile";
+import RoomAvatar from "../avatars/RoomAvatar";
+import MemberAvatar from "../avatars/MemberAvatar";
+import RoomInfoLine from "./RoomInfoLine";
+
+interface IProps {
+ room: Room;
+ onJoinButtonClicked: () => void;
+ onRejectButtonClicked: () => void;
+}
+
+// XXX This component is currently only used for spaces and video rooms, though
+// surely we should expand its use to all rooms for consistency? This already
+// handles the text room case, though we would need to add support for ignoring
+// and viewing invite reasons to achieve parity with the default invite screen.
+const RoomPreviewCard: FC = ({ room, onJoinButtonClicked, onRejectButtonClicked }) => {
+ const cli = useContext(MatrixClientContext);
+ const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
+ const myMembership = useMyRoomMembership(room);
+ useDispatcher(defaultDispatcher, payload => {
+ if (payload.action === Action.JoinRoomError && payload.roomId === room.roomId) {
+ setBusy(false); // stop the spinner, join failed
+ }
+ });
+
+ const [busy, setBusy] = useState(false);
+
+ const joinRule = useRoomState(room, state => state.getJoinRule());
+ const cannotJoin = getEffectiveMembership(myMembership) === EffectiveMembership.Leave
+ && joinRule !== JoinRule.Public;
+
+ const viewLabs = () => defaultDispatcher.dispatch({
+ action: Action.ViewUserSettings,
+ initialTabId: UserTab.Labs,
+ });
+
+ let inviterSection: JSX.Element;
+ let joinButtons: JSX.Element;
+ if (myMembership === "join") {
+ joinButtons = (
+ {
+ defaultDispatcher.dispatch({
+ action: "leave_room",
+ room_id: room.roomId,
+ });
+ }}
+ >
+ { _t("Leave") }
+
+ );
+ } else if (myMembership === "invite") {
+ const inviteSender = room.getMember(cli.getUserId())?.events.member?.getSender();
+ const inviter = inviteSender && room.getMember(inviteSender);
+
+ if (inviteSender) {
+ inviterSection =
+
+
+
+ { _t(" invites you", {}, {
+ inviter: () => { inviter?.name || inviteSender } ,
+ }) }
+
+ { inviter ?
+ { inviteSender }
+
: null }
+
+
;
+ }
+
+ joinButtons = <>
+ {
+ setBusy(true);
+ onRejectButtonClicked();
+ }}
+ >
+ { _t("Reject") }
+
+ {
+ setBusy(true);
+ onJoinButtonClicked();
+ }}
+ >
+ { _t("Accept") }
+
+ >;
+ } else {
+ joinButtons = (
+ {
+ onJoinButtonClicked();
+ if (!cli.isGuest()) {
+ // user will be shown a modal that won't fire a room join error
+ setBusy(true);
+ }
+ }}
+ disabled={cannotJoin}
+ >
+ { _t("Join") }
+
+ );
+ }
+
+ if (busy) {
+ joinButtons = ;
+ }
+
+ let avatarRow: JSX.Element;
+ if (room.isElementVideoRoom()) {
+ avatarRow = <>
+
+
+ >;
+ } else if (room.isSpaceRoom()) {
+ avatarRow = ;
+ } else {
+ avatarRow = ;
+ }
+
+ let notice: string;
+ if (cannotJoin) {
+ notice = _t("To view %(roomName)s, you need an invite", {
+ roomName: room.name,
+ });
+ } else if (room.isElementVideoRoom() && !videoRoomsEnabled) {
+ notice = myMembership === "join"
+ ? _t("To view, please enable video rooms in Labs first")
+ : _t("To join, please enable video rooms in Labs first");
+
+ joinButtons =
+ { _t("Show Labs settings") }
+ ;
+ }
+
+ return
+ { inviterSection }
+
+ { avatarRow }
+
+
+
+
+
+
+ { room.getJoinRule() === "public" &&
}
+ { notice ?
+ { notice }
+
: null }
+
+ { joinButtons }
+
+
;
+};
+
+export default RoomPreviewCard;
diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx
index 82d82cd12c1..24b0a66fb1d 100644
--- a/src/components/views/rooms/RoomSublist.tsx
+++ b/src/components/views/rooms/RoomSublist.tsx
@@ -39,7 +39,6 @@ import ContextMenu, {
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorithms/models";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
-import dis from "../../../dispatcher/dispatcher";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import NotificationBadge from "./NotificationBadge";
@@ -442,7 +441,7 @@ export default class RoomSublist extends React.Component {
}
if (room) {
- dis.dispatch({
+ defaultDispatcher.dispatch({
action: Action.ViewRoom,
room_id: room.roomId,
show_room_tile: true, // to make sure the room gets scrolled into view
diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx
index 7c3be7bedae..93c0336a4c9 100644
--- a/src/components/views/rooms/RoomTile.tsx
+++ b/src/components/views/rooms/RoomTile.tsx
@@ -24,7 +24,6 @@ import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
-import dis from '../../../dispatcher/dispatcher';
import defaultDispatcher from '../../../dispatcher/dispatcher';
import { Action } from "../../../dispatcher/actions";
import SettingsStore from "../../../settings/SettingsStore";
@@ -92,8 +91,8 @@ const messagePreviewId = (roomId: string) => `mx_RoomTile_messagePreview_${roomI
export const contextMenuBelow = (elementRect: PartialDOMRect) => {
// align the context menu's icons with the icon which opened the context menu
- const left = elementRect.left + window.pageXOffset - 9;
- const top = elementRect.bottom + window.pageYOffset + 17;
+ const left = elementRect.left + window.scrollX - 9;
+ const top = elementRect.bottom + window.scrollY + 17;
const chevronFace = ChevronFace.None;
return { left, top, chevronFace };
};
@@ -269,7 +268,7 @@ export default class RoomTile extends React.PureComponent {
const action = getKeyBindingsManager().getAccessibilityAction(ev);
- dis.dispatch({
+ defaultDispatcher.dispatch({
action: Action.ViewRoom,
show_room_tile: true, // make sure the room is visible in the list
room_id: this.props.room.roomId,
@@ -370,7 +369,7 @@ export default class RoomTile extends React.PureComponent {
const isApplied = RoomListStore.instance.getTagsForRoom(this.props.room).includes(tagId);
const removeTag = isApplied ? tagId : inverseTag;
const addTag = isApplied ? null : tagId;
- dis.dispatch(RoomListActions.tagRoom(
+ defaultDispatcher.dispatch(RoomListActions.tagRoom(
MatrixClientPeg.get(),
this.props.room,
removeTag,
@@ -395,7 +394,7 @@ export default class RoomTile extends React.PureComponent {
ev.preventDefault();
ev.stopPropagation();
- dis.dispatch({
+ defaultDispatcher.dispatch({
action: 'leave_room',
room_id: this.props.room.roomId,
});
@@ -408,7 +407,7 @@ export default class RoomTile extends React.PureComponent {
ev.preventDefault();
ev.stopPropagation();
- dis.dispatch({
+ defaultDispatcher.dispatch({
action: 'forget_room',
room_id: this.props.room.roomId,
});
@@ -419,7 +418,7 @@ export default class RoomTile extends React.PureComponent {
ev.preventDefault();
ev.stopPropagation();
- dis.dispatch({
+ defaultDispatcher.dispatch({
action: 'open_room_settings',
room_id: this.props.room.roomId,
});
@@ -432,7 +431,7 @@ export default class RoomTile extends React.PureComponent {
ev.preventDefault();
ev.stopPropagation();
- dis.dispatch({
+ defaultDispatcher.dispatch({
action: 'copy_room',
room_id: this.props.room.roomId,
});
@@ -443,7 +442,7 @@ export default class RoomTile extends React.PureComponent {
ev.preventDefault();
ev.stopPropagation();
- dis.dispatch({
+ defaultDispatcher.dispatch({
action: 'view_invite',
roomId: this.props.room.roomId,
});
@@ -744,12 +743,12 @@ export default class RoomTile extends React.PureComponent {
participantCount = this.state.videoMembers.size;
break;
case VideoStatus.Connecting:
- videoText = _t("Connecting...");
+ videoText = _t("Joining…");
videoActive = true;
participantCount = this.state.videoMembers.size;
break;
case VideoStatus.Connected:
- videoText = _t("Connected");
+ videoText = _t("Joined");
videoActive = true;
participantCount = this.state.jitsiParticipants.length;
}
diff --git a/src/components/views/rooms/SearchBar.tsx b/src/components/views/rooms/SearchBar.tsx
index 20a9e081a0c..aadc89efb2d 100644
--- a/src/components/views/rooms/SearchBar.tsx
+++ b/src/components/views/rooms/SearchBar.tsx
@@ -20,10 +20,10 @@ import classNames from "classnames";
import AccessibleButton from "../elements/AccessibleButton";
import { _t } from '../../../languageHandler';
-import DesktopBuildsNotice, { WarningKind } from "../elements/DesktopBuildsNotice";
import { PosthogScreenTracker } from '../../../PosthogTrackers';
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
+import SearchWarning, { WarningKind } from "../elements/SearchWarning";
interface IProps {
onCancelClick: () => void;
@@ -127,7 +127,7 @@ export default class SearchBar extends React.Component {
-
+
>
);
}
diff --git a/src/components/views/rooms/Stickerpicker.tsx b/src/components/views/rooms/Stickerpicker.tsx
index 3c24c241135..02748c0ea1d 100644
--- a/src/components/views/rooms/Stickerpicker.tsx
+++ b/src/components/views/rooms/Stickerpicker.tsx
@@ -26,7 +26,6 @@ import AccessibleButton from '../elements/AccessibleButton';
import WidgetUtils, { IWidgetEvent } from '../../../utils/WidgetUtils';
import PersistedElement from "../elements/PersistedElement";
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
-import SettingsStore from "../../../settings/SettingsStore";
import ContextMenu, { ChevronFace } from "../../structures/ContextMenu";
import { WidgetType } from "../../../widgets/WidgetType";
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
@@ -339,20 +338,12 @@ export default class Stickerpicker extends React.PureComponent {
* Launch the integration manager on the stickers integration page
*/
private launchManageIntegrations = (): void => {
- // TODO: Open the right integration manager for the widget
- if (SettingsStore.getValue("feature_many_integration_managers")) {
- IntegrationManagers.sharedInstance().openAll(
- this.props.room,
- `type_${WidgetType.STICKERPICKER.preferred}`,
- this.state.widgetId,
- );
- } else {
- IntegrationManagers.sharedInstance().getPrimaryManager().open(
- this.props.room,
- `type_${WidgetType.STICKERPICKER.preferred}`,
- this.state.widgetId,
- );
- }
+ // noinspection JSIgnoredPromiseFromCall
+ IntegrationManagers.sharedInstance().getPrimaryManager().open(
+ this.props.room,
+ `type_${WidgetType.STICKERPICKER.preferred}`,
+ this.state.widgetId,
+ );
};
public render(): JSX.Element {
diff --git a/src/components/views/rooms/ThreadSummary.tsx b/src/components/views/rooms/ThreadSummary.tsx
index 3cf3b03f9ac..56ed1f9ff87 100644
--- a/src/components/views/rooms/ThreadSummary.tsx
+++ b/src/components/views/rooms/ThreadSummary.tsx
@@ -14,16 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { useContext } from "react";
+import React, { useContext, useState } from "react";
import { Thread, ThreadEvent } from "matrix-js-sdk/src/models/thread";
-import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
+import { IContent, MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
import { _t } from "../../../languageHandler";
import { CardContext } from "../right_panel/context";
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import { showThread } from "../../../dispatcher/dispatch-actions/threads";
import PosthogTrackers from "../../../PosthogTrackers";
-import { useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
+import { useTypedEventEmitter, useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
import RoomContext from "../../../contexts/RoomContext";
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
import MemberAvatar from "../avatars/MemberAvatar";
@@ -76,18 +76,21 @@ export const ThreadMessagePreview = ({ thread, showDisplayname = false }: IPrevi
const cli = useContext(MatrixClientContext);
const lastReply = useTypedEventEmitterState(thread, ThreadEvent.Update, () => thread.replyToEvent);
- // track the replacing event id as a means to regenerate the thread message preview
- const replacingEventId = useTypedEventEmitterState(
- lastReply,
- MatrixEventEvent.Replaced,
- () => lastReply?.replacingEventId(),
- );
+ // track the content as a means to regenerate the thread message preview upon edits & decryption
+ const [content, setContent] = useState(lastReply?.getContent());
+ useTypedEventEmitter(lastReply, MatrixEventEvent.Replaced, () => {
+ setContent(lastReply.getContent());
+ });
+ const awaitDecryption = lastReply?.shouldAttemptDecryption() || lastReply?.isBeingDecrypted();
+ useTypedEventEmitter(awaitDecryption ? lastReply : null, MatrixEventEvent.Decrypted, () => {
+ setContent(lastReply.getContent());
+ });
const preview = useAsyncMemo(async () => {
if (!lastReply) return;
await cli.decryptEventIfNeeded(lastReply);
return MessagePreviewStore.instance.generatePreviewForEvent(lastReply);
- }, [lastReply, replacingEventId]);
+ }, [lastReply, content]);
if (!preview) return null;
return <>
@@ -101,7 +104,7 @@ export const ThreadMessagePreview = ({ thread, showDisplayname = false }: IPrevi
{ showDisplayname &&
{ lastReply.sender?.name ?? lastReply.getSender() }
}
-
+
{ preview }
diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx
index 84898c8d086..27201bedbab 100644
--- a/src/components/views/rooms/VoiceRecordComposerTile.tsx
+++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx
@@ -196,7 +196,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent
{
if (content.creator) {
creator = { _t("This bridge was provisioned by .", {}, {
user: () => {
const bot = { _t("This bridge is managed by .", {}, {
user: () => {
await EventIndexPeg.initEventIndex();
await EventIndexPeg.get().addInitialCheckpoints();
- await EventIndexPeg.get().startCrawler();
+ EventIndexPeg.get().startCrawler();
await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, true);
await this.updateState();
};
diff --git a/src/components/views/settings/KeyboardShortcut.tsx b/src/components/views/settings/KeyboardShortcut.tsx
index 14dcf77d241..3e4f65b8c58 100644
--- a/src/components/views/settings/KeyboardShortcut.tsx
+++ b/src/components/views/settings/KeyboardShortcut.tsx
@@ -18,7 +18,7 @@ import React from "react";
import { ALTERNATE_KEY_NAME, KEY_ICON } from "../../../accessibility/KeyboardShortcuts";
import { KeyCombo } from "../../../KeyBindingsManager";
-import { isMac, Key } from "../../../Keyboard";
+import { IS_MAC, Key } from "../../../Keyboard";
import { _t } from "../../../languageHandler";
interface IKeyboardKeyProps {
@@ -45,7 +45,7 @@ export const KeyboardShortcut: React.FC = ({ value }) =>
const modifiersElement = [];
if (value.ctrlOrCmdKey) {
- modifiersElement.push( );
+ modifiersElement.push( );
} else if (value.ctrlKey) {
modifiersElement.push( );
} else if (value.metaKey) {
diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx
index 36d3e55d852..e0874084251 100644
--- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx
+++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx
@@ -211,8 +211,8 @@ export default class GeneralUserSettingsTab extends React.Component
Mozilla Foundation
used under the terms of
- Apache 2.0 .
+ Apache 2.0 .
The
diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx b/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx
index 9e0b31b2ad5..14b27b0515e 100644
--- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx
+++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx
@@ -92,14 +92,6 @@ export default class LabsUserSettingsTab extends React.Component<{}, IState> {
);
});
- groups.getOrCreate(LabGroup.Widgets, []).push(
- ,
- );
-
groups.getOrCreate(LabGroup.Experimental, []).push(
{
- // persist that the user has interacted with this, use it to dismiss the beta dot
- localStorage.setItem("mx_seenSpaces", "1");
if (!isPanelCollapsed) setPanelCollapsed(true);
openMenu();
};
- let betaDot: JSX.Element;
- if (!localStorage.getItem("mx_seenSpaces") && !SpaceStore.instance.spacePanelSpaces.length) {
- betaDot =
;
- }
-
return
- { betaDot }
{ contextMenu }
;
@@ -365,7 +357,7 @@ const SpacePanel = () => {
{ isPanelCollapsed ? _t("Expand") : _t("Collapse") }
- { isMac
+ { IS_MAC
? "⌘ + ⇧ + D"
: _t(ALTERNATE_KEY_NAME[Key.CONTROL]) + " + " +
_t(ALTERNATE_KEY_NAME[Key.SHIFT]) + " + D"
diff --git a/src/components/views/spaces/SpaceSettingsGeneralTab.tsx b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx
index b572122da1b..8247ee25e93 100644
--- a/src/components/views/spaces/SpaceSettingsGeneralTab.tsx
+++ b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx
@@ -25,8 +25,8 @@ import AccessibleButton from "../elements/AccessibleButton";
import SpaceBasicSettings from "./SpaceBasicSettings";
import { avatarUrlForRoom } from "../../../Avatar";
import { IDialogProps } from "../dialogs/IDialogProps";
-import { getTopic } from "../elements/RoomTopic";
import { leaveSpace } from "../../../utils/leave-behaviour";
+import { getTopic } from "../../../hooks/room/useTopic";
interface IProps extends IDialogProps {
matrixClient: MatrixClient;
diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx
index 6acd9845ef3..b038dbb7e46 100644
--- a/src/components/views/spaces/SpaceTreeLevel.tsx
+++ b/src/components/views/spaces/SpaceTreeLevel.tsx
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { ComponentProps, ComponentType, createRef, InputHTMLAttributes, LegacyRef } from "react";
+import React, { MouseEvent, ComponentProps, ComponentType, createRef, InputHTMLAttributes, LegacyRef } from "react";
import classNames from "classnames";
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { DraggableProvidedDragHandleProps } from "react-beautiful-dnd";
@@ -25,6 +25,8 @@ import { SpaceKey } from "../../../stores/spaces";
import SpaceTreeLevelLayoutStore from "../../../stores/spaces/SpaceTreeLevelLayoutStore";
import NotificationBadge from "../rooms/NotificationBadge";
import { _t } from "../../../languageHandler";
+import defaultDispatcher from "../../../dispatcher/dispatcher";
+import { Action } from "../../../dispatcher/actions";
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
import { toRightOf, useContextMenu } from "../../structures/ContextMenu";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
@@ -82,9 +84,15 @@ export const SpaceButton: React.FC
= ({
ariaLabel = _t("Jump to first invite.");
}
+ const jumpToNotification = (ev: MouseEvent) => {
+ ev.stopPropagation();
+ ev.preventDefault();
+ SpaceStore.instance.setActiveRoomInSpace(spaceKey ?? space.roomId);
+ };
+
notifBadge =
SpaceStore.instance.setActiveRoomInSpace(spaceKey ?? space.roomId)}
+ onClick={jumpToNotification}
forceCount={false}
notification={notificationState}
aria-label={ariaLabel}
@@ -103,6 +111,10 @@ export const SpaceButton: React.FC = ({
/>;
}
+ const viewSpaceHome = () => defaultDispatcher.dispatch({ action: Action.ViewRoom, room_id: space.roomId });
+ const activateSpace = () => SpaceStore.instance.setActiveSpace(spaceKey ?? space.roomId);
+ const onClick = props.onClick ?? (selected && space ? viewSpaceHome : activateSpace);
+
return (
= ({
mx_SpaceButton_narrow: isNarrow,
})}
title={label}
- onClick={spaceKey ? () => SpaceStore.instance.setActiveSpace(spaceKey) : props.onClick}
+ onClick={onClick}
onContextMenu={openMenu}
forceHide={!isNarrow || menuDisplayed}
inputRef={handle}
@@ -261,12 +273,6 @@ export class SpaceItem extends React.PureComponent {
}
};
- private onClick = (ev: React.MouseEvent) => {
- ev.preventDefault();
- ev.stopPropagation();
- SpaceStore.instance.setActiveSpace(this.props.space.roomId);
- };
-
render() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { space, activeSpaces, isNested, isPanelCollapsed, onExpand, parents, innerRef, dragHandleProps,
@@ -328,7 +334,6 @@ export class SpaceItem extends React.PureComponent {
notificationState={notificationState}
isNarrow={isPanelCollapsed}
avatarSize={isNested ? 24 : 32}
- onClick={this.onClick}
onKeyDown={this.onKeyDown}
ContextMenuComponent={this.props.space.getMyMembership() === "join" ? SpaceContextMenu : undefined}
>
diff --git a/src/components/views/toasts/VerificationRequestToast.tsx b/src/components/views/toasts/VerificationRequestToast.tsx
index b424f439006..3786f5c858a 100644
--- a/src/components/views/toasts/VerificationRequestToast.tsx
+++ b/src/components/views/toasts/VerificationRequestToast.tsx
@@ -66,7 +66,7 @@ export default class VerificationRequestToast extends React.PureComponent
+Copyright 2021 - 2022 Šimon Brandner
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { createRef, CSSProperties } from 'react';
+import React, { createRef } from 'react';
import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import classNames from 'classnames';
import { CallFeed } from 'matrix-js-sdk/src/webrtc/callFeed';
@@ -36,6 +36,7 @@ import CallViewSidebar from './CallViewSidebar';
import CallViewHeader from './CallView/CallViewHeader';
import CallViewButtons from "./CallView/CallViewButtons";
import PlatformPeg from "../../../PlatformPeg";
+import { ActionPayload } from "../../../dispatcher/payloads";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
@@ -69,8 +70,9 @@ interface IState {
vidMuted: boolean;
screensharing: boolean;
callState: CallState;
- primaryFeed: CallFeed;
- secondaryFeeds: Array;
+ primaryFeed?: CallFeed;
+ secondaryFeed?: CallFeed;
+ sidebarFeeds: Array;
sidebarShown: boolean;
}
@@ -104,13 +106,13 @@ function exitFullscreen() {
export default class CallView extends React.Component {
private dispatcherRef: string;
- private contentRef = createRef();
+ private contentWrapperRef = createRef();
private buttonsRef = createRef();
constructor(props: IProps) {
super(props);
- const { primary, secondary } = CallView.getOrderedFeeds(this.props.call.getFeeds());
+ const { primary, secondary, sidebar } = CallView.getOrderedFeeds(this.props.call.getFeeds());
this.state = {
isLocalOnHold: this.props.call.isLocalOnHold(),
@@ -120,19 +122,20 @@ export default class CallView extends React.Component {
screensharing: this.props.call.isScreensharing(),
callState: this.props.call.state,
primaryFeed: primary,
- secondaryFeeds: secondary,
+ secondaryFeed: secondary,
+ sidebarFeeds: sidebar,
sidebarShown: true,
};
this.updateCallListeners(null, this.props.call);
}
- public componentDidMount() {
+ public componentDidMount(): void {
this.dispatcherRef = dis.register(this.onAction);
document.addEventListener('keydown', this.onNativeKeyDown);
}
- public componentWillUnmount() {
+ public componentWillUnmount(): void {
if (getFullScreenElement()) {
exitFullscreen();
}
@@ -143,11 +146,12 @@ export default class CallView extends React.Component {
}
static getDerivedStateFromProps(props: IProps): Partial {
- const { primary, secondary } = CallView.getOrderedFeeds(props.call.getFeeds());
+ const { primary, secondary, sidebar } = CallView.getOrderedFeeds(props.call.getFeeds());
return {
primaryFeed: primary,
- secondaryFeeds: secondary,
+ secondaryFeed: secondary,
+ sidebarFeeds: sidebar,
};
}
@@ -165,14 +169,14 @@ export default class CallView extends React.Component {
this.updateCallListeners(null, this.props.call);
}
- private onAction = (payload) => {
+ private onAction = (payload: ActionPayload): void => {
switch (payload.action) {
case 'video_fullscreen': {
- if (!this.contentRef.current) {
+ if (!this.contentWrapperRef.current) {
return;
}
if (payload.fullscreen) {
- requestFullscreen(this.contentRef.current);
+ requestFullscreen(this.contentWrapperRef.current);
} else if (getFullScreenElement()) {
exitFullscreen();
}
@@ -181,7 +185,7 @@ export default class CallView extends React.Component {
}
};
- private updateCallListeners(oldCall: MatrixCall, newCall: MatrixCall) {
+ private updateCallListeners(oldCall: MatrixCall, newCall: MatrixCall): void {
if (oldCall === newCall) return;
if (oldCall) {
@@ -198,29 +202,30 @@ export default class CallView extends React.Component {
}
}
- private onCallState = (state) => {
+ private onCallState = (state: CallState): void => {
this.setState({
callState: state,
});
};
- private onFeedsChanged = (newFeeds: Array) => {
- const { primary, secondary } = CallView.getOrderedFeeds(newFeeds);
+ private onFeedsChanged = (newFeeds: Array): void => {
+ const { primary, secondary, sidebar } = CallView.getOrderedFeeds(newFeeds);
this.setState({
primaryFeed: primary,
- secondaryFeeds: secondary,
+ secondaryFeed: secondary,
+ sidebarFeeds: sidebar,
micMuted: this.props.call.isMicrophoneMuted(),
vidMuted: this.props.call.isLocalVideoMuted(),
});
};
- private onCallLocalHoldUnhold = () => {
+ private onCallLocalHoldUnhold = (): void => {
this.setState({
isLocalOnHold: this.props.call.isLocalOnHold(),
});
};
- private onCallRemoteHoldUnhold = () => {
+ private onCallRemoteHoldUnhold = (): void => {
this.setState({
isRemoteOnHold: this.props.call.isRemoteOnHold(),
// update both here because isLocalOnHold changes when we hold the call too
@@ -228,12 +233,22 @@ export default class CallView extends React.Component {
});
};
- private onMouseMove = () => {
+ private onMouseMove = (): void => {
this.buttonsRef.current?.showControls();
};
- static getOrderedFeeds(feeds: Array): { primary: CallFeed, secondary: Array } {
- let primary;
+ static getOrderedFeeds(
+ feeds: Array,
+ ): { primary?: CallFeed, secondary?: CallFeed, sidebar: Array } {
+ if (feeds.length <= 2) {
+ return {
+ primary: feeds.find((feed) => !feed.isLocal()),
+ secondary: feeds.find((feed) => feed.isLocal()),
+ sidebar: [],
+ };
+ }
+
+ let primary: CallFeed;
// Try to use a screensharing as primary, a remote one if possible
const screensharingFeeds = feeds.filter((feed) => feed.purpose === SDPStreamMetadataPurpose.Screenshare);
@@ -243,18 +258,25 @@ export default class CallView extends React.Component {
primary = feeds.find((feed) => !feed.isLocal());
}
- const secondary = [...feeds];
+ const sidebar = [...feeds];
// Remove the primary feed from the array
- if (primary) secondary.splice(secondary.indexOf(primary), 1);
- secondary.sort((a, b) => {
+ if (primary) sidebar.splice(sidebar.indexOf(primary), 1);
+ sidebar.sort((a, b) => {
if (a.isLocal() && !b.isLocal()) return -1;
if (!a.isLocal() && b.isLocal()) return 1;
return 0;
});
- return { primary, secondary };
+ return { primary, sidebar };
}
+ private onMaximizeClick = (): void => {
+ dis.dispatch({
+ action: 'video_fullscreen',
+ fullscreen: true,
+ });
+ };
+
private onMicMuteClick = async (): Promise => {
const newVal = !this.state.micMuted;
this.setState({ micMuted: await this.props.call.setMicrophoneMuted(newVal) });
@@ -336,7 +358,7 @@ export default class CallView extends React.Component {
private renderCallControls(): JSX.Element {
const { call, pipMode } = this.props;
- const { primaryFeed, callState, micMuted, vidMuted, screensharing, sidebarShown } = this.state;
+ const { callState, micMuted, vidMuted, screensharing, sidebarShown, secondaryFeed, sidebarFeeds } = this.state;
// If SDPStreamMetadata isn't supported don't show video mute button in voice calls
const vidMuteButtonShown = call.opponentSupportsSDPStreamMetadata() || call.hasLocalUserMediaVideoTrack;
@@ -348,13 +370,8 @@ export default class CallView extends React.Component {
(call.opponentSupportsSDPStreamMetadata() || call.hasLocalUserMediaVideoTrack) &&
call.state === CallState.Connected
);
- // To show the sidebar we need secondary feeds, if we don't have them,
- // we can hide this button. If we are in PiP, sidebar is also hidden, so
- // we can hide the button too
- const sidebarButtonShown = (
- primaryFeed?.purpose === SDPStreamMetadataPurpose.Screenshare ||
- call.isScreensharing()
- );
+ // Show the sidebar button only if there is something to hide/show
+ const sidebarButtonShown = (secondaryFeed && !secondaryFeed.isVideoMuted()) || sidebarFeeds.length > 0;
// The dial pad & 'more' button actions are only relevant in a connected call
const contextMenuButtonShown = callState === CallState.Connected;
const dialpadButtonShown = (
@@ -391,158 +408,126 @@ export default class CallView extends React.Component {
);
}
- public render() {
- const client = MatrixClientPeg.get();
- const callRoomId = CallHandler.instance.roomIdForCall(this.props.call);
- const secondaryCallRoomId = CallHandler.instance.roomIdForCall(this.props.secondaryCall);
- const callRoom = client.getRoom(callRoomId);
- const secCallRoom = this.props.secondaryCall ? client.getRoom(secondaryCallRoomId) : null;
- const avatarSize = this.props.pipMode ? 76 : 160;
- const transfereeCall = CallHandler.instance.getTransfereeForCallId(this.props.call.callId);
- const isOnHold = this.state.isLocalOnHold || this.state.isRemoteOnHold;
- const isScreensharing = this.props.call.isScreensharing();
- const sidebarShown = this.state.sidebarShown;
- const someoneIsScreensharing = this.props.call.getFeeds().some((feed) => {
+ private renderToast(): JSX.Element {
+ const { call } = this.props;
+ const someoneIsScreensharing = call.getFeeds().some((feed) => {
return feed.purpose === SDPStreamMetadataPurpose.Screenshare;
});
- const call = this.props.call;
- let contentView: React.ReactNode;
- let holdTransferContent;
+ if (!someoneIsScreensharing) return null;
- if (transfereeCall) {
- const transferTargetRoom = MatrixClientPeg.get().getRoom(
- CallHandler.instance.roomIdForCall(this.props.call),
- );
- const transferTargetName = transferTargetRoom ? transferTargetRoom.name : _t("unknown person");
+ const isScreensharing = call.isScreensharing();
+ const { primaryFeed, sidebarShown } = this.state;
+ const sharerName = primaryFeed.getMember().name;
- const transfereeRoom = MatrixClientPeg.get().getRoom(
- CallHandler.instance.roomIdForCall(transfereeCall),
- );
- const transfereeName = transfereeRoom ? transfereeRoom.name : _t("unknown person");
-
- holdTransferContent =
- { _t(
- "Consulting with %(transferTarget)s.
Transfer to %(transferee)s ",
- {
- transferTarget: transferTargetName,
- transferee: transfereeName,
- },
- {
- a: sub =>
- { sub }
- ,
- },
- ) }
-
;
- } else if (isOnHold) {
- let onHoldText = null;
- if (this.state.isRemoteOnHold) {
- const holdString = CallHandler.instance.hasAnyUnheldCall() ?
- _td("You held the call Switch ") : _td("You held the call Resume ");
- onHoldText = _t(holdString, {}, {
- a: sub =>
- { sub }
- ,
- });
- } else if (this.state.isLocalOnHold) {
- onHoldText = _t("%(peerName)s held the call", {
- peerName: this.props.call.getOpponentMember().name,
- });
- }
- holdTransferContent =
- { onHoldText }
-
;
+ let text = isScreensharing
+ ? _t("You are presenting")
+ : _t('%(sharerName)s is presenting', { sharerName });
+ if (!sidebarShown) {
+ text += " • " + (call.isLocalVideoMuted()
+ ? _t("Your camera is turned off")
+ : _t("Your camera is still enabled"));
}
- let sidebar;
- if (
- !isOnHold &&
- !transfereeCall &&
- sidebarShown &&
- (call.hasLocalUserMediaVideoTrack || someoneIsScreensharing)
- ) {
- sidebar = (
-
+ { text }
+
+ );
+ }
+
+ private renderContent(): JSX.Element {
+ const { pipMode, call, onResize } = this.props;
+ const { isLocalOnHold, isRemoteOnHold, sidebarShown, primaryFeed, secondaryFeed, sidebarFeeds } = this.state;
+
+ const callRoom = MatrixClientPeg.get().getRoom(call.roomId);
+ const avatarSize = pipMode ? 76 : 160;
+ const transfereeCall = CallHandler.instance.getTransfereeForCallId(call.callId);
+ const isOnHold = isLocalOnHold || isRemoteOnHold;
+
+ let secondaryFeedElement: React.ReactNode;
+ if (sidebarShown && secondaryFeed && !secondaryFeed.isVideoMuted()) {
+ secondaryFeedElement = (
+
);
}
- // This is a bit messy. I can't see a reason to have two onHold/transfer screens
- if (isOnHold || transfereeCall) {
- if (call.hasLocalUserMediaVideoTrack || call.hasRemoteUserMediaVideoTrack) {
- const containerClasses = classNames({
- mx_CallView_content: true,
- mx_CallView_video: true,
- mx_CallView_video_hold: isOnHold,
- });
- let onHoldBackground = null;
- const backgroundStyle: CSSProperties = {};
- const backgroundAvatarUrl = avatarUrlForMember(
- // is it worth getting the size of the div to pass here?
- this.props.call.getOpponentMember(), 1024, 1024, 'crop',
+ if (transfereeCall || isOnHold) {
+ const containerClasses = classNames("mx_CallView_content", {
+ mx_CallView_content_hold: isOnHold,
+ });
+ const backgroundAvatarUrl = avatarUrlForMember(call.getOpponentMember(), 1024, 1024, 'crop');
+
+ let holdTransferContent: React.ReactNode;
+ if (transfereeCall) {
+ const transferTargetRoom = MatrixClientPeg.get().getRoom(
+ CallHandler.instance.roomIdForCall(call),
);
- backgroundStyle.backgroundImage = 'url(' + backgroundAvatarUrl + ')';
- onHoldBackground =
;
-
- contentView = (
-
- { onHoldBackground }
- { holdTransferContent }
- { this.renderCallControls() }
-
+ const transferTargetName = transferTargetRoom ? transferTargetRoom.name : _t("unknown person");
+ const transfereeRoom = MatrixClientPeg.get().getRoom(
+ CallHandler.instance.roomIdForCall(transfereeCall),
);
+ const transfereeName = transfereeRoom ? transfereeRoom.name : _t("unknown person");
+
+ holdTransferContent =
+ { _t(
+ "Consulting with %(transferTarget)s.
Transfer to %(transferee)s ",
+ {
+ transferTarget: transferTargetName,
+ transferee: transfereeName,
+ },
+ {
+ a: sub =>
+ { sub }
+ ,
+ },
+ ) }
+
;
} else {
- const classes = classNames({
- mx_CallView_content: true,
- mx_CallView_voice: true,
- mx_CallView_voice_hold: isOnHold,
- });
-
- contentView = (
-
-
- { holdTransferContent }
- { this.renderCallControls() }
+ let onHoldText: React.ReactNode;
+ if (isRemoteOnHold) {
+ onHoldText = _t(
+ CallHandler.instance.hasAnyUnheldCall()
+ ? _td("You held the call
Switch ")
+ : _td("You held the call
Resume "),
+ {},
+ {
+ a: sub =>
+ { sub }
+ ,
+ },
+ );
+ } else if (isLocalOnHold) {
+ onHoldText = _t("%(peerName)s held the call", {
+ peerName: call.getOpponentMember().name,
+ });
+ }
+
+ holdTransferContent = (
+
+ { onHoldText }
);
}
- } else if (this.props.call.noIncomingFeeds()) {
- // Here we're reusing the css classes from voice on hold, because
- // I am lazy. If this gets merged, the CallView might be subject
- // to change anyway - I might take an axe to this file in order to
- // try to get other things working
- const classes = classNames({
- mx_CallView_content: true,
- mx_CallView_voice: true,
- });
- // Saying "Connecting" here isn't really true, but the best thing
- // I can come up with, but this might be subject to change as well
- contentView = (
-
- { sidebar }
-
+ return (
+
+
+ { holdTransferContent }
+
+ );
+ } else if (call.noIncomingFeeds()) {
+ return (
+
+
-
{ _t("Connecting") }
- { this.renderCallControls() }
+
{ _t("Connecting") }
+ { secondaryFeedElement }
);
- } else {
- const containerClasses = classNames({
- mx_CallView_content: true,
- mx_CallView_video: true,
- });
-
- let toast;
- if (someoneIsScreensharing) {
- const sharerName = this.state.primaryFeed.getMember().name;
- let text = isScreensharing
- ? _t("You are presenting")
- : _t('%(sharerName)s is presenting', { sharerName });
- if (!this.state.sidebarShown) {
- text += " • " + (this.props.call.isLocalVideoMuted()
- ? _t("Your camera is turned off")
- : _t("Your camera is still enabled"));
- }
-
- toast = (
-
- { text }
-
- );
- }
-
- contentView = (
+ } else if (pipMode) {
+ return (
- { toast }
- { sidebar }
- { this.renderCallControls() }
+
+ );
+ } else if (secondaryFeed) {
+ return (
+
+
+ { secondaryFeedElement }
+
+ );
+ } else {
+ return (
+
+
+ { sidebarShown && }
);
}
+ }
+
+ public render(): JSX.Element {
+ const {
+ call,
+ secondaryCall,
+ pipMode,
+ showApps,
+ onMouseDownOnHeader,
+ } = this.props;
+ const {
+ sidebarShown,
+ sidebarFeeds,
+ } = this.state;
+
+ const client = MatrixClientPeg.get();
+ const callRoomId = CallHandler.instance.roomIdForCall(call);
+ const secondaryCallRoomId = CallHandler.instance.roomIdForCall(secondaryCall);
+ const callRoom = client.getRoom(callRoomId);
+ const secCallRoom = secondaryCall ? client.getRoom(secondaryCallRoomId) : null;
const callViewClasses = classNames({
mx_CallView: true,
- mx_CallView_pip: this.props.pipMode,
- mx_CallView_large: !this.props.pipMode,
- mx_CallView_belowWidget: this.props.showApps, // css to correct the margins if the call is below the AppsDrawer.
+ mx_CallView_pip: pipMode,
+ mx_CallView_large: !pipMode,
+ mx_CallView_sidebar: sidebarShown && sidebarFeeds.length !== 0 && !pipMode,
+ mx_CallView_belowWidget: showApps, // css to correct the margins if the call is below the AppsDrawer.
});
return
- { contentView }
+
+ { this.renderToast() }
+ { this.renderContent() }
+ { this.renderCallControls() }
+
;
}
}
diff --git a/src/components/views/voip/CallView/CallViewButtons.tsx b/src/components/views/voip/CallView/CallViewButtons.tsx
index 1d373694b39..d8f07c826bf 100644
--- a/src/components/views/voip/CallView/CallViewButtons.tsx
+++ b/src/components/views/voip/CallView/CallViewButtons.tsx
@@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { createRef } from "react";
+import React, { createRef, useState } from "react";
import classNames from "classnames";
import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
@@ -26,28 +26,37 @@ import DialpadContextMenu from "../../context_menus/DialpadContextMenu";
import { Alignment } from "../../elements/Tooltip";
import {
alwaysAboveLeftOf,
+ alwaysAboveRightOf,
ChevronFace,
ContextMenuTooltipButton,
+ useContextMenu,
} from '../../../structures/ContextMenu';
import { _t } from "../../../../languageHandler";
+import DeviceContextMenu from "../../context_menus/DeviceContextMenu";
+import { MediaDeviceKindEnum } from "../../../../MediaDeviceHandler";
// Height of the header duplicated from CSS because we need to subtract it from our max
// height to get the max height of the video
const CONTEXT_MENU_VPADDING = 8; // How far the context menu sits above the button (px)
-const TOOLTIP_Y_OFFSET = -24;
-
const CONTROLS_HIDE_DELAY = 2000;
-interface IButtonProps {
+interface IButtonProps extends Omit
, "title"> {
state: boolean;
className: string;
- onLabel: string;
- offLabel: string;
- onClick: () => void;
+ onLabel?: string;
+ offLabel?: string;
+ onClick: (event: React.MouseEvent) => void;
}
-const CallViewToggleButton: React.FC = ({ state: isOn, className, onLabel, offLabel, onClick }) => {
+const CallViewToggleButton: React.FC = ({
+ children,
+ state: isOn,
+ className,
+ onLabel,
+ offLabel,
+ ...props
+}) => {
const classes = classNames("mx_CallViewButtons_button", className, {
mx_CallViewButtons_button_on: isOn,
mx_CallViewButtons_button_off: !isOn,
@@ -56,11 +65,47 @@ const CallViewToggleButton: React.FC = ({ state: isOn, className,
return (
+ {...props}
+ >
+ { children }
+
+ );
+};
+
+interface IDropdownButtonProps extends IButtonProps {
+ deviceKinds: MediaDeviceKindEnum[];
+}
+
+const CallViewDropdownButton: React.FC = ({ state, deviceKinds, ...props }) => {
+ const [menuDisplayed, buttonRef, openMenu, closeMenu] = useContextMenu();
+ const [hoveringDropdown, setHoveringDropdown] = useState(false);
+
+ const classes = classNames("mx_CallViewButtons_button", "mx_CallViewButtons_dropdownButton", {
+ mx_CallViewButtons_dropdownButton_collapsed: !menuDisplayed,
+ });
+
+ const onClick = (event: React.MouseEvent): void => {
+ event.stopPropagation();
+ openMenu();
+ };
+
+ return (
+
+ setHoveringDropdown(hovering)}
+ state={state}
+ />
+ { menuDisplayed && }
+
);
};
@@ -219,21 +264,22 @@ export default class CallViewButtons extends React.Component {
isExpanded={this.state.showDialpad}
title={_t("Dialpad")}
alignment={Alignment.Top}
- yOffset={TOOLTIP_Y_OFFSET}
/> }
-
- { this.props.buttonsVisibility.vidMute && }
{ this.props.buttonsVisibility.screensharing && {
isExpanded={this.state.showMoreMenu}
title={_t("More")}
alignment={Alignment.Top}
- yOffset={TOOLTIP_Y_OFFSET}
/> }
);
diff --git a/src/components/views/voip/CallView/CallViewHeader.tsx b/src/components/views/voip/CallView/CallViewHeader.tsx
index 182ab2878a4..fd585994d79 100644
--- a/src/components/views/voip/CallView/CallViewHeader.tsx
+++ b/src/components/views/voip/CallView/CallViewHeader.tsx
@@ -19,50 +19,39 @@ import React from 'react';
import { _t } from '../../../../languageHandler';
import RoomAvatar from '../../avatars/RoomAvatar';
-import dis from '../../../../dispatcher/dispatcher';
-import { Action } from '../../../../dispatcher/actions';
import AccessibleTooltipButton from '../../elements/AccessibleTooltipButton';
-import { ViewRoomPayload } from "../../../../dispatcher/payloads/ViewRoomPayload";
-interface CallViewHeaderProps {
- pipMode: boolean;
- callRooms?: Room[];
- onPipMouseDown: (event: React.MouseEvent
) => void;
+interface CallControlsProps {
+ onExpand?: () => void;
+ onPin?: () => void;
+ onMaximize?: () => void;
}
-const onFullscreenClick = () => {
- dis.dispatch({
- action: 'video_fullscreen',
- fullscreen: true,
- });
-};
-
-const onExpandClick = (roomId: string) => {
- dis.dispatch({
- action: Action.ViewRoom,
- room_id: roomId,
- metricsTrigger: "WebFloatingCallWindow",
- });
-};
-
-type CallControlsProps = Pick & {
- roomId: string;
-};
-const CallViewHeaderControls: React.FC = ({ pipMode = false, roomId }) => {
+const CallViewHeaderControls: React.FC = ({ onExpand, onPin, onMaximize }) => {
return
- { !pipMode &&
}
- { pipMode &&
}
+ { onExpand &&
onExpandClick(roomId)}
+ onClick={onExpand}
title={_t("Return to call")}
/> }
;
};
-const SecondaryCallInfo: React.FC<{ callRoom: Room }> = ({ callRoom }) => {
+
+interface ISecondaryCallInfoProps {
+ callRoom: Room;
+}
+
+const SecondaryCallInfo: React.FC = ({ callRoom }) => {
return
@@ -71,19 +60,31 @@ const SecondaryCallInfo: React.FC<{ callRoom: Room }> = ({ callRoom }) => {
;
};
+interface CallViewHeaderProps {
+ pipMode: boolean;
+ callRooms?: Room[];
+ onPipMouseDown: (event: React.MouseEvent) => void;
+ onExpand?: () => void;
+ onPin?: () => void;
+ onMaximize?: () => void;
+}
+
const CallViewHeader: React.FC = ({
pipMode = false,
callRooms = [],
onPipMouseDown,
+ onExpand,
+ onPin,
+ onMaximize,
}) => {
const [callRoom, onHoldCallRoom] = callRooms;
- const { roomId, name: callRoomName } = callRoom;
+ const callRoomName = callRoom.name;
if (!pipMode) {
return ;
}
return (
@@ -96,7 +97,7 @@ const CallViewHeader: React.FC = ({
{ callRoomName }
{ onHoldCallRoom && }
-
+
);
};
diff --git a/src/components/views/voip/PipView.tsx b/src/components/views/voip/PipView.tsx
index db3ef0187dc..613a542d70f 100644
--- a/src/components/views/voip/PipView.tsx
+++ b/src/components/views/voip/PipView.tsx
@@ -19,6 +19,7 @@ import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call'
import { EventSubscription } from 'fbemitter';
import { logger } from "matrix-js-sdk/src/logger";
import classNames from 'classnames';
+import { Room } from "matrix-js-sdk/src/models/room";
import CallView from "./CallView";
import { RoomViewStore } from '../../../stores/RoomViewStore';
@@ -29,9 +30,10 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
import PictureInPictureDragger from './PictureInPictureDragger';
import dis from '../../../dispatcher/dispatcher';
import { Action } from "../../../dispatcher/actions";
-import { WidgetLayoutStore } from '../../../stores/widgets/WidgetLayoutStore';
+import { Container, WidgetLayoutStore } from '../../../stores/widgets/WidgetLayoutStore';
import CallViewHeader from './CallView/CallViewHeader';
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from '../../../stores/ActiveWidgetStore';
+import WidgetStore, { IApp } from "../../../stores/WidgetStore";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
const SHOW_CALL_IN_STATES = [
@@ -64,6 +66,16 @@ interface IState {
moving: boolean;
}
+const getRoomAndAppForWidget = (widgetId: string, roomId: string): [Room, IApp] => {
+ if (!widgetId) return;
+ if (!roomId) return;
+
+ const room = MatrixClientPeg.get().getRoom(roomId);
+ const app = WidgetStore.instance.getApps(roomId).find((app) => app.id === widgetId);
+
+ return [room, app];
+};
+
// Splits a list of calls into one 'primary' one and a list
// (which should be a single element) of other calls.
// The primary will be the one not on hold, or an arbitrary one
@@ -232,6 +244,38 @@ export default class PipView extends React.Component {
}
};
+ private onMaximize = (): void => {
+ const widgetId = this.state.persistentWidgetId;
+ const roomId = this.state.persistentRoomId;
+
+ if (this.state.showWidgetInPip && widgetId && roomId) {
+ const [room, app] = getRoomAndAppForWidget(widgetId, roomId);
+ WidgetLayoutStore.instance.moveToContainer(room, app, Container.Center);
+ } else {
+ dis.dispatch({
+ action: 'video_fullscreen',
+ fullscreen: true,
+ });
+ }
+ };
+
+ private onPin = (): void => {
+ if (!this.state.showWidgetInPip) return;
+
+ const [room, app] = getRoomAndAppForWidget(this.state.persistentWidgetId, this.state.persistentRoomId);
+ WidgetLayoutStore.instance.moveToContainer(room, app, Container.Top);
+ };
+
+ private onExpand = (): void => {
+ const widgetId = this.state.persistentWidgetId;
+ if (!widgetId || !this.state.showWidgetInPip) return;
+
+ dis.dispatch({
+ action: Action.ViewRoom,
+ room_id: this.state.persistentRoomId,
+ });
+ };
+
// Accepts a persistentWidgetId to be able to skip awaiting the setState for persistentWidgetId
public updateShowWidgetInPip(
persistentWidgetId = this.state.persistentWidgetId,
@@ -276,7 +320,9 @@ export default class PipView extends React.Component {
mx_CallView_pip: pipMode,
mx_CallView_large: !pipMode,
});
- const roomForWidget = MatrixClientPeg.get().getRoom(this.state.persistentRoomId);
+ const roomId = this.state.persistentRoomId;
+ const roomForWidget = MatrixClientPeg.get().getRoom(roomId);
+ const viewingCallRoom = this.state.viewedRoomId === roomId;
pipContent = ({ onStartMoving, _onResize }) =>
@@ -284,10 +330,13 @@ export default class PipView extends React.Component
{
onPipMouseDown={(event) => { onStartMoving(event); this.onStartMoving.bind(this)(); }}
pipMode={pipMode}
callRooms={[roomForWidget]}
+ onExpand={!viewingCallRoom && this.onExpand}
+ onPin={viewingCallRoom && this.onPin}
+ onMaximize={viewingCallRoom && this.onMaximize}
/>
;
diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx
index a9f83e9bc23..b9155fe51d5 100644
--- a/src/components/views/voip/VideoFeed.tsx
+++ b/src/components/views/voip/VideoFeed.tsx
@@ -1,5 +1,6 @@
/*
-Copyright 2015, 2016, 2019 The Matrix.org Foundation C.I.C.
+Copyright 2015, 2016, 2019, 2020, 2021 The Matrix.org Foundation C.I.C.
+Copyright 2021 - 2022 Šimon Brandner
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -39,7 +40,8 @@ interface IProps {
// due to a change in video metadata
onResize?: (e: Event) => void;
- primary: boolean;
+ primary?: boolean;
+ secondary?: boolean;
}
interface IState {
@@ -161,6 +163,7 @@ export default class VideoFeed extends React.PureComponent {
audioMuted: this.props.feed.isAudioMuted(),
videoMuted: this.props.feed.isVideoMuted(),
});
+ this.playMedia();
};
private onMuteStateChanged = () => {
@@ -177,9 +180,11 @@ export default class VideoFeed extends React.PureComponent {
};
render() {
- const { pipMode, primary, feed } = this.props;
+ const { pipMode, primary, secondary, feed } = this.props;
const wrapperClasses = classnames("mx_VideoFeed", {
+ mx_VideoFeed_primary: primary,
+ mx_VideoFeed_secondary: secondary,
mx_VideoFeed_voice: this.state.videoMuted,
});
const micIconClasses = classnames("mx_VideoFeed_mic", {
diff --git a/src/components/views/voip/VideoLobby.tsx b/src/components/views/voip/VideoLobby.tsx
index 862e58fe630..08de1e1f49b 100644
--- a/src/components/views/voip/VideoLobby.tsx
+++ b/src/components/views/voip/VideoLobby.tsx
@@ -185,7 +185,7 @@ const VideoLobby: FC<{ room: Room }> = ({ room }) => {
const overflow = connectedMembers.size > shownMembers.length;
facePile =
- { _t("%(count)s people connected", { count: connectedMembers.size }) }
+ { _t("%(count)s people joined", { count: connectedMembers.size }) }
;
}
@@ -232,7 +232,7 @@ const VideoLobby: FC<{ room: Room }> = ({ room }) => {
disabled={connecting}
onClick={connect}
>
- { _t("Connect now") }
+ { _t("Join") }
;
};
diff --git a/src/contexts/RoomContext.ts b/src/contexts/RoomContext.ts
index e4fe89ed0bc..09c0e29a6c6 100644
--- a/src/contexts/RoomContext.ts
+++ b/src/contexts/RoomContext.ts
@@ -35,7 +35,6 @@ const RoomContext = createContext
({
shouldPeek: true,
membersLoaded: false,
numUnreadMessages: 0,
- guestsCanJoin: false,
canPeek: false,
showApps: false,
isPeeking: false,
diff --git a/src/createRoom.ts b/src/createRoom.ts
index 66177d812b1..955e3a5707a 100644
--- a/src/createRoom.ts
+++ b/src/createRoom.ts
@@ -132,8 +132,6 @@ export default async function createRoom(opts: IOpts): Promise {
events: {
// Allow all users to send video member updates
[VIDEO_CHANNEL_MEMBER]: 0,
- // Make widgets immutable, even to admins
- "im.vector.modular.widgets": 200,
// Annoyingly, we have to reiterate all the defaults here
[EventType.RoomName]: 50,
[EventType.RoomAvatar]: 50,
@@ -144,10 +142,6 @@ export default async function createRoom(opts: IOpts): Promise {
[EventType.RoomServerAcl]: 100,
[EventType.RoomEncryption]: 100,
},
- users: {
- // Temporarily give ourselves the power to set up a widget
- [client.getUserId()]: 200,
- },
};
}
}
@@ -270,11 +264,6 @@ export default async function createRoom(opts: IOpts): Promise {
if (opts.roomType === RoomType.ElementVideo) {
// Set up video rooms with a Jitsi widget
await addVideoChannel(roomId, createOpts.name);
-
- // Reset our power level back to admin so that the widget becomes immutable
- const room = client.getRoom(roomId);
- const plEvent = room?.currentState.getStateEvents(EventType.RoomPowerLevels, "");
- await client.setPowerLevel(roomId, client.getUserId(), 100, plEvent);
}
}).then(function() {
// NB createRoom doesn't block on the client seeing the echo that the
diff --git a/src/customisations/Media.ts b/src/customisations/Media.ts
index ce8f88f6f12..6d9e9a8b62c 100644
--- a/src/customisations/Media.ts
+++ b/src/customisations/Media.ts
@@ -16,6 +16,7 @@
import { MatrixClient } from "matrix-js-sdk/src/client";
import { ResizeMethod } from "matrix-js-sdk/src/@types/partials";
+import { Optional } from "matrix-events-sdk";
import { MatrixClientPeg } from "../MatrixClientPeg";
import { IMediaEventContent, IPreparedMedia, prepEventContentAsMedia } from "./models/IMediaEventContent";
@@ -60,7 +61,7 @@ export class Media {
* The MXC URI of the thumbnail media, if a thumbnail is recorded. Null/undefined
* otherwise.
*/
- public get thumbnailMxc(): string | undefined | null {
+ public get thumbnailMxc(): Optional {
return this.prepared.thumbnail?.mxc;
}
diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts
index 3fdbccb71b9..27f0b423c07 100644
--- a/src/dispatcher/actions.ts
+++ b/src/dispatcher/actions.ts
@@ -278,12 +278,6 @@ export enum Action {
*/
OpenReportEventDialog = "open_report_event_dialog",
- /**
- * Fired when the tabbed integration manager dialog needs to be opened.
- * Payload: OpenTabbedIntegrationManagerDialogPayload
- */
- OpenTabbedIntegrationManagerDialog = "open_tabbed_imanager_dialog",
-
/**
* Fired when something within the application has determined that a logout,
* or logout-like behaviour, needs to happen. Specifically meant to target
@@ -312,4 +306,16 @@ export enum Action {
* Opens a dialog to add an existing object to a space. Used with a OpenAddExistingToSpaceDialogPayload.
*/
OpenAddToExistingSpaceDialog = "open_add_to_existing_space_dialog",
+
+ /**
+ * Let components know that they should log any useful debugging information
+ * because we're probably about to send bug report which includes all of the
+ * logs. Fires with no payload.
+ */
+ DumpDebugLogs = "dump_debug_logs",
+
+ /**
+ * Show current room topic
+ */
+ ShowRoomTopic = "show_room_topic"
}
diff --git a/src/dispatcher/dispatcher.ts b/src/dispatcher/dispatcher.ts
index 15e2f8f9bc6..4d4f83d4def 100644
--- a/src/dispatcher/dispatcher.ts
+++ b/src/dispatcher/dispatcher.ts
@@ -67,9 +67,8 @@ export class MatrixDispatcher extends Dispatcher {
export const defaultDispatcher = new MatrixDispatcher();
-const anyGlobal = global;
-if (!anyGlobal.mxDispatcher) {
- anyGlobal.mxDispatcher = defaultDispatcher;
+if (!window.mxDispatcher) {
+ window.mxDispatcher = defaultDispatcher;
}
export default defaultDispatcher;
diff --git a/src/dispatcher/payloads/ComposerInsertPayload.ts b/src/dispatcher/payloads/ComposerInsertPayload.ts
index e5aab51a215..31fa466ef17 100644
--- a/src/dispatcher/payloads/ComposerInsertPayload.ts
+++ b/src/dispatcher/payloads/ComposerInsertPayload.ts
@@ -28,7 +28,7 @@ export enum ComposerType {
interface IBaseComposerInsertPayload extends ActionPayload {
action: Action.ComposerInsert;
timelineRenderingType: TimelineRenderingType;
- composerType?: ComposerType; // falsey if should be re-dispatched to the correct composer
+ composerType?: ComposerType; // falsy if should be re-dispatched to the correct composer
}
interface IComposerInsertMentionPayload extends IBaseComposerInsertPayload {
diff --git a/src/editor/autocomplete.ts b/src/editor/autocomplete.ts
index 30e992d629c..ad949042478 100644
--- a/src/editor/autocomplete.ts
+++ b/src/editor/autocomplete.ts
@@ -59,8 +59,8 @@ export default class AutocompleteWrapperModel {
return ac && ac.countCompletions() > 0;
}
- public async confirmCompletion(): Promise {
- await this.getAutocompleterComponent().onConfirmCompletion();
+ public confirmCompletion(): void {
+ this.getAutocompleterComponent().onConfirmCompletion();
this.updateCallback({ close: true });
}
diff --git a/src/editor/dom.ts b/src/editor/dom.ts
index 6226f74acb8..d65137aca69 100644
--- a/src/editor/dom.ts
+++ b/src/editor/dom.ts
@@ -17,6 +17,8 @@ limitations under the License.
import { CARET_NODE_CHAR, isCaretNode } from "./render";
import DocumentOffset from "./offset";
+import EditorModel from "./model";
+import Range from "./range";
type Predicate = (node: Node) => boolean;
type Callback = (node: Node) => void;
@@ -122,7 +124,7 @@ function getTextAndOffsetToNode(editor: HTMLDivElement, selectionNode: Node) {
let foundNode = false;
let text = "";
- function enterNodeCallback(node) {
+ function enterNodeCallback(node: HTMLElement) {
if (!foundNode) {
if (node === selectionNode) {
foundNode = true;
@@ -148,12 +150,12 @@ function getTextAndOffsetToNode(editor: HTMLDivElement, selectionNode: Node) {
return true;
}
- function leaveNodeCallback(node) {
+ function leaveNodeCallback(node: HTMLElement) {
// if this is not the last DIV (which are only used as line containers atm)
// we don't just check if there is a nextSibling because sometimes the caret ends up
// after the last DIV and it creates a newline if you type then,
// whereas you just want it to be appended to the current line
- if (node.tagName === "DIV" && node.nextSibling && node.nextSibling.tagName === "DIV") {
+ if (node.tagName === "DIV" && (node.nextSibling)?.tagName === "DIV") {
text += "\n";
if (!foundNode) {
offsetToNode += 1;
@@ -167,7 +169,7 @@ function getTextAndOffsetToNode(editor: HTMLDivElement, selectionNode: Node) {
}
// get text value of text node, ignoring ZWS if it's a caret node
-function getTextNodeValue(node) {
+function getTextNodeValue(node: Node): string {
const nodeText = node.nodeValue;
// filter out ZWS for caret nodes
if (isCaretNode(node.parentElement)) {
@@ -176,7 +178,7 @@ function getTextNodeValue(node) {
if (nodeText.length !== 1) {
return nodeText.replace(CARET_NODE_CHAR, "");
} else {
- // only contains ZWS, which is ignored, so return emtpy string
+ // only contains ZWS, which is ignored, so return empty string
return "";
}
} else {
@@ -184,7 +186,7 @@ function getTextNodeValue(node) {
}
}
-export function getRangeForSelection(editor, model, selection) {
+export function getRangeForSelection(editor: HTMLDivElement, model: EditorModel, selection: Selection): Range {
const focusOffset = getSelectionOffsetAndText(
editor,
selection.focusNode,
diff --git a/src/editor/operations.ts b/src/editor/operations.ts
index 40a438cc562..9f363a63dd6 100644
--- a/src/editor/operations.ts
+++ b/src/editor/operations.ts
@@ -37,7 +37,7 @@ export function formatRange(range: Range, action: Formatting): void {
range.trim();
}
- // Edgecase when just selecting whitespace or new line.
+ // Edge case when just selecting whitespace or new line.
// There should be no reason to format whitespace, so we can just return.
if (range.length === 0) {
return;
@@ -216,20 +216,18 @@ export function formatRangeAsCode(range: Range): void {
replaceRangeAndExpandSelection(range, parts);
}
-export function formatRangeAsLink(range: Range) {
+export function formatRangeAsLink(range: Range, text?: string) {
const { model } = range;
const { partCreator } = model;
- const linkRegex = /\[(.*?)\]\(.*?\)/g;
+ const linkRegex = /\[(.*?)]\(.*?\)/g;
const isFormattedAsLink = linkRegex.test(range.text);
if (isFormattedAsLink) {
const linkDescription = range.text.replace(linkRegex, "$1");
const newParts = [partCreator.plain(linkDescription)];
- const prefixLength = 1;
- const suffixLength = range.length - (linkDescription.length + 2);
- replaceRangeAndAutoAdjustCaret(range, newParts, true, prefixLength, suffixLength);
+ replaceRangeAndMoveCaret(range, newParts, 0);
} else {
// We set offset to -1 here so that the caret lands between the brackets
- replaceRangeAndMoveCaret(range, [partCreator.plain("[" + range.text + "]" + "()")], -1);
+ replaceRangeAndMoveCaret(range, [partCreator.plain("[" + range.text + "]" + "(" + (text ?? "") + ")")], -1);
}
}
diff --git a/src/editor/parts.ts b/src/editor/parts.ts
index 8d5ef54dd97..ded9975f7a2 100644
--- a/src/editor/parts.ts
+++ b/src/editor/parts.ts
@@ -453,7 +453,7 @@ class RoomPillPart extends PillPart {
}
protected get className() {
- return "mx_RoomPill mx_Pill";
+ return "mx_Pill " + (this.room.isSpaceRoom() ? "mx_SpacePill" : "mx_RoomPill");
}
}
diff --git a/src/hooks/room/useTopic.ts b/src/hooks/room/useTopic.ts
new file mode 100644
index 00000000000..b01065c37ca
--- /dev/null
+++ b/src/hooks/room/useTopic.ts
@@ -0,0 +1,40 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { useEffect, useState } from "react";
+import { EventType } from "matrix-js-sdk/src/@types/event";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
+
+import { useTypedEventEmitter } from "../useEventEmitter";
+
+export const getTopic = (room: Room) => {
+ return room?.currentState?.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic;
+};
+
+export function useTopic(room: Room): string {
+ const [topic, setTopic] = useState(getTopic(room));
+ useTypedEventEmitter(room.currentState, RoomStateEvent.Events, (ev: MatrixEvent) => {
+ if (ev.getType() !== EventType.RoomTopic) return;
+ setTopic(getTopic(room));
+ });
+ useEffect(() => {
+ setTopic(getTopic(room));
+ }, [room]);
+
+ return topic;
+}
diff --git a/src/hooks/useCombinedRefs.ts b/src/hooks/useCombinedRefs.ts
deleted file mode 100644
index 0b1e2a6c0c7..00000000000
--- a/src/hooks/useCombinedRefs.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { useRef, useEffect } from 'react';
-
-// Takes in multiple React refs and combines them to reference the same target/element
-//
-// via https://itnext.io/reusing-the-ref-from-forwardref-with-react-hooks-4ce9df693dd
-export const useCombinedRefs = (...refs) => {
- const targetRef = useRef();
-
- useEffect(() => {
- refs.forEach(ref => {
- if (!ref) return;
-
- if (typeof ref === 'function') {
- ref(targetRef.current);
- } else {
- ref.current = targetRef.current;
- }
- });
- }, [refs]);
-
- return targetRef;
-};
diff --git a/src/hooks/useFocus.ts b/src/hooks/useFocus.ts
new file mode 100644
index 00000000000..f84bc49be26
--- /dev/null
+++ b/src/hooks/useFocus.ts
@@ -0,0 +1,29 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { useState } from "react";
+
+export default function useFocus(
+): [boolean, {onFocus: () => void, onBlur: () => void}] {
+ const [focused, setFocused] = useState(false);
+
+ const props = {
+ onFocus: () => setFocused(true),
+ onBlur: () => setFocused(false),
+ };
+
+ return [focused, props];
+}
diff --git a/src/hooks/useHover.ts b/src/hooks/useHover.ts
new file mode 100644
index 00000000000..644c3a630aa
--- /dev/null
+++ b/src/hooks/useHover.ts
@@ -0,0 +1,33 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { useState } from "react";
+
+export default function useHover(
+ ignoreHover?: (ev: React.MouseEvent) => boolean,
+): [boolean, { onMouseOver: () => void, onMouseLeave: () => void, onMouseMove: (ev: React.MouseEvent) => void }] {
+ const [hovered, setHoverState] = useState(false);
+
+ const props = {
+ onMouseOver: () => setHoverState(true),
+ onMouseLeave: () => setHoverState(false),
+ onMouseMove: (ev: React.MouseEvent): void => {
+ setHoverState(!ignoreHover(ev));
+ },
+ };
+
+ return [hovered, props];
+}
diff --git a/src/hooks/useRoomMembers.ts b/src/hooks/useRoomMembers.ts
index a2d4e0a2c8f..52dc3853b8f 100644
--- a/src/hooks/useRoomMembers.ts
+++ b/src/hooks/useRoomMembers.ts
@@ -15,7 +15,7 @@ limitations under the License.
*/
import { useState } from "react";
-import { Room } from "matrix-js-sdk/src/models/room";
+import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { throttle } from "lodash";
@@ -23,7 +23,7 @@ import { throttle } from "lodash";
import { useTypedEventEmitter } from "./useEventEmitter";
// Hook to simplify watching Matrix Room joined members
-export const useRoomMembers = (room: Room, throttleWait = 250) => {
+export const useRoomMembers = (room: Room, throttleWait = 250): RoomMember[] => {
const [members, setMembers] = useState(room.getJoinedMembers());
useTypedEventEmitter(room.currentState, RoomStateEvent.Update, throttle(() => {
setMembers(room.getJoinedMembers());
@@ -32,10 +32,19 @@ export const useRoomMembers = (room: Room, throttleWait = 250) => {
};
// Hook to simplify watching Matrix Room joined member count
-export const useRoomMemberCount = (room: Room, throttleWait = 250) => {
+export const useRoomMemberCount = (room: Room, throttleWait = 250): number => {
const [count, setCount] = useState(room.getJoinedMemberCount());
useTypedEventEmitter(room.currentState, RoomStateEvent.Update, throttle(() => {
setCount(room.getJoinedMemberCount());
}, throttleWait, { leading: true, trailing: true }));
return count;
};
+
+// Hook to simplify watching the local user's membership in a room
+export const useMyRoomMembership = (room: Room): string => {
+ const [membership, setMembership] = useState(room.getMyMembership());
+ useTypedEventEmitter(room, RoomEvent.MyMembership, () => {
+ setMembership(room.getMyMembership());
+ });
+ return membership;
+};
diff --git a/src/hooks/useUserStatusMessage.ts b/src/hooks/useUserStatusMessage.ts
deleted file mode 100644
index 74b6c1dbf27..00000000000
--- a/src/hooks/useUserStatusMessage.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
-Copyright 2021 Šimon Brandner
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { MatrixClient } from "matrix-js-sdk/src/client";
-import { User, UserEvent } from "matrix-js-sdk/src/models/user";
-import { useContext } from "react";
-
-import MatrixClientContext from "../contexts/MatrixClientContext";
-import { useTypedEventEmitterState } from "./useEventEmitter";
-import { Member } from "../components/views/right_panel/UserInfo";
-import { useFeatureEnabled } from "./useSettings";
-
-const getUser = (cli: MatrixClient, user: Member): User => cli.getUser(user?.userId);
-const getStatusMessage = (cli: MatrixClient, user: Member): string => {
- return getUser(cli, user)?.unstable_statusMessage;
-};
-
-// Hook to simplify handling Matrix User status
-export const useUserStatusMessage = (member?: Member): string => {
- const cli = useContext(MatrixClientContext);
- const enabled = useFeatureEnabled("feature_custom_status");
- const user = enabled ? getUser(cli, member) : undefined;
- return useTypedEventEmitterState(user, UserEvent._UnstableStatusMessage, () => {
- return getStatusMessage(cli, user);
- });
-};
diff --git a/src/indexing/BaseEventIndexManager.ts b/src/indexing/BaseEventIndexManager.ts
index 8caa8a20dd7..982bb3543fc 100644
--- a/src/indexing/BaseEventIndexManager.ts
+++ b/src/indexing/BaseEventIndexManager.ts
@@ -91,7 +91,7 @@ export default abstract class BaseEventIndexManager {
*
* @param {MatrixEvent} ev The event that should be added to the index.
* @param {IMatrixProfile} profile The profile of the event sender at the
- * time of the event receival.
+ * time the event was received.
*
* @return {Promise} A promise that will resolve when the was queued up for
* addition.
diff --git a/src/indexing/EventIndex.ts b/src/indexing/EventIndex.ts
index 85ff7038de0..c941b8aac16 100644
--- a/src/indexing/EventIndex.ts
+++ b/src/indexing/EventIndex.ts
@@ -173,7 +173,6 @@ export default class EventIndex extends EventEmitter {
// A sync was done, presumably we queued up some live events,
// commit them now.
await indexManager.commitLiveEvents();
- return;
}
};
@@ -597,7 +596,7 @@ export default class EventIndex extends EventEmitter {
continue;
}
- // If all events were already indexed we assume that we catched
+ // If all events were already indexed we assume that we caught
// up with our index and don't need to crawl the room further.
// Let us delete the checkpoint in that case, otherwise push
// the new checkpoint to be used by the crawler.
@@ -613,7 +612,7 @@ export default class EventIndex extends EventEmitter {
this.crawlerCheckpoints.push(newCheckpoint);
}
} catch (e) {
- logger.log("EventIndex: Error durring a crawl", e);
+ logger.log("EventIndex: Error during a crawl", e);
// An error occurred, put the checkpoint back so we
// can retry.
this.crawlerCheckpoints.push(checkpoint);
@@ -650,7 +649,6 @@ export default class EventIndex extends EventEmitter {
this.removeListeners();
this.stopCrawler();
await indexManager.closeEventIndex();
- return;
}
/**
@@ -799,7 +797,7 @@ export default class EventIndex extends EventEmitter {
// to get our events in the BACKWARDS direction but populate them in the
// forwards direction.
// This needs to happen because a fill request might come with an
- // exisitng timeline e.g. if you close and re-open the FilePanel.
+ // existing timeline e.g. if you close and re-open the FilePanel.
if (fromEvent === null) {
matrixEvents.reverse();
direction = direction == EventTimeline.BACKWARDS ? EventTimeline.FORWARDS: EventTimeline.BACKWARDS;
diff --git a/src/indexing/EventIndexPeg.ts b/src/indexing/EventIndexPeg.ts
index 94948258a0a..097a86fbc14 100644
--- a/src/indexing/EventIndexPeg.ts
+++ b/src/indexing/EventIndexPeg.ts
@@ -29,6 +29,12 @@ import { SettingLevel } from "../settings/SettingLevel";
const INDEX_VERSION = 1;
+/**
+ * Holds the current instance of the `EventIndex` to use across the codebase.
+ * Looking for an `EventIndex`? Just look for the `EventIndexPeg` on the peg
+ * board. "Peg" is the literal meaning of something you hang something on. So
+ * you'll find a `EventIndex` hanging on the `EventIndexPeg`.
+ */
export class EventIndexPeg {
public index: EventIndex = null;
public error: Error = null;
@@ -67,7 +73,7 @@ export class EventIndexPeg {
/**
* Initialize the event index.
*
- * @returns {boolean} True if the event index was succesfully initialized,
+ * @returns {boolean} True if the event index was successfully initialized,
* false otherwise.
*/
async initEventIndex() {
@@ -118,7 +124,7 @@ export class EventIndexPeg {
}
/**
- * Check if event indexing support is installed for the platfrom.
+ * Check if event indexing support is installed for the platform.
*
* Event indexing might require additional optional modules to be installed,
* this tells us if those are installed. Note that this should only be
diff --git a/src/integrations/IntegrationManagerInstance.ts b/src/integrations/IntegrationManagerInstance.ts
index 440720b7495..dc6a2fb9deb 100644
--- a/src/integrations/IntegrationManagerInstance.ts
+++ b/src/integrations/IntegrationManagerInstance.ts
@@ -75,7 +75,7 @@ export class IntegrationManagerInstance {
client.setTermsInteractionCallback((policyInfo, agreedUrls) => {
// To avoid visual glitching of two modals stacking briefly, we customise the
// terms dialog sizing when it will appear for the integration manager so that
- // it gets the same basic size as the IM's own modal.
+ // it gets the same basic size as the integration manager's own modal.
return dialogTermsInteractionCallback(
policyInfo, agreedUrls, 'mx_TermsDialog_forIntegrationManager',
);
diff --git a/src/integrations/IntegrationManagers.ts b/src/integrations/IntegrationManagers.ts
index 32bb224f741..448b71d8ad2 100644
--- a/src/integrations/IntegrationManagers.ts
+++ b/src/integrations/IntegrationManagers.ts
@@ -19,7 +19,6 @@ import { logger } from "matrix-js-sdk/src/logger";
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
import type { MatrixEvent } from "matrix-js-sdk/src/models/event";
-import type { Room } from "matrix-js-sdk/src/models/room";
import SdkConfig from '../SdkConfig';
import Modal from '../Modal';
import { IntegrationManagerInstance, Kind } from "./IntegrationManagerInstance";
@@ -27,13 +26,7 @@ import IntegrationsImpossibleDialog from "../components/views/dialogs/Integratio
import IntegrationsDisabledDialog from "../components/views/dialogs/IntegrationsDisabledDialog";
import WidgetUtils from "../utils/WidgetUtils";
import { MatrixClientPeg } from "../MatrixClientPeg";
-import SettingsStore from "../settings/SettingsStore";
import { compare } from "../utils/strings";
-import defaultDispatcher from "../dispatcher/dispatcher";
-import {
- OpenTabbedIntegrationManagerDialogPayload,
-} from "../dispatcher/payloads/OpenTabbedIntegrationManagerDialogPayload";
-import { Action } from "../dispatcher/actions";
const KIND_PREFERENCE = [
// Ordered: first is most preferred, last is least preferred.
@@ -181,23 +174,6 @@ export class IntegrationManagers {
Modal.createTrackedDialog('Integrations impossible', '', IntegrationsImpossibleDialog);
}
- openAll(room: Room = null, screen: string = null, integrationId: string = null): void {
- if (!SettingsStore.getValue("integrationProvisioning")) {
- return this.showDisabledDialog();
- }
-
- if (this.managers.length === 0) {
- return this.openNoManagerDialog();
- }
-
- defaultDispatcher.dispatch({
- action: Action.OpenTabbedIntegrationManagerDialog,
- room,
- screen,
- integrationId,
- });
- }
-
showDisabledDialog(): void {
Modal.createTrackedDialog('Integrations disabled', '', IntegrationsDisabledDialog);
}
diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx
index 6dfef6d1aeb..1d3fef16668 100644
--- a/src/languageHandler.tsx
+++ b/src/languageHandler.tsx
@@ -88,8 +88,25 @@ export function _td(s: string): string { // eslint-disable-line @typescript-esli
* */
const translateWithFallback = (text: string, options?: object): { translated?: string, isFallback?: boolean } => {
const translated = counterpart.translate(text, { ...options, fallbackLocale: counterpart.getLocale() });
- if (!translated || /^missing translation:/.test(translated)) {
+ if (!translated || translated.startsWith("missing translation:")) {
const fallbackTranslated = counterpart.translate(text, { ...options, locale: FALLBACK_LOCALE });
+ if ((!fallbackTranslated || fallbackTranslated.startsWith("missing translation:"))
+ && process.env.NODE_ENV !== "development") {
+ // Even the translation via FALLBACK_LOCALE failed; this can happen if
+ //
+ // 1. The string isn't in the translations dictionary, usually because you're in develop
+ // and haven't run yarn i18n
+ // 2. Loading the translation resources over the network failed, which can happen due to
+ // to network or if the client tried to load a translation that's been removed from the
+ // server.
+ //
+ // At this point, its the lesser evil to show the untranslated text, which
+ // will be in English, so the user can still make out *something*, rather than an opaque
+ // "missing translation" error.
+ //
+ // Don't do this in develop so people remember to run yarn i18n.
+ return { translated: text, isFallback: true };
+ }
return { translated: fallbackTranslated, isFallback: true };
}
return { translated };
@@ -539,27 +556,13 @@ function getLangsJson(): Promise {
});
}
-function weblateToCounterpart(inTrs: object): object {
- const outTrs = {};
-
- for (const key of Object.keys(inTrs)) {
- const keyParts = key.split('|', 2);
- if (keyParts.length === 2) {
- let obj = outTrs[keyParts[0]];
- if (obj === undefined) {
- obj = {};
- outTrs[keyParts[0]] = obj;
- }
- obj[keyParts[1]] = inTrs[key];
- } else {
- outTrs[key] = inTrs[key];
- }
- }
-
- return outTrs;
+interface ICounterpartTranslation {
+ [key: string]: string | {
+ [pluralisation: string]: string;
+ };
}
-async function getLanguageRetry(langPath: string, num = 3): Promise {
+async function getLanguageRetry(langPath: string, num = 3): Promise {
return retry(() => getLanguage(langPath), num, e => {
logger.log("Failed to load i18n", langPath);
logger.error(e);
@@ -567,7 +570,7 @@ async function getLanguageRetry(langPath: string, num = 3): Promise {
});
}
-function getLanguage(langPath: string): Promise {
+function getLanguage(langPath: string): Promise {
return new Promise((resolve, reject) => {
request(
{ method: "GET", url: langPath },
@@ -580,7 +583,7 @@ function getLanguage(langPath: string): Promise {
reject(new Error(`Failed to load ${langPath}, got ${response.status}`));
return;
}
- resolve(weblateToCounterpart(JSON.parse(body)));
+ resolve(JSON.parse(body));
},
);
});
diff --git a/src/notifications/VectorPushRulesDefinitions.ts b/src/notifications/VectorPushRulesDefinitions.ts
index 85cefd51941..1d6aff8a13f 100644
--- a/src/notifications/VectorPushRulesDefinitions.ts
+++ b/src/notifications/VectorPushRulesDefinitions.ts
@@ -132,7 +132,7 @@ export const VectorPushRulesDefinitions = {
}),
// Messages just sent to a group chat room
- // 1:1 room messages are catched by the .m.rule.room_one_to_one rule if any defined
+ // 1:1 room messages are caught by the .m.rule.room_one_to_one rule if any defined
// By opposition, all other room messages are from group chat rooms.
".m.rule.message": new VectorPushRuleDefinition({
description: _td("Messages in group chats"), // passed through _t() translation in src/components/views/settings/Notifications.js
@@ -144,7 +144,7 @@ export const VectorPushRulesDefinitions = {
}),
// Encrypted messages just sent to a group chat room
- // Encrypted 1:1 room messages are catched by the .m.rule.encrypted_room_one_to_one rule if any defined
+ // Encrypted 1:1 room messages are caught by the .m.rule.encrypted_room_one_to_one rule if any defined
// By opposition, all other room messages are from group chat rooms.
".m.rule.encrypted": new VectorPushRuleDefinition({
description: _td("Encrypted messages in group chats"), // passed through _t() translation in src/components/views/settings/Notifications.js
diff --git a/src/performance/index.ts b/src/performance/index.ts
index ad523f0c649..9ea8dbd2151 100644
--- a/src/performance/index.ts
+++ b/src/performance/index.ts
@@ -71,7 +71,7 @@ export default class PerformanceMonitor {
* with the start marker
* @param name Name of the recording
* @param id Specify an identifier appended to the measurement name
- * @returns {void}
+ * @returns The measurement
*/
stop(name: string, id?: string): PerformanceEntry {
if (!this.supportsPerformanceApi()) {
@@ -165,7 +165,8 @@ export default class PerformanceMonitor {
* @returns {string} a compound of the name and identifier if present
*/
private buildKey(name: string, id?: string): string {
- return `${name}${id ? `:${id}` : ''}`;
+ const suffix = id ? `:${id}` : '';
+ return `${name}${suffix}`;
}
}
diff --git a/src/rageshake/rageshake.ts b/src/rageshake/rageshake.ts
index acffa254af0..44a60acd08c 100644
--- a/src/rageshake/rageshake.ts
+++ b/src/rageshake/rageshake.ts
@@ -524,7 +524,7 @@ export async function getLogsForReport() {
if (global.mx_rage_store) {
// flush most recent logs
await global.mx_rage_store.flush();
- return await global.mx_rage_store.consume();
+ return global.mx_rage_store.consume();
} else {
return [{
lines: global.mx_rage_logger.flush(true),
diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts
index 4822a9039c4..53b8f78c984 100644
--- a/src/rageshake/submit-rageshake.ts
+++ b/src/rageshake/submit-rageshake.ts
@@ -212,7 +212,7 @@ export default async function sendBugReport(bugReportEndpoint: string, opts: IOp
const body = await collectBugReport(opts);
progressCallback(_t("Uploading logs"));
- return await submitReport(bugReportEndpoint, body, progressCallback);
+ return submitReport(bugReportEndpoint, body, progressCallback);
}
/**
diff --git a/src/resizer/item.ts b/src/resizer/item.ts
index 868cd8230f2..5d5c95a2573 100644
--- a/src/resizer/item.ts
+++ b/src/resizer/item.ts
@@ -30,7 +30,7 @@ export default class ResizeItem {
) {
this.reverse = resizer.isReverseResizeHandle(handle);
if (container) {
- this.domNode = (container);
+ this.domNode = container;
} else {
this.domNode = (this.reverse ? handle.nextElementSibling : handle.previousElementSibling);
}
diff --git a/src/resizer/resizer.ts b/src/resizer/resizer.ts
index f9e762aeaac..39bb3db20ae 100644
--- a/src/resizer/resizer.ts
+++ b/src/resizer/resizer.ts
@@ -195,11 +195,11 @@ export default class Resizer {
return { sizer, distributor };
}
- private getResizeHandles() {
+ private getResizeHandles(): HTMLElement[] {
if (this?.config?.handler) {
return [this.config.handler];
}
if (!this.container?.children) return [];
- return Array.from(this.container.querySelectorAll(`.${this.classNames.handle}`)) as HTMLElement[];
+ return Array.from(this.container.querySelectorAll(`.${this.classNames.handle}`));
}
}
diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx
index e386160c9c8..a5ca0dbf843 100644
--- a/src/settings/Settings.tsx
+++ b/src/settings/Settings.tsx
@@ -23,7 +23,6 @@ import {
NotificationBodyEnabledController,
NotificationsEnabledController,
} from "./controllers/NotificationControllers";
-import CustomStatusController from "./controllers/CustomStatusController";
import ThemeController from './controllers/ThemeController';
import PushToMatrixClientController from './controllers/PushToMatrixClientController';
import ReloadOnChangeController from "./controllers/ReloadOnChangeController";
@@ -32,7 +31,7 @@ import SystemFontController from './controllers/SystemFontController';
import UseSystemFontController from './controllers/UseSystemFontController';
import { SettingLevel } from "./SettingLevel";
import SettingController from "./controllers/SettingController";
-import { isMac } from '../Keyboard';
+import { IS_MAC } from '../Keyboard';
import UIFeatureController from "./controllers/UIFeatureController";
import { UIFeature } from "./UIFeature";
import { OrderedMultiController } from "./controllers/OrderedMultiController";
@@ -204,13 +203,6 @@ export const SETTINGS: {[setting: string]: ISetting} = {
supportedLevels: LEVELS_FEATURE,
default: false,
},
- "feature_dnd": {
- isFeature: true,
- labsGroup: LabGroup.Profile,
- displayName: _td("Show options to enable 'Do not disturb' mode"),
- supportedLevels: LEVELS_FEATURE,
- default: false,
- },
"feature_latex_maths": {
isFeature: true,
labsGroup: LabGroup.Messaging,
@@ -272,14 +264,6 @@ export const SETTINGS: {[setting: string]: ISetting} = {
},
},
- "feature_custom_status": {
- isFeature: true,
- labsGroup: LabGroup.Profile,
- displayName: _td("Custom user status messages"),
- supportedLevels: LEVELS_FEATURE,
- default: false,
- controller: new CustomStatusController(),
- },
"feature_video_rooms": {
isFeature: true,
labsGroup: LabGroup.Rooms,
@@ -296,13 +280,6 @@ export const SETTINGS: {[setting: string]: ISetting} = {
supportedLevels: LEVELS_FEATURE,
default: false,
},
- "feature_many_integration_managers": {
- isFeature: true,
- labsGroup: LabGroup.Experimental,
- displayName: _td("Multiple integration managers (requires manual setup)"),
- supportedLevels: LEVELS_FEATURE,
- default: false,
- },
"feature_mjolnir": {
isFeature: true,
labsGroup: LabGroup.Moderation,
@@ -358,11 +335,6 @@ export const SETTINGS: {[setting: string]: ISetting} = {
displayName: _td("Show current avatar and name for users in message history"),
default: false,
},
- "doNotDisturb": {
- supportedLevels: [SettingLevel.DEVICE],
- default: false,
- controller: new IncompatibleController("feature_dnd", false, false),
- },
"mjolnirRooms": {
supportedLevels: [SettingLevel.ACCOUNT],
default: [],
@@ -612,12 +584,12 @@ export const SETTINGS: {[setting: string]: ISetting} = {
},
"ctrlFForSearch": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
- displayName: isMac ? _td("Use Command + F to search timeline") : _td("Use Ctrl + F to search timeline"),
+ displayName: IS_MAC ? _td("Use Command + F to search timeline") : _td("Use Ctrl + F to search timeline"),
default: true,
},
"MessageComposerInput.ctrlEnterToSend": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
- displayName: isMac ? _td("Use Command + Enter to send a message") : _td("Use Ctrl + Enter to send a message"),
+ displayName: IS_MAC ? _td("Use Command + Enter to send a message") : _td("Use Ctrl + Enter to send a message"),
default: false,
},
"MessageComposerInput.surroundWith": {
@@ -698,15 +670,15 @@ export const SETTINGS: {[setting: string]: ISetting} = {
},
"webrtc_audiooutput": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
- default: null,
+ default: "default",
},
"webrtc_audioinput": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
- default: null,
+ default: "default",
},
"webrtc_videoinput": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
- default: null,
+ default: "default",
},
"language": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
@@ -1023,6 +995,26 @@ export const SETTINGS: {[setting: string]: ISetting} = {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
default: false,
},
+ "debug_scroll_panel": {
+ supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
+ default: false,
+ },
+ "debug_timeline_panel": {
+ supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
+ default: false,
+ },
+ "audioInputMuted": {
+ supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
+ default: false,
+ },
+ "videoInputMuted": {
+ supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
+ default: false,
+ },
+ "videoChannelRoomId": {
+ supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
+ default: null,
+ },
[UIFeature.RoomHistorySettings]: {
supportedLevels: LEVELS_UI_FEATURE,
default: true,
diff --git a/src/settings/controllers/CustomStatusController.ts b/src/settings/controllers/CustomStatusController.ts
deleted file mode 100644
index c7dfad0b3bf..00000000000
--- a/src/settings/controllers/CustomStatusController.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
-Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-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 SettingController from "./SettingController";
-import dis from "../../dispatcher/dispatcher";
-import { SettingLevel } from "../SettingLevel";
-
-export default class CustomStatusController extends SettingController {
- public onChange(level: SettingLevel, roomId: string, newValue: any) {
- // Dispatch setting change so that some components that are still visible when the
- // Settings page is open (such as RoomTiles) can reflect the change.
- dis.dispatch({
- action: "feature_custom_status_changed",
- });
- }
-}
diff --git a/src/settings/watchers/FontWatcher.ts b/src/settings/watchers/FontWatcher.ts
index 952a2eb9f3f..c70dd78349f 100644
--- a/src/settings/watchers/FontWatcher.ts
+++ b/src/settings/watchers/FontWatcher.ts
@@ -60,7 +60,7 @@ export class FontWatcher implements IWatcher {
if (fontSize !== size) {
SettingsStore.setValue("baseFontSize", null, SettingLevel.DEVICE, fontSize);
}
- (document.querySelector(":root")).style.fontSize = toPx(fontSize);
+ document.querySelector(":root").style.fontSize = toPx(fontSize);
};
private setSystemFont = ({ useSystemFont, font }) => {
diff --git a/src/settings/watchers/ThemeWatcher.ts b/src/settings/watchers/ThemeWatcher.ts
index 9b5f7466e27..68de4524308 100644
--- a/src/settings/watchers/ThemeWatcher.ts
+++ b/src/settings/watchers/ThemeWatcher.ts
@@ -15,8 +15,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { logger } from "matrix-js-sdk/src/logger";
-
import SettingsStore from '../SettingsStore';
import dis from '../../dispatcher/dispatcher';
import { Action } from '../../dispatcher/actions';
@@ -116,22 +114,6 @@ export default class ThemeWatcher {
}
}
- private themeBasedOnSystem() {
- let newTheme: string;
- if (this.preferDark.matches) {
- newTheme = 'dark';
- } else if (this.preferLight.matches) {
- newTheme = 'light';
- }
- if (this.preferHighContrast.matches) {
- const hcTheme = findHighContrastTheme(newTheme);
- if (hcTheme) {
- newTheme = hcTheme;
- }
- }
- return newTheme;
- }
-
public isSystemThemeSupported() {
return this.preferDark.matches || this.preferLight.matches;
}
diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts
index cb1018326c1..bc5c342c739 100644
--- a/src/stores/BreadcrumbsStore.ts
+++ b/src/stores/BreadcrumbsStore.ts
@@ -26,6 +26,7 @@ import { SettingLevel } from "../settings/SettingLevel";
import { Action } from "../dispatcher/actions";
import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayload";
import { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload";
+import { JoinRoomPayload } from "../dispatcher/payloads/JoinRoomPayload";
const MAX_ROOMS = 20; // arbitrary
const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90s, the time we wait for an autojoined room to show up
@@ -65,9 +66,8 @@ export class BreadcrumbsStore extends AsyncStoreWithClient {
return this.matrixClient?.getVisibleRooms().length >= 20;
}
- protected async onAction(payload: SettingUpdatedPayload | ViewRoomPayload) {
+ protected async onAction(payload: SettingUpdatedPayload | ViewRoomPayload | JoinRoomPayload) {
if (!this.matrixClient) return;
-
if (payload.action === Action.SettingUpdated) {
if (payload.settingName === 'breadcrumb_rooms') {
await this.updateRooms();
@@ -84,8 +84,12 @@ export class BreadcrumbsStore extends AsyncStoreWithClient {
} else {
// The tests might not result in a valid room object.
const room = this.matrixClient.getRoom(payload.room_id);
- if (room) await this.appendRoom(room);
+ const membership = room?.getMyMembership();
+ if (room && membership==="join") await this.appendRoom(room);
}
+ } else if (payload.action === Action.JoinRoom) {
+ const room = this.matrixClient.getRoom(payload.roomId);
+ if (room) await this.appendRoom(room);
}
}
diff --git a/src/stores/OwnBeaconStore.ts b/src/stores/OwnBeaconStore.ts
index 609b85fae25..1dbf8668fcb 100644
--- a/src/stores/OwnBeaconStore.ts
+++ b/src/stores/OwnBeaconStore.ts
@@ -43,7 +43,7 @@ import {
TimedGeoUri,
watchPosition,
} from "../utils/beacon";
-import { getCurrentPosition } from "../utils/beacon/geolocation";
+import { getCurrentPosition } from "../utils/beacon";
const isOwnBeacon = (beacon: Beacon, userId: string): boolean => beacon.beaconInfoOwner === userId;
@@ -418,10 +418,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient {
this.stopPollingLocation();
try {
- this.clearPositionWatch = await watchPosition(
- this.onWatchedPosition,
- this.onGeolocationError,
- );
+ this.clearPositionWatch = watchPosition(this.onWatchedPosition, this.onGeolocationError);
} catch (error) {
this.onGeolocationError(error?.message);
// don't set locationInterval if geolocation failed to setup
@@ -459,7 +456,7 @@ export class OwnBeaconStore extends AsyncStoreWithClient {
private onWatchedPosition = (position: GeolocationPosition) => {
const timedGeoPosition = mapGeolocationPositionToTimedGeo(position);
- // if this is our first position, publish immediateley
+ // if this is our first position, publish immediately
if (!this.lastPublishedPositionTimestamp) {
this.publishLocationToBeacons(timedGeoPosition);
} else {
diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx
index 77909ae6129..f9871c57528 100644
--- a/src/stores/RoomViewStore.tsx
+++ b/src/stores/RoomViewStore.tsx
@@ -25,6 +25,8 @@ import { JoinedRoom as JoinedRoomEvent } from "matrix-analytics-events/types/typ
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import { Room } from "matrix-js-sdk/src/models/room";
import { ClientEvent } from "matrix-js-sdk/src/client";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { Optional } from "matrix-events-sdk";
import dis from '../dispatcher/dispatcher';
import { MatrixClientPeg } from '../MatrixClientPeg';
@@ -53,30 +55,30 @@ const INITIAL_STATE = {
// Whether we're joining the currently viewed room (see isJoining())
joining: false,
// Any error that has occurred during joining
- joinError: null,
+ joinError: null as Error,
// The room ID of the room currently being viewed
- roomId: null,
+ roomId: null as string,
// The event to scroll to when the room is first viewed
- initialEventId: null,
- initialEventPixelOffset: null,
+ initialEventId: null as string,
+ initialEventPixelOffset: null as number,
// Whether to highlight the initial event
isInitialEventHighlighted: false,
// whether to scroll `event_id` into view
initialEventScrollIntoView: true,
// The room alias of the room (or null if not originally specified in view_room)
- roomAlias: null,
+ roomAlias: null as string,
// Whether the current room is loading
roomLoading: false,
// Any error that has occurred during loading
- roomLoadError: null,
+ roomLoadError: null as MatrixError,
- replyingToEvent: null,
+ replyingToEvent: null as MatrixEvent,
shouldPeek: false,
- viaServers: [],
+ viaServers: [] as string[],
wasContextSwitch: false,
};
@@ -103,12 +105,12 @@ export class RoomViewStore extends Store {
super(dis);
}
- public addRoomListener(roomId: string, fn: Listener) {
+ public addRoomListener(roomId: string, fn: Listener): void {
if (!this.roomIdActivityListeners[roomId]) this.roomIdActivityListeners[roomId] = [];
this.roomIdActivityListeners[roomId].push(fn);
}
- public removeRoomListener(roomId: string, fn: Listener) {
+ public removeRoomListener(roomId: string, fn: Listener): void {
if (this.roomIdActivityListeners[roomId]) {
const i = this.roomIdActivityListeners[roomId].indexOf(fn);
if (i > -1) {
@@ -119,7 +121,7 @@ export class RoomViewStore extends Store {
}
}
- private emitForRoom(roomId: string, isActive: boolean) {
+ private emitForRoom(roomId: string, isActive: boolean): void {
if (!this.roomIdActivityListeners[roomId]) return;
for (const fn of this.roomIdActivityListeners[roomId]) {
@@ -127,7 +129,7 @@ export class RoomViewStore extends Store {
}
}
- private setState(newState: Partial) {
+ private setState(newState: Partial): void {
// If values haven't changed, there's nothing to do.
// This only tries a shallow comparison, so unchanged objects will slip
// through, but that's probably okay for now.
@@ -160,7 +162,7 @@ export class RoomViewStore extends Store {
this.__emitChange();
}
- protected __onDispatch(payload) { // eslint-disable-line @typescript-eslint/naming-convention
+ protected __onDispatch(payload): void { // eslint-disable-line @typescript-eslint/naming-convention
switch (payload.action) {
// view_room:
// - room_alias: '#somealias:matrix.org'
@@ -367,7 +369,7 @@ export class RoomViewStore extends Store {
}
}
- private viewRoomError(payload: ViewRoomErrorPayload) {
+ private viewRoomError(payload: ViewRoomErrorPayload): void {
this.setState({
roomId: payload.room_id,
roomAlias: payload.room_alias,
@@ -376,7 +378,7 @@ export class RoomViewStore extends Store {
});
}
- private async joinRoom(payload: JoinRoomPayload) {
+ private async joinRoom(payload: JoinRoomPayload): Promise {
this.setState({
joining: true,
});
@@ -407,7 +409,7 @@ export class RoomViewStore extends Store {
dis.dispatch({
action: Action.JoinRoomError,
roomId,
- err: err,
+ err,
});
}
}
@@ -415,14 +417,14 @@ export class RoomViewStore extends Store {
private getInvitingUserId(roomId: string): string {
const cli = MatrixClientPeg.get();
const room = cli.getRoom(roomId);
- if (room && room.getMyMembership() === "invite") {
+ if (room?.getMyMembership() === "invite") {
const myMember = room.getMember(cli.getUserId());
const inviteEvent = myMember ? myMember.events.member : null;
return inviteEvent && inviteEvent.getSender();
}
}
- public showJoinRoomError(err: MatrixError, roomId: string) {
+ public showJoinRoomError(err: MatrixError, roomId: string): void {
let description: ReactNode = err.message ? err.message : JSON.stringify(err);
logger.log("Failed to join room:", description);
@@ -452,7 +454,7 @@ export class RoomViewStore extends Store {
});
}
- private joinRoomError(payload: JoinRoomErrorPayload) {
+ private joinRoomError(payload: JoinRoomErrorPayload): void {
this.setState({
joining: false,
joinError: payload.err,
@@ -460,42 +462,42 @@ export class RoomViewStore extends Store {
this.showJoinRoomError(payload.err, payload.roomId);
}
- public reset() {
+ public reset(): void {
this.state = Object.assign({}, INITIAL_STATE);
}
// The room ID of the room currently being viewed
- public getRoomId() {
+ public getRoomId(): Optional {
return this.state.roomId;
}
// The event to scroll to when the room is first viewed
- public getInitialEventId() {
+ public getInitialEventId(): Optional {
return this.state.initialEventId;
}
// Whether to highlight the initial event
- public isInitialEventHighlighted() {
+ public isInitialEventHighlighted(): boolean {
return this.state.isInitialEventHighlighted;
}
// Whether to avoid jumping to the initial event
- public initialEventScrollIntoView() {
+ public initialEventScrollIntoView(): boolean {
return this.state.initialEventScrollIntoView;
}
// The room alias of the room (or null if not originally specified in view_room)
- public getRoomAlias() {
+ public getRoomAlias(): Optional {
return this.state.roomAlias;
}
// Whether the current room is loading (true whilst resolving an alias)
- public isRoomLoading() {
+ public isRoomLoading(): boolean {
return this.state.roomLoading;
}
// Any error that has occurred during loading
- public getRoomLoadError() {
+ public getRoomLoadError(): Optional {
return this.state.roomLoadError;
}
@@ -504,7 +506,7 @@ export class RoomViewStore extends Store {
// since we should still consider a join to be in progress until the room
// & member events come down the sync.
//
- // This flag remains true after the room has been sucessfully joined,
+ // This flag remains true after the room has been successfully joined,
// (this store doesn't listen for the appropriate member events)
// so you should always observe the joined state from the member event
// if a room object is present.
@@ -522,25 +524,25 @@ export class RoomViewStore extends Store {
// // show join prompt
// }
// }
- public isJoining() {
+ public isJoining(): boolean {
return this.state.joining;
}
// Any error that has occurred during joining
- public getJoinError() {
+ public getJoinError(): Optional {
return this.state.joinError;
}
// The mxEvent if one is currently being replied to/quoted
- public getQuotingEvent() {
+ public getQuotingEvent(): Optional {
return this.state.replyingToEvent;
}
- public shouldPeek() {
+ public shouldPeek(): boolean {
return this.state.shouldPeek;
}
- public getWasContextSwitch() {
+ public getWasContextSwitch(): boolean {
return this.state.wasContextSwitch;
}
}
diff --git a/src/stores/SetupEncryptionStore.ts b/src/stores/SetupEncryptionStore.ts
index d1341893240..b5df05b2ff4 100644
--- a/src/stores/SetupEncryptionStore.ts
+++ b/src/stores/SetupEncryptionStore.ts
@@ -100,7 +100,7 @@ export class SetupEncryptionStore extends EventEmitter {
public async fetchKeyInfo(): Promise {
const cli = MatrixClientPeg.get();
- const keys = await cli.isSecretStored('m.cross_signing.master', false);
+ const keys = await cli.isSecretStored('m.cross_signing.master');
if (keys === null || Object.keys(keys).length === 0) {
this.keyId = null;
this.keyInfo = null;
diff --git a/src/stores/VideoChannelStore.ts b/src/stores/VideoChannelStore.ts
index d32f748fb7f..9ab521b50ff 100644
--- a/src/stores/VideoChannelStore.ts
+++ b/src/stores/VideoChannelStore.ts
@@ -15,17 +15,16 @@ limitations under the License.
*/
import EventEmitter from "events";
+import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { ClientWidgetApi, IWidgetApiRequest } from "matrix-widget-api";
+import SettingsStore from "../settings/SettingsStore";
+import { SettingLevel } from "../settings/SettingLevel";
import defaultDispatcher from "../dispatcher/dispatcher";
import { ActionPayload } from "../dispatcher/payloads";
import { ElementWidgetActions } from "./widgets/ElementWidgetActions";
import { WidgetMessagingStore, WidgetMessagingStoreEvent } from "./widgets/WidgetMessagingStore";
-import {
- VIDEO_CHANNEL_MEMBER,
- IVideoChannelMemberContent,
- getVideoChannel,
-} from "../utils/VideoChannelUtils";
+import { getVideoChannel, addOurDevice, removeOurDevice } from "../utils/VideoChannelUtils";
import { timeout } from "../utils/promise";
import WidgetUtils from "../utils/WidgetUtils";
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
@@ -82,9 +81,13 @@ export default class VideoChannelStore extends AsyncStoreWithClient {
private activeChannel: ClientWidgetApi;
- private _roomId: string;
- public get roomId(): string { return this._roomId; }
- private set roomId(value: string) { this._roomId = value; }
+ // This is persisted to settings so we can detect unclean disconnects
+ public get roomId(): string | null { return SettingsStore.getValue("videoChannelRoomId"); }
+ private set roomId(value: string | null) {
+ SettingsStore.setValue("videoChannelRoomId", null, SettingLevel.DEVICE, value);
+ }
+
+ private get room(): Room { return this.matrixClient.getRoom(this.roomId); }
private _connected = false;
public get connected(): boolean { return this._connected; }
@@ -94,18 +97,14 @@ export default class VideoChannelStore extends AsyncStoreWithClient {
public get participants(): IJitsiParticipant[] { return this._participants; }
private set participants(value: IJitsiParticipant[]) { this._participants = value; }
- private _audioMuted = localStorage.getItem("mx_audioMuted") === "true";
- public get audioMuted(): boolean { return this._audioMuted; }
+ public get audioMuted(): boolean { return SettingsStore.getValue("audioInputMuted"); }
public set audioMuted(value: boolean) {
- this._audioMuted = value;
- localStorage.setItem("mx_audioMuted", value.toString());
+ SettingsStore.setValue("audioInputMuted", null, SettingLevel.DEVICE, value);
}
- private _videoMuted = localStorage.getItem("mx_videoMuted") === "true";
- public get videoMuted(): boolean { return this._videoMuted; }
+ public get videoMuted(): boolean { return SettingsStore.getValue("videoInputMuted"); }
public set videoMuted(value: boolean) {
- this._videoMuted = value;
- localStorage.setItem("mx_videoMuted", value.toString());
+ SettingsStore.setValue("videoInputMuted", null, SettingLevel.DEVICE, value);
}
public connect = async (roomId: string, audioDevice: MediaDeviceInfo, videoDevice: MediaDeviceInfo) => {
@@ -158,6 +157,10 @@ export default class VideoChannelStore extends AsyncStoreWithClient {
messaging.on(`action:${ElementWidgetActions.UnmuteAudio}`, this.onUnmuteAudio);
messaging.on(`action:${ElementWidgetActions.MuteVideo}`, this.onMuteVideo);
messaging.on(`action:${ElementWidgetActions.UnmuteVideo}`, this.onUnmuteVideo);
+ // Empirically, it's possible for Jitsi Meet to crash instantly at startup,
+ // sending a hangup event that races with the rest of this method, so we also
+ // need to add the hangup listener now rather than later
+ messaging.once(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
this.emit(VideoChannelEvent.StartConnect, roomId);
@@ -185,6 +188,7 @@ export default class VideoChannelStore extends AsyncStoreWithClient {
messaging.off(`action:${ElementWidgetActions.UnmuteAudio}`, this.onUnmuteAudio);
messaging.off(`action:${ElementWidgetActions.MuteVideo}`, this.onMuteVideo);
messaging.off(`action:${ElementWidgetActions.UnmuteVideo}`, this.onUnmuteVideo);
+ messaging.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
this.emit(VideoChannelEvent.Disconnect, roomId);
@@ -192,13 +196,13 @@ export default class VideoChannelStore extends AsyncStoreWithClient {
}
this.connected = true;
- messaging.once(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
+ this.room.on(RoomEvent.MyMembership, this.onMyMembership);
window.addEventListener("beforeunload", this.setDisconnected);
this.emit(VideoChannelEvent.Connect, roomId);
// Tell others that we're connected, by adding our device to room state
- this.updateDevices(roomId, devices => Array.from(new Set(devices).add(this.matrixClient.getDeviceId())));
+ await addOurDevice(this.room);
};
public disconnect = async () => {
@@ -214,11 +218,14 @@ export default class VideoChannelStore extends AsyncStoreWithClient {
};
public setDisconnected = async () => {
+ const roomId = this.roomId;
+ const room = this.room;
+
this.activeChannel.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
this.activeChannel.off(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants);
+ room.off(RoomEvent.MyMembership, this.onMyMembership);
window.removeEventListener("beforeunload", this.setDisconnected);
- const roomId = this.roomId;
this.activeChannel = null;
this.roomId = null;
this.connected = false;
@@ -227,11 +234,7 @@ export default class VideoChannelStore extends AsyncStoreWithClient {
this.emit(VideoChannelEvent.Disconnect, roomId);
// Tell others that we're disconnected, by removing our device from room state
- await this.updateDevices(roomId, devices => {
- const devicesSet = new Set(devices);
- devicesSet.delete(this.matrixClient.getDeviceId());
- return Array.from(devicesSet);
- });
+ await removeOurDevice(room);
};
private ack = (ev: CustomEvent) => {
@@ -240,18 +243,11 @@ export default class VideoChannelStore extends AsyncStoreWithClient {
this.activeChannel.transport.reply(ev.detail, {});
};
- private updateDevices = async (roomId: string, fn: (devices: string[]) => string[]) => {
- const room = this.matrixClient.getRoom(roomId);
- const devicesState = room.currentState.getStateEvents(VIDEO_CHANNEL_MEMBER, this.matrixClient.getUserId());
- const devices = devicesState?.getContent()?.devices ?? [];
-
- await this.matrixClient.sendStateEvent(
- roomId, VIDEO_CHANNEL_MEMBER, { devices: fn(devices) }, this.matrixClient.getUserId(),
- );
- };
-
private onHangup = async (ev: CustomEvent) => {
this.ack(ev);
+ // In case this hangup is caused by Jitsi Meet crashing at startup,
+ // wait for the connection event in order to avoid racing
+ if (!this.connected) await waitForEvent(this, VideoChannelEvent.Connect);
await this.setDisconnected();
};
@@ -280,4 +276,8 @@ export default class VideoChannelStore extends AsyncStoreWithClient {
this.videoMuted = false;
this.ack(ev);
};
+
+ private onMyMembership = (room: Room, membership: string) => {
+ if (membership !== "join") this.setDisconnected();
+ };
}
diff --git a/src/stores/WidgetEchoStore.ts b/src/stores/WidgetEchoStore.ts
index 34b9b4aa47b..2923d46b09e 100644
--- a/src/stores/WidgetEchoStore.ts
+++ b/src/stores/WidgetEchoStore.ts
@@ -44,9 +44,9 @@ class WidgetEchoStore extends EventEmitter {
}
/**
- * Gets the widgets for a room, substracting those that are pending deletion.
+ * Gets the widgets for a room, subtracting those that are pending deletion.
* Widgets that are pending addition are not included, since widgets are
- * represted as MatrixEvents, so to do this we'd have to create fake MatrixEvents,
+ * represented as MatrixEvents, so to do this we'd have to create fake MatrixEvents,
* and we don't really need the actual widget events anyway since we just want to
* show a spinner / prevent widgets being added twice.
*
diff --git a/src/stores/local-echo/RoomEchoChamber.ts b/src/stores/local-echo/RoomEchoChamber.ts
index f1e1fd6a4da..284aada23ea 100644
--- a/src/stores/local-echo/RoomEchoChamber.ts
+++ b/src/stores/local-echo/RoomEchoChamber.ts
@@ -49,8 +49,8 @@ export class RoomEchoChamber extends GenericEchoChamber {
if (event.getType() === EventType.PushRules) {
- const currentVolume = this.properties.get(CachedRoomKey.NotificationVolume) as RoomNotifState;
- const newVolume = getRoomNotifsState(this.context.room.roomId) as RoomNotifState;
+ const currentVolume = this.properties.get(CachedRoomKey.NotificationVolume);
+ const newVolume = getRoomNotifsState(this.context.room.roomId);
if (currentVolume !== newVolume) {
this.updateNotificationVolume();
}
diff --git a/src/stores/right-panel/RightPanelStore.ts b/src/stores/right-panel/RightPanelStore.ts
index bb4ddc4fcbf..341474293ec 100644
--- a/src/stores/right-panel/RightPanelStore.ts
+++ b/src/stores/right-panel/RightPanelStore.ts
@@ -141,7 +141,6 @@ export default class RightPanelStore extends ReadyWatchingStore {
const hist = this.byRoom[rId]?.history ?? [];
hist[hist.length - 1].state = cardState;
this.emitAndUpdateSettings();
- return;
} else if (targetPhase !== this.currentCard?.phase) {
// Set right panel and erase history.
this.show();
diff --git a/src/stores/room-list/MessagePreviewStore.ts b/src/stores/room-list/MessagePreviewStore.ts
index 4ab7f96ff6c..2fa0abc70db 100644
--- a/src/stores/room-list/MessagePreviewStore.ts
+++ b/src/stores/room-list/MessagePreviewStore.ts
@@ -31,12 +31,16 @@ import { CallHangupEvent } from "./previews/CallHangupEvent";
import { StickerEventPreview } from "./previews/StickerEventPreview";
import { ReactionEventPreview } from "./previews/ReactionEventPreview";
import { UPDATE_EVENT } from "../AsyncStore";
+import { IPreview } from "./previews/IPreview";
// Emitted event for when a room's preview has changed. First argument will the room for which
// the change happened.
const ROOM_PREVIEW_CHANGED = "room_preview_changed";
-const PREVIEWS = {
+const PREVIEWS: Record = {
'm.room.message': {
isState: false,
previewer: new MessageEventPreview(),
@@ -122,10 +126,7 @@ export class MessagePreviewStore extends AsyncStoreWithClient {
public generatePreviewForEvent(event: MatrixEvent): string {
const previewDef = PREVIEWS[event.getType()];
- // TODO: Handle case where we don't have
- if (!previewDef) return '';
- const previewText = previewDef.previewer.getTextFor(event, null, true);
- return previewText ?? '';
+ return previewDef?.previewer.getTextFor(event, null, true) ?? "";
}
private async generatePreview(room: Room, tagId?: TagID) {
diff --git a/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts
index af937e92af0..9466a35940a 100644
--- a/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts
+++ b/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts
@@ -32,9 +32,6 @@ export function shouldCauseReorder(event: MatrixEvent): boolean {
// Never ignore membership changes
if (type === EventType.RoomMember && prevContent.membership !== content.membership) return true;
- // Ignore status changes
- // XXX: This should be an enum
- if (type === "im.vector.user_status") return false;
// Ignore display name changes
if (type === EventType.RoomMember && prevContent.displayname !== content.displayname) return false;
// Ignore avatar changes
@@ -63,58 +60,55 @@ export const sortRooms = (rooms: Room[]): Room[] => {
}
const tsCache: { [roomId: string]: number } = {};
- const getLastTs = (r: Room) => {
- if (tsCache[r.roomId]) {
- return tsCache[r.roomId];
- }
- const ts = (() => {
- // Apparently we can have rooms without timelines, at least under testing
- // environments. Just return MAX_INT when this happens.
- if (!r || !r.timeline) {
- return Number.MAX_SAFE_INTEGER;
- }
+ return rooms.sort((a, b) => {
+ const roomALastTs = tsCache[a.roomId] ?? getLastTs(a, myUserId);
+ const roomBLastTs = tsCache[b.roomId] ?? getLastTs(b, myUserId);
- // If the room hasn't been joined yet, it probably won't have a timeline to
- // parse. We'll still fall back to the timeline if this fails, but chances
- // are we'll at least have our own membership event to go off of.
- const effectiveMembership = getEffectiveMembership(r.getMyMembership());
- if (effectiveMembership !== EffectiveMembership.Join) {
- const membershipEvent = r.currentState.getStateEvents("m.room.member", myUserId);
- if (membershipEvent && !Array.isArray(membershipEvent)) {
- return membershipEvent.getTs();
- }
- }
+ tsCache[a.roomId] = roomALastTs;
+ tsCache[b.roomId] = roomBLastTs;
- for (let i = r.timeline.length - 1; i >= 0; --i) {
- const ev = r.timeline[i];
- if (!ev.getTs()) continue; // skip events that don't have timestamps (tests only?)
+ return roomBLastTs - roomALastTs;
+ });
+};
- if (
- (ev.getSender() === myUserId && shouldCauseReorder(ev)) ||
- Unread.eventTriggersUnreadCount(ev)
- ) {
- return ev.getTs();
- }
- }
+const getLastTs = (r: Room, userId: string) => {
+ const ts = (() => {
+ // Apparently we can have rooms without timelines, at least under testing
+ // environments. Just return MAX_INT when this happens.
+ if (!r?.timeline) {
+ return Number.MAX_SAFE_INTEGER;
+ }
- // we might only have events that don't trigger the unread indicator,
- // in which case use the oldest event even if normally it wouldn't count.
- // This is better than just assuming the last event was forever ago.
- if (r.timeline.length && r.timeline[0].getTs()) {
- return r.timeline[0].getTs();
- } else {
- return Number.MAX_SAFE_INTEGER;
+ // If the room hasn't been joined yet, it probably won't have a timeline to
+ // parse. We'll still fall back to the timeline if this fails, but chances
+ // are we'll at least have our own membership event to go off of.
+ const effectiveMembership = getEffectiveMembership(r.getMyMembership());
+ if (effectiveMembership !== EffectiveMembership.Join) {
+ const membershipEvent = r.currentState.getStateEvents(EventType.RoomMember, userId);
+ if (membershipEvent && !Array.isArray(membershipEvent)) {
+ return membershipEvent.getTs();
}
- })();
+ }
- tsCache[r.roomId] = ts;
- return ts;
- };
+ for (let i = r.timeline.length - 1; i >= 0; --i) {
+ const ev = r.timeline[i];
+ if (!ev.getTs()) continue; // skip events that don't have timestamps (tests only?)
- return rooms.sort((a, b) => {
- return getLastTs(b) - getLastTs(a);
- });
+ if (
+ (ev.getSender() === userId && shouldCauseReorder(ev)) ||
+ Unread.eventTriggersUnreadCount(ev)
+ ) {
+ return ev.getTs();
+ }
+ }
+
+ // we might only have events that don't trigger the unread indicator,
+ // in which case use the oldest event even if normally it wouldn't count.
+ // This is better than just assuming the last event was forever ago.
+ return r.timeline[0]?.getTs() ?? Number.MAX_SAFE_INTEGER;
+ })();
+ return ts;
};
/**
@@ -125,4 +119,8 @@ export class RecentAlgorithm implements IAlgorithm {
public sortRooms(rooms: Room[], tagId: TagID): Room[] {
return sortRooms(rooms);
}
+
+ public getLastTs(room: Room, userId: string): number {
+ return getLastTs(room, userId);
+ }
}
diff --git a/src/stores/room-list/previews/IPreview.ts b/src/stores/room-list/previews/IPreview.ts
index 6049d9f8a29..f45476dd2f4 100644
--- a/src/stores/room-list/previews/IPreview.ts
+++ b/src/stores/room-list/previews/IPreview.ts
@@ -26,7 +26,8 @@ export interface IPreview {
* Gets the text which represents the event as a preview.
* @param event The event to preview.
* @param tagId Optional. The tag where the room the event was sent in resides.
+ * @param isThread Optional. Whether the preview being generated is for a thread summary.
* @returns The preview.
*/
- getTextFor(event: MatrixEvent, tagId?: TagID): string | null;
+ getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string | null;
}
diff --git a/src/stores/room-list/previews/MessageEventPreview.ts b/src/stores/room-list/previews/MessageEventPreview.ts
index 3813b1879e4..e98ac5f403d 100644
--- a/src/stores/room-list/previews/MessageEventPreview.ts
+++ b/src/stores/room-list/previews/MessageEventPreview.ts
@@ -15,6 +15,7 @@ limitations under the License.
*/
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { MsgType, RelationType } from "matrix-js-sdk/src/@types/event";
import { IPreview } from "./IPreview";
import { TagID } from "../models";
@@ -27,16 +28,17 @@ export class MessageEventPreview implements IPreview {
public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string {
let eventContent = event.getContent();
- if (event.isRelation("m.replace")) {
+ if (event.isRelation(RelationType.Replace)) {
// It's an edit, generate the preview on the new text
eventContent = event.getContent()['m.new_content'];
}
- if (!eventContent || !eventContent['body']) return null; // invalid for our purposes
+ if (!eventContent?.['body']) return null; // invalid for our purposes
- let body = (eventContent['body'] || '').trim();
- const msgtype = eventContent['msgtype'];
- if (!body || !msgtype) return null; // invalid event, no preview
+ let body = eventContent['body'].trim();
+ if (!body) return null; // invalid event, no preview
+ // A msgtype is actually required in the spec but the app is a bit softer on this requirement
+ const msgtype = eventContent['msgtype'] ?? MsgType.Text;
const hasHtml = eventContent.format === "org.matrix.custom.html" && eventContent.formatted_body;
if (hasHtml) {
@@ -62,7 +64,7 @@ export class MessageEventPreview implements IPreview {
body = sanitizeForTranslation(body);
- if (msgtype === 'm.emote') {
+ if (msgtype === MsgType.Emote) {
return _t("* %(senderName)s %(emote)s", { senderName: getSenderName(event), emote: body });
}
diff --git a/src/stores/spaces/SpaceStore.ts b/src/stores/spaces/SpaceStore.ts
index 3550e84323a..0d9d55e8897 100644
--- a/src/stores/spaces/SpaceStore.ts
+++ b/src/stores/spaces/SpaceStore.ts
@@ -741,7 +741,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
const newPath = new Set(parentPath).add(spaceId);
childSpaces.forEach(childSpace => {
- traverseSpace(childSpace.roomId, newPath) ?? [];
+ traverseSpace(childSpace.roomId, newPath);
});
hiddenChildren.get(spaceId)?.forEach(roomId => {
roomIds.add(roomId);
@@ -812,8 +812,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
this.updateNotificationStates(notificationStatesToUpdate);
};
- private switchSpaceIfNeeded = () => {
- const roomId = RoomViewStore.instance.getRoomId();
+ private switchSpaceIfNeeded = (roomId = RoomViewStore.instance.getRoomId()) => {
if (!this.isRoomInSpace(this.activeSpace, roomId) && !this.matrixClient.getRoom(roomId)?.isSpaceRoom()) {
this.switchToRelatedSpace(roomId);
}
@@ -865,7 +864,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
// if the room currently being viewed was just joined then switch to its related space
if (newMembership === "join" && room.roomId === RoomViewStore.instance.getRoomId()) {
- this.switchToRelatedSpace(room.roomId);
+ this.switchSpaceIfNeeded(room.roomId);
}
}
return;
@@ -970,9 +969,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
};
private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEv?: MatrixEvent) => {
- if (!room.isSpaceRoom()) return;
-
- if (ev.getType() === EventType.SpaceOrder) {
+ if (room.isSpaceRoom() && ev.getType() === EventType.SpaceOrder) {
this.spaceOrderLocalEchoMap.delete(room.roomId); // clear any local echo
const order = ev.getContent()?.order;
const lastOrder = lastEv?.getContent()?.order;
@@ -1085,7 +1082,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
});
const enabledMetaSpaces = SettingsStore.getValue("Spaces.enabledMetaSpaces");
- this._enabledMetaSpaces = metaSpaceOrder.filter(k => enabledMetaSpaces[k]) as MetaSpace[];
+ this._enabledMetaSpaces = metaSpaceOrder.filter(k => enabledMetaSpaces[k]);
this._allRoomsInHome = SettingsStore.getValue("Spaces.allRoomsInHome");
this._showSpaceDMBadges = SettingsStore.getValue("Spaces.showSpaceDMBadges");
@@ -1142,8 +1139,8 @@ 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.roomId, false);
- } else if (!this.isRoomInSpace(this.activeSpace, roomId)) {
- this.switchToRelatedSpace(roomId);
+ } else {
+ this.switchSpaceIfNeeded(roomId);
}
// Persist last viewed room from a space
@@ -1180,8 +1177,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
}
case Action.SettingUpdated: {
- const settingUpdatedPayload = payload as SettingUpdatedPayload;
- switch (settingUpdatedPayload.settingName) {
+ switch (payload.settingName) {
case "Spaces.allRoomsInHome": {
const newValue = SettingsStore.getValue("Spaces.allRoomsInHome");
if (this.allRoomsInHome !== newValue) {
@@ -1206,7 +1202,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
case "Spaces.enabledMetaSpaces": {
const newValue = SettingsStore.getValue("Spaces.enabledMetaSpaces");
- const enabledMetaSpaces = metaSpaceOrder.filter(k => newValue[k]) as MetaSpace[];
+ const enabledMetaSpaces = metaSpaceOrder.filter(k => newValue[k]);
if (arrayHasDiff(this._enabledMetaSpaces, enabledMetaSpaces)) {
const hadPeopleOrHomeEnabled = this.enabledMetaSpaces.some(s => {
return s === MetaSpace.Home || s === MetaSpace.People;
@@ -1237,9 +1233,9 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
case "Spaces.showPeopleInSpace":
// getSpaceFilteredUserIds will return the appropriate value
- this.emit(settingUpdatedPayload.roomId);
+ this.emit(payload.roomId);
if (!this.enabledMetaSpaces.some(s => s === MetaSpace.Home || s === MetaSpace.People)) {
- this.updateNotificationStates([settingUpdatedPayload.roomId]);
+ this.updateNotificationStates([payload.roomId]);
}
break;
}
diff --git a/src/stores/spaces/flattenSpaceHierarchy.ts b/src/stores/spaces/flattenSpaceHierarchy.ts
index 9d94cd4a8d5..138947c3957 100644
--- a/src/stores/spaces/flattenSpaceHierarchy.ts
+++ b/src/stores/spaces/flattenSpaceHierarchy.ts
@@ -38,7 +38,7 @@ const traverseSpaceDescendants = (
};
/**
- * Helper function to traverse space heirachy and flatten
+ * Helper function to traverse space hierarchy and flatten
* @param spaceEntityMap ie map of rooms or dm userIds
* @param spaceDescendantMap map of spaces and their children
* @returns set of all rooms
diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts
index 2784dcf0d96..5f95e441093 100644
--- a/src/stores/widgets/StopGapWidget.ts
+++ b/src/stores/widgets/StopGapWidget.ts
@@ -344,20 +344,12 @@ export class StopGapWidget extends EventEmitter {
const integType = data?.integType;
const integId = data?.integId;
- // TODO: Open the right integration manager for the widget
- if (SettingsStore.getValue("feature_many_integration_managers")) {
- IntegrationManagers.sharedInstance().openAll(
- MatrixClientPeg.get().getRoom(RoomViewStore.instance.getRoomId()),
- `type_${integType}`,
- integId,
- );
- } else {
- IntegrationManagers.sharedInstance().getPrimaryManager().open(
- MatrixClientPeg.get().getRoom(RoomViewStore.instance.getRoomId()),
- `type_${integType}`,
- integId,
- );
- }
+ // noinspection JSIgnoredPromiseFromCall
+ IntegrationManagers.sharedInstance().getPrimaryManager().open(
+ MatrixClientPeg.get().getRoom(RoomViewStore.instance.getRoomId()),
+ `type_${integType}`,
+ integId,
+ );
},
);
}
diff --git a/src/stores/widgets/WidgetMessagingStore.ts b/src/stores/widgets/WidgetMessagingStore.ts
index d954af6d609..bcc46c0e43a 100644
--- a/src/stores/widgets/WidgetMessagingStore.ts
+++ b/src/stores/widgets/WidgetMessagingStore.ts
@@ -91,7 +91,7 @@ export class WidgetMessagingStore extends AsyncStoreWithClient {
/**
* Gets the widget messaging class for a given widget UID.
* @param {string} widgetUid The widget UID.
- * @returns {ClientWidgetApi} The widget API, or a falsey value if not found.
+ * @returns {ClientWidgetApi} The widget API, or a falsy value if not found.
*/
public getMessagingForUid(widgetUid: string): ClientWidgetApi {
return this.widgetMap.get(widgetUid);
diff --git a/src/toasts/ServerLimitToast.tsx b/src/toasts/ServerLimitToast.tsx
index 9a104f552ec..972b46b39f4 100644
--- a/src/toasts/ServerLimitToast.tsx
+++ b/src/toasts/ServerLimitToast.tsx
@@ -26,7 +26,7 @@ const TOAST_KEY = "serverlimit";
export const showToast = (limitType: string, onHideToast: () => void, adminContact?: string, syncError?: boolean) => {
const errorText = messageForResourceLimitError(limitType, adminContact, {
'monthly_active_user': _td("Your homeserver has exceeded its user limit."),
- 'hs_blocked': _td("This homeserver has been blocked by it's administrator."),
+ 'hs_blocked': _td("This homeserver has been blocked by its administrator."),
'': _td("Your homeserver has exceeded one of its resource limits."),
});
const contactText = messageForResourceLimitError(limitType, adminContact, {
diff --git a/src/usercontent/index.ts b/src/usercontent/index.ts
index df551e88e61..91a384cfc09 100644
--- a/src/usercontent/index.ts
+++ b/src/usercontent/index.ts
@@ -1,3 +1,19 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
let hasCalled = false;
function remoteRender(event: MessageEvent): void {
const data = event.data;
diff --git a/src/utils/DMRoomMap.ts b/src/utils/DMRoomMap.ts
index 69ab4e192bb..811522a667c 100644
--- a/src/utils/DMRoomMap.ts
+++ b/src/utils/DMRoomMap.ts
@@ -20,6 +20,7 @@ import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
import { logger } from "matrix-js-sdk/src/logger";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { Optional } from "matrix-events-sdk";
import { MatrixClientPeg } from '../MatrixClientPeg';
@@ -141,7 +142,7 @@ export default class DMRoomMap {
/**
* Gets the DM room which the given IDs share, if any.
* @param {string[]} ids The identifiers (user IDs and email addresses) to look for.
- * @returns {Room} The DM room which all IDs given share, or falsey if no common room.
+ * @returns {Room} The DM room which all IDs given share, or falsy if no common room.
*/
public getDMRoomForIdentifiers(ids: string[]): Room {
// TODO: [Canonical DMs] Handle lookups for email addresses.
@@ -159,7 +160,7 @@ export default class DMRoomMap {
return joinedRooms[0];
}
- public getUserIdForRoomId(roomId: string) {
+ public getUserIdForRoomId(roomId: string): Optional {
if (this.roomToUser == null) {
// we lazily populate roomToUser so you can use
// this class just to call getDMRoomsForUserId
diff --git a/src/utils/DialogOpener.ts b/src/utils/DialogOpener.ts
index dadce60ef6e..8c5c9c374ba 100644
--- a/src/utils/DialogOpener.ts
+++ b/src/utils/DialogOpener.ts
@@ -23,7 +23,6 @@ import ForwardDialog from "../components/views/dialogs/ForwardDialog";
import { MatrixClientPeg } from "../MatrixClientPeg";
import { Action } from "../dispatcher/actions";
import ReportEventDialog from "../components/views/dialogs/ReportEventDialog";
-import TabbedIntegrationManagerDialog from "../components/views/dialogs/TabbedIntegrationManagerDialog";
import SpacePreferencesDialog from "../components/views/dialogs/SpacePreferencesDialog";
import SpaceSettingsDialog from "../components/views/dialogs/SpaceSettingsDialog";
import InviteDialog from "../components/views/dialogs/InviteDialog";
@@ -73,17 +72,6 @@ export class DialogOpener {
mxEvent: payload.event,
}, 'mx_Dialog_reportEvent');
break;
- case Action.OpenTabbedIntegrationManagerDialog:
- Modal.createTrackedDialog(
- 'Tabbed Integration Manager', '', TabbedIntegrationManagerDialog,
- {
- room: payload.room,
- screen: payload.screen,
- integrationId: payload.integrationId,
- },
- 'mx_TabbedIntegrationManagerDialog',
- );
- break;
case Action.OpenSpacePreferences:
Modal.createTrackedDialog("Space preferences", "", SpacePreferencesDialog, {
initialTabId: payload.initalTabId,
diff --git a/src/utils/ErrorUtils.tsx b/src/utils/ErrorUtils.tsx
index 52c9c470f85..6af61aca2e1 100644
--- a/src/utils/ErrorUtils.tsx
+++ b/src/utils/ErrorUtils.tsx
@@ -25,7 +25,7 @@ import { _t, _td, Tags, TranslatedString } from '../languageHandler';
*
* @param {string} limitType The limit_type from the error
* @param {string} adminContact The admin_contact from the error
- * @param {Object} strings Translateable string for different
+ * @param {Object} strings Translatable string for different
* limit_type. Must include at least the empty string key
* which is the default. Strings may include an 'a' tag
* for the admin contact link.
diff --git a/src/utils/EventRenderingUtils.ts b/src/utils/EventRenderingUtils.ts
index a44db854fe2..ced38748049 100644
--- a/src/utils/EventRenderingUtils.ts
+++ b/src/utils/EventRenderingUtils.ts
@@ -17,13 +17,12 @@ limitations under the License.
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
import { M_POLL_START } from "matrix-events-sdk";
-import { M_LOCATION } from "matrix-js-sdk/src/@types/location";
import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon";
import SettingsStore from "../settings/SettingsStore";
import { haveRendererForEvent, JitsiEventFactory, JSONEventFactory, pickFactory } from "../events/EventTileFactory";
import { MatrixClientPeg } from "../MatrixClientPeg";
-import { getMessageModerationState, MessageModerationState } from "./EventUtils";
+import { getMessageModerationState, isLocationEvent, MessageModerationState } from "./EventUtils";
export function getEventDisplayInfo(mxEvent: MatrixEvent, showHiddenEvents: boolean, hideEvent?: boolean): {
isInfoMessage: boolean;
@@ -80,12 +79,8 @@ export function getEventDisplayInfo(mxEvent: MatrixEvent, showHiddenEvents: bool
const noBubbleEvent = (
(eventType === EventType.RoomMessage && msgtype === MsgType.Emote) ||
M_POLL_START.matches(eventType) ||
- M_LOCATION.matches(eventType) ||
M_BEACON_INFO.matches(eventType) ||
- (
- eventType === EventType.RoomMessage &&
- M_LOCATION.matches(msgtype)
- )
+ isLocationEvent(mxEvent)
);
// If we're showing hidden events in the timeline, we should use the
diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts
index bbfaf3f6138..34e24340fcf 100644
--- a/src/utils/EventUtils.ts
+++ b/src/utils/EventUtils.ts
@@ -277,7 +277,10 @@ export const isLocationEvent = (event: MatrixEvent): boolean => {
export function canForward(event: MatrixEvent): boolean {
return !(
- isLocationEvent(event) ||
M_POLL_START.matches(event.getType())
);
}
+
+export function hasThreadSummary(event: MatrixEvent): boolean {
+ return event.isThreadRoot && event.getThread()?.length && !!event.getThread().replyToEvent;
+}
diff --git a/src/utils/FileUtils.ts b/src/utils/FileUtils.ts
index 21d29280733..948f023d7e7 100644
--- a/src/utils/FileUtils.ts
+++ b/src/utils/FileUtils.ts
@@ -37,7 +37,7 @@ export function presentableTextForFile(
shortened = false,
): string {
let text = fallbackText;
- if (content.body && content.body.length > 0) {
+ if (content.body?.length > 0) {
// The content body should be the name of the file including a
// file extension.
text = content.body;
@@ -58,13 +58,13 @@ export function presentableTextForFile(
text = `${fileName}...${extension}`;
}
- if (content.info && content.info.size && withSize) {
+ if (content.info?.size && withSize) {
// If we know the size of the file then add it as human readable
// string to the end of the link text so that the user knows how
// big a file they are downloading.
// The content.info also contains a MIME-type but we don't display
// it since it is "ugly", users generally aren't aware what it
- // means and the type of the attachment can usually be inferrered
+ // means and the type of the attachment can usually be inferred
// from the file extension.
text += ' (' + filesize(content.info.size) + ')';
}
diff --git a/src/utils/LazyValue.ts b/src/utils/LazyValue.ts
index 9cdcda489a3..70ffb1106ca 100644
--- a/src/utils/LazyValue.ts
+++ b/src/utils/LazyValue.ts
@@ -29,7 +29,7 @@ export class LazyValue {
* Whether or not a cached value is present.
*/
public get present(): boolean {
- // we use a tracking variable just in case the final value is falsey
+ // we use a tracking variable just in case the final value is falsy
return this.done;
}
diff --git a/src/utils/MegolmExportEncryption.ts b/src/utils/MegolmExportEncryption.ts
index b88b21132ab..8d7f0a00ad3 100644
--- a/src/utils/MegolmExportEncryption.ts
+++ b/src/utils/MegolmExportEncryption.ts
@@ -261,7 +261,7 @@ async function deriveKeys(salt: Uint8Array, iterations: number, password: string
throw friendlyError('subtleCrypto.importKey failed for HMAC key: ' + e, cryptoFailMsg());
});
- return await Promise.all([aesProm, hmacProm]);
+ return Promise.all([aesProm, hmacProm]);
}
const HEADER_LINE = '-----BEGIN MEGOLM SESSION DATA-----';
diff --git a/src/utils/MultiInviter.ts b/src/utils/MultiInviter.ts
index 9916916f8c7..cace1cb8762 100644
--- a/src/utils/MultiInviter.ts
+++ b/src/utils/MultiInviter.ts
@@ -168,10 +168,19 @@ export default class MultiInviter {
}
if (!ignoreProfile && SettingsStore.getValue("promptBeforeInviteUnknownUsers", this.roomId)) {
- const profile = await this.matrixClient.getProfileInfo(addr);
- if (!profile) {
- // noinspection ExceptionCaughtLocallyJS
- throw new Error("User has no profile");
+ try {
+ await this.matrixClient.getProfileInfo(addr);
+ } catch (err) {
+ // The error handling during the invitation process covers any API.
+ // Some errors must to me mapped from profile API errors to more specific ones to avoid collisions.
+ switch (err.errcode) {
+ case 'M_FORBIDDEN':
+ throw new MatrixError({ errcode: 'M_PROFILE_UNDISCLOSED' });
+ case 'M_NOT_FOUND':
+ throw new MatrixError({ errcode: 'M_USER_NOT_FOUND' });
+ default:
+ throw err;
+ }
}
}
diff --git a/src/utils/Receipt.ts b/src/utils/Receipt.ts
index 2a626decc47..4b1c0ffbfba 100644
--- a/src/utils/Receipt.ts
+++ b/src/utils/Receipt.ts
@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
/**
@@ -29,9 +30,8 @@ export function findReadReceiptFromUserId(receiptEvent: MatrixEvent, userId: str
const receiptKeys = Object.keys(receiptEvent.getContent());
for (let i = 0; i < receiptKeys.length; ++i) {
const rcpt = receiptEvent.getContent()[receiptKeys[i]];
- if (rcpt['m.read'] && rcpt['m.read'][userId]) {
- return rcpt;
- }
+ if (rcpt[ReceiptType.Read]?.[userId]) return rcpt;
+ if (rcpt[ReceiptType.ReadPrivate]?.[userId]) return rcpt;
}
return null;
diff --git a/src/utils/Singleflight.ts b/src/utils/Singleflight.ts
index 07d82efa3ee..93822594a2b 100644
--- a/src/utils/Singleflight.ts
+++ b/src/utils/Singleflight.ts
@@ -34,7 +34,7 @@ const keyMap = new EnhancedMap>();
* second call comes through late. There are various functions named "forget"
* to have the cache be cleared of a result.
*
- * Singleflights in our usecase are tied to an instance of something, combined
+ * Singleflights in our use case are tied to an instance of something, combined
* with a string key to differentiate between multiple possible actions. This
* means that a "save" key will be scoped to the instance which defined it and
* not leak between other instances. This is done to avoid having to concatenate
diff --git a/src/utils/StorageManager.ts b/src/utils/StorageManager.ts
index 7d9ce885f74..ed37064920c 100644
--- a/src/utils/StorageManager.ts
+++ b/src/utils/StorageManager.ts
@@ -18,6 +18,7 @@ import { LocalStorageCryptoStore } from 'matrix-js-sdk/src/crypto/store/localSto
import { IndexedDBStore } from "matrix-js-sdk/src/store/indexeddb";
import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
import { logger } from "matrix-js-sdk/src/logger";
+import { MatrixClient } from 'matrix-js-sdk/src/client';
import Analytics from '../Analytics';
@@ -25,7 +26,7 @@ const localStorage = window.localStorage;
// just *accessing* indexedDB throws an exception in firefox with
// indexeddb disabled.
-let indexedDB;
+let indexedDB: IDBFactory;
try {
indexedDB = window.indexedDB;
} catch (e) {}
@@ -161,7 +162,7 @@ async function checkCryptoStore() {
track("Crypto store using IndexedDB inaccessible");
}
try {
- exists = await LocalStorageCryptoStore.exists(localStorage);
+ exists = LocalStorageCryptoStore.exists(localStorage);
log(`Crypto store using local storage contains data? ${exists}`);
return { exists, healthy: true };
} catch (e) {
@@ -172,12 +173,10 @@ async function checkCryptoStore() {
return { exists, healthy: false };
}
-export function trackStores(client) {
- if (client.store && client.store.on) {
- client.store.on("degraded", () => {
- track("Sync store using IndexedDB degraded to memory");
- });
- }
+export function trackStores(client: MatrixClient) {
+ client.store?.on?.("degraded", () => {
+ track("Sync store using IndexedDB degraded to memory");
+ });
}
/**
@@ -188,16 +187,16 @@ export function trackStores(client) {
* and if it is true and not crypto data is found, an error is
* presented to the user.
*
- * @param {bool} cryptoInited True if crypto has been set up
+ * @param {boolean} cryptoInited True if crypto has been set up
*/
-export function setCryptoInitialised(cryptoInited) {
- localStorage.setItem("mx_crypto_initialised", cryptoInited);
+export function setCryptoInitialised(cryptoInited: boolean) {
+ localStorage.setItem("mx_crypto_initialised", String(cryptoInited));
}
/* Simple wrapper functions around IndexedDB.
*/
-let idb = null;
+let idb: IDBDatabase = null;
async function idbInit(): Promise {
if (!indexedDB) {
@@ -206,8 +205,8 @@ async function idbInit(): Promise {
idb = await new Promise((resolve, reject) => {
const request = indexedDB.open("matrix-react-sdk", 1);
request.onerror = reject;
- request.onsuccess = (event) => { resolve(request.result); };
- request.onupgradeneeded = (event) => {
+ request.onsuccess = () => { resolve(request.result); };
+ request.onupgradeneeded = () => {
const db = request.result;
db.createObjectStore("pickleKey");
db.createObjectStore("account");
@@ -266,6 +265,6 @@ export async function idbDelete(
const objectStore = txn.objectStore(table);
const request = objectStore.delete(key);
request.onerror = reject;
- request.onsuccess = (event) => { resolve(); };
+ request.onsuccess = () => { resolve(); };
});
}
diff --git a/src/utils/VideoChannelUtils.ts b/src/utils/VideoChannelUtils.ts
index cc3c99d980c..c4c757e29f1 100644
--- a/src/utils/VideoChannelUtils.ts
+++ b/src/utils/VideoChannelUtils.ts
@@ -16,6 +16,8 @@ limitations under the License.
import { useState } from "react";
import { throttle } from "lodash";
+import { Optional } from "matrix-events-sdk";
+import { IMyDevice } from "matrix-js-sdk/src/client";
import { CallType } from "matrix-js-sdk/src/webrtc/call";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
@@ -26,14 +28,16 @@ import WidgetStore, { IApp } from "../stores/WidgetStore";
import { WidgetType } from "../widgets/WidgetType";
import WidgetUtils from "./WidgetUtils";
-export const VIDEO_CHANNEL = "io.element.video";
-export const VIDEO_CHANNEL_MEMBER = "io.element.video.member";
+const STUCK_DEVICE_TIMEOUT_MS = 1000 * 60 * 60;
-export interface IVideoChannelMemberContent {
+interface IVideoChannelMemberContent {
// Connected device IDs
devices: string[];
}
+export const VIDEO_CHANNEL = "io.element.video";
+export const VIDEO_CHANNEL_MEMBER = "io.element.video.member";
+
export const getVideoChannel = (roomId: string): IApp => {
const apps = WidgetStore.instance.getApps(roomId);
return apps.find(app => WidgetType.JITSI.matches(app.type) && app.id === VIDEO_CHANNEL);
@@ -72,3 +76,54 @@ export const useConnectedMembers = (
}, throttleMs, { leading: true, trailing: true }));
return members;
};
+
+const updateDevices = async (room: Optional, fn: (devices: string[] | null) => string[]) => {
+ if (room?.getMyMembership() !== "join") return;
+
+ const devicesState = room.currentState.getStateEvents(VIDEO_CHANNEL_MEMBER, room.client.getUserId());
+ const devices = devicesState?.getContent()?.devices ?? [];
+ const newDevices = fn(devices);
+
+ if (newDevices) {
+ await room.client.sendStateEvent(
+ room.roomId, VIDEO_CHANNEL_MEMBER, { devices: newDevices }, room.client.getUserId(),
+ );
+ }
+};
+
+export const addOurDevice = async (room: Room) => {
+ await updateDevices(room, devices => Array.from(new Set(devices).add(room.client.getDeviceId())));
+};
+
+export const removeOurDevice = async (room: Room) => {
+ await updateDevices(room, devices => {
+ const devicesSet = new Set(devices);
+ devicesSet.delete(room.client.getDeviceId());
+ return Array.from(devicesSet);
+ });
+};
+
+/**
+ * Fixes devices that may have gotten stuck in video channel member state after
+ * an unclean disconnection, by filtering out logged out devices, inactive
+ * devices, and our own device (if we're disconnected).
+ * @param {Room} room The room to fix
+ * @param {boolean} connectedLocalEcho Local echo of whether this device is connected
+ */
+export const fixStuckDevices = async (room: Room, connectedLocalEcho: boolean) => {
+ const now = new Date().valueOf();
+ const { devices: myDevices } = await room.client.getDevices();
+ const deviceMap = new Map(myDevices.map(d => [d.device_id, d]));
+
+ await updateDevices(room, devices => {
+ const newDevices = devices.filter(d => {
+ const device = deviceMap.get(d);
+ return device?.last_seen_ts
+ && !(d === room.client.getDeviceId() && !connectedLocalEcho)
+ && (now - device.last_seen_ts) < STUCK_DEVICE_TIMEOUT_MS;
+ });
+
+ // Skip the update if the devices are unchanged
+ return newDevices.length === devices.length ? null : newDevices;
+ });
+};
diff --git a/src/utils/WidgetUtils.ts b/src/utils/WidgetUtils.ts
index 8537e035837..b2f33b22253 100644
--- a/src/utils/WidgetUtils.ts
+++ b/src/utils/WidgetUtils.ts
@@ -17,7 +17,7 @@ limitations under the License.
import * as url from "url";
import { base32 } from "rfc4648";
-import { Capability, IWidget, IWidgetData, MatrixCapabilities } from "matrix-widget-api";
+import { IWidget, IWidgetData } from "matrix-widget-api";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { logger } from "matrix-js-sdk/src/logger";
@@ -29,7 +29,6 @@ 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 { WidgetType } from "../widgets/WidgetType";
import { Jitsi } from "../widgets/Jitsi";
@@ -496,21 +495,6 @@ export default class WidgetUtils {
return app as IApp;
}
- static getCapWhitelistForAppTypeInRoomId(appType: string, roomId: string): Capability[] {
- const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", roomId);
-
- const capWhitelist = enableScreenshots ? [MatrixCapabilities.Screenshots] : [];
-
- // Obviously anyone that can add a widget can claim it's a jitsi widget,
- // so this doesn't really offer much over the set of domains we load
- // widgets from at all, but it probably makes sense for sanity.
- if (WidgetType.JITSI.matches(appType)) {
- capWhitelist.push(MatrixCapabilities.AlwaysOnScreen);
- }
-
- return capWhitelist;
- }
-
static getLocalJitsiWrapperUrl(opts: {forLocalRender?: boolean, auth?: string} = {}) {
// NB. we can't just encodeURIComponent all of these because the $ signs need to be there
const queryStringParts = [
@@ -560,12 +544,8 @@ export default class WidgetUtils {
}
static editWidget(room: Room, app: IApp): void {
- // TODO: Open the right manager for the widget
- if (SettingsStore.getValue("feature_many_integration_managers")) {
- IntegrationManagers.sharedInstance().openAll(room, 'type_' + app.type, app.id);
- } else {
- IntegrationManagers.sharedInstance().getPrimaryManager().open(room, 'type_' + app.type, app.id);
- }
+ // noinspection JSIgnoredPromiseFromCall
+ IntegrationManagers.sharedInstance().getPrimaryManager().open(room, 'type_' + app.type, app.id);
}
static isManagedByManager(app) {
diff --git a/src/utils/beacon/geolocation.ts b/src/utils/beacon/geolocation.ts
index efc3a9b44c3..6925ca73b58 100644
--- a/src/utils/beacon/geolocation.ts
+++ b/src/utils/beacon/geolocation.ts
@@ -136,7 +136,8 @@ export const getCurrentPosition = async (): Promise => {
export type ClearWatchCallback = () => void;
export const watchPosition = (
onWatchPosition: PositionCallback,
- onWatchPositionError: (error: GeolocationError) => void): ClearWatchCallback => {
+ onWatchPositionError: (error: GeolocationError) => void,
+): ClearWatchCallback => {
try {
const onError = (error) => onWatchPositionError(mapGeolocationError(error));
const watchId = getGeolocation().watchPosition(onWatchPosition, onError, GeolocationOptions);
diff --git a/src/utils/beacon/useBeacon.ts b/src/utils/beacon/useBeacon.ts
index 91b00104a17..e1dcfc49758 100644
--- a/src/utils/beacon/useBeacon.ts
+++ b/src/utils/beacon/useBeacon.ts
@@ -54,7 +54,7 @@ export const useBeacon = (beaconInfoEvent: MatrixEvent): Beacon | undefined => {
}
}, [beaconInfoEvent, matrixClient]);
- // beacon update will fire when this beacon is superceded
+ // beacon update will fire when this beacon is superseded
// check the updated event id for equality to the matrix event
const beaconInstanceEventId = useEventEmitterState(
beacon,
diff --git a/src/utils/beacon/useOwnLiveBeacons.ts b/src/utils/beacon/useOwnLiveBeacons.ts
index 1eb892f0c33..d83a66a1d4f 100644
--- a/src/utils/beacon/useOwnLiveBeacons.ts
+++ b/src/utils/beacon/useOwnLiveBeacons.ts
@@ -80,7 +80,9 @@ export const useOwnLiveBeacons = (liveBeaconIds: BeaconIdentifier[]): LiveBeacon
};
const onResetLocationPublishError = () => {
- liveBeaconIds.map(beaconId => OwnBeaconStore.instance.resetLocationPublishError(beaconId));
+ liveBeaconIds.forEach(beaconId => {
+ OwnBeaconStore.instance.resetLocationPublishError(beaconId);
+ });
};
return {
diff --git a/src/utils/direct-messages.ts b/src/utils/direct-messages.ts
index 0932cc9aaf1..e67c01c7cad 100644
--- a/src/utils/direct-messages.ts
+++ b/src/utils/direct-messages.ts
@@ -175,7 +175,7 @@ export class ThreepidMember extends Member {
this.id = id;
}
- // This is a getter that would be falsey on all other implementations. Until we have
+ // This is a getter that would be falsy on all other implementations. Until we have
// better type support in the react-sdk we can use this trick to determine the kind
// of 3PID we're dealing with, if any.
get isEmail(): boolean {
diff --git a/src/utils/drawable.ts b/src/utils/drawable.ts
index 31f7bc8cec7..5c95fb3889c 100644
--- a/src/utils/drawable.ts
+++ b/src/utils/drawable.ts
@@ -23,7 +23,7 @@ export async function getDrawable(url: string): Promise {
if ('createImageBitmap' in window) {
const response = await fetch(url);
const blob = await response.blob();
- return await createImageBitmap(blob);
+ return createImageBitmap(blob);
} else {
return new Promise((resolve, reject) => {
const img = document.createElement("img");
diff --git a/src/utils/exportUtils/HtmlExport.tsx b/src/utils/exportUtils/HtmlExport.tsx
index d87c15adee5..b6db2673873 100644
--- a/src/utils/exportUtils/HtmlExport.tsx
+++ b/src/utils/exportUtils/HtmlExport.tsx
@@ -34,8 +34,7 @@ import * as Avatar from "../../Avatar";
import EventTile from "../../components/views/rooms/EventTile";
import DateSeparator from "../../components/views/messages/DateSeparator";
import BaseAvatar from "../../components/views/avatars/BaseAvatar";
-import { ExportType } from "./exportUtils";
-import { IExportOptions } from "./exportUtils";
+import { ExportType, IExportOptions } from "./exportUtils";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import getExportCSS from "./exportCSS";
import { textForEvent } from "../../TextForEvent";
@@ -418,7 +417,7 @@ export default class HTMLExporter extends Exporter {
content += body;
prevEvent = event;
}
- return await this.wrapHTML(content);
+ return this.wrapHTML(content);
}
public async export() {
diff --git a/src/utils/exportUtils/exportCustomCSS.css b/src/utils/exportUtils/exportCustomCSS.css
index 0a0a2c20054..a62f8906499 100644
--- a/src/utils/exportUtils/exportCustomCSS.css
+++ b/src/utils/exportUtils/exportCustomCSS.css
@@ -120,7 +120,7 @@ a.mx_reply_anchor:hover {
}
.mx_ReplyChain_Export {
- margin-top: 0px;
+ margin-top: 0;
margin-bottom: 5px;
}
diff --git a/src/utils/image-media.ts b/src/utils/image-media.ts
index a57e4b841ab..b7681333821 100644
--- a/src/utils/image-media.ts
+++ b/src/utils/image-media.ts
@@ -84,8 +84,8 @@ export async function createThumbnail(
} catch (e) {
// Fallback support for other browsers (Safari and Firefox for now)
canvas = document.createElement("canvas");
- (canvas as HTMLCanvasElement).width = targetWidth;
- (canvas as HTMLCanvasElement).height = targetHeight;
+ canvas.width = targetWidth;
+ canvas.height = targetHeight;
context = canvas.getContext("2d");
}
diff --git a/src/utils/location/map.ts b/src/utils/location/map.ts
index 5627bf5c0ed..b3b74d14495 100644
--- a/src/utils/location/map.ts
+++ b/src/utils/location/map.ts
@@ -63,39 +63,6 @@ export const createMarker = (coords: GeolocationCoordinates, element: HTMLElemen
return marker;
};
-export const createMapWithCoords = (
- coords: GeolocationCoordinates,
- interactive: boolean,
- bodyId: string,
- markerId: string,
- onError: (error: Error) => void,
-): maplibregl.Map => {
- try {
- const map = createMap(interactive, bodyId, onError);
-
- const coordinates = new maplibregl.LngLat(coords.longitude, coords.latitude);
- // center on coordinates
- map.setCenter(coordinates);
-
- const marker = createMarker(coords, document.getElementById(markerId));
- marker.addTo(map);
-
- map.on('error', (e) => {
- logger.error(
- "Failed to load map: check map_style_url in config.json has a "
- + "valid URL and API key",
- e.error,
- );
- onError(new Error(LocationShareError.MapStyleUrlNotReachable));
- });
-
- return map;
- } catch (e) {
- logger.error("Failed to render map", e);
- onError(e);
- }
-};
-
const makeLink = (coords: GeolocationCoordinates): string => {
return (
"https://www.openstreetmap.org/" +
diff --git a/src/utils/permalinks/ElementPermalinkConstructor.ts b/src/utils/permalinks/ElementPermalinkConstructor.ts
index b901581ca62..01525081a60 100644
--- a/src/utils/permalinks/ElementPermalinkConstructor.ts
+++ b/src/utils/permalinks/ElementPermalinkConstructor.ts
@@ -80,7 +80,7 @@ export default class ElementPermalinkConstructor extends PermalinkConstructor {
}
/**
- * Parses an app route (`(user|room)/identifer`) to a Matrix entity
+ * Parses an app route (`(user|room)/identifier`) to a Matrix entity
* (room, user).
* @param {string} route The app route
* @returns {PermalinkParts}
diff --git a/src/utils/permalinks/Permalinks.ts b/src/utils/permalinks/Permalinks.ts
index 8c213a8cffc..d4d66270519 100644
--- a/src/utils/permalinks/Permalinks.ts
+++ b/src/utils/permalinks/Permalinks.ts
@@ -32,6 +32,8 @@ import MatrixSchemePermalinkConstructor from "./MatrixSchemePermalinkConstructor
// to add to permalinks. The servers are appended as ?via=example.org
const MAX_SERVER_CANDIDATES = 3;
+const ANY_REGEX = /.*/;
+
// Permalinks can have servers appended to them so that the user
// receiving them can have a fighting chance at joining the room.
// These servers are called "candidates" at this point because
@@ -207,7 +209,7 @@ export class RoomPermalinkCreator {
private updateAllowedServers() {
const bannedHostsRegexps = [];
- let allowedHostsRegexps = [new RegExp(".*")]; // default allow everyone
+ let allowedHostsRegexps = [ANY_REGEX]; // default allow everyone
if (this.room.currentState) {
const aclEvent = this.room.currentState.getStateEvents("m.room.server_acl", "");
if (aclEvent && aclEvent.getContent()) {
@@ -272,7 +274,7 @@ export function makeUserPermalink(userId: string): string {
export function makeRoomPermalink(roomId: string): string {
if (!roomId) {
- throw new Error("can't permalink a falsey roomId");
+ throw new Error("can't permalink a falsy roomId");
}
// If the roomId isn't actually a room ID, don't try to list the servers.
diff --git a/src/utils/pillify.tsx b/src/utils/pillify.tsx
index 2032d86b690..87b95007e32 100644
--- a/src/utils/pillify.tsx
+++ b/src/utils/pillify.tsx
@@ -21,7 +21,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { MatrixClientPeg } from '../MatrixClientPeg';
import SettingsStore from "../settings/SettingsStore";
-import Pill from "../components/views/elements/Pill";
+import Pill, { PillType } from "../components/views/elements/Pill";
import { parsePermalink } from "./permalinks/Permalinks";
/**
@@ -113,7 +113,7 @@ export function pillifyLinks(nodes: ArrayLike, mxEvent: MatrixEvent, pi
const pillContainer = document.createElement('span');
const pill = {
@@ -60,14 +58,14 @@ export const makeSpaceParentEvent = (room: Room, canonical = false) => ({
});
export function showSpaceSettings(space: Room) {
- dis.dispatch({
+ defaultDispatcher.dispatch({
action: Action.OpenSpaceSettings,
space,
});
}
export const showAddExistingRooms = (space: Room): void => {
- dis.dispatch({
+ defaultDispatcher.dispatch({
action: Action.OpenAddToExistingSpaceDialog,
space,
});
@@ -168,7 +166,7 @@ export const bulkSpaceBehaviour = async (
};
export const showSpacePreferences = (space: Room, initialTabId?: SpacePreferenceTab) => {
- dis.dispatch({
+ defaultDispatcher.dispatch({
action: Action.OpenSpacePreferences,
space,
initialTabId,
diff --git a/src/utils/strings.ts b/src/utils/strings.ts
index 41f80ed1c53..8bf039656d8 100644
--- a/src/utils/strings.ts
+++ b/src/utils/strings.ts
@@ -24,7 +24,7 @@ import { logger } from "matrix-js-sdk/src/logger";
export async function copyPlaintext(text: string): Promise {
try {
- if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
+ if (navigator?.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return true;
} else {
diff --git a/src/utils/useTooltip.tsx b/src/utils/useTooltip.tsx
index b40239001ba..98b6ffa1bda 100644
--- a/src/utils/useTooltip.tsx
+++ b/src/utils/useTooltip.tsx
@@ -1,3 +1,19 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
import React, { ComponentProps, useState } from "react";
import Tooltip from "../components/views/elements/Tooltip";
diff --git a/src/utils/validate/index.ts b/src/utils/validate/index.ts
index f4357cbc17f..8a690b9d4db 100644
--- a/src/utils/validate/index.ts
+++ b/src/utils/validate/index.ts
@@ -1 +1,17 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
export * from "./numberInRange";
diff --git a/src/utils/validate/numberInRange.ts b/src/utils/validate/numberInRange.ts
index dda5af8f07c..181641c9354 100644
--- a/src/utils/validate/numberInRange.ts
+++ b/src/utils/validate/numberInRange.ts
@@ -1,3 +1,18 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
/**
* Validates that a value is
diff --git a/test/.eslintrc.js b/test/.eslintrc.js
index c46b24aa57f..ee22692130f 100644
--- a/test/.eslintrc.js
+++ b/test/.eslintrc.js
@@ -1,3 +1,19 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
module.exports = {
env: {
mocha: true,
diff --git a/test/DecryptionFailureTracker-test.js b/test/DecryptionFailureTracker-test.js
index b0494f97aa6..997c4913c3d 100644
--- a/test/DecryptionFailureTracker-test.js
+++ b/test/DecryptionFailureTracker-test.js
@@ -57,6 +57,33 @@ describe('DecryptionFailureTracker', function() {
done();
});
+ it('tracks a failed decryption with expected raw error for a visible event', function(done) {
+ const failedDecryptionEvent = createFailedDecryptionEvent();
+
+ let count = 0;
+ let reportedRawCode = "";
+ const tracker = new DecryptionFailureTracker((total, errcode, rawCode) => {
+ count += total;
+ reportedRawCode = rawCode;
+ }, () => "UnknownError");
+
+ tracker.addVisibleEvent(failedDecryptionEvent);
+
+ const err = new MockDecryptionError('INBOUND_SESSION_MISMATCH_ROOM_ID');
+ tracker.eventDecrypted(failedDecryptionEvent, err);
+
+ // Pretend "now" is Infinity
+ tracker.checkFailures(Infinity);
+
+ // Immediately track the newest failures
+ tracker.trackFailures();
+
+ expect(count).not.toBe(0, 'should track a failure for an event that failed decryption');
+ expect(reportedRawCode).toBe('INBOUND_SESSION_MISMATCH_ROOM_ID', 'Should add the rawCode to the event context');
+
+ done();
+ });
+
it('tracks a failed decryption for an event that becomes visible later', function(done) {
const failedDecryptionEvent = createFailedDecryptionEvent();
diff --git a/test/RoomNotifs-test.ts b/test/RoomNotifs-test.ts
index 703f681e329..3f486205dfc 100644
--- a/test/RoomNotifs-test.ts
+++ b/test/RoomNotifs-test.ts
@@ -79,7 +79,7 @@ describe("RoomNotifs test", () => {
rule_id: "!roomId:server",
enabled: true,
default: false,
- actions: [{ set_tweak: TweakName.Sound }],
+ actions: [{ set_tweak: TweakName.Sound, value: "default" }],
});
expect(getRoomNotifsState("!roomId:server")).toBe(RoomNotifState.AllMessagesLoud);
});
diff --git a/test/ScalarAuthClient-test.js b/test/ScalarAuthClient-test.ts
similarity index 83%
rename from test/ScalarAuthClient-test.js
rename to test/ScalarAuthClient-test.ts
index a597c2cb2e5..3b6fcf77b2b 100644
--- a/test/ScalarAuthClient-test.js
+++ b/test/ScalarAuthClient-test.ts
@@ -19,6 +19,8 @@ import { MatrixClientPeg } from '../src/MatrixClientPeg';
import { stubClient } from './test-utils';
describe('ScalarAuthClient', function() {
+ const apiUrl = 'test.com/api';
+ const uiUrl = 'test.com/app';
beforeEach(function() {
window.localStorage.getItem = jest.fn((arg) => {
if (arg === "mx_scalar_token") return "brokentoken";
@@ -27,15 +29,17 @@ describe('ScalarAuthClient', function() {
});
it('should request a new token if the old one fails', async function() {
- const sac = new ScalarAuthClient();
+ const sac = new ScalarAuthClient(apiUrl, uiUrl);
- sac.getAccountName = jest.fn((arg) => {
+ // @ts-ignore unhappy with Promise calls
+ jest.spyOn(sac, 'getAccountName').mockImplementation((arg: string) => {
switch (arg) {
case "brokentoken":
return Promise.reject({
message: "Invalid token",
});
case "wokentoken":
+ default:
return Promise.resolve(MatrixClientPeg.get().getUserId());
}
});
@@ -49,6 +53,8 @@ describe('ScalarAuthClient', function() {
await sac.connect();
expect(sac.exchangeForScalarToken).toBeCalledWith('this is your openid token');
+ expect(sac.hasCredentials).toBeTruthy();
+ // @ts-ignore private property
expect(sac.scalarToken).toEqual('wokentoken');
});
});
diff --git a/test/Terms-test.js b/test/Terms-test.tsx
similarity index 68%
rename from test/Terms-test.js
rename to test/Terms-test.tsx
index 2bd5b6f43d0..7e22731518c 100644
--- a/test/Terms-test.js
+++ b/test/Terms-test.tsx
@@ -14,10 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import * as Matrix from 'matrix-js-sdk/src/matrix';
+import {
+ MatrixEvent,
+ EventType,
+ SERVICE_TYPES,
+} from 'matrix-js-sdk/src/matrix';
import { startTermsFlow, Service } from '../src/Terms';
-import { stubClient } from './test-utils';
+import { getMockClientWithEventEmitter } from './test-utils';
import { MatrixClientPeg } from '../src/MatrixClientPeg';
const POLICY_ONE = {
@@ -36,17 +40,31 @@ const POLICY_TWO = {
},
};
-const IM_SERVICE_ONE = new Service(Matrix.SERVICE_TYPES.IM, 'https://imone.test', 'a token token');
-const IM_SERVICE_TWO = new Service(Matrix.SERVICE_TYPES.IM, 'https://imtwo.test', 'a token token');
+const IM_SERVICE_ONE = new Service(SERVICE_TYPES.IM, 'https://imone.test', 'a token token');
+const IM_SERVICE_TWO = new Service(SERVICE_TYPES.IM, 'https://imtwo.test', 'a token token');
describe('Terms', function() {
+ const mockClient = getMockClientWithEventEmitter({
+ getAccountData: jest.fn(),
+ getTerms: jest.fn(),
+ agreeToTerms: jest.fn(),
+ setAccountData: jest.fn(),
+ });
+
beforeEach(function() {
- stubClient();
+ jest.clearAllMocks();
+ mockClient.getAccountData.mockReturnValue(null);
+ mockClient.getTerms.mockResolvedValue(null);
+ mockClient.setAccountData.mockResolvedValue({});
+ });
+
+ afterAll(() => {
+ jest.spyOn(MatrixClientPeg, 'get').mockRestore();
});
it('should prompt for all terms & services if no account data', async function() {
- MatrixClientPeg.get().getAccountData = jest.fn().mockReturnValue(null);
- MatrixClientPeg.get().getTerms = jest.fn().mockReturnValue({
+ mockClient.getAccountData.mockReturnValue(null);
+ mockClient.getTerms.mockResolvedValue({
policies: {
"policy_the_first": POLICY_ONE,
},
@@ -65,24 +83,26 @@ describe('Terms', function() {
});
it('should not prompt if all policies are signed in account data', async function() {
- MatrixClientPeg.get().getAccountData = jest.fn().mockReturnValue({
- getContent: jest.fn().mockReturnValue({
+ const directEvent = new MatrixEvent({
+ type: EventType.Direct,
+ content: {
accepted: ["http://example.com/one"],
- }),
+ },
});
- MatrixClientPeg.get().getTerms = jest.fn().mockReturnValue({
+ mockClient.getAccountData.mockReturnValue(directEvent);
+ mockClient.getTerms.mockResolvedValue({
policies: {
"policy_the_first": POLICY_ONE,
},
});
- MatrixClientPeg.get().agreeToTerms = jest.fn();
+ mockClient.agreeToTerms;
const interactionCallback = jest.fn();
await startTermsFlow([IM_SERVICE_ONE], interactionCallback);
expect(interactionCallback).not.toHaveBeenCalled();
- expect(MatrixClientPeg.get().agreeToTerms).toBeCalledWith(
- Matrix.SERVICE_TYPES.IM,
+ expect(mockClient.agreeToTerms).toBeCalledWith(
+ SERVICE_TYPES.IM,
'https://imone.test',
'a token token',
["http://example.com/one"],
@@ -90,18 +110,20 @@ describe('Terms', function() {
});
it("should prompt for only terms that aren't already signed", async function() {
- MatrixClientPeg.get().getAccountData = jest.fn().mockReturnValue({
- getContent: jest.fn().mockReturnValue({
+ const directEvent = new MatrixEvent({
+ type: EventType.Direct,
+ content: {
accepted: ["http://example.com/one"],
- }),
+ },
});
- MatrixClientPeg.get().getTerms = jest.fn().mockReturnValue({
+ mockClient.getAccountData.mockReturnValue(directEvent);
+
+ mockClient.getTerms.mockResolvedValue({
policies: {
"policy_the_first": POLICY_ONE,
"policy_the_second": POLICY_TWO,
},
});
- MatrixClientPeg.get().agreeToTerms = jest.fn();
const interactionCallback = jest.fn().mockResolvedValue(["http://example.com/one", "http://example.com/two"]);
await startTermsFlow([IM_SERVICE_ONE], interactionCallback);
@@ -114,8 +136,8 @@ describe('Terms', function() {
},
},
], ["http://example.com/one"]);
- expect(MatrixClientPeg.get().agreeToTerms).toBeCalledWith(
- Matrix.SERVICE_TYPES.IM,
+ expect(mockClient.agreeToTerms).toBeCalledWith(
+ SERVICE_TYPES.IM,
'https://imone.test',
'a token token',
["http://example.com/one", "http://example.com/two"],
@@ -123,13 +145,15 @@ describe('Terms', function() {
});
it("should prompt for only services with un-agreed policies", async function() {
- MatrixClientPeg.get().getAccountData = jest.fn().mockReturnValue({
- getContent: jest.fn().mockReturnValue({
+ const directEvent = new MatrixEvent({
+ type: EventType.Direct,
+ content: {
accepted: ["http://example.com/one"],
- }),
+ },
});
+ mockClient.getAccountData.mockReturnValue(directEvent);
- MatrixClientPeg.get().getTerms = jest.fn((serviceType, baseUrl, accessToken) => {
+ mockClient.getTerms.mockImplementation(async (_serviceTypes: SERVICE_TYPES, baseUrl: string) => {
switch (baseUrl) {
case 'https://imone.test':
return {
@@ -146,8 +170,6 @@ describe('Terms', function() {
}
});
- MatrixClientPeg.get().agreeToTerms = jest.fn();
-
const interactionCallback = jest.fn().mockResolvedValue(["http://example.com/one", "http://example.com/two"]);
await startTermsFlow([IM_SERVICE_ONE, IM_SERVICE_TWO], interactionCallback);
@@ -159,14 +181,14 @@ describe('Terms', function() {
},
},
], ["http://example.com/one"]);
- expect(MatrixClientPeg.get().agreeToTerms).toBeCalledWith(
- Matrix.SERVICE_TYPES.IM,
+ expect(mockClient.agreeToTerms).toBeCalledWith(
+ SERVICE_TYPES.IM,
'https://imone.test',
'a token token',
["http://example.com/one"],
);
- expect(MatrixClientPeg.get().agreeToTerms).toBeCalledWith(
- Matrix.SERVICE_TYPES.IM,
+ expect(mockClient.agreeToTerms).toBeCalledWith(
+ SERVICE_TYPES.IM,
'https://imtwo.test',
'a token token',
["http://example.com/two"],
diff --git a/test/TextForEvent-test.ts b/test/TextForEvent-test.ts
index f9e4eba3240..c886c22b9ef 100644
--- a/test/TextForEvent-test.ts
+++ b/test/TextForEvent-test.ts
@@ -1,3 +1,19 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
import { EventType, MatrixEvent } from "matrix-js-sdk/src/matrix";
import TestRenderer from 'react-test-renderer';
import { ReactElement } from "react";
diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js
index 769e90c9bbb..c511dd63a6d 100644
--- a/test/components/structures/MessagePanel-test.js
+++ b/test/components/structures/MessagePanel-test.js
@@ -699,7 +699,7 @@ describe('MessagePanel', function() {
});
describe("shouldFormContinuation", () => {
- it("does not form continuations from thread roots", () => {
+ it("does not form continuations from thread roots which have summaries", () => {
const message1 = TestUtilsMatrix.mkMessage({
event: true,
room: "!room:id",
@@ -730,6 +730,14 @@ describe("shouldFormContinuation", () => {
});
expect(shouldFormContinuation(message1, message2, false, true)).toEqual(true);
+ expect(shouldFormContinuation(message2, threadRoot, false, true)).toEqual(true);
+ expect(shouldFormContinuation(threadRoot, message3, false, true)).toEqual(true);
+
+ const thread = {
+ length: 1,
+ replyToEvent: {},
+ };
+ jest.spyOn(threadRoot, "getThread").mockReturnValue(thread);
expect(shouldFormContinuation(message2, threadRoot, false, true)).toEqual(false);
expect(shouldFormContinuation(threadRoot, message3, false, true)).toEqual(false);
});
diff --git a/test/components/structures/VideoRoomView-test.tsx b/test/components/structures/VideoRoomView-test.tsx
index 11d747103d9..cf73c8a1394 100644
--- a/test/components/structures/VideoRoomView-test.tsx
+++ b/test/components/structures/VideoRoomView-test.tsx
@@ -17,11 +17,22 @@ limitations under the License.
import React from "react";
import { mount } from "enzyme";
import { act } from "react-dom/test-utils";
+import { mocked } from "jest-mock";
+import { MatrixClient, IMyDevice } from "matrix-js-sdk/src/client";
+import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixWidgetType } from "matrix-widget-api";
-import { stubClient, stubVideoChannelStore, mkRoom, wrapInMatrixClientContext } from "../../test-utils";
+import {
+ stubClient,
+ stubVideoChannelStore,
+ StubVideoChannelStore,
+ mkRoom,
+ wrapInMatrixClientContext,
+ mockStateEventImplementation,
+ mkVideoChannelMember,
+} from "../../test-utils";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
-import { VIDEO_CHANNEL } from "../../../src/utils/VideoChannelUtils";
+import { VIDEO_CHANNEL, VIDEO_CHANNEL_MEMBER } from "../../../src/utils/VideoChannelUtils";
import WidgetStore from "../../../src/stores/WidgetStore";
import _VideoRoomView from "../../../src/components/structures/VideoRoomView";
import VideoLobby from "../../../src/components/views/voip/VideoLobby";
@@ -30,7 +41,6 @@ import AppTile from "../../../src/components/views/elements/AppTile";
const VideoRoomView = wrapInMatrixClientContext(_VideoRoomView);
describe("VideoRoomView", () => {
- stubClient();
jest.spyOn(WidgetStore.instance, "getApps").mockReturnValue([{
id: VIDEO_CHANNEL,
eventId: "$1:example.org",
@@ -45,22 +55,53 @@ describe("VideoRoomView", () => {
value: { enumerateDevices: () => [] },
});
- const cli = MatrixClientPeg.get();
- const room = mkRoom(cli, "!1:example.org");
+ let cli: MatrixClient;
+ let room: Room;
+ let store: StubVideoChannelStore;
- let store;
beforeEach(() => {
+ stubClient();
+ cli = MatrixClientPeg.get();
+ jest.spyOn(WidgetStore.instance, "matrixClient", "get").mockReturnValue(cli);
store = stubVideoChannelStore();
+ room = mkRoom(cli, "!1:example.org");
});
- afterEach(() => {
- jest.clearAllMocks();
+ it("removes stuck devices on mount", async () => {
+ // Simulate an unclean disconnect
+ store.roomId = "!1:example.org";
+
+ const devices: IMyDevice[] = [
+ {
+ device_id: cli.getDeviceId(),
+ last_seen_ts: new Date().valueOf(),
+ },
+ {
+ device_id: "went offline 2 hours ago",
+ last_seen_ts: new Date().valueOf() - 1000 * 60 * 60 * 2,
+ },
+ ];
+ mocked(cli).getDevices.mockResolvedValue({ devices });
+
+ // Make both devices be stuck
+ mocked(room.currentState).getStateEvents.mockImplementation(mockStateEventImplementation([
+ mkVideoChannelMember(cli.getUserId(), devices.map(d => d.device_id)),
+ ]));
+
+ mount( );
+ // Wait for state to settle
+ await act(() => Promise.resolve());
+
+ // All devices should have been removed
+ expect(cli.sendStateEvent).toHaveBeenLastCalledWith(
+ "!1:example.org", VIDEO_CHANNEL_MEMBER, { devices: [] }, cli.getUserId(),
+ );
});
it("shows lobby and keeps widget loaded when disconnected", async () => {
const view = mount( );
// Wait for state to settle
- await act(async () => Promise.resolve());
+ await act(() => Promise.resolve());
expect(view.find(VideoLobby).exists()).toEqual(true);
expect(view.find(AppTile).exists()).toEqual(true);
@@ -70,7 +111,7 @@ describe("VideoRoomView", () => {
store.connect("!1:example.org");
const view = mount( );
// Wait for state to settle
- await act(async () => Promise.resolve());
+ await act(() => Promise.resolve());
expect(view.find(VideoLobby).exists()).toEqual(false);
expect(view.find(AppTile).exists()).toEqual(true);
diff --git a/test/components/structures/auth/Login-test.tsx b/test/components/structures/auth/Login-test.tsx
index af7e2599f0b..44c44ffd26b 100644
--- a/test/components/structures/auth/Login-test.tsx
+++ b/test/components/structures/auth/Login-test.tsx
@@ -146,4 +146,19 @@ describe('Login', function() {
const ssoButtons = ReactTestUtils.scryRenderedDOMComponentsWithClass(root, "mx_SSOButton");
expect(ssoButtons.length).toBe(3);
});
+
+ it("should show single SSO button if identity_providers is null", async () => {
+ mockClient.loginFlows.mockResolvedValue({
+ flows: [{
+ "type": "m.login.sso",
+ }],
+ });
+
+ const root = render();
+
+ await flushPromises();
+
+ const ssoButtons = ReactTestUtils.scryRenderedDOMComponentsWithClass(root, "mx_SSOButton");
+ expect(ssoButtons.length).toBe(1);
+ });
});
diff --git a/test/components/structures/auth/Registration-test.js b/test/components/structures/auth/Registration-test.tsx
similarity index 81%
rename from test/components/structures/auth/Registration-test.js
rename to test/components/structures/auth/Registration-test.tsx
index 7a6fb0c9762..45bb25b79e3 100644
--- a/test/components/structures/auth/Registration-test.js
+++ b/test/components/structures/auth/Registration-test.tsx
@@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector Ltd
+Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -18,6 +19,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import ReactTestUtils from 'react-dom/test-utils';
import { createClient } from 'matrix-js-sdk/src/matrix';
+import { mocked } from 'jest-mock';
import SdkConfig, { DEFAULTS } from '../../../../src/SdkConfig';
import { createTestClient, mkServerConfig } from "../../../test-utils";
@@ -37,7 +39,7 @@ describe('Registration', function() {
});
parentDiv = document.createElement('div');
document.body.appendChild(parentDiv);
- createClient.mockImplementation(() => createTestClient());
+ mocked(createClient).mockImplementation(() => createTestClient());
});
afterEach(function() {
@@ -46,14 +48,18 @@ describe('Registration', function() {
SdkConfig.unset(); // we touch the config, so clean up
});
+ const defaultProps = {
+ defaultDeviceDisplayName: 'test-device-display-name',
+ serverConfig: mkServerConfig("https://matrix.org", "https://vector.im"),
+ makeRegistrationUrl: jest.fn(),
+ onLoggedIn: jest.fn(),
+ onLoginClick: jest.fn(),
+ onServerConfigChange: jest.fn(),
+ };
function render() {
- return ReactDOM.render( {}}
- onLoggedIn={() => {}}
- onLoginClick={() => {}}
- onServerConfigChange={() => {}}
- />, parentDiv);
+ return ReactDOM.render( , parentDiv) as React.Component;
}
it('should show server picker', async function() {
diff --git a/test/components/views/beacon/LeftPanelLiveShareWarning-test.tsx b/test/components/views/beacon/LeftPanelLiveShareWarning-test.tsx
index d02f4d0c3d4..8a10f735759 100644
--- a/test/components/views/beacon/LeftPanelLiveShareWarning-test.tsx
+++ b/test/components/views/beacon/LeftPanelLiveShareWarning-test.tsx
@@ -34,6 +34,7 @@ jest.mock('../../../../src/stores/OwnBeaconStore', () => {
public getBeaconById = jest.fn();
public getLiveBeaconIds = jest.fn().mockReturnValue([]);
public readonly beaconUpdateErrors = new Map();
+ public readonly beacons = new Map();
}
return {
// @ts-ignore
@@ -103,6 +104,10 @@ describe(' ', () => {
mocked(OwnBeaconStore.instance).getLiveBeaconIds.mockReturnValue([beacon2.identifier, beacon1.identifier]);
});
+ afterAll(() => {
+ jest.spyOn(document, 'addEventListener').mockRestore();
+ });
+
it('renders correctly when not minimized', () => {
const component = getComponent();
expect(component).toMatchSnapshot();
@@ -163,7 +168,7 @@ describe(' ', () => {
const component = getComponent();
// error mode
expect(component.find('.mx_LeftPanelLiveShareWarning').at(0).text()).toEqual(
- 'An error occured whilst sharing your live location',
+ 'An error occurred whilst sharing your live location',
);
act(() => {
@@ -195,6 +200,24 @@ describe(' ', () => {
expect(component.html()).toBe(null);
});
+ it('refreshes beacon liveness monitors when pagevisibilty changes to visible', () => {
+ OwnBeaconStore.instance.beacons.set(beacon1.identifier, beacon1);
+ OwnBeaconStore.instance.beacons.set(beacon2.identifier, beacon2);
+ const beacon1MonitorSpy = jest.spyOn(beacon1, 'monitorLiveness');
+ const beacon2MonitorSpy = jest.spyOn(beacon1, 'monitorLiveness');
+
+ jest.spyOn(document, 'addEventListener').mockImplementation(
+ (_e, listener) => (listener as EventListener)(new Event('')),
+ );
+
+ expect(beacon1MonitorSpy).not.toHaveBeenCalled();
+
+ getComponent();
+
+ expect(beacon1MonitorSpy).toHaveBeenCalled();
+ expect(beacon2MonitorSpy).toHaveBeenCalled();
+ });
+
describe('stopping errors', () => {
it('renders stopping error', () => {
OwnBeaconStore.instance.beaconUpdateErrors.set(beacon2.identifier, new Error('error'));
diff --git a/test/components/views/beacon/RoomLiveShareWarning-test.tsx b/test/components/views/beacon/RoomLiveShareWarning-test.tsx
index 13ae42fce4f..2a6956c92b4 100644
--- a/test/components/views/beacon/RoomLiveShareWarning-test.tsx
+++ b/test/components/views/beacon/RoomLiveShareWarning-test.tsx
@@ -359,7 +359,7 @@ describe(' ', () => {
// renders wire error ui
expect(component.find('.mx_RoomLiveShareWarning_label').text()).toEqual(
- 'An error occured whilst sharing your live location, please try again',
+ 'An error occurred whilst sharing your live location, please try again',
);
expect(findByTestId(component, 'room-live-share-wire-error-close-button').length).toBeTruthy();
});
diff --git a/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap b/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap
index e590cbcd9f3..a79a47b5892 100644
--- a/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap
+++ b/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap
@@ -148,83 +148,86 @@ exports[` renders marker when beacon has location 1`] = `
className="mx_Marker mx_Username_color4"
id="!room:server_@alice:server"
>
-
-
+
-
-
- A
-
-
+ A
+
+
-
-
-
-
+ title="@alice:server"
+ />
+
+
+
+
+
diff --git a/test/components/views/beacon/__snapshots__/LeftPanelLiveShareWarning-test.tsx.snap b/test/components/views/beacon/__snapshots__/LeftPanelLiveShareWarning-test.tsx.snap
index d1d6dd56c02..0dd32a93870 100644
--- a/test/components/views/beacon/__snapshots__/LeftPanelLiveShareWarning-test.tsx.snap
+++ b/test/components/views/beacon/__snapshots__/LeftPanelLiveShareWarning-test.tsx.snap
@@ -69,7 +69,7 @@ exports[` when user has live location monitor rende
role="button"
tabIndex={0}
>
- An error occured whilst sharing your live location
+ An error occurred whilst sharing your live location
diff --git a/test/components/views/beacon/__snapshots__/RoomLiveShareWarning-test.tsx.snap b/test/components/views/beacon/__snapshots__/RoomLiveShareWarning-test.tsx.snap
index c9695ddf582..8701c83c91b 100644
--- a/test/components/views/beacon/__snapshots__/RoomLiveShareWarning-test.tsx.snap
+++ b/test/components/views/beacon/__snapshots__/RoomLiveShareWarning-test.tsx.snap
@@ -32,7 +32,7 @@ exports[` when user has live beacons and geolocation is
- An error occured whilst sharing your live location, please try again
+ An error occurred whilst sharing your live location, please try again
({
+ copyPlaintext: jest.fn(),
+ getSelectedText: jest.fn(),
+}));
+jest.mock("../../../../src/utils/EventUtils", () => ({
+ canEditContent: jest.fn(),
+ canForward: jest.fn(),
+ isContentActionable: jest.fn(),
+ isLocationEvent: jest.fn(),
+}));
+
+describe('MessageContextMenu', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
-describe('MessageContextMenu>', () => {
it('allows forwarding a room message', () => {
+ mocked(canForward).mockReturnValue(true);
+ mocked(isContentActionable).mockReturnValue(true);
+
const eventContent = MessageEvent.from("hello");
- const menu = createMessageContextMenu(eventContent);
+ const menu = createMenuWithContent(eventContent);
expect(menu.find('div[aria-label="Forward"]')).toHaveLength(1);
});
it('does not allow forwarding a poll', () => {
+ mocked(canForward).mockReturnValue(false);
+
const eventContent = PollStartEvent.from("why?", ["42"], M_POLL_KIND_DISCLOSED);
- const menu = createMessageContextMenu(eventContent);
+ const menu = createMenuWithContent(eventContent);
expect(menu.find('div[aria-label="Forward"]')).toHaveLength(0);
});
+
+ it('does show copy link button when supplied a link', () => {
+ const eventContent = MessageEvent.from("hello");
+ const props = {
+ link: "https://google.com/",
+ };
+ const menu = createMenuWithContent(eventContent, props);
+ const copyLinkButton = menu.find('a[aria-label="Copy link"]');
+ expect(copyLinkButton).toHaveLength(1);
+ expect(copyLinkButton.props().href).toBe(props.link);
+ });
+
+ it('does not show copy link button when not supplied a link', () => {
+ const eventContent = MessageEvent.from("hello");
+ const menu = createMenuWithContent(eventContent);
+ const copyLinkButton = menu.find('a[aria-label="Copy link"]');
+ expect(copyLinkButton).toHaveLength(0);
+ });
+
+ describe("right click", () => {
+ it('copy button does work as expected', () => {
+ const text = "hello";
+ const eventContent = MessageEvent.from(text);
+ mocked(getSelectedText).mockReturnValue(text);
+
+ const menu = createRightClickMenuWithContent(eventContent);
+ const copyButton = menu.find('div[aria-label="Copy"]');
+ copyButton.simulate("mousedown");
+ expect(copyPlaintext).toHaveBeenCalledWith(text);
+ });
+
+ it('copy button is not shown when there is nothing to copy', () => {
+ const text = "hello";
+ const eventContent = MessageEvent.from(text);
+ mocked(getSelectedText).mockReturnValue("");
+
+ const menu = createRightClickMenuWithContent(eventContent);
+ const copyButton = menu.find('div[aria-label="Copy"]');
+ expect(copyButton).toHaveLength(0);
+ });
+
+ it('shows edit button when we can edit', () => {
+ const eventContent = MessageEvent.from("hello");
+ mocked(canEditContent).mockReturnValue(true);
+
+ const menu = createRightClickMenuWithContent(eventContent);
+ const editButton = menu.find('div[aria-label="Edit"]');
+ expect(editButton).toHaveLength(1);
+ });
+
+ it('does not show edit button when we cannot edit', () => {
+ const eventContent = MessageEvent.from("hello");
+ mocked(canEditContent).mockReturnValue(false);
+
+ const menu = createRightClickMenuWithContent(eventContent);
+ const editButton = menu.find('div[aria-label="Edit"]');
+ expect(editButton).toHaveLength(0);
+ });
+
+ it('shows reply button when we can reply', () => {
+ const eventContent = MessageEvent.from("hello");
+ const context = {
+ canSendMessages: true,
+ };
+ mocked(isContentActionable).mockReturnValue(true);
+
+ const menu = createRightClickMenuWithContent(eventContent, context);
+ const replyButton = menu.find('div[aria-label="Reply"]');
+ expect(replyButton).toHaveLength(1);
+ });
+
+ it('does not show reply button when we cannot reply', () => {
+ const eventContent = MessageEvent.from("hello");
+ const context = {
+ canSendMessages: true,
+ };
+ mocked(isContentActionable).mockReturnValue(false);
+
+ const menu = createRightClickMenuWithContent(eventContent, context);
+ const replyButton = menu.find('div[aria-label="Reply"]');
+ expect(replyButton).toHaveLength(0);
+ });
+
+ it('shows react button when we can react', () => {
+ const eventContent = MessageEvent.from("hello");
+ const context = {
+ canReact: true,
+ };
+ mocked(isContentActionable).mockReturnValue(true);
+
+ const menu = createRightClickMenuWithContent(eventContent, context);
+ const reactButton = menu.find('div[aria-label="React"]');
+ expect(reactButton).toHaveLength(1);
+ });
+
+ it('does not show react button when we cannot react', () => {
+ const eventContent = MessageEvent.from("hello");
+ const context = {
+ canReact: false,
+ };
+
+ const menu = createRightClickMenuWithContent(eventContent, context);
+ const reactButton = menu.find('div[aria-label="React"]');
+ expect(reactButton).toHaveLength(0);
+ });
+
+ it('shows view in room button when the event is a thread root', () => {
+ const eventContent = MessageEvent.from("hello");
+ const mxEvent = new MatrixEvent(eventContent.serialize());
+ mxEvent.getThread = () => ({ rootEvent: mxEvent }) as Thread;
+ const props = {
+ rightClick: true,
+ };
+ const context = {
+ timelineRenderingType: TimelineRenderingType.Thread,
+ };
+
+ const menu = createMenu(mxEvent, props, context);
+ const reactButton = menu.find('div[aria-label="View in room"]');
+ expect(reactButton).toHaveLength(1);
+ });
+
+ it('does not show view in room button when the event is not a thread root', () => {
+ const eventContent = MessageEvent.from("hello");
+
+ const menu = createRightClickMenuWithContent(eventContent);
+ const reactButton = menu.find('div[aria-label="View in room"]');
+ expect(reactButton).toHaveLength(0);
+ });
+ });
});
-function createMessageContextMenu(eventContent: ExtensibleEvent) {
+function createRightClickMenuWithContent(
+ eventContent: ExtensibleEvent,
+ context?: Partial,
+): ReactWrapper {
+ return createMenuWithContent(eventContent, { rightClick: true }, context);
+}
+
+function createMenuWithContent(
+ eventContent: ExtensibleEvent,
+ props?: Partial>,
+ context?: Partial,
+): ReactWrapper {
+ const mxEvent = new MatrixEvent(eventContent.serialize());
+ return createMenu(mxEvent, props, context);
+}
+
+function createMenu(
+ mxEvent: MatrixEvent,
+ props?: Partial>,
+ context: Partial = {},
+): ReactWrapper {
TestUtils.stubClient();
const client = MatrixClientPeg.get();
@@ -52,17 +228,19 @@ function createMessageContextMenu(eventContent: ExtensibleEvent) {
},
);
- const mxEvent = new MatrixEvent(eventContent.serialize());
mxEvent.setStatus(EventStatus.SENT);
client.getUserId = jest.fn().mockReturnValue("@user:example.com");
client.getRoom = jest.fn().mockReturnValue(room);
return mount(
- {})}
- />,
+
+
+ ,
);
}
diff --git a/test/components/views/dialogs/AccessSecretStorageDialog-test.js b/test/components/views/dialogs/AccessSecretStorageDialog-test.js
deleted file mode 100644
index d90b23baac1..00000000000
--- a/test/components/views/dialogs/AccessSecretStorageDialog-test.js
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
-Copyright 2020 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import React from 'react';
-import { mount } from 'enzyme';
-import { act } from 'react-dom/test-utils';
-
-import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
-import { stubClient } from '../../../test-utils';
-import { findById, flushPromises } from '../../../test-utils';
-import AccessSecretStorageDialog from "../../../../src/components/views/dialogs/security/AccessSecretStorageDialog";
-
-describe("AccessSecretStorageDialog", function() {
- it("Closes the dialog if _onRecoveryKeyNext is called with a valid key", async () => {
- const onFinished = jest.fn();
- const checkPrivateKey = jest.fn().mockResolvedValue(true);
- const wrapper = mount(
- ,
- );
- wrapper.setState({
- recoveryKeyValid: true,
- recoveryKey: "a",
- });
- const e = { preventDefault: () => {} };
-
- wrapper.find('form').simulate('submit', e);
-
- await flushPromises();
-
- expect(checkPrivateKey).toHaveBeenCalledWith({ recoveryKey: "a" });
- expect(onFinished).toHaveBeenCalledWith({ recoveryKey: "a" });
- });
-
- it("Considers a valid key to be valid", async function() {
- const wrapper = mount(
- true}
- />,
- );
- stubClient();
- MatrixClientPeg.get().keyBackupKeyFromRecoveryKey = () => 'a raw key';
- MatrixClientPeg.get().checkSecretStorageKey = () => true;
-
- const v = "asdf";
- const e = { target: { value: v } };
- act(() => {
- findById(wrapper, 'mx_securityKey').find('input').simulate('change', e);
- });
- // force a validation now because it debounces
- await wrapper.instance().validateRecoveryKey();
- const { recoveryKeyValid } = wrapper.instance().state;
- expect(recoveryKeyValid).toBe(true);
- });
-
- it("Notifies the user if they input an invalid Security Key", async function() {
- const wrapper = mount(
- false}
- />,
- );
- const e = { target: { value: "a" } };
- stubClient();
- MatrixClientPeg.get().keyBackupKeyFromRecoveryKey = () => {
- throw new Error("that's no key");
- };
-
- act(() => {
- findById(wrapper, 'mx_securityKey').find('input').simulate('change', e);
- });
- // force a validation now because it debounces
- await wrapper.instance().validateRecoveryKey();
-
- const { recoveryKeyValid, recoveryKeyCorrect } = wrapper.instance().state;
- expect(recoveryKeyValid).toBe(false);
- expect(recoveryKeyCorrect).toBe(false);
-
- wrapper.setProps({});
- const notification = wrapper.find(".mx_AccessSecretStorageDialog_recoveryKeyFeedback");
- expect(notification.props().children).toEqual("Invalid Security Key");
- });
-
- it("Notifies the user if they input an invalid passphrase", async function() {
- const wrapper = mount(
- false}
- onFinished={() => {}}
- keyInfo={{
- passphrase: {
- salt: 'nonempty',
- iterations: 2,
- },
- }}
- />,
- );
- const e = { target: { value: "a" } };
- stubClient();
- MatrixClientPeg.get().isValidRecoveryKey = () => false;
- wrapper.instance().onPassPhraseChange(e);
- await wrapper.instance().onPassPhraseNext({ preventDefault: () => { } });
-
- wrapper.setProps({});
- const notification = wrapper.find(".mx_AccessSecretStorageDialog_keyStatus");
- expect(notification.props().children).toEqual(
- ["\uD83D\uDC4E ", "Unable to access secret storage. Please verify that you " +
- "entered the correct Security Phrase."]);
- });
-});
diff --git a/test/components/views/dialogs/AccessSecretStorageDialog-test.tsx b/test/components/views/dialogs/AccessSecretStorageDialog-test.tsx
new file mode 100644
index 00000000000..1376c18c3b2
--- /dev/null
+++ b/test/components/views/dialogs/AccessSecretStorageDialog-test.tsx
@@ -0,0 +1,166 @@
+/*
+Copyright 2020, 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+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 { mount, ReactWrapper } from 'enzyme';
+import { act } from 'react-dom/test-utils';
+import { IPassphraseInfo } from 'matrix-js-sdk/src/crypto/api';
+
+import { findByTestId, getMockClientWithEventEmitter, unmockClientPeg } from '../../../test-utils';
+import { findById, flushPromises } from '../../../test-utils';
+import AccessSecretStorageDialog from "../../../../src/components/views/dialogs/security/AccessSecretStorageDialog";
+
+describe("AccessSecretStorageDialog", () => {
+ const mockClient = getMockClientWithEventEmitter({
+ keyBackupKeyFromRecoveryKey: jest.fn(),
+ checkSecretStorageKey: jest.fn(),
+ isValidRecoveryKey: jest.fn(),
+ });
+ const defaultProps = {
+ onFinished: jest.fn(),
+ checkPrivateKey: jest.fn(),
+ keyInfo: undefined,
+ };
+ const getComponent = (props ={}): ReactWrapper =>
+ mount( );
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockClient.keyBackupKeyFromRecoveryKey.mockReturnValue('a raw key' as unknown as Uint8Array);
+ mockClient.isValidRecoveryKey.mockReturnValue(false);
+ });
+
+ afterAll(() => {
+ unmockClientPeg();
+ });
+
+ it("Closes the dialog when the form is submitted with a valid key", async () => {
+ const onFinished = jest.fn();
+ const checkPrivateKey = jest.fn().mockResolvedValue(true);
+ const wrapper = getComponent({ onFinished, checkPrivateKey });
+
+ // force into valid state
+ act(() => {
+ wrapper.setState({
+ recoveryKeyValid: true,
+ recoveryKey: "a",
+ });
+ });
+ const e = { preventDefault: () => {} };
+
+ act(() => {
+ wrapper.find('form').simulate('submit', e);
+ });
+
+ await flushPromises();
+
+ expect(checkPrivateKey).toHaveBeenCalledWith({ recoveryKey: "a" });
+ expect(onFinished).toHaveBeenCalledWith({ recoveryKey: "a" });
+ });
+
+ it("Considers a valid key to be valid", async () => {
+ const checkPrivateKey = jest.fn().mockResolvedValue(true);
+ const wrapper = getComponent({ checkPrivateKey });
+ mockClient.keyBackupKeyFromRecoveryKey.mockReturnValue('a raw key' as unknown as Uint8Array);
+ mockClient.checkSecretStorageKey.mockResolvedValue(true);
+
+ const v = "asdf";
+ const e = { target: { value: v } };
+ act(() => {
+ findById(wrapper, 'mx_securityKey').find('input').simulate('change', e);
+ wrapper.setProps({});
+ });
+ await act(async () => {
+ // force a validation now because it debounces
+ // @ts-ignore
+ await wrapper.instance().validateRecoveryKey();
+ wrapper.setProps({});
+ });
+
+ const submitButton = findByTestId(wrapper, 'dialog-primary-button').at(0);
+ // submit button is enabled when key is valid
+ expect(submitButton.props().disabled).toBeFalsy();
+ expect(wrapper.find('.mx_AccessSecretStorageDialog_recoveryKeyFeedback').text()).toEqual('Looks good!');
+ });
+
+ it("Notifies the user if they input an invalid Security Key", async () => {
+ const checkPrivateKey = jest.fn().mockResolvedValue(false);
+ const wrapper = getComponent({ checkPrivateKey });
+ const e = { target: { value: "a" } };
+ mockClient.keyBackupKeyFromRecoveryKey.mockImplementation(() => {
+ throw new Error("that's no key");
+ });
+
+ act(() => {
+ findById(wrapper, 'mx_securityKey').find('input').simulate('change', e);
+ });
+ // force a validation now because it debounces
+ // @ts-ignore private
+ await wrapper.instance().validateRecoveryKey();
+
+ const submitButton = findByTestId(wrapper, 'dialog-primary-button').at(0);
+ // submit button is disabled when recovery key is invalid
+ expect(submitButton.props().disabled).toBeTruthy();
+ expect(
+ wrapper.find('.mx_AccessSecretStorageDialog_recoveryKeyFeedback').text(),
+ ).toEqual('Invalid Security Key');
+
+ wrapper.setProps({});
+ const notification = wrapper.find(".mx_AccessSecretStorageDialog_recoveryKeyFeedback");
+ expect(notification.props().children).toEqual("Invalid Security Key");
+ });
+
+ it("Notifies the user if they input an invalid passphrase", async function() {
+ const keyInfo = {
+ name: 'test',
+ algorithm: 'test',
+ iv: 'test',
+ mac: '1:2:3:4',
+ passphrase: {
+ // this type is weird in js-sdk
+ // cast 'm.pbkdf2' to itself
+ algorithm: 'm.pbkdf2' as IPassphraseInfo['algorithm'],
+ iterations: 2,
+ salt: 'nonempty',
+ },
+ };
+ const checkPrivateKey = jest.fn().mockResolvedValue(false);
+ const wrapper = getComponent({ checkPrivateKey, keyInfo });
+ mockClient.isValidRecoveryKey.mockReturnValue(false);
+
+ // update passphrase
+ act(() => {
+ const e = { target: { value: "a" } };
+ findById(wrapper, 'mx_passPhraseInput').at(1).simulate('change', e);
+ });
+ wrapper.setProps({});
+
+ // input updated
+ expect(findById(wrapper, 'mx_passPhraseInput').at(0).props().value).toEqual('a');
+
+ // submit the form
+ act(() => {
+ wrapper.find('form').at(0).simulate('submit');
+ });
+ await flushPromises();
+
+ wrapper.setProps({});
+ const notification = wrapper.find(".mx_AccessSecretStorageDialog_keyStatus");
+ expect(notification.props().children).toEqual(
+ ["\uD83D\uDC4E ", "Unable to access secret storage. Please verify that you " +
+ "entered the correct Security Phrase."]);
+ });
+});
diff --git a/test/components/views/dialogs/ForwardDialog-test.tsx b/test/components/views/dialogs/ForwardDialog-test.tsx
index 089a92a2b32..aedd1cfc16f 100644
--- a/test/components/views/dialogs/ForwardDialog-test.tsx
+++ b/test/components/views/dialogs/ForwardDialog-test.tsx
@@ -18,6 +18,9 @@ import React from "react";
import { mount } from "enzyme";
import { act } from "react-dom/test-utils";
import { MatrixEvent, EventType } from "matrix-js-sdk/src/matrix";
+import { ReactWrapper } from "enzyme";
+import { LocationAssetType, M_ASSET, M_LOCATION, M_TIMESTAMP } from "matrix-js-sdk/src/@types/location";
+import { TEXT_NODE_TYPE } from "matrix-js-sdk/src/@types/extensible_events";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import ForwardDialog from "../../../../src/components/views/dialogs/ForwardDialog";
@@ -25,10 +28,13 @@ import DMRoomMap from "../../../../src/utils/DMRoomMap";
import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks";
import {
getMockClientWithEventEmitter,
+ makeLegacyLocationEvent,
+ makeLocationEvent,
mkEvent,
mkMessage,
mkStubRoom,
} from "../../../test-utils";
+import { TILE_SERVER_WK_KEY } from "../../../../src/utils/WellKnownUtils";
describe("ForwardDialog", () => {
const sourceRoom = "!111111111111111111:example.org";
@@ -58,6 +64,9 @@ describe("ForwardDialog", () => {
}),
decryptEventIfNeeded: jest.fn(),
sendEvent: jest.fn(),
+ getClientWellKnown: jest.fn().mockReturnValue({
+ [TILE_SERVER_WK_KEY.name]: { map_style_url: 'maps.com' },
+ }),
});
const defaultRooms = ["a", "A", "b"].map(name => mkStubRoom(name, name, mockClient));
@@ -199,4 +208,93 @@ describe("ForwardDialog", () => {
const secondButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").last();
expect(secondButton.prop("disabled")).toBe(false);
});
+
+ describe('Location events', () => {
+ // 14.03.2022 16:15
+ const now = 1647270879403;
+ const roomId = "a";
+ const geoUri = "geo:51.5076,-0.1276";
+ const legacyLocationEvent = makeLegacyLocationEvent(geoUri);
+ const modernLocationEvent = makeLocationEvent(geoUri);
+ const pinDropLocationEvent = makeLocationEvent(geoUri, LocationAssetType.Pin);
+
+ beforeEach(() => {
+ // legacy events will default timestamp to Date.now()
+ // mock a stable now for easy assertion
+ jest.spyOn(Date, 'now').mockReturnValue(now);
+ });
+
+ afterAll(() => {
+ jest.spyOn(Date, 'now').mockRestore();
+ });
+
+ const sendToFirstRoom = (wrapper: ReactWrapper): void =>
+ act(() => {
+ const sendToFirstRoomButton = wrapper.find("AccessibleButton.mx_ForwardList_sendButton").first();
+ sendToFirstRoomButton.simulate("click");
+ });
+
+ it('converts legacy location events to pin drop shares', async () => {
+ const wrapper = await mountForwardDialog(legacyLocationEvent);
+
+ expect(wrapper.find('MLocationBody').length).toBeTruthy();
+ sendToFirstRoom(wrapper);
+
+ // text and description from original event are removed
+ // text gets new default message from event values
+ // timestamp is defaulted to now
+ const text = `Location ${geoUri} at ${new Date(now).toISOString()}`;
+ const expectedStrippedContent = {
+ ...modernLocationEvent.getContent(),
+ body: text,
+ [TEXT_NODE_TYPE.name]: text,
+ [M_TIMESTAMP.name]: now,
+ [M_ASSET.name]: { type: LocationAssetType.Pin },
+ [M_LOCATION.name]: {
+ uri: geoUri,
+ description: undefined,
+ },
+ };
+ expect(mockClient.sendEvent).toHaveBeenCalledWith(
+ roomId, legacyLocationEvent.getType(), expectedStrippedContent,
+ );
+ });
+
+ it('removes personal information from static self location shares', async () => {
+ const wrapper = await mountForwardDialog(modernLocationEvent);
+
+ expect(wrapper.find('MLocationBody').length).toBeTruthy();
+ sendToFirstRoom(wrapper);
+
+ const timestamp = M_TIMESTAMP.findIn(modernLocationEvent.getContent());
+ // text and description from original event are removed
+ // text gets new default message from event values
+ const text = `Location ${geoUri} at ${new Date(timestamp).toISOString()}`;
+ const expectedStrippedContent = {
+ ...modernLocationEvent.getContent(),
+ body: text,
+ [TEXT_NODE_TYPE.name]: text,
+ [M_ASSET.name]: { type: LocationAssetType.Pin },
+ [M_LOCATION.name]: {
+ uri: geoUri,
+ description: undefined,
+ },
+ };
+ expect(mockClient.sendEvent).toHaveBeenCalledWith(
+ roomId, modernLocationEvent.getType(), expectedStrippedContent,
+ );
+ });
+
+ it('forwards pin drop event', async () => {
+ const wrapper = await mountForwardDialog(pinDropLocationEvent);
+
+ expect(wrapper.find('MLocationBody').length).toBeTruthy();
+
+ sendToFirstRoom(wrapper);
+
+ expect(mockClient.sendEvent).toHaveBeenCalledWith(
+ roomId, pinDropLocationEvent.getType(), pinDropLocationEvent.getContent(),
+ );
+ });
+ });
});
diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js
deleted file mode 100644
index 798a30886c6..00000000000
--- a/test/components/views/dialogs/InteractiveAuthDialog-test.js
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
-Copyright 2016 OpenMarket Ltd
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import React from 'react';
-import ReactDOM from 'react-dom';
-import ReactTestUtils from 'react-dom/test-utils';
-import MatrixReactTestUtils from 'matrix-react-test-utils';
-import { sleep } from "matrix-js-sdk/src/utils";
-
-import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
-import * as TestUtilsMatrix from '../../../test-utils';
-import InteractiveAuthDialog from "../../../../src/components/views/dialogs/InteractiveAuthDialog";
-
-describe('InteractiveAuthDialog', function() {
- let parentDiv;
-
- beforeEach(function() {
- TestUtilsMatrix.stubClient();
- parentDiv = document.createElement('div');
- document.body.appendChild(parentDiv);
- });
-
- afterEach(function() {
- ReactDOM.unmountComponentAtNode(parentDiv);
- parentDiv.remove();
- });
-
- it('Should successfully complete a password flow', function() {
- const onFinished = jest.fn();
- const doRequest = jest.fn().mockResolvedValue({ a: 1 });
-
- // tell the stub matrixclient to return a real userid
- const client = MatrixClientPeg.get();
- client.credentials = { userId: "@user:id" };
-
- const dlg = ReactDOM.render(
- , parentDiv);
-
- // wait for a password box and a submit button
- return MatrixReactTestUtils.waitForRenderedDOMComponentWithTag(dlg, "form", 2).then((formNode) => {
- const inputNodes = ReactTestUtils.scryRenderedDOMComponentsWithTag(
- dlg, "input",
- );
- let passwordNode;
- let submitNode;
- for (const node of inputNodes) {
- if (node.type == 'password') {
- passwordNode = node;
- } else if (node.type == 'submit') {
- submitNode = node;
- }
- }
- expect(passwordNode).toBeTruthy();
- expect(submitNode).toBeTruthy();
-
- // submit should be disabled
- expect(submitNode.disabled).toBe(true);
-
- // put something in the password box, and hit enter; that should
- // trigger a request
- passwordNode.value = "s3kr3t";
- ReactTestUtils.Simulate.change(passwordNode);
- expect(submitNode.disabled).toBe(false);
- ReactTestUtils.Simulate.submit(formNode, {});
-
- expect(doRequest).toHaveBeenCalledTimes(1);
- expect(doRequest).toBeCalledWith(expect.objectContaining({
- session: "sess",
- type: "m.login.password",
- password: "s3kr3t",
- identifier: {
- type: "m.id.user",
- user: "@user:id",
- },
- }));
- // let the request complete
- return sleep(1);
- }).then(sleep(1)).then(() => {
- expect(onFinished).toBeCalledTimes(1);
- expect(onFinished).toBeCalledWith(true, { a: 1 });
- });
- });
-});
diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.tsx b/test/components/views/dialogs/InteractiveAuthDialog-test.tsx
new file mode 100644
index 00000000000..787dbd2cd5f
--- /dev/null
+++ b/test/components/views/dialogs/InteractiveAuthDialog-test.tsx
@@ -0,0 +1,106 @@
+/*
+Copyright 2016 OpenMarket Ltd
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import { mount, ReactWrapper } from 'enzyme';
+
+import InteractiveAuthDialog from "../../../../src/components/views/dialogs/InteractiveAuthDialog";
+import { flushPromises, getMockClientWithEventEmitter, unmockClientPeg } from '../../../test-utils';
+
+describe('InteractiveAuthDialog', function() {
+ const mockClient = getMockClientWithEventEmitter({
+ generateClientSecret: jest.fn().mockReturnValue('t35tcl1Ent5ECr3T'),
+ });
+
+ const defaultProps = {
+ matrixClient: mockClient,
+ makeRequest: jest.fn().mockResolvedValue(undefined),
+ onFinished: jest.fn(),
+ };
+ const getComponent = (props = {}) => mount( );
+
+ beforeEach(function() {
+ jest.clearAllMocks();
+ mockClient.credentials = null;
+ });
+
+ afterAll(() => {
+ unmockClientPeg();
+ });
+
+ const getSubmitButton = (wrapper: ReactWrapper) => wrapper.find('[type="submit"]').at(0);
+
+ it('Should successfully complete a password flow', async () => {
+ const onFinished = jest.fn();
+ const makeRequest = jest.fn().mockResolvedValue({ a: 1 });
+
+ mockClient.credentials = { userId: "@user:id" };
+ const authData = {
+ session: "sess",
+ flows: [
+ { "stages": ["m.login.password"] },
+ ],
+ };
+
+ const wrapper = getComponent({ makeRequest, onFinished, authData });
+
+ const passwordNode = wrapper.find('input[type="password"]').at(0);
+ const submitNode = getSubmitButton(wrapper);
+
+ const formNode = wrapper.find('form').at(0);
+ expect(passwordNode).toBeTruthy();
+ expect(submitNode).toBeTruthy();
+
+ // submit should be disabled
+ expect(submitNode.props().disabled).toBe(true);
+
+ // put something in the password box
+ act(() => {
+ passwordNode.simulate('change', { target: { value: "s3kr3t" } });
+ wrapper.setProps({});
+ });
+
+ expect(wrapper.find('input[type="password"]').at(0).props().value).toEqual("s3kr3t");
+ expect(getSubmitButton(wrapper).props().disabled).toBe(false);
+
+ // hit enter; that should trigger a request
+ act(() => {
+ formNode.simulate('submit');
+ });
+
+ // wait for auth request to resolve
+ await flushPromises();
+
+ expect(makeRequest).toHaveBeenCalledTimes(1);
+ expect(makeRequest).toBeCalledWith(expect.objectContaining({
+ session: "sess",
+ type: "m.login.password",
+ password: "s3kr3t",
+ identifier: {
+ type: "m.id.user",
+ user: "@user:id",
+ },
+ }));
+
+ expect(onFinished).toBeCalledTimes(1);
+ expect(onFinished).toBeCalledWith(true, { a: 1 });
+ });
+});
diff --git a/test/components/views/dialogs/__snapshots__/ExportDialog-test.tsx.snap b/test/components/views/dialogs/__snapshots__/ExportDialog-test.tsx.snap
index f97344beff3..b922d8b8ba3 100644
--- a/test/components/views/dialogs/__snapshots__/ExportDialog-test.tsx.snap
+++ b/test/components/views/dialogs/__snapshots__/ExportDialog-test.tsx.snap
@@ -245,19 +245,23 @@ Array [
-
- Cancel
-
-
- Export
-
+
+ Cancel
+
+
+ Export
+
+
}
@@ -466,19 +470,23 @@ Array [
-
- Cancel
-
-
- Export
-
+
+ Cancel
+
+
+ Export
+
+
}
@@ -815,22 +823,26 @@ Array [
-
- Cancel
-
-
- Export
-
+
+ Cancel
+
+
+ Export
+
+
@@ -1086,19 +1098,23 @@ Array [
-
- Cancel
-
-
- Export
-
+
+ Cancel
+
+
+ Export
+
+
}
@@ -1307,19 +1323,23 @@ Array [
-
- Cancel
-
-
- Export
-
+
+ Cancel
+
+
+ Export
+
+
}
@@ -1656,22 +1676,26 @@ Array [
-
- Cancel
-
-
- Export
-
+
+ Cancel
+
+
+ Export
+
+
@@ -1914,19 +1938,23 @@ Array [
-
- Cancel
-
-
- Export
-
+
+ Cancel
+
+
+ Export
+
+
}
@@ -2135,19 +2163,23 @@ Array [
-
- Cancel
-
-
- Export
-
+
+ Cancel
+
+
+ Export
+
+
}
@@ -2484,22 +2516,26 @@ Array [
-
- Cancel
-
-
- Export
-
+
+ Cancel
+
+
+ Export
+
+
@@ -2833,22 +2869,26 @@ Array [
-
- Cancel
-
-
- Export
-
+
+ Cancel
+
+
+ Export
+
+
,
diff --git a/test/components/views/elements/EventListSummary-test.js b/test/components/views/elements/EventListSummary-test.tsx
similarity index 78%
rename from test/components/views/elements/EventListSummary-test.js
rename to test/components/views/elements/EventListSummary-test.tsx
index 9ca5863aeed..0a1102e86f2 100644
--- a/test/components/views/elements/EventListSummary-test.js
+++ b/test/components/views/elements/EventListSummary-test.tsx
@@ -1,16 +1,37 @@
-import React from 'react';
-import ReactTestUtils from 'react-dom/test-utils';
-import ShallowRenderer from "react-test-renderer/shallow";
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
-import * as testUtils from '../../../test-utils';
-import _EventListSummary from "../../../../src/components/views/elements/EventListSummary";
+ http://www.apache.org/licenses/LICENSE-2.0
-// Give ELS a matrixClient in its child context
-const EventListSummary = testUtils.wrapInMatrixClientContext(_EventListSummary);
+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 { mount, ReactWrapper } from 'enzyme';
+import { MatrixEvent, RoomMember } from 'matrix-js-sdk/src/matrix';
+
+import {
+ getMockClientWithEventEmitter,
+ mkMembership,
+ mockClientMethodsUser,
+ unmockClientPeg,
+} from '../../../test-utils';
+import EventListSummary from "../../../../src/components/views/elements/EventListSummary";
+import { Layout } from '../../../../src/settings/enums/Layout';
+import MatrixClientContext from '../../../../src/contexts/MatrixClientContext';
describe('EventListSummary', function() {
+ const roomId = '!room:server.org';
// Generate dummy event tiles for use in simulating an expanded MELS
- const generateTiles = (events) => {
+ const generateTiles = (events: MatrixEvent[]) => {
return events.map((e) => {
return (
@@ -35,22 +56,28 @@ describe('EventListSummary', function() {
* Optional. Defaults to `parameters.userId`.
* @returns {MatrixEvent} the event created.
*/
- const generateMembershipEvent = (eventId, parameters) => {
- const e = testUtils.mkMembership({
+ interface MembershipEventParams {
+ senderId?: string;
+ userId: string;
+ membership: string;
+ prevMembership?: string;
+ }
+ const generateMembershipEvent = (
+ eventId: string, { senderId, userId, membership, prevMembership }: MembershipEventParams,
+ ): MatrixEvent => {
+ const member = new RoomMember(roomId, userId);
+ // Use localpart as display name;
+ member.name = userId.match(/@([^:]*):/)[1];
+ jest.spyOn(member, 'getAvatarUrl').mockReturnValue('avatar.jpeg');
+ jest.spyOn(member, 'getMxcAvatarUrl').mockReturnValue('mxc://avatar.url/image.png');
+ const e = mkMembership({
event: true,
- user: parameters.senderId || parameters.userId,
- skey: parameters.userId,
- mship: parameters.membership,
- prevMship: parameters.prevMembership,
- target: {
- // Use localpart as display name
- name: parameters.userId.match(/@([^:]*):/)[1],
- userId: parameters.userId,
- getAvatarUrl: () => {
- return "avatar.jpeg";
- },
- getMxcAvatarUrl: () => 'mxc://avatar.url/image.png',
- },
+ room: roomId,
+ user: senderId || userId,
+ skey: userId,
+ mship: membership,
+ prevMship: prevMembership,
+ target: member,
});
// Override random event ID to allow for equality tests against tiles from
// generateTiles
@@ -59,7 +86,7 @@ describe('EventListSummary', function() {
};
// Generate mock MatrixEvents from the array of parameters
- const generateEvents = (parameters) => {
+ const generateEvents = (parameters: MembershipEventParams[]) => {
const res = [];
for (let i = 0; i < parameters.length; i++) {
res.push(generateMembershipEvent(`event${i}`, parameters[i]));
@@ -83,8 +110,28 @@ describe('EventListSummary', function() {
return eventsForUsers;
};
+ const mockClient = getMockClientWithEventEmitter({
+ ...mockClientMethodsUser(),
+ });
+
+ const defaultProps = {
+ layout: Layout.Bubble,
+ events: [],
+ children: [],
+ };
+ const renderComponent = (props = {}): ReactWrapper => {
+ return mount(
+
+ ,
+ );
+ };
+
beforeEach(function() {
- testUtils.stubClient();
+ jest.clearAllMocks();
+ });
+
+ afterAll(() => {
+ unmockClientPeg();
});
it('renders expanded events if there are less than props.threshold', function() {
@@ -99,12 +146,9 @@ describe('EventListSummary', function() {
threshold: 3,
};
- const renderer = new ShallowRenderer();
- renderer.render(
);
- const wrapper = renderer.getRenderOutput(); // matrix cli context wrapper
- const result = wrapper.props.children;
+ const wrapper = renderComponent(props); // matrix cli context wrapper
- expect(result.props.children).toEqual([
+ expect(wrapper.find('GenericEventListSummary').props().children).toEqual([
Expanded membership
,
]);
});
@@ -122,12 +166,9 @@ describe('EventListSummary', function() {
threshold: 3,
};
- const renderer = new ShallowRenderer();
- renderer.render(
);
- const wrapper = renderer.getRenderOutput(); // matrix cli context wrapper
- const result = wrapper.props.children;
+ const wrapper = renderComponent(props); // matrix cli context wrapper
- expect(result.props.children).toEqual([
+ expect(wrapper.find('GenericEventListSummary').props().children).toEqual([
Expanded membership
,
Expanded membership
,
]);
@@ -147,13 +188,9 @@ describe('EventListSummary', function() {
threshold: 3,
};
- const instance = ReactTestUtils.renderIntoDocument(
-
,
- );
- const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
- instance, "mx_GenericEventListSummary_summary",
- );
- const summaryText = summary.textContent;
+ const wrapper = renderComponent(props);
+ const summary = wrapper.find(".mx_GenericEventListSummary_summary");
+ const summaryText = summary.text();
expect(summaryText).toBe("user_1 joined and left and joined");
});
@@ -183,13 +220,9 @@ describe('EventListSummary', function() {
threshold: 3,
};
- const instance = ReactTestUtils.renderIntoDocument(
-
,
- );
- const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
- instance, "mx_GenericEventListSummary_summary",
- );
- const summaryText = summary.textContent;
+ const wrapper = renderComponent(props);
+ const summary = wrapper.find(".mx_GenericEventListSummary_summary");
+ const summaryText = summary.text();
expect(summaryText).toBe("user_1 joined and left 7 times");
});
@@ -231,13 +264,9 @@ describe('EventListSummary', function() {
threshold: 3,
};
- const instance = ReactTestUtils.renderIntoDocument(
-
,
- );
- const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
- instance, "mx_GenericEventListSummary_summary",
- );
- const summaryText = summary.textContent;
+ const wrapper = renderComponent(props);
+ const summary = wrapper.find(".mx_GenericEventListSummary_summary");
+ const summaryText = summary.text();
expect(summaryText).toBe(
"user_1 was unbanned, joined and left 7 times and was invited",
@@ -283,13 +312,9 @@ describe('EventListSummary', function() {
threshold: 3,
};
- const instance = ReactTestUtils.renderIntoDocument(
-
,
- );
- const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
- instance, "mx_GenericEventListSummary_summary",
- );
- const summaryText = summary.textContent;
+ const wrapper = renderComponent(props);
+ const summary = wrapper.find(".mx_GenericEventListSummary_summary");
+ const summaryText = summary.text();
expect(summaryText).toBe(
"user_1 was unbanned, joined and left 2 times, was banned, " +
@@ -342,13 +367,9 @@ describe('EventListSummary', function() {
threshold: 3,
};
- const instance = ReactTestUtils.renderIntoDocument(
-
,
- );
- const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
- instance, "mx_GenericEventListSummary_summary",
- );
- const summaryText = summary.textContent;
+ const wrapper = renderComponent(props);
+ const summary = wrapper.find(".mx_GenericEventListSummary_summary");
+ const summaryText = summary.text();
expect(summaryText).toBe(
"user_1 and one other were unbanned, joined and left 2 times and were banned",
@@ -380,13 +401,9 @@ describe('EventListSummary', function() {
threshold: 3,
};
- const instance = ReactTestUtils.renderIntoDocument(
-
,
- );
- const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
- instance, "mx_GenericEventListSummary_summary",
- );
- const summaryText = summary.textContent;
+ const wrapper = renderComponent(props);
+ const summary = wrapper.find(".mx_GenericEventListSummary_summary");
+ const summaryText = summary.text();
expect(summaryText).toBe(
"user_0 and 19 others were unbanned, joined and left 2 times and were banned",
@@ -430,13 +447,9 @@ describe('EventListSummary', function() {
threshold: 3,
};
- const instance = ReactTestUtils.renderIntoDocument(
-
,
- );
- const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
- instance, "mx_GenericEventListSummary_summary",
- );
- const summaryText = summary.textContent;
+ const wrapper = renderComponent(props);
+ const summary = wrapper.find(".mx_GenericEventListSummary_summary");
+ const summaryText = summary.text();
expect(summaryText).toBe(
"user_2 was unbanned and joined and left 2 times, user_1 was unbanned, " +
@@ -504,13 +517,9 @@ describe('EventListSummary', function() {
threshold: 3,
};
- const instance = ReactTestUtils.renderIntoDocument(
-
,
- );
- const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
- instance, "mx_GenericEventListSummary_summary",
- );
- const summaryText = summary.textContent;
+ const wrapper = renderComponent(props);
+ const summary = wrapper.find(".mx_GenericEventListSummary_summary");
+ const summaryText = summary.text();
expect(summaryText).toBe(
"user_1 was invited, was banned, joined, rejected their invitation, left, " +
@@ -551,13 +560,9 @@ describe('EventListSummary', function() {
threshold: 3,
};
- const instance = ReactTestUtils.renderIntoDocument(
-
,
- );
- const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
- instance, "mx_GenericEventListSummary_summary",
- );
- const summaryText = summary.textContent;
+ const wrapper = renderComponent(props);
+ const summary = wrapper.find(".mx_GenericEventListSummary_summary");
+ const summaryText = summary.text();
expect(summaryText).toBe(
"user_1 and one other rejected their invitations and " +
@@ -586,13 +591,9 @@ describe('EventListSummary', function() {
threshold: 1, // threshold = 1 to force collapse
};
- const instance = ReactTestUtils.renderIntoDocument(
-
,
- );
- const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
- instance, "mx_GenericEventListSummary_summary",
- );
- const summaryText = summary.textContent;
+ const wrapper = renderComponent(props);
+ const summary = wrapper.find(".mx_GenericEventListSummary_summary");
+ const summaryText = summary.text();
expect(summaryText).toBe(
"user_1 rejected their invitation 2 times",
@@ -614,13 +615,9 @@ describe('EventListSummary', function() {
threshold: 3,
};
- const instance = ReactTestUtils.renderIntoDocument(
-
,
- );
- const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
- instance, "mx_GenericEventListSummary_summary",
- );
- const summaryText = summary.textContent;
+ const wrapper = renderComponent(props);
+ const summary = wrapper.find(".mx_GenericEventListSummary_summary");
+ const summaryText = summary.text();
expect(summaryText).toBe(
"user_1 and user_2 joined 2 times",
@@ -641,13 +638,9 @@ describe('EventListSummary', function() {
threshold: 3,
};
- const instance = ReactTestUtils.renderIntoDocument(
-
,
- );
- const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
- instance, "mx_GenericEventListSummary_summary",
- );
- const summaryText = summary.textContent;
+ const wrapper = renderComponent(props);
+ const summary = wrapper.find(".mx_GenericEventListSummary_summary");
+ const summaryText = summary.text();
expect(summaryText).toBe(
"user_1, user_2 and one other joined",
@@ -666,13 +659,9 @@ describe('EventListSummary', function() {
threshold: 3,
};
- const instance = ReactTestUtils.renderIntoDocument(
-
,
- );
- const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
- instance, "mx_GenericEventListSummary_summary",
- );
- const summaryText = summary.textContent;
+ const wrapper = renderComponent(props);
+ const summary = wrapper.find(".mx_GenericEventListSummary_summary");
+ const summaryText = summary.text();
expect(summaryText).toBe(
"user_0, user_1 and 18 others joined",
diff --git a/test/components/views/elements/Linkify-test.tsx b/test/components/views/elements/Linkify-test.tsx
new file mode 100644
index 00000000000..f663d4c9273
--- /dev/null
+++ b/test/components/views/elements/Linkify-test.tsx
@@ -0,0 +1,90 @@
+/*
+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 React, { useState } from "react";
+import { mount } from "enzyme";
+
+import { Linkify } from "../../../../src/components/views/elements/Linkify";
+
+describe("Linkify", () => {
+ it("linkifies the context", () => {
+ const wrapper = mount(
+ https://perdu.com
+ );
+ expect(wrapper.html()).toBe(
+ "
",
+ );
+ });
+
+ it("correctly linkifies a room alias", () => {
+ const wrapper = mount(
+ #element-web:matrix.org
+ );
+ expect(wrapper.html()).toBe(
+ "
",
+ );
+ });
+
+ it("changes the root tag name", () => {
+ const TAG_NAME = "p";
+
+ const wrapper = mount(
+ Hello world!
+ );
+
+ expect(wrapper.find("p")).toHaveLength(1);
+ });
+
+ it("relinkifies on update", () => {
+ function DummyTest() {
+ const [n, setN] = useState(0);
+ function onClick() {
+ setN(n + 1);
+ }
+
+ // upon clicking the element, change the content, and expect
+ // linkify to update
+ return
+
+ { n % 2 === 0
+ ? "https://perdu.com"
+ : "https://matrix.org" }
+
+
;
+ }
+
+ const wrapper = mount(
);
+
+ expect(wrapper.html()).toBe(
+ "
",
+ );
+
+ wrapper.find('div').at(0).simulate('click');
+
+ expect(wrapper.html()).toBe(
+ "
",
+ );
+ });
+});
diff --git a/test/components/views/elements/TooltipTarget-test.tsx b/test/components/views/elements/TooltipTarget-test.tsx
index cf7657bc400..a57cdf7187a 100644
--- a/test/components/views/elements/TooltipTarget-test.tsx
+++ b/test/components/views/elements/TooltipTarget-test.tsx
@@ -1,3 +1,19 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
import React from "react";
import {
renderIntoDocument,
@@ -14,7 +30,6 @@ describe('
', () => {
"className": 'test className',
"tooltipClassName": 'test tooltipClassName',
"label": 'test label',
- "yOffset": 1,
"alignment": Alignment.Left,
"id": 'test id',
'data-test-id': 'test',
@@ -48,13 +63,17 @@ describe('
', () => {
expect(getVisibleTooltip()).toBeFalsy();
});
- it('displays tooltip on mouseover', () => {
- const wrapper = getComponent();
- act(() => {
- Simulate.mouseOver(wrapper);
- });
- expect(getVisibleTooltip()).toMatchSnapshot();
- });
+ for (const alignment in Alignment) {
+ if (isNaN(Number(alignment))) {
+ it(`displays ${alignment} aligned tooltip on mouseover`, () => {
+ const wrapper = getComponent({ alignment: Alignment[alignment] });
+ act(() => {
+ Simulate.mouseOver(wrapper);
+ });
+ expect(getVisibleTooltip()).toMatchSnapshot();
+ });
+ }
+ }
it('hides tooltip on mouseleave', () => {
const wrapper = getComponent();
diff --git a/test/components/views/elements/__snapshots__/TooltipTarget-test.tsx.snap b/test/components/views/elements/__snapshots__/TooltipTarget-test.tsx.snap
index d0d01f53807..bdaab7071bb 100644
--- a/test/components/views/elements/__snapshots__/TooltipTarget-test.tsx.snap
+++ b/test/components/views/elements/__snapshots__/TooltipTarget-test.tsx.snap
@@ -1,9 +1,81 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`
displays tooltip on mouseover 1`] = `
+exports[`
displays Bottom aligned tooltip on mouseover 1`] = `
+`;
+
+exports[`
displays InnerBottom aligned tooltip on mouseover 1`] = `
+
+`;
+
+exports[`
displays Left aligned tooltip on mouseover 1`] = `
+
+`;
+
+exports[`
displays Natural aligned tooltip on mouseover 1`] = `
+
+`;
+
+exports[`
displays Right aligned tooltip on mouseover 1`] = `
+
+`;
+
+exports[`
displays Top aligned tooltip on mouseover 1`] = `
+
+`;
+
+exports[`
displays TopRight aligned tooltip on mouseover 1`] = `
+
+
diff --git a/test/components/views/location/__snapshots__/Marker-test.tsx.snap b/test/components/views/location/__snapshots__/Marker-test.tsx.snap
index b7596d1af8a..71391f9c08f 100644
--- a/test/components/views/location/__snapshots__/Marker-test.tsx.snap
+++ b/test/components/views/location/__snapshots__/Marker-test.tsx.snap
@@ -8,13 +8,15 @@ exports[` renders with location icon when no room member 1`] = `
className="mx_Marker mx_Marker_defaultColor"
id="abc123"
>
-
+ className="mx_Marker_border"
+ >
+
+
+
`;
diff --git a/test/components/views/location/__snapshots__/SmartMarker-test.tsx.snap b/test/components/views/location/__snapshots__/SmartMarker-test.tsx.snap
index d20c9bcd6ce..cfcba2e0dbe 100644
--- a/test/components/views/location/__snapshots__/SmartMarker-test.tsx.snap
+++ b/test/components/views/location/__snapshots__/SmartMarker-test.tsx.snap
@@ -24,13 +24,15 @@ exports[` creates a marker on mount 1`] = `
-
+ className="mx_Marker_border"
+ >
+
+
+
@@ -61,13 +63,15 @@ exports[` removes marker on unmount 1`] = `
-
+ className="mx_Marker_border"
+ >
+
+
+
diff --git a/test/components/views/messages/TextualBody-test.tsx b/test/components/views/messages/TextualBody-test.tsx
index fe4dbf99467..ecb597695b8 100644
--- a/test/components/views/messages/TextualBody-test.tsx
+++ b/test/components/views/messages/TextualBody-test.tsx
@@ -254,12 +254,7 @@ describe(" ", () => {
const wrapper = getComponent({ mxEvent: ev }, matrixClient);
expect(wrapper.text()).toBe("Hey Member");
const content = wrapper.find(".mx_EventTile_body");
- expect(content.html()).toBe('' +
- 'Hey ' +
- '' +
- ' Member ' +
- ' ');
+ expect(content.html()).toMatchSnapshot();
});
it("pills do not appear in code blocks", () => {
diff --git a/test/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap b/test/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap
index cc742223eba..f4914b510d1 100644
--- a/test/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap
+++ b/test/components/views/messages/__snapshots__/MLocationBody-test.tsx.snap
@@ -95,6 +95,7 @@ exports[`MLocationBody without error renders map correctly 1`] =
onBlur={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
+ onMouseMove={[Function]}
onMouseOver={[Function]}
tabIndex={0}
>
@@ -167,13 +168,15 @@ exports[`MLocationBody without error renders map correctly 1`] =
className="mx_Marker mx_Marker_defaultColor"
id="mx_MLocationBody_$2_1f9acffa-marker"
>
-
+ className="mx_Marker_border"
+ >
+
+
+
diff --git a/test/components/views/messages/__snapshots__/TextualBody-test.tsx.snap b/test/components/views/messages/__snapshots__/TextualBody-test.tsx.snap
index 15d3b7e208f..2b6ba33cbc7 100644
--- a/test/components/views/messages/__snapshots__/TextualBody-test.tsx.snap
+++ b/test/components/views/messages/__snapshots__/TextualBody-test.tsx.snap
@@ -13,3 +13,5 @@ exports[` renders formatted m.text correctly pills do not appear
"
`;
+
+exports[` renders formatted m.text correctly pills get injected correctly into the DOM 1`] = `"Hey Member "`;
diff --git a/test/components/views/right_panel/UserInfo-test.tsx b/test/components/views/right_panel/UserInfo-test.tsx
index 9966a994c4d..41c9debf409 100644
--- a/test/components/views/right_panel/UserInfo-test.tsx
+++ b/test/components/views/right_panel/UserInfo-test.tsx
@@ -57,7 +57,7 @@ describe(' ', () => {
isRoomEncrypted: jest.fn().mockReturnValue(false),
doesServerSupportUnstableFeature: jest.fn().mockReturnValue(false),
mxcUrlToHttp: jest.fn().mockReturnValue('mock-mxcUrlToHttp'),
- removeListerner: jest.fn(),
+ removeListener: jest.fn(),
currentState: {
on: jest.fn(),
},
diff --git a/test/components/views/rooms/BasicMessageComposer-test.tsx b/test/components/views/rooms/BasicMessageComposer-test.tsx
new file mode 100644
index 00000000000..838119492a5
--- /dev/null
+++ b/test/components/views/rooms/BasicMessageComposer-test.tsx
@@ -0,0 +1,65 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from 'react';
+import { mount, ReactWrapper } from 'enzyme';
+import { MatrixClient, Room } from 'matrix-js-sdk/src/matrix';
+
+import BasicMessageComposer from '../../../../src/components/views/rooms/BasicMessageComposer';
+import * as TestUtils from "../../../test-utils";
+import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
+import EditorModel from "../../../../src/editor/model";
+import { createPartCreator, createRenderer } from "../../../editor/mock";
+
+describe("BasicMessageComposer", () => {
+ const renderer = createRenderer();
+ const pc = createPartCreator();
+
+ beforeEach(() => {
+ TestUtils.stubClient();
+ });
+
+ it("should allow a user to paste a URL without it being mangled", () => {
+ const model = new EditorModel([], pc, renderer);
+
+ const wrapper = render(model);
+
+ wrapper.find(".mx_BasicMessageComposer_input").simulate("paste", {
+ clipboardData: {
+ getData: type => {
+ if (type === "text/plain") {
+ return "https://element.io";
+ }
+ },
+ },
+ });
+
+ expect(model.parts).toHaveLength(1);
+ expect(model.parts[0].text).toBe("https://element.io");
+ });
+});
+
+function render(model: EditorModel): ReactWrapper {
+ const client: MatrixClient = MatrixClientPeg.get();
+
+ const roomId = '!1234567890:domain';
+ const userId = client.getUserId();
+ const room = new Room(roomId, client, userId);
+
+ return mount((
+
+ ));
+}
diff --git a/test/components/views/rooms/MessageComposerButtons-test.tsx b/test/components/views/rooms/MessageComposerButtons-test.tsx
index 1ec08e455d0..d9f867b67e4 100644
--- a/test/components/views/rooms/MessageComposerButtons-test.tsx
+++ b/test/components/views/rooms/MessageComposerButtons-test.tsx
@@ -209,7 +209,6 @@ function createRoomState(room: Room, narrow: boolean): IRoomState {
shouldPeek: true,
membersLoaded: false,
numUnreadMessages: 0,
- guestsCanJoin: false,
canPeek: false,
showApps: false,
isPeeking: false,
diff --git a/test/components/views/rooms/ReadReceiptGroup-test.tsx b/test/components/views/rooms/ReadReceiptGroup-test.tsx
index 28f1caa511b..3d1bafedbc7 100644
--- a/test/components/views/rooms/ReadReceiptGroup-test.tsx
+++ b/test/components/views/rooms/ReadReceiptGroup-test.tsx
@@ -1,3 +1,19 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
import { determineAvatarPosition, readReceiptTooltip } from "../../../../src/components/views/rooms/ReadReceiptGroup";
describe("ReadReceiptGroup", () => {
@@ -34,46 +50,46 @@ describe("ReadReceiptGroup", () => {
describe("AvatarPosition", () => {
// The avatar slots are numbered from right to left
// That means currently, we’ve got the slots | 3 | 2 | 1 | 0 | each with 10px distance to the next one.
- // We want to fill slots so the first avatar is in the left-most slot without leaving any slots at the right
+ // We want to fill slots so the first avatar is in the right-most slot without leaving any slots at the left
// unoccupied.
it("to handle the non-overflowing case correctly", () => {
- expect(determineAvatarPosition(0, 1, 4))
+ expect(determineAvatarPosition(0, 4))
.toEqual({ hidden: false, position: 0 });
- expect(determineAvatarPosition(0, 2, 4))
- .toEqual({ hidden: false, position: 1 });
- expect(determineAvatarPosition(1, 2, 4))
+ expect(determineAvatarPosition(0, 4))
.toEqual({ hidden: false, position: 0 });
-
- expect(determineAvatarPosition(0, 3, 4))
- .toEqual({ hidden: false, position: 2 });
- expect(determineAvatarPosition(1, 3, 4))
+ expect(determineAvatarPosition(1, 4))
.toEqual({ hidden: false, position: 1 });
- expect(determineAvatarPosition(2, 3, 4))
- .toEqual({ hidden: false, position: 0 });
- expect(determineAvatarPosition(0, 4, 4))
- .toEqual({ hidden: false, position: 3 });
- expect(determineAvatarPosition(1, 4, 4))
- .toEqual({ hidden: false, position: 2 });
- expect(determineAvatarPosition(2, 4, 4))
+ expect(determineAvatarPosition(0, 4))
+ .toEqual({ hidden: false, position: 0 });
+ expect(determineAvatarPosition(1, 4))
.toEqual({ hidden: false, position: 1 });
- expect(determineAvatarPosition(3, 4, 4))
+ expect(determineAvatarPosition(2, 4))
+ .toEqual({ hidden: false, position: 2 });
+
+ expect(determineAvatarPosition(0, 4))
.toEqual({ hidden: false, position: 0 });
+ expect(determineAvatarPosition(1, 4))
+ .toEqual({ hidden: false, position: 1 });
+ expect(determineAvatarPosition(2, 4))
+ .toEqual({ hidden: false, position: 2 });
+ expect(determineAvatarPosition(3, 4))
+ .toEqual({ hidden: false, position: 3 });
});
it("to handle the overflowing case correctly", () => {
- expect(determineAvatarPosition(0, 6, 4))
- .toEqual({ hidden: false, position: 3 });
- expect(determineAvatarPosition(1, 6, 4))
- .toEqual({ hidden: false, position: 2 });
- expect(determineAvatarPosition(2, 6, 4))
- .toEqual({ hidden: false, position: 1 });
- expect(determineAvatarPosition(3, 6, 4))
+ expect(determineAvatarPosition(0, 4))
.toEqual({ hidden: false, position: 0 });
- expect(determineAvatarPosition(4, 6, 4))
+ expect(determineAvatarPosition(1, 4))
+ .toEqual({ hidden: false, position: 1 });
+ expect(determineAvatarPosition(2, 4))
+ .toEqual({ hidden: false, position: 2 });
+ expect(determineAvatarPosition(3, 4))
+ .toEqual({ hidden: false, position: 3 });
+ expect(determineAvatarPosition(4, 4))
.toEqual({ hidden: true, position: 0 });
- expect(determineAvatarPosition(5, 6, 4))
+ expect(determineAvatarPosition(5, 4))
.toEqual({ hidden: true, position: 0 });
});
});
diff --git a/test/components/views/rooms/RoomHeader-test.tsx b/test/components/views/rooms/RoomHeader-test.tsx
index 1037b0377ce..b7c07f320d8 100644
--- a/test/components/views/rooms/RoomHeader-test.tsx
+++ b/test/components/views/rooms/RoomHeader-test.tsx
@@ -1,3 +1,19 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
import React from 'react';
import { mount, ReactWrapper } from 'enzyme';
import { Room, PendingEventOrdering, MatrixEvent, MatrixClient } from 'matrix-js-sdk/src/matrix';
diff --git a/test/components/views/rooms/RoomList-test.js b/test/components/views/rooms/RoomList-test.tsx
similarity index 80%
rename from test/components/views/rooms/RoomList-test.js
rename to test/components/views/rooms/RoomList-test.tsx
index f13f24da8b6..7b9d3ee3113 100644
--- a/test/components/views/rooms/RoomList-test.js
+++ b/test/components/views/rooms/RoomList-test.tsx
@@ -1,7 +1,27 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
import React from 'react';
import ReactTestUtils from 'react-dom/test-utils';
import ReactDOM from 'react-dom';
-import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk/src/matrix';
+import {
+ PendingEventOrdering,
+ Room,
+ RoomMember,
+} from 'matrix-js-sdk/src/matrix';
import * as TestUtils from '../../../test-utils';
import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
@@ -13,6 +33,8 @@ import RoomListLayoutStore from "../../../../src/stores/room-list/RoomListLayout
import RoomList from "../../../../src/components/views/rooms/RoomList";
import RoomSublist from "../../../../src/components/views/rooms/RoomSublist";
import RoomTile from "../../../../src/components/views/rooms/RoomTile";
+import { getMockClientWithEventEmitter, mockClientMethodsUser } from '../../../test-utils';
+import ResizeNotifier from '../../../../src/utils/ResizeNotifier';
function generateRoomId() {
return '!' + Math.random().toString().slice(2, 10) + ':domain';
@@ -22,7 +44,7 @@ describe('RoomList', () => {
function createRoom(opts) {
const room = new Room(generateRoomId(), MatrixClientPeg.get(), client.getUserId(), {
// The room list now uses getPendingEvents(), so we need a detached ordering.
- pendingEventOrdering: "detached",
+ pendingEventOrdering: PendingEventOrdering.Detached,
});
if (opts) {
Object.assign(room, opts);
@@ -31,25 +53,38 @@ describe('RoomList', () => {
}
let parentDiv = null;
- let client = null;
let root = null;
const myUserId = '@me:domain';
const movingRoomId = '!someroomid';
- let movingRoom;
- let otherRoom;
+ let movingRoom: Room | undefined;
+ let otherRoom: Room | undefined;
+
+ let myMember: RoomMember | undefined;
+ let myOtherMember: RoomMember | undefined;
+
+ const client = getMockClientWithEventEmitter({
+ ...mockClientMethodsUser(myUserId),
+ getRooms: jest.fn(),
+ getVisibleRooms: jest.fn(),
+ getRoom: jest.fn(),
+ });
- let myMember;
- let myOtherMember;
+ const defaultProps = {
+ onKeyDown: jest.fn(),
+ onFocus: jest.fn(),
+ onBlur: jest.fn(),
+ onResize: jest.fn(),
+ resizeNotifier: {} as unknown as ResizeNotifier,
+ isMinimized: false,
+ activeSpace: '',
+ };
beforeEach(async function(done) {
RoomListStoreClass.TEST_MODE = true;
+ jest.clearAllMocks();
- TestUtils.stubClient();
- client = MatrixClientPeg.get();
client.credentials = { userId: myUserId };
- //revert this to prototype method as the test-utils monkey-patches this to return a hardcoded value
- client.getUserId = MatrixClient.prototype.getUserId;
DMRoomMap.makeShared();
@@ -58,7 +93,7 @@ describe('RoomList', () => {
const WrappedRoomList = TestUtils.wrapInMatrixClientContext(RoomList);
root = ReactDOM.render(
- {}} />,
+ ,
parentDiv,
);
ReactTestUtils.findRenderedComponentWithType(root, RoomList);
@@ -83,7 +118,7 @@ describe('RoomList', () => {
}[userId]);
// Mock the matrix client
- client.getRooms = () => [
+ const mockRooms = [
movingRoom,
otherRoom,
createRoom({ tags: { 'm.favourite': { order: 0.1 } }, name: 'Some other room' }),
@@ -91,14 +126,15 @@ describe('RoomList', () => {
createRoom({ tags: { 'm.lowpriority': {} }, name: 'Some unimportant room' }),
createRoom({ tags: { 'custom.tag': {} }, name: 'Some room customly tagged' }),
];
- client.getVisibleRooms = client.getRooms;
+ client.getRooms.mockReturnValue(mockRooms);
+ client.getVisibleRooms.mockReturnValue(mockRooms);
const roomMap = {};
client.getRooms().forEach((r) => {
roomMap[r.roomId] = r;
});
- client.getRoom = (roomId) => roomMap[roomId];
+ client.getRoom.mockImplementation((roomId) => roomMap[roomId]);
// Now that everything has been set up, prepare and update the store
await RoomListStore.instance.makeReady(client);
@@ -155,6 +191,7 @@ describe('RoomList', () => {
movingRoom.tags = { [oldTagId]: {} };
} else if (oldTagId === DefaultTagID.DM) {
// Mock inverse m.direct
+ // @ts-ignore forcing private property
DMRoomMap.shared().roomToUser = {
[movingRoom.roomId]: '@someotheruser:domain',
};
diff --git a/test/components/views/rooms/RoomTile-test.tsx b/test/components/views/rooms/RoomTile-test.tsx
index d07360f6d4f..7b4f712c6a9 100644
--- a/test/components/views/rooms/RoomTile-test.tsx
+++ b/test/components/views/rooms/RoomTile-test.tsx
@@ -81,11 +81,11 @@ describe("RoomTile", () => {
act(() => { store.startConnect("!1:example.org"); });
tile.update();
- expect(tile.find(".mx_RoomTile_videoIndicator").text()).toEqual("Connecting...");
+ expect(tile.find(".mx_RoomTile_videoIndicator").text()).toEqual("Joining…");
act(() => { store.connect("!1:example.org"); });
tile.update();
- expect(tile.find(".mx_RoomTile_videoIndicator").text()).toEqual("Connected");
+ expect(tile.find(".mx_RoomTile_videoIndicator").text()).toEqual("Joined");
act(() => { store.disconnect(); });
tile.update();
diff --git a/test/components/views/rooms/SearchBar-test.tsx b/test/components/views/rooms/SearchBar-test.tsx
index b72f838bb9a..d1a62ba688d 100644
--- a/test/components/views/rooms/SearchBar-test.tsx
+++ b/test/components/views/rooms/SearchBar-test.tsx
@@ -17,7 +17,7 @@ limitations under the License.
import React from 'react';
import { mount } from "enzyme";
-import DesktopBuildsNotice from "../../../../src/components/views/elements/DesktopBuildsNotice";
+import SearchWarning from "../../../../src/components/views/elements/SearchWarning";
import { PosthogScreenTracker } from "../../../../src/PosthogTrackers";
import SearchBar, { SearchScope } from "../../../../src/components/views/rooms/SearchBar";
import { KeyBindingAction } from "../../../../src/accessibility/KeyboardShortcuts";
@@ -39,8 +39,8 @@ jest.mock("../../../../src/KeyBindingsManager", () => ({
{ getAccessibilityAction: jest.fn(() => mockCurrentEvent) })),
}));
-/** mock out DesktopBuildsNotice component so it doesn't affect the result of our test */
-jest.mock('../../../../src/components/views/elements/DesktopBuildsNotice', () => ({
+/** mock out SearchWarning component so it doesn't affect the result of our test */
+jest.mock('../../../../src/components/views/elements/SearchWarning', () => ({
__esModule: true,
WarningKind: {
get Search() { // eslint-disable-line @typescript-eslint/naming-convention
@@ -73,13 +73,13 @@ describe("SearchBar", () => {
it("must render child components and pass necessary props", () => {
const postHogScreenTracker = wrapper.find(PosthogScreenTracker);
- const desktopBuildNotice = wrapper.find(DesktopBuildsNotice);
+ const searchWarning = wrapper.find(SearchWarning);
expect(postHogScreenTracker.length).toBe(1);
- expect(desktopBuildNotice.length).toBe(1);
+ expect(searchWarning.length).toBe(1);
expect(postHogScreenTracker.props().screenName).toEqual("RoomSearch");
- expect(desktopBuildNotice.props().isRoomEncrypted).toEqual(searchProps.isRoomEncrypted);
- expect(desktopBuildNotice.props().kind).toEqual(mockWarningKind);
+ expect(searchWarning.props().isRoomEncrypted).toEqual(searchProps.isRoomEncrypted);
+ expect(searchWarning.props().kind).toEqual(mockWarningKind);
});
it("must not search when input value is empty", () => {
diff --git a/test/components/views/settings/CryptographyPanel-test.tsx b/test/components/views/settings/CryptographyPanel-test.tsx
index 6748d6858f2..c46aa09f5a9 100644
--- a/test/components/views/settings/CryptographyPanel-test.tsx
+++ b/test/components/views/settings/CryptographyPanel-test.tsx
@@ -1,3 +1,19 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
import React, { ReactElement } from 'react';
import ReactDOM from 'react-dom';
import { MatrixClient } from 'matrix-js-sdk/src/matrix';
diff --git a/test/components/views/settings/__snapshots__/KeyboardShortcut-test.tsx.snap b/test/components/views/settings/__snapshots__/KeyboardShortcut-test.tsx.snap
index e4468c802f0..23062d79809 100644
--- a/test/components/views/settings/__snapshots__/KeyboardShortcut-test.tsx.snap
+++ b/test/components/views/settings/__snapshots__/KeyboardShortcut-test.tsx.snap
@@ -32,7 +32,7 @@ exports[`KeyboardShortcut doesn't render same modifier twice 1`] = `
>
- missing translation: en|Ctrl
+ Ctrl
+
@@ -70,7 +70,7 @@ exports[`KeyboardShortcut doesn't render same modifier twice 2`] = `
>
- missing translation: en|Ctrl
+ Ctrl
+
@@ -95,7 +95,7 @@ exports[`KeyboardShortcut renders alternative key name 1`] = `
>
- missing translation: en|Page Down
+ Page Down
+
diff --git a/test/components/views/settings/tabs/user/__snapshots__/KeyboardUserSettingsTab-test.tsx.snap b/test/components/views/settings/tabs/user/__snapshots__/KeyboardUserSettingsTab-test.tsx.snap
index caeea350f7b..71bab7f6129 100644
--- a/test/components/views/settings/tabs/user/__snapshots__/KeyboardUserSettingsTab-test.tsx.snap
+++ b/test/components/views/settings/tabs/user/__snapshots__/KeyboardUserSettingsTab-test.tsx.snap
@@ -8,7 +8,7 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
- missing translation: en|Keyboard
+ Keyboard
- missing translation: en|Composer
+ Composer
@@ -59,7 +59,7 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
>
- missing translation: en|Ctrl
+ Ctrl
+
@@ -103,7 +103,7 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
>
- missing translation: en|Ctrl
+ Ctrl
+
@@ -145,7 +145,7 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
- missing translation: en|Navigation
+ Navigation
@@ -173,7 +173,7 @@ exports[`KeyboardUserSettingsTab renders list of keyboard shortcuts 1`] = `
>
- missing translation: en|Enter
+ Enter
diff --git a/test/components/views/spaces/SpaceSettingsVisibilityTab-test.tsx b/test/components/views/spaces/SpaceSettingsVisibilityTab-test.tsx
index 78cc6028631..efb6097f36c 100644
--- a/test/components/views/spaces/SpaceSettingsVisibilityTab-test.tsx
+++ b/test/components/views/spaces/SpaceSettingsVisibilityTab-test.tsx
@@ -1,3 +1,19 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
import React from "react";
import { mocked } from 'jest-mock';
import {
diff --git a/test/components/views/spaces/SpaceTreeLevel-test.tsx b/test/components/views/spaces/SpaceTreeLevel-test.tsx
new file mode 100644
index 00000000000..09661c71f4f
--- /dev/null
+++ b/test/components/views/spaces/SpaceTreeLevel-test.tsx
@@ -0,0 +1,85 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from "react";
+import { mount } from "enzyme";
+
+import { stubClient, mkRoom } from "../../../test-utils";
+import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
+import DMRoomMap from "../../../../src/utils/DMRoomMap";
+import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
+import { Action } from "../../../../src/dispatcher/actions";
+import { SpaceButton } from "../../../../src/components/views/spaces/SpaceTreeLevel";
+import { MetaSpace, SpaceKey } from "../../../../src/stores/spaces";
+import SpaceStore from "../../../../src/stores/spaces/SpaceStore";
+
+jest.mock("../../../../src/stores/spaces/SpaceStore", () => {
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const EventEmitter = require("events");
+ class MockSpaceStore extends EventEmitter {
+ activeSpace: SpaceKey = "!space1";
+ setActiveSpace = jest.fn();
+ }
+
+ return { instance: new MockSpaceStore() };
+});
+
+describe("SpaceButton", () => {
+ stubClient();
+ const space = mkRoom(MatrixClientPeg.get(), "!1:example.org");
+ DMRoomMap.makeShared();
+
+ const dispatchSpy = jest.spyOn(defaultDispatcher, "dispatch");
+
+ afterEach(jest.clearAllMocks);
+
+ describe("real space", () => {
+ it("activates the space on click", () => {
+ const button = mount(
);
+
+ expect(SpaceStore.instance.setActiveSpace).not.toHaveBeenCalled();
+ button.simulate("click");
+ expect(SpaceStore.instance.setActiveSpace).toHaveBeenCalledWith("!1:example.org");
+ });
+
+ it("navigates to the space home on click if already active", () => {
+ const button = mount(
);
+
+ expect(dispatchSpy).not.toHaveBeenCalled();
+ button.simulate("click");
+ expect(dispatchSpy).toHaveBeenCalledWith({ action: Action.ViewRoom, room_id: "!1:example.org" });
+ });
+ });
+
+ describe("metaspace", () => {
+ it("activates the metaspace on click", () => {
+ const button = mount(
);
+
+ expect(SpaceStore.instance.setActiveSpace).not.toHaveBeenCalled();
+ button.simulate("click");
+ expect(SpaceStore.instance.setActiveSpace).toHaveBeenCalledWith(MetaSpace.People);
+ });
+
+ it("does nothing on click if already active", () => {
+ const button = mount(
);
+
+ button.simulate("click");
+ expect(dispatchSpy).not.toHaveBeenCalled();
+ // Re-activating the metaspace is a no-op
+ expect(SpaceStore.instance.setActiveSpace).toHaveBeenCalledWith(MetaSpace.People);
+ });
+ });
+});
diff --git a/test/components/views/typography/Heading-test.tsx b/test/components/views/typography/Heading-test.tsx
index 186d15ff90f..37704029336 100644
--- a/test/components/views/typography/Heading-test.tsx
+++ b/test/components/views/typography/Heading-test.tsx
@@ -1,3 +1,19 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
import React from 'react';
import { renderIntoDocument } from 'react-dom/test-utils';
diff --git a/test/components/views/voip/VideoLobby-test.tsx b/test/components/views/voip/VideoLobby-test.tsx
index 2d69709dc76..bf3a4a5d598 100644
--- a/test/components/views/voip/VideoLobby-test.tsx
+++ b/test/components/views/voip/VideoLobby-test.tsx
@@ -92,7 +92,7 @@ describe("VideoLobby", () => {
// Only Alice should display as connected
const memberText = lobby.find(".mx_VideoLobby_connectedMembers").children().at(0).text();
- expect(memberText).toEqual("1 person connected");
+ expect(memberText).toEqual("1 person joined");
expect(lobby.find(FacePile).find(MemberAvatar).props().member.userId).toEqual("@alice:example.org");
});
diff --git a/test/createRoom-test.ts b/test/createRoom-test.ts
index 5846823cfd0..c37edaff86a 100644
--- a/test/createRoom-test.ts
+++ b/test/createRoom-test.ts
@@ -37,35 +37,21 @@ describe("createRoom", () => {
setupAsyncStoreWithClient(WidgetStore.instance, client);
jest.spyOn(WidgetUtils, "waitForRoomWidget").mockResolvedValue();
- const userId = client.getUserId();
const roomId = await createRoom({ roomType: RoomType.ElementVideo });
-
const [[{
power_level_content_override: {
- users: {
- [userId]: userPower,
- },
- events: {
- "im.vector.modular.widgets": widgetPower,
- [VIDEO_CHANNEL_MEMBER]: videoMemberPower,
- },
+ events: { [VIDEO_CHANNEL_MEMBER]: videoMemberPower },
},
- }]] = mocked(client.createRoom).mock.calls as any;
+ }]] = mocked(client.createRoom).mock.calls as any; // no good type
const [[widgetRoomId, widgetStateKey, , widgetId]] = mocked(client.sendStateEvent).mock.calls;
- // We should have had enough power to be able to set up the Jitsi widget
- expect(userPower).toBeGreaterThanOrEqual(widgetPower);
- // and should have actually set it up
+ // We should have set up the Jitsi widget
expect(widgetRoomId).toEqual(roomId);
expect(widgetStateKey).toEqual("im.vector.modular.widgets");
expect(widgetId).toEqual(VIDEO_CHANNEL);
// All members should be able to update their connected devices
expect(videoMemberPower).toEqual(0);
- // Jitsi widget should be immutable for admins
- expect(widgetPower).toBeGreaterThan(100);
- // and we should have been reset back to admin
- expect(client.setPowerLevel).toHaveBeenCalledWith(roomId, userId, 100, undefined);
});
});
diff --git a/test/editor/mock.ts b/test/editor/mock.ts
index bc6eafc8cc0..bddddbf7cb6 100644
--- a/test/editor/mock.ts
+++ b/test/editor/mock.ts
@@ -18,6 +18,7 @@ import { Room, MatrixClient } from "matrix-js-sdk/src/matrix";
import AutocompleteWrapperModel from "../../src/editor/autocomplete";
import { PartCreator } from "../../src/editor/parts";
+import DocumentPosition from "../../src/editor/position";
class MockAutoComplete {
public _updateCallback;
@@ -78,11 +79,11 @@ export function createPartCreator(completions = []) {
}
export function createRenderer() {
- const render = (c) => {
+ const render = (c: DocumentPosition) => {
render.caret = c;
render.count += 1;
};
render.count = 0;
- render.caret = null;
+ render.caret = null as DocumentPosition;
return render;
}
diff --git a/test/editor/operations-test.ts b/test/editor/operations-test.ts
index 3e4de224179..6af732e5bd5 100644
--- a/test/editor/operations-test.ts
+++ b/test/editor/operations-test.ts
@@ -17,21 +17,88 @@ limitations under the License.
import EditorModel from "../../src/editor/model";
import { createPartCreator, createRenderer } from "./mock";
import {
- toggleInlineFormat,
- selectRangeOfWordAtCaret,
formatRange,
formatRangeAsCode,
+ formatRangeAsLink,
+ selectRangeOfWordAtCaret,
+ toggleInlineFormat,
} from "../../src/editor/operations";
import { Formatting } from "../../src/components/views/rooms/MessageComposerFormatBar";
import { longestBacktickSequence } from '../../src/editor/deserialize';
const SERIALIZED_NEWLINE = { "text": "\n", "type": "newline" };
-describe('editor/operations: formatting operations', () => {
- describe('toggleInlineFormat', () => {
- it('works for words', () => {
- const renderer = createRenderer();
- const pc = createPartCreator();
+describe("editor/operations: formatting operations", () => {
+ const renderer = createRenderer();
+ const pc = createPartCreator();
+
+ describe("formatRange", () => {
+ it.each([
+ [Formatting.Bold, "hello **world**!"],
+ ])("should correctly wrap format %s", (formatting: Formatting, expected: string) => {
+ const model = new EditorModel([
+ pc.plain("hello world!"),
+ ], pc, renderer);
+
+ const range = model.startRange(model.positionForOffset(6, false),
+ model.positionForOffset(11, false)); // around "world"
+
+ expect(range.parts[0].text).toBe("world");
+ expect(model.serializeParts()).toEqual([{ "text": "hello world!", "type": "plain" }]);
+ formatRange(range, formatting);
+ expect(model.serializeParts()).toEqual([{ "text": expected, "type": "plain" }]);
+ });
+
+ it("should apply to word range is within if length 0", () => {
+ const model = new EditorModel([
+ pc.plain("hello world!"),
+ ], pc, renderer);
+
+ const range = model.startRange(model.positionForOffset(6, false));
+
+ expect(model.serializeParts()).toEqual([{ "text": "hello world!", "type": "plain" }]);
+ formatRange(range, Formatting.Bold);
+ expect(model.serializeParts()).toEqual([{ "text": "hello **world!**", "type": "plain" }]);
+ });
+
+ it("should do nothing for a range with length 0 at initialisation", () => {
+ const model = new EditorModel([
+ pc.plain("hello world!"),
+ ], pc, renderer);
+
+ const range = model.startRange(model.positionForOffset(6, false));
+ range.setWasEmpty(false);
+
+ expect(model.serializeParts()).toEqual([{ "text": "hello world!", "type": "plain" }]);
+ formatRange(range, Formatting.Bold);
+ expect(model.serializeParts()).toEqual([{ "text": "hello world!", "type": "plain" }]);
+ });
+ });
+
+ describe("formatRangeAsLink", () => {
+ it.each([
+ // Caret is denoted by | in the expectation string
+ ["testing", "[testing](|)", ""],
+ ["testing", "[testing](foobar|)", "foobar"],
+ ["[testing]()", "testing|", ""],
+ ["[testing](foobar)", "testing|", ""],
+ ])("converts %s -> %s", (input: string, expectation: string, text: string) => {
+ const model = new EditorModel([
+ pc.plain(`foo ${input} bar`),
+ ], pc, renderer);
+
+ const range = model.startRange(model.positionForOffset(4, false),
+ model.positionForOffset(4 + input.length, false)); // around input
+
+ expect(range.parts[0].text).toBe(input);
+ formatRangeAsLink(range, text);
+ expect(renderer.caret.offset).toBe(4 + expectation.indexOf("|"));
+ expect(model.parts[0].text).toBe("foo " + expectation.replace("|", "") + " bar");
+ });
+ });
+
+ describe("toggleInlineFormat", () => {
+ it("works for words", () => {
const model = new EditorModel([
pc.plain("hello world!"),
], pc, renderer);
diff --git a/test/end-to-end-tests/src/scenario.ts b/test/end-to-end-tests/src/scenario.ts
index 4ce762f36f4..dc6e1309d78 100644
--- a/test/end-to-end-tests/src/scenario.ts
+++ b/test/end-to-end-tests/src/scenario.ts
@@ -30,8 +30,6 @@ import { stickerScenarios } from './scenarios/sticker';
import { userViewScenarios } from "./scenarios/user-view";
import { ssoCustomisationScenarios } from "./scenarios/sso-customisations";
import { updateScenarios } from "./scenarios/update";
-import { threadsScenarios } from "./scenarios/threads";
-import { enableThreads } from "./usecases/threads";
export async function scenario(createSession: (s: string) => Promise
,
restCreator: RestSessionCreator): Promise {
@@ -51,12 +49,6 @@ export async function scenario(createSession: (s: string) => Promise Promise {
- console.log(" threads tests:");
-
- // Alice sends message
- await sendMessage(alice, "Hey bob, what do you think about X?");
-
- // Bob responds via a thread
- await startThread(bob, "I think its Y!");
-
- // Alice sees thread summary and opens thread panel
- await assertTimelineThreadSummary(alice, "bob", "I think its Y!");
- await assertTimelineThreadSummary(bob, "bob", "I think its Y!");
- await clickTimelineThreadSummary(alice);
-
- // Bob closes right panel
- await closeRoomRightPanel(bob);
-
- // Alice responds in thread
- await sendThreadMessage(alice, "Great!");
- await assertTimelineThreadSummary(alice, "alice", "Great!");
- await assertTimelineThreadSummary(bob, "alice", "Great!");
-
- // Alice reacts to Bob's message instead
- await reactThreadMessage(alice, "😁");
- await assertTimelineThreadSummary(alice, "alice", "Great!");
- await assertTimelineThreadSummary(bob, "alice", "Great!");
- await redactThreadMessage(alice);
- await assertTimelineThreadSummary(alice, "bob", "I think its Y!");
- await assertTimelineThreadSummary(bob, "bob", "I think its Y!");
-
- // Bob sees notification dot on the thread header icon
- await assertThreadListHasUnreadIndicator(bob);
-
- // Bob opens thread list and inspects it
- await openThreadListPanel(bob);
-
- // Bob opens thread in right panel via thread list
- await clickLatestThreadInThreadListPanel(bob);
-
- // Bob responds to thread
- await sendThreadMessage(bob, "Testing threads s'more :)");
- await assertTimelineThreadSummary(alice, "bob", "Testing threads s'more :)");
- await assertTimelineThreadSummary(bob, "bob", "Testing threads s'more :)");
-
- // Bob edits thread response
- await editThreadMessage(bob, "Testing threads some more :)");
- await assertTimelineThreadSummary(alice, "bob", "Testing threads some more :)");
- await assertTimelineThreadSummary(bob, "bob", "Testing threads some more :)");
-}
diff --git a/test/end-to-end-tests/src/usecases/threads.ts b/test/end-to-end-tests/src/usecases/threads.ts
deleted file mode 100644
index 24ccd7064a8..00000000000
--- a/test/end-to-end-tests/src/usecases/threads.ts
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { strict as assert } from "assert";
-
-import { ElementSession } from "../session";
-
-export async function enableThreads(session: ElementSession): Promise {
- session.log.step(`enables threads`);
- await session.page.evaluate(() => {
- window.localStorage.setItem("mx_seen_feature_thread_experimental", "1"); // inhibit dialog
- window["mxSettingsStore"].setValue("feature_thread", null, "device", true);
- });
- session.log.done();
-}
-
-async function clickReplyInThread(session: ElementSession): Promise {
- const events = await session.queryAll(".mx_EventTile_line");
- const event = events[events.length - 1];
- await event.hover();
- const button = await event.$(".mx_MessageActionBar_threadButton");
- await button.click();
-}
-
-export async function sendThreadMessage(session: ElementSession, message: string): Promise {
- session.log.step(`sends thread response "${message}"`);
- const composer = await session.query(".mx_ThreadView .mx_BasicMessageComposer_input");
- await composer.click();
- await composer.type(message);
-
- const text = await session.innerText(composer);
- assert.equal(text.trim(), message.trim());
- await composer.press("Enter");
- // wait for the message to appear sent
- await session.query(".mx_ThreadView .mx_EventTile_last:not(.mx_EventTile_sending)");
- session.log.done();
-}
-
-export async function editThreadMessage(session: ElementSession, message: string): Promise {
- session.log.step(`edits thread response "${message}"`);
- const events = await session.queryAll(".mx_EventTile_line");
- const event = events[events.length - 1];
- await event.hover();
- const button = await event.$(".mx_MessageActionBar_editButton");
- await button.click();
-
- const composer = await session.query(".mx_ThreadView .mx_EditMessageComposer .mx_BasicMessageComposer_input");
- await composer.click({ clickCount: 3 });
- await composer.type(message);
-
- const text = await session.innerText(composer);
- assert.equal(text.trim(), message.trim());
- await composer.press("Enter");
- // wait for the edit to appear sent
- await session.query(".mx_ThreadView .mx_EventTile_last:not(.mx_EventTile_sending)");
- session.log.done();
-}
-
-export async function redactThreadMessage(session: ElementSession): Promise {
- session.log.startGroup(`redacts latest thread response`);
-
- const events = await session.queryAll(".mx_ThreadView .mx_EventTile_line");
- const event = events[events.length - 1];
- await event.hover();
-
- session.log.step(`clicks the ... button`);
- let button = await event.$('.mx_MessageActionBar [aria-label="Options"]');
- await button.click();
- session.log.done();
-
- session.log.step(`clicks the remove option`);
- button = await session.query('.mx_IconizedContextMenu_item[aria-label="Remove"]');
- await button.click();
- session.log.done();
-
- session.log.step(`confirms in the dialog`);
- button = await session.query(".mx_Dialog_primary");
- await button.click();
- session.log.done();
-
- await session.query(".mx_ThreadView .mx_RedactedBody");
- await session.delay(1000); // give the app a chance to settle
-
- session.log.endGroup();
-}
-
-export async function reactThreadMessage(session: ElementSession, reaction: string): Promise {
- session.log.startGroup(`reacts to latest thread response`);
-
- const events = await session.queryAll(".mx_ThreadView .mx_EventTile_line");
- const event = events[events.length - 1];
- await event.hover();
-
- session.log.step(`clicks the reaction button`);
- let button = await event.$('.mx_MessageActionBar [aria-label="React"]');
- await button.click();
- session.log.done();
-
- session.log.step(`selects reaction`);
- button = await session.query(`.mx_EmojiPicker_item_wrapper[aria-label=${reaction}]`);
- await button.click;
- session.log.done();
-
- session.log.step(`clicks away`);
- button = await session.query(".mx_ContextualMenu_background");
- await button.click();
- session.log.done();
-
- session.log.endGroup();
-}
-
-export async function startThread(session: ElementSession, response: string): Promise {
- session.log.startGroup(`creates thread on latest message`);
-
- await clickReplyInThread(session);
- await sendThreadMessage(session, response);
-
- session.log.endGroup();
-}
-
-export async function assertTimelineThreadSummary(
- session: ElementSession,
- sender: string,
- content: string,
-): Promise {
- session.log.step("asserts the timeline thread summary is as expected");
- const summaries = await session.queryAll(".mx_MainSplit_timeline .mx_ThreadSummary");
- const summary = summaries[summaries.length - 1];
- assert.equal(await session.innerText(await summary.$(".mx_ThreadSummary_sender")), sender);
- assert.equal(await session.innerText(await summary.$(".mx_ThreadSummary_content")), content);
- session.log.done();
-}
-
-export async function clickTimelineThreadSummary(session: ElementSession): Promise {
- session.log.step(`clicks the latest thread summary in the timeline`);
-
- const summaries = await session.queryAll(".mx_MainSplit_timeline .mx_ThreadSummary");
- await summaries[summaries.length - 1].click();
-
- session.log.done();
-}
diff --git a/test/globalSetup.js b/test/globalSetup.js
index 3d1e8924fcd..83b2ac971b4 100644
--- a/test/globalSetup.js
+++ b/test/globalSetup.js
@@ -1,3 +1,19 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
module.exports = async () => {
process.env.TZ = 'UTC';
};
diff --git a/test/i18n-test/languageHandler-test.tsx b/test/i18n-test/languageHandler-test.tsx
index b2349f7d1d8..9c15bfd3feb 100644
--- a/test/i18n-test/languageHandler-test.tsx
+++ b/test/i18n-test/languageHandler-test.tsx
@@ -1,3 +1,19 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
import React from 'react';
import {
@@ -11,6 +27,10 @@ import {
import { stubClient } from '../test-utils';
describe('languageHandler', function() {
+ /*
+ See /__mocks__/browser-request.js/ for how we are stubbing out translations
+ to provide fixture data for these tests
+ */
const basicString = 'Rooms';
const selfClosingTagSub = 'Accept to continue:';
const textInTagSub = 'Upgrade to your own domain';
@@ -19,6 +39,7 @@ describe('languageHandler', function() {
type TestCase = [string, string, Record, Record, TranslatedString];
const testCasesEn: TestCase[] = [
+ // description of the test case, translationString, variables, tags, expected result
['translates a basic string', basicString, {}, undefined, 'Rooms'],
[
'handles plurals when count is 0',
@@ -201,4 +222,17 @@ describe('languageHandler', function() {
});
});
});
+
+ describe('when languages dont load', () => {
+ it('_t', async () => {
+ const STRING_NOT_IN_THE_DICTIONARY = "a string that isn't in the translations dictionary";
+ expect(_t(STRING_NOT_IN_THE_DICTIONARY, {}, undefined)).toEqual(STRING_NOT_IN_THE_DICTIONARY);
+ });
+
+ it('_tDom', async () => {
+ const STRING_NOT_IN_THE_DICTIONARY = "a string that isn't in the translations dictionary";
+ expect(_tDom(STRING_NOT_IN_THE_DICTIONARY, {}, undefined)).toEqual(
+ { STRING_NOT_IN_THE_DICTIONARY } );
+ });
+ });
});
diff --git a/test/setupTests.js b/test/setupTests.js
index 649a914c096..0c7617eb2b1 100644
--- a/test/setupTests.js
+++ b/test/setupTests.js
@@ -1,3 +1,19 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
import Adapter from "@wojtekmaj/enzyme-adapter-react-17";
import { configure } from "enzyme";
import "blob-polyfill"; // /~https://github.com/jsdom/jsdom/issues/2555
diff --git a/test/stores/RoomViewStore-test.js b/test/stores/RoomViewStore-test.js
deleted file mode 100644
index d948fa64969..00000000000
--- a/test/stores/RoomViewStore-test.js
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
-Copyright 2017 - 2022 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-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 { RoomViewStore } from '../../src/stores/RoomViewStore';
-import { Action } from '../../src/dispatcher/actions';
-import { MatrixClientPeg as peg } from '../../src/MatrixClientPeg';
-import * as testUtils from '../test-utils';
-
-const dispatch = testUtils.getDispatchForStore(RoomViewStore.instance);
-
-jest.mock('../../src/utils/DMRoomMap', () => {
- const mock = {
- getUserIdForRoomId: jest.fn(),
- getDMRoomsForUserId: jest.fn(),
- };
-
- return {
- shared: jest.fn().mockReturnValue(mock),
- sharedInstance: mock,
- };
-});
-
-describe('RoomViewStore', function() {
- beforeEach(function() {
- testUtils.stubClient();
- peg.get().credentials = { userId: "@test:example.com" };
- peg.get().on = jest.fn();
- peg.get().off = jest.fn();
-
- // Reset the state of the store
- RoomViewStore.instance.reset();
- });
-
- it('can be used to view a room by ID and join', function(done) {
- peg.get().joinRoom = async (roomAddress) => {
- expect(roomAddress).toBe("!randomcharacters:aser.ver");
- done();
- };
-
- dispatch({ action: Action.ViewRoom, room_id: '!randomcharacters:aser.ver' });
- dispatch({ action: 'join_room' });
- expect(RoomViewStore.instance.isJoining()).toBe(true);
- });
-
- it('can be used to view a room by alias and join', function(done) {
- const token = RoomViewStore.instance.addListener(() => {
- // Wait until the room alias has resolved and the room ID is
- if (!RoomViewStore.instance.isRoomLoading()) {
- expect(RoomViewStore.instance.getRoomId()).toBe("!randomcharacters:aser.ver");
- dispatch({ action: 'join_room' });
- expect(RoomViewStore.instance.isJoining()).toBe(true);
- }
- });
-
- peg.get().getRoomIdForAlias.mockResolvedValue({ room_id: "!randomcharacters:aser.ver" });
- peg.get().joinRoom = async (roomAddress) => {
- token.remove(); // stop RVS listener
- expect(roomAddress).toBe("#somealias2:aser.ver");
- done();
- };
-
- dispatch({ action: Action.ViewRoom, room_alias: '#somealias2:aser.ver' });
- });
-});
diff --git a/test/stores/RoomViewStore-test.tsx b/test/stores/RoomViewStore-test.tsx
new file mode 100644
index 00000000000..15137835597
--- /dev/null
+++ b/test/stores/RoomViewStore-test.tsx
@@ -0,0 +1,86 @@
+/*
+Copyright 2017 - 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+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 { Room } from 'matrix-js-sdk/src/matrix';
+
+import { RoomViewStore } from '../../src/stores/RoomViewStore';
+import { Action } from '../../src/dispatcher/actions';
+import * as testUtils from '../test-utils';
+import { flushPromises, getMockClientWithEventEmitter } from '../test-utils';
+
+const dispatch = testUtils.getDispatchForStore(RoomViewStore.instance);
+
+jest.mock('../../src/utils/DMRoomMap', () => {
+ const mock = {
+ getUserIdForRoomId: jest.fn(),
+ getDMRoomsForUserId: jest.fn(),
+ };
+
+ return {
+ shared: jest.fn().mockReturnValue(mock),
+ sharedInstance: mock,
+ };
+});
+
+describe('RoomViewStore', function() {
+ const userId = '@alice:server';
+ const mockClient = getMockClientWithEventEmitter({
+ joinRoom: jest.fn(),
+ getRoom: jest.fn(),
+ getRoomIdForAlias: jest.fn(),
+ });
+ const room = new Room('!room:server', mockClient, userId);
+
+ beforeEach(function() {
+ jest.clearAllMocks();
+ mockClient.credentials = { userId: "@test:example.com" };
+ mockClient.joinRoom.mockResolvedValue(room);
+ mockClient.getRoom.mockReturnValue(room);
+
+ // Reset the state of the store
+ RoomViewStore.instance.reset();
+ });
+
+ it('can be used to view a room by ID and join', async () => {
+ dispatch({ action: Action.ViewRoom, room_id: '!randomcharacters:aser.ver' });
+ dispatch({ action: 'join_room' });
+ await flushPromises();
+ expect(mockClient.joinRoom).toHaveBeenCalledWith('!randomcharacters:aser.ver', { viaServers: [] });
+ expect(RoomViewStore.instance.isJoining()).toBe(true);
+ });
+
+ it('can be used to view a room by alias and join', async () => {
+ const roomId = "!randomcharacters:aser.ver";
+ const alias = "#somealias2:aser.ver";
+
+ mockClient.getRoomIdForAlias.mockResolvedValue({ room_id: roomId, servers: [] });
+
+ dispatch({ action: Action.ViewRoom, room_alias: alias });
+ await flushPromises();
+ await flushPromises();
+
+ // roomId is set to id of the room alias
+ expect(RoomViewStore.instance.getRoomId()).toBe(roomId);
+
+ // join the room
+ dispatch({ action: 'join_room' });
+
+ expect(RoomViewStore.instance.isJoining()).toBeTruthy();
+ await flushPromises();
+
+ expect(mockClient.joinRoom).toHaveBeenCalledWith(alias, { viaServers: [] });
+ });
+});
diff --git a/test/stores/VideoChannelStore-test.ts b/test/stores/VideoChannelStore-test.ts
index fc8752ec76f..09508d477a4 100644
--- a/test/stores/VideoChannelStore-test.ts
+++ b/test/stores/VideoChannelStore-test.ts
@@ -17,7 +17,7 @@ limitations under the License.
import { mocked } from "jest-mock";
import { Widget, ClientWidgetApi, MatrixWidgetType, IWidgetApiRequest } from "matrix-widget-api";
-import { stubClient, setupAsyncStoreWithClient } from "../test-utils";
+import { stubClient, setupAsyncStoreWithClient, mkRoom } from "../test-utils";
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
import WidgetStore, { IApp } from "../../src/stores/WidgetStore";
import { WidgetMessagingStore } from "../../src/stores/widgets/WidgetMessagingStore";
@@ -51,6 +51,7 @@ describe("VideoChannelStore", () => {
const cli = MatrixClientPeg.get();
setupAsyncStoreWithClient(WidgetMessagingStore.instance, cli);
setupAsyncStoreWithClient(store, cli);
+ mocked(cli).getRoom.mockReturnValue(mkRoom(cli, "!1:example.org"));
let resolveMessageSent: () => void;
messageSent = new Promise(resolve => resolveMessageSent = resolve);
diff --git a/test/stores/room-list/algorithms/RecentAlgorithm-test.ts b/test/stores/room-list/algorithms/RecentAlgorithm-test.ts
new file mode 100644
index 00000000000..40ce53f2251
--- /dev/null
+++ b/test/stores/room-list/algorithms/RecentAlgorithm-test.ts
@@ -0,0 +1,127 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { Room } from "matrix-js-sdk/src/models/room";
+
+import { stubClient, mkRoom, mkMessage } from "../../../test-utils";
+import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
+import "../../../../src/stores/room-list/RoomListStore";
+import { RecentAlgorithm } from "../../../../src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
+import { EffectiveMembership } from "../../../../src/utils/membership";
+
+describe("RecentAlgorithm", () => {
+ let algorithm;
+ let cli;
+ beforeEach(() => {
+ stubClient();
+ cli = MatrixClientPeg.get();
+ algorithm = new RecentAlgorithm();
+ });
+
+ describe("getLastTs", () => {
+ it("returns the last ts", () => {
+ const room = new Room("room123", cli, "@john:matrix.org");
+
+ const event1 = mkMessage({
+ room: room.roomId,
+ msg: "Hello world!",
+ user: "@alice:matrix.org",
+ ts: 5,
+ event: true,
+ });
+ const event2 = mkMessage({
+ room: room.roomId,
+ msg: "Howdy!",
+ user: "@bob:matrix.org",
+ ts: 10,
+ event: true,
+ });
+
+ room.getMyMembership = () => "join";
+
+ room.addLiveEvents([event1]);
+ expect(algorithm.getLastTs(room, "@jane:matrix.org")).toBe(5);
+ expect(algorithm.getLastTs(room, "@john:matrix.org")).toBe(5);
+
+ room.addLiveEvents([event2]);
+
+ expect(algorithm.getLastTs(room, "@jane:matrix.org")).toBe(10);
+ expect(algorithm.getLastTs(room, "@john:matrix.org")).toBe(10);
+ });
+
+ it("returns a fake ts for rooms without a timeline", () => {
+ const room = mkRoom(cli, "!new:example.org");
+ room.timeline = undefined;
+ expect(algorithm.getLastTs(room, "@john:matrix.org")).toBe(Number.MAX_SAFE_INTEGER);
+ });
+
+ it("works when not a member", () => {
+ const room = mkRoom(cli, "!new:example.org");
+ room.getMyMembership.mockReturnValue(EffectiveMembership.Invite);
+ expect(algorithm.getLastTs(room, "@john:matrix.org")).toBe(Number.MAX_SAFE_INTEGER);
+ });
+ });
+
+ describe("sortRooms", () => {
+ it("orders rooms per last message ts", () => {
+ const room1 = new Room("room1", cli, "@bob:matrix.org");
+ const room2 = new Room("room2", cli, "@bob:matrix.org");
+
+ room1.getMyMembership = () => "join";
+ room2.getMyMembership = () => "join";
+
+ const evt = mkMessage({
+ room: room1.roomId,
+ msg: "Hello world!",
+ user: "@alice:matrix.org",
+ ts: 5,
+ event: true,
+ });
+ const evt2 = mkMessage({
+ room: room2.roomId,
+ msg: "Hello world!",
+ user: "@alice:matrix.org",
+ ts: 2,
+ event: true,
+ });
+
+ room1.addLiveEvents([evt]);
+ room2.addLiveEvents([evt2]);
+
+ expect(algorithm.sortRooms([room2, room1])).toEqual([room1, room2]);
+ });
+
+ it("orders rooms without messages first", () => {
+ const room1 = new Room("room1", cli, "@bob:matrix.org");
+ const room2 = new Room("room2", cli, "@bob:matrix.org");
+
+ room1.getMyMembership = () => "join";
+ room2.getMyMembership = () => "join";
+
+ const evt = mkMessage({
+ room: room1.roomId,
+ msg: "Hello world!",
+ user: "@alice:matrix.org",
+ ts: 5,
+ event: true,
+ });
+
+ room1.addLiveEvents([evt]);
+
+ expect(algorithm.sortRooms([room2, room1])).toEqual([room2, room1]);
+ });
+ });
+});
diff --git a/test/test-utils/client.ts b/test/test-utils/client.ts
index bd7b832f728..ed4cabc501c 100644
--- a/test/test-utils/client.ts
+++ b/test/test-utils/client.ts
@@ -53,3 +53,18 @@ export const getMockClientWithEventEmitter = (
return mock;
};
+export const unmockClientPeg = () => jest.spyOn(MatrixClientPeg, 'get').mockRestore();
+
+/**
+ * Returns basic mocked client methods related to the current user
+ * ```
+ * const mockClient = getMockClientWithEventEmitter({
+ ...mockClientMethodsUser('@mytestuser:domain'),
+ });
+ * ```
+ */
+export const mockClientMethodsUser = (userId = '@alice:domain') => ({
+ getUserId: jest.fn().mockReturnValue(userId),
+ isGuest: jest.fn().mockReturnValue(false),
+ mxcUrlToHttp: jest.fn().mockReturnValue('mock-mxcUrlToHttp'),
+});
diff --git a/test/test-utils/index.ts b/test/test-utils/index.ts
index 44ea28c9660..0b187ce8f16 100644
--- a/test/test-utils/index.ts
+++ b/test/test-utils/index.ts
@@ -1,3 +1,19 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
export * from './beacon';
export * from './client';
export * from './location';
diff --git a/test/test-utils/location.ts b/test/test-utils/location.ts
index 05bba669582..39d84ef3d61 100644
--- a/test/test-utils/location.ts
+++ b/test/test-utils/location.ts
@@ -16,14 +16,14 @@ limitations under the License.
import { LocationAssetType, M_LOCATION } from "matrix-js-sdk/src/@types/location";
import { makeLocationContent } from "matrix-js-sdk/src/content-helpers";
-import { MatrixEvent } from "matrix-js-sdk/src/matrix";
+import { MatrixEvent, EventType } from "matrix-js-sdk/src/matrix";
let id = 1;
export const makeLegacyLocationEvent = (geoUri: string): MatrixEvent => {
return new MatrixEvent(
{
"event_id": `$${++id}`,
- "type": M_LOCATION.name,
+ "type": EventType.RoomMessage,
"content": {
"body": "Something about where I am",
"msgtype": "m.location",
diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts
index fbe7c3aa4a9..6db49c731d5 100644
--- a/test/test-utils/test-utils.ts
+++ b/test/test-utils/test-utils.ts
@@ -1,3 +1,19 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
import EventEmitter from "events";
import { mocked, MockedObject } from 'jest-mock';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
@@ -63,6 +79,7 @@ export function createTestClient(): MatrixClient {
getUserId: jest.fn().mockReturnValue("@userId:matrix.rog"),
getUser: jest.fn().mockReturnValue({ on: jest.fn() }),
getDeviceId: jest.fn().mockReturnValue("ABCDEFGHI"),
+ getDevices: jest.fn().mockResolvedValue({ devices: [{ device_id: "ABCDEFGHI" }] }),
credentials: { userId: "@userId:matrix.rog" },
getPushActionsForEvent: jest.fn(),
diff --git a/test/test-utils/video.ts b/test/test-utils/video.ts
index 77fdfb8fcc0..ffc3ac2883a 100644
--- a/test/test-utils/video.ts
+++ b/test/test-utils/video.ts
@@ -22,24 +22,25 @@ import { VIDEO_CHANNEL_MEMBER } from "../../src/utils/VideoChannelUtils";
import VideoChannelStore, { VideoChannelEvent, IJitsiParticipant } from "../../src/stores/VideoChannelStore";
export class StubVideoChannelStore extends EventEmitter {
- private _roomId: string;
- public get roomId(): string { return this._roomId; }
+ private _roomId: string | null;
+ public get roomId(): string | null { return this._roomId; }
+ public set roomId(value: string | null) { this._roomId = value; }
private _connected: boolean;
public get connected(): boolean { return this._connected; }
public get participants(): IJitsiParticipant[] { return []; }
public startConnect = (roomId: string) => {
- this._roomId = roomId;
+ this.roomId = roomId;
this.emit(VideoChannelEvent.StartConnect, roomId);
};
public connect = jest.fn((roomId: string) => {
- this._roomId = roomId;
+ this.roomId = roomId;
this._connected = true;
this.emit(VideoChannelEvent.Connect, roomId);
});
public disconnect = jest.fn(() => {
const roomId = this._roomId;
- this._roomId = null;
+ this.roomId = null;
this._connected = false;
this.emit(VideoChannelEvent.Disconnect, roomId);
});
diff --git a/test/useTopic-test.tsx b/test/useTopic-test.tsx
new file mode 100644
index 00000000000..75096b43e48
--- /dev/null
+++ b/test/useTopic-test.tsx
@@ -0,0 +1,69 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { mount } from "enzyme";
+import { act } from "react-dom/test-utils";
+
+import { useTopic } from "../src/hooks/room/useTopic";
+import { mkEvent, stubClient } from "./test-utils";
+import { MatrixClientPeg } from "../src/MatrixClientPeg";
+
+describe("useTopic", () => {
+ it("should display the room topic", () => {
+ stubClient();
+ const room = new Room("!TESTROOM", MatrixClientPeg.get(), "@alice:example.org");
+ const topic = mkEvent({
+ type: 'm.room.topic',
+ room: '!TESTROOM',
+ user: '@alice:example.org',
+ content: {
+ topic: 'Test topic',
+ },
+ ts: 123,
+ event: true,
+ });
+
+ room.addLiveEvents([topic]);
+
+ function RoomTopic() {
+ const topic = useTopic(room);
+ return { topic }
;
+ }
+
+ const wrapper = mount( );
+
+ expect(wrapper.text()).toBe("Test topic");
+
+ const updatedTopic = mkEvent({
+ type: 'm.room.topic',
+ room: '!TESTROOM',
+ user: '@alice:example.org',
+ content: {
+ topic: 'New topic',
+ },
+ ts: 666,
+ event: true,
+ });
+
+ act(() => {
+ room.addLiveEvents([updatedTopic]);
+ });
+
+ expect(wrapper.text()).toBe("New topic");
+ });
+});
diff --git a/test/utils/EventUtils-test.ts b/test/utils/EventUtils-test.ts
index 674162f548d..df65b7e5f35 100644
--- a/test/utils/EventUtils-test.ts
+++ b/test/utils/EventUtils-test.ts
@@ -315,11 +315,11 @@ describe('EventUtils', () => {
});
describe('canForward()', () => {
- it('returns false for a location event', () => {
+ it('returns true for a location event', () => {
const event = new MatrixEvent({
type: M_LOCATION.name,
});
- expect(canForward(event)).toBe(false);
+ expect(canForward(event)).toBe(true);
});
it('returns false for a poll event', () => {
const event = makePollStartEvent('Who?', userId);
diff --git a/test/utils/MultiInviter-test.ts b/test/utils/MultiInviter-test.ts
new file mode 100644
index 00000000000..1d5420aa8f5
--- /dev/null
+++ b/test/utils/MultiInviter-test.ts
@@ -0,0 +1,147 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { mocked } from 'jest-mock';
+import { MatrixClient } from 'matrix-js-sdk/src/matrix';
+
+import { MatrixClientPeg } from '../../src/MatrixClientPeg';
+import Modal, { ModalManager } from '../../src/Modal';
+import SettingsStore from '../../src/settings/SettingsStore';
+import MultiInviter, { CompletionStates } from '../../src/utils/MultiInviter';
+import * as TestUtilsMatrix from '../test-utils';
+
+const ROOMID = '!room:server';
+
+const MXID1 = '@user1:server';
+const MXID2 = '@user2:server';
+const MXID3 = '@user3:server';
+
+const MXID_PROFILE_STATES = {
+ [MXID1]: Promise.resolve({}),
+ [MXID2]: Promise.reject({ errcode: 'M_FORBIDDEN' }),
+ [MXID3]: Promise.reject({ errcode: 'M_NOT_FOUND' }),
+};
+
+jest.mock('../../src/Modal', () => ({
+ createTrackedDialog: jest.fn(),
+}));
+
+jest.mock('../../src/settings/SettingsStore', () => ({
+ getValue: jest.fn(),
+}));
+
+const mockPromptBeforeInviteUnknownUsers = (value: boolean) => {
+ mocked(SettingsStore.getValue).mockImplementation(
+ (settingName: string, roomId: string = null, _excludeDefault = false): any => {
+ if (settingName === 'promptBeforeInviteUnknownUsers' && roomId === ROOMID) {
+ return value;
+ }
+ },
+ );
+};
+
+const mockCreateTrackedDialog = (callbackName: 'onInviteAnyways'|'onGiveUp') => {
+ mocked(Modal.createTrackedDialog).mockImplementation(
+ (
+ _analyticsAction: string,
+ _analyticsInfo: string,
+ ...rest: Parameters
+ ): any => {
+ rest[1][callbackName]();
+ },
+ );
+};
+
+const expectAllInvitedResult = (result: CompletionStates) => {
+ expect(result).toEqual({
+ [MXID1]: 'invited',
+ [MXID2]: 'invited',
+ [MXID3]: 'invited',
+ });
+};
+
+describe('MultiInviter', () => {
+ let client: jest.Mocked;
+ let inviter: MultiInviter;
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+
+ TestUtilsMatrix.stubClient();
+ client = MatrixClientPeg.get() as jest.Mocked;
+
+ client.invite = jest.fn();
+ client.invite.mockResolvedValue({});
+
+ client.getProfileInfo = jest.fn();
+ client.getProfileInfo.mockImplementation((userId: string) => {
+ return MXID_PROFILE_STATES[userId] || Promise.reject();
+ });
+
+ inviter = new MultiInviter(ROOMID);
+ });
+
+ describe('invite', () => {
+ describe('with promptBeforeInviteUnknownUsers = false', () => {
+ beforeEach(() => mockPromptBeforeInviteUnknownUsers(false));
+
+ it('should invite all users', async () => {
+ const result = await inviter.invite([MXID1, MXID2, MXID3]);
+
+ expect(client.invite).toHaveBeenCalledTimes(3);
+ expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, undefined, undefined);
+ expect(client.invite).toHaveBeenNthCalledWith(2, ROOMID, MXID2, undefined, undefined);
+ expect(client.invite).toHaveBeenNthCalledWith(3, ROOMID, MXID3, undefined, undefined);
+
+ expectAllInvitedResult(result);
+ });
+ });
+
+ describe('with promptBeforeInviteUnknownUsers = true and', () => {
+ beforeEach(() => mockPromptBeforeInviteUnknownUsers(true));
+
+ describe('confirming the unknown user dialog', () => {
+ beforeEach(() => mockCreateTrackedDialog('onInviteAnyways'));
+
+ it('should invite all users', async () => {
+ const result = await inviter.invite([MXID1, MXID2, MXID3]);
+
+ expect(client.invite).toHaveBeenCalledTimes(3);
+ expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, undefined, undefined);
+ expect(client.invite).toHaveBeenNthCalledWith(2, ROOMID, MXID2, undefined, undefined);
+ expect(client.invite).toHaveBeenNthCalledWith(3, ROOMID, MXID3, undefined, undefined);
+
+ expectAllInvitedResult(result);
+ });
+ });
+
+ describe('declining the unknown user dialog', () => {
+ beforeEach(() => mockCreateTrackedDialog('onGiveUp'));
+
+ it('should only invite existing users', async () => {
+ const result = await inviter.invite([MXID1, MXID2, MXID3]);
+
+ expect(client.invite).toHaveBeenCalledTimes(1);
+ expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, undefined, undefined);
+
+ // The resolved state is 'invited' for all users.
+ // With the above client expectations, the test ensures that only the first user is invited.
+ expectAllInvitedResult(result);
+ });
+ });
+ });
+ });
+});
diff --git a/test/utils/permalinks/Permalinks-test.js b/test/utils/permalinks/Permalinks-test.js
deleted file mode 100644
index ddca34fb766..00000000000
--- a/test/utils/permalinks/Permalinks-test.js
+++ /dev/null
@@ -1,465 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-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 { MatrixClientPeg as peg } from '../../../src/MatrixClientPeg';
-import {
- makeRoomPermalink,
- makeUserPermalink,
- parsePermalink,
- RoomPermalinkCreator,
-} from "../../../src/utils/permalinks/Permalinks";
-import * as testUtils from "../../test-utils";
-
-function mockRoom(roomId, members, serverACL) {
- members.forEach(m => m.membership = "join");
- const powerLevelsUsers = members.reduce((pl, member) => {
- if (Number.isFinite(member.powerLevel)) {
- pl[member.userId] = member.powerLevel;
- }
- return pl;
- }, {});
-
- return {
- roomId,
- getCanonicalAlias: () => null,
- getJoinedMembers: () => members,
- getMember: (userId) => members.find(m => m.userId === userId),
- currentState: {
- getStateEvents: (type, key) => {
- if (key) {
- return null;
- }
- let content;
- switch (type) {
- case "m.room.server_acl":
- content = serverACL;
- break;
- case "m.room.power_levels":
- content = { users: powerLevelsUsers, users_default: 0 };
- break;
- }
- if (content) {
- return {
- getContent: () => content,
- };
- } else {
- return null;
- }
- },
- },
- on: () => undefined,
- removeListener: () => undefined,
- };
-}
-
-describe('Permalinks', function() {
- beforeEach(function() {
- testUtils.stubClient();
- peg.get().credentials = { userId: "@test:example.com" };
- });
-
- it('should pick no candidate servers when the room has no members', function() {
- const room = mockRoom("!fake:example.org", []);
- const creator = new RoomPermalinkCreator(room);
- creator.load();
- expect(creator._serverCandidates).toBeTruthy();
- expect(creator._serverCandidates.length).toBe(0);
- });
-
- it('should pick a candidate server for the highest power level user in the room', function() {
- const room = mockRoom("!fake:example.org", [
- {
- userId: "@alice:pl_50",
- powerLevel: 50,
- },
- {
- userId: "@alice:pl_75",
- powerLevel: 75,
- },
- {
- userId: "@alice:pl_95",
- powerLevel: 95,
- },
- ]);
- const creator = new RoomPermalinkCreator(room);
- creator.load();
- expect(creator._serverCandidates).toBeTruthy();
- expect(creator._serverCandidates.length).toBe(3);
- expect(creator._serverCandidates[0]).toBe("pl_95");
- // we don't check the 2nd and 3rd servers because that is done by the next test
- });
-
- it('should change candidate server when highest power level user leaves the room', function() {
- const roomId = "!fake:example.org";
- const member95 = {
- userId: "@alice:pl_95",
- powerLevel: 95,
- roomId,
- };
- const room = mockRoom(roomId, [
- {
- userId: "@alice:pl_50",
- powerLevel: 50,
- roomId,
- },
- {
- userId: "@alice:pl_75",
- powerLevel: 75,
- roomId,
- },
- member95,
- ]);
- const creator = new RoomPermalinkCreator(room, null);
- creator.load();
- expect(creator._serverCandidates[0]).toBe("pl_95");
- member95.membership = "left";
- creator.onRoomStateUpdate();
- expect(creator._serverCandidates[0]).toBe("pl_75");
- member95.membership = "join";
- creator.onRoomStateUpdate();
- expect(creator._serverCandidates[0]).toBe("pl_95");
- });
-
- it('should pick candidate servers based on user population', function() {
- const room = mockRoom("!fake:example.org", [
- {
- userId: "@alice:first",
- powerLevel: 0,
- },
- {
- userId: "@bob:first",
- powerLevel: 0,
- },
- {
- userId: "@charlie:first",
- powerLevel: 0,
- },
- {
- userId: "@alice:second",
- powerLevel: 0,
- },
- {
- userId: "@bob:second",
- powerLevel: 0,
- },
- {
- userId: "@charlie:third",
- powerLevel: 0,
- },
- ]);
- const creator = new RoomPermalinkCreator(room);
- creator.load();
- expect(creator._serverCandidates).toBeTruthy();
- expect(creator._serverCandidates.length).toBe(3);
- expect(creator._serverCandidates[0]).toBe("first");
- expect(creator._serverCandidates[1]).toBe("second");
- expect(creator._serverCandidates[2]).toBe("third");
- });
-
- it('should pick prefer candidate servers with higher power levels', function() {
- const room = mockRoom("!fake:example.org", [
- {
- userId: "@alice:first",
- powerLevel: 100,
- },
- {
- userId: "@alice:second",
- powerLevel: 0,
- },
- {
- userId: "@bob:second",
- powerLevel: 0,
- },
- {
- userId: "@charlie:third",
- powerLevel: 0,
- },
- ]);
- const creator = new RoomPermalinkCreator(room);
- creator.load();
- expect(creator._serverCandidates.length).toBe(3);
- expect(creator._serverCandidates[0]).toBe("first");
- expect(creator._serverCandidates[1]).toBe("second");
- expect(creator._serverCandidates[2]).toBe("third");
- });
-
- it('should pick a maximum of 3 candidate servers', function() {
- const room = mockRoom("!fake:example.org", [
- {
- userId: "@alice:alpha",
- powerLevel: 100,
- },
- {
- userId: "@alice:bravo",
- powerLevel: 0,
- },
- {
- userId: "@alice:charlie",
- powerLevel: 0,
- },
- {
- userId: "@alice:delta",
- powerLevel: 0,
- },
- {
- userId: "@alice:echo",
- powerLevel: 0,
- },
- ]);
- const creator = new RoomPermalinkCreator(room);
- creator.load();
- expect(creator._serverCandidates).toBeTruthy();
- expect(creator._serverCandidates.length).toBe(3);
- });
-
- it('should not consider IPv4 hosts', function() {
- const room = mockRoom("!fake:example.org", [
- {
- userId: "@alice:127.0.0.1",
- powerLevel: 100,
- },
- ]);
- const creator = new RoomPermalinkCreator(room);
- creator.load();
- expect(creator._serverCandidates).toBeTruthy();
- expect(creator._serverCandidates.length).toBe(0);
- });
-
- it('should not consider IPv6 hosts', function() {
- const room = mockRoom("!fake:example.org", [
- {
- userId: "@alice:[::1]",
- powerLevel: 100,
- },
- ]);
- const creator = new RoomPermalinkCreator(room);
- creator.load();
- expect(creator._serverCandidates).toBeTruthy();
- expect(creator._serverCandidates.length).toBe(0);
- });
-
- it('should not consider IPv4 hostnames with ports', function() {
- const room = mockRoom("!fake:example.org", [
- {
- userId: "@alice:127.0.0.1:8448",
- powerLevel: 100,
- },
- ]);
- const creator = new RoomPermalinkCreator(room);
- creator.load();
- expect(creator._serverCandidates).toBeTruthy();
- expect(creator._serverCandidates.length).toBe(0);
- });
-
- it('should not consider IPv6 hostnames with ports', function() {
- const room = mockRoom("!fake:example.org", [
- {
- userId: "@alice:[::1]:8448",
- powerLevel: 100,
- },
- ]);
- const creator = new RoomPermalinkCreator(room);
- creator.load();
- expect(creator._serverCandidates).toBeTruthy();
- expect(creator._serverCandidates.length).toBe(0);
- });
-
- it('should work with hostnames with ports', function() {
- const room = mockRoom("!fake:example.org", [
- {
- userId: "@alice:example.org:8448",
- powerLevel: 100,
- },
- ]);
-
- const creator = new RoomPermalinkCreator(room);
- creator.load();
- expect(creator._serverCandidates).toBeTruthy();
- expect(creator._serverCandidates.length).toBe(1);
- expect(creator._serverCandidates[0]).toBe("example.org:8448");
- });
-
- it('should not consider servers explicitly denied by ACLs', function() {
- const room = mockRoom("!fake:example.org", [
- {
- userId: "@alice:evilcorp.com",
- powerLevel: 100,
- },
- {
- userId: "@bob:chat.evilcorp.com",
- powerLevel: 0,
- },
- ], {
- deny: ["evilcorp.com", "*.evilcorp.com"],
- allow: ["*"],
- });
- const creator = new RoomPermalinkCreator(room);
- creator.load();
- expect(creator._serverCandidates).toBeTruthy();
- expect(creator._serverCandidates.length).toBe(0);
- });
-
- it('should not consider servers not allowed by ACLs', function() {
- const room = mockRoom("!fake:example.org", [
- {
- userId: "@alice:evilcorp.com",
- powerLevel: 100,
- },
- {
- userId: "@bob:chat.evilcorp.com",
- powerLevel: 0,
- },
- ], {
- deny: [],
- allow: [], // implies "ban everyone"
- });
- const creator = new RoomPermalinkCreator(room);
- creator.load();
- expect(creator._serverCandidates).toBeTruthy();
- expect(creator._serverCandidates.length).toBe(0);
- });
-
- it('should consider servers not explicitly banned by ACLs', function() {
- const room = mockRoom("!fake:example.org", [
- {
- userId: "@alice:evilcorp.com",
- powerLevel: 100,
- },
- {
- userId: "@bob:chat.evilcorp.com",
- powerLevel: 0,
- },
- ], {
- deny: ["*.evilcorp.com"], // evilcorp.com is still good though
- allow: ["*"],
- });
- const creator = new RoomPermalinkCreator(room);
- creator.load();
- expect(creator._serverCandidates).toBeTruthy();
- expect(creator._serverCandidates.length).toBe(1);
- expect(creator._serverCandidates[0]).toEqual("evilcorp.com");
- });
-
- it('should consider servers not disallowed by ACLs', function() {
- const room = mockRoom("!fake:example.org", [
- {
- userId: "@alice:evilcorp.com",
- powerLevel: 100,
- },
- {
- userId: "@bob:chat.evilcorp.com",
- powerLevel: 0,
- },
- ], {
- deny: [],
- allow: ["evilcorp.com"], // implies "ban everyone else"
- });
- const creator = new RoomPermalinkCreator(room);
- creator.load();
- expect(creator._serverCandidates).toBeTruthy();
- expect(creator._serverCandidates.length).toBe(1);
- expect(creator._serverCandidates[0]).toEqual("evilcorp.com");
- });
-
- it('should generate an event permalink for room IDs with no candidate servers', function() {
- const room = mockRoom("!somewhere:example.org", []);
- const creator = new RoomPermalinkCreator(room);
- creator.load();
- const result = creator.forEvent("$something:example.com");
- expect(result).toBe("https://matrix.to/#/!somewhere:example.org/$something:example.com");
- });
-
- it('should generate an event permalink for room IDs with some candidate servers', function() {
- const room = mockRoom("!somewhere:example.org", [
- {
- userId: "@alice:first",
- powerLevel: 100,
- },
- {
- userId: "@bob:second",
- powerLevel: 0,
- },
- ]);
- const creator = new RoomPermalinkCreator(room);
- creator.load();
- const result = creator.forEvent("$something:example.com");
- expect(result).toBe("https://matrix.to/#/!somewhere:example.org/$something:example.com?via=first&via=second");
- });
-
- it('should generate a room permalink for room IDs with some candidate servers', function() {
- peg.get().getRoom = (roomId) => {
- return mockRoom(roomId, [
- {
- userId: "@alice:first",
- powerLevel: 100,
- },
- {
- userId: "@bob:second",
- powerLevel: 0,
- },
- ]);
- };
- const result = makeRoomPermalink("!somewhere:example.org");
- expect(result).toBe("https://matrix.to/#/!somewhere:example.org?via=first&via=second");
- });
-
- it('should generate a room permalink for room aliases with no candidate servers', function() {
- peg.get().getRoom = () => null;
- const result = makeRoomPermalink("#somewhere:example.org");
- expect(result).toBe("https://matrix.to/#/#somewhere:example.org");
- });
-
- it('should generate a room permalink for room aliases without candidate servers', function() {
- peg.get().getRoom = (roomId) => {
- return mockRoom(roomId, [
- {
- userId: "@alice:first",
- powerLevel: 100,
- },
- {
- userId: "@bob:second",
- powerLevel: 0,
- },
- ]);
- };
- const result = makeRoomPermalink("#somewhere:example.org");
- expect(result).toBe("https://matrix.to/#/#somewhere:example.org");
- });
-
- it('should generate a user permalink', function() {
- const result = makeUserPermalink("@someone:example.org");
- expect(result).toBe("https://matrix.to/#/@someone:example.org");
- });
-
- it('should correctly parse room permalinks with a via argument', () => {
- const result = parsePermalink("https://matrix.to/#/!room_id:server?via=some.org");
- expect(result.roomIdOrAlias).toBe("!room_id:server");
- expect(result.viaServers).toEqual(["some.org"]);
- });
-
- it('should correctly parse room permalink via arguments', () => {
- const result = parsePermalink("https://matrix.to/#/!room_id:server?via=foo.bar&via=bar.foo");
- expect(result.roomIdOrAlias).toBe("!room_id:server");
- expect(result.viaServers).toEqual(["foo.bar", "bar.foo"]);
- });
-
- it('should correctly parse event permalink via arguments', () => {
- const result = parsePermalink("https://matrix.to/#/!room_id:server/$event_id/some_thing_here/foobar" +
- "?via=m1.org&via=m2.org");
- expect(result.eventId).toBe("$event_id/some_thing_here/foobar");
- expect(result.roomIdOrAlias).toBe("!room_id:server");
- expect(result.viaServers).toEqual(["m1.org", "m2.org"]);
- });
-});
diff --git a/test/utils/permalinks/Permalinks-test.ts b/test/utils/permalinks/Permalinks-test.ts
new file mode 100644
index 00000000000..0c1104d2855
--- /dev/null
+++ b/test/utils/permalinks/Permalinks-test.ts
@@ -0,0 +1,378 @@
+/*
+Copyright 2018 New Vector Ltd
+Copyright 2019, 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+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 {
+ Room,
+ RoomMember,
+ EventType,
+ MatrixEvent,
+} from 'matrix-js-sdk/src/matrix';
+
+import { MatrixClientPeg } from '../../../src/MatrixClientPeg';
+import {
+ makeRoomPermalink,
+ makeUserPermalink,
+ parsePermalink,
+ RoomPermalinkCreator,
+} from "../../../src/utils/permalinks/Permalinks";
+import { getMockClientWithEventEmitter } from '../../test-utils';
+
+describe('Permalinks', function() {
+ const userId = '@test:example.com';
+ const mockClient = getMockClientWithEventEmitter({
+ getUserId: jest.fn().mockReturnValue(userId),
+ getRoom: jest.fn(),
+ });
+ mockClient.credentials = { userId };
+
+ const makeMemberWithPL = (roomId: Room['roomId'], userId: string, powerLevel: number): RoomMember => {
+ const member = new RoomMember(roomId, userId);
+ member.powerLevel = powerLevel;
+ return member;
+ };
+
+ function mockRoom(
+ roomId: Room['roomId'], members: RoomMember[], serverACLContent?: { deny?: string[], allow?: string[]},
+ ): Room {
+ members.forEach(m => m.membership = "join");
+ const powerLevelsUsers = members.reduce((pl, member) => {
+ if (Number.isFinite(member.powerLevel)) {
+ pl[member.userId] = member.powerLevel;
+ }
+ return pl;
+ }, {});
+
+ const room = new Room(roomId, mockClient, userId);
+
+ const powerLevels = new MatrixEvent({
+ type: EventType.RoomPowerLevels,
+ room_id: roomId,
+ state_key: '',
+ content: {
+ users: powerLevelsUsers, users_default: 0,
+ },
+ });
+ const serverACL = serverACLContent ? new MatrixEvent({
+ type: EventType.RoomServerAcl,
+ room_id: roomId,
+ state_key: '',
+ content: serverACLContent,
+ }) : undefined;
+ const stateEvents = serverACL ? [powerLevels, serverACL] : [powerLevels];
+ room.currentState.setStateEvents(stateEvents);
+
+ jest.spyOn(room, 'getCanonicalAlias').mockReturnValue(null);
+ jest.spyOn(room, 'getJoinedMembers').mockReturnValue(members);
+ jest.spyOn(room, 'getMember').mockImplementation((userId) => members.find(m => m.userId === userId));
+
+ return room;
+ }
+ beforeEach(function() {
+ jest.clearAllMocks();
+ });
+
+ afterAll(() => {
+ jest.spyOn(MatrixClientPeg, 'get').mockRestore();
+ });
+
+ it('should pick no candidate servers when the room has no members', function() {
+ const room = mockRoom("!fake:example.org", []);
+ const creator = new RoomPermalinkCreator(room);
+ creator.load();
+ expect(creator.serverCandidates).toBeTruthy();
+ expect(creator.serverCandidates.length).toBe(0);
+ });
+
+ it('should pick a candidate server for the highest power level user in the room', function() {
+ const roomId = "!fake:example.org";
+ const alice50 = makeMemberWithPL(roomId, "@alice:pl_50", 50);
+ const alice75 = makeMemberWithPL(roomId, "@alice:pl_75", 75);
+ const alice95 = makeMemberWithPL(roomId, "@alice:pl_95", 95);
+ const room = mockRoom("!fake:example.org", [
+ alice50,
+ alice75,
+ alice95,
+ ]);
+ const creator = new RoomPermalinkCreator(room);
+ creator.load();
+ expect(creator.serverCandidates).toBeTruthy();
+ expect(creator.serverCandidates.length).toBe(3);
+ expect(creator.serverCandidates[0]).toBe("pl_95");
+ // we don't check the 2nd and 3rd servers because that is done by the next test
+ });
+
+ it('should change candidate server when highest power level user leaves the room', function() {
+ const roomId = "!fake:example.org";
+ const member95 = makeMemberWithPL(roomId, "@alice:pl_95", 95);
+
+ const room = mockRoom(roomId, [
+ makeMemberWithPL(roomId, "@alice:pl_50", 50),
+ makeMemberWithPL(roomId, "@alice:pl_75", 75),
+ member95,
+ ]);
+ const creator = new RoomPermalinkCreator(room, null);
+ creator.load();
+ expect(creator.serverCandidates[0]).toBe("pl_95");
+ member95.membership = "left";
+ // @ts-ignore illegal private property
+ creator.onRoomStateUpdate();
+ expect(creator.serverCandidates[0]).toBe("pl_75");
+ member95.membership = "join";
+ // @ts-ignore illegal private property
+ creator.onRoomStateUpdate();
+ expect(creator.serverCandidates[0]).toBe("pl_95");
+ });
+
+ it('should pick candidate servers based on user population', function() {
+ const roomId = "!fake:example.org";
+ const room = mockRoom(roomId, [
+ makeMemberWithPL(roomId, "@alice:first", 0),
+ makeMemberWithPL(roomId, "@bob:first", 0),
+ makeMemberWithPL(roomId, "@charlie:first", 0),
+ makeMemberWithPL(roomId, "@alice:second", 0),
+ makeMemberWithPL(roomId, "@bob:second", 0),
+ makeMemberWithPL(roomId, "@charlie:third", 0),
+ ]);
+ const creator = new RoomPermalinkCreator(room);
+ creator.load();
+ expect(creator.serverCandidates).toBeTruthy();
+ expect(creator.serverCandidates.length).toBe(3);
+ expect(creator.serverCandidates[0]).toBe("first");
+ expect(creator.serverCandidates[1]).toBe("second");
+ expect(creator.serverCandidates[2]).toBe("third");
+ });
+
+ it('should pick prefer candidate servers with higher power levels', function() {
+ const roomId = "!fake:example.org";
+ const room = mockRoom(roomId, [
+ makeMemberWithPL(roomId, "@alice:first", 100),
+ makeMemberWithPL(roomId, "@alice:second", 0),
+ makeMemberWithPL(roomId, "@bob:second", 0),
+ makeMemberWithPL(roomId, "@charlie:third", 0),
+ ]);
+ const creator = new RoomPermalinkCreator(room);
+ creator.load();
+ expect(creator.serverCandidates.length).toBe(3);
+ expect(creator.serverCandidates[0]).toBe("first");
+ expect(creator.serverCandidates[1]).toBe("second");
+ expect(creator.serverCandidates[2]).toBe("third");
+ });
+
+ it('should pick a maximum of 3 candidate servers', function() {
+ const roomId = "!fake:example.org";
+ const room = mockRoom(roomId, [
+ makeMemberWithPL(roomId, "@alice:alpha", 100),
+ makeMemberWithPL(roomId, "@alice:bravo", 0),
+ makeMemberWithPL(roomId, "@alice:charlie", 0),
+ makeMemberWithPL(roomId, "@alice:delta", 0),
+ makeMemberWithPL(roomId, "@alice:echo", 0),
+ ]);
+ const creator = new RoomPermalinkCreator(room);
+ creator.load();
+ expect(creator.serverCandidates).toBeTruthy();
+ expect(creator.serverCandidates.length).toBe(3);
+ });
+
+ it('should not consider IPv4 hosts', function() {
+ const roomId = "!fake:example.org";
+ const room = mockRoom(roomId, [
+ makeMemberWithPL(roomId, "@alice:127.0.0.1", 100),
+ ]);
+ const creator = new RoomPermalinkCreator(room);
+ creator.load();
+ expect(creator.serverCandidates).toBeTruthy();
+ expect(creator.serverCandidates.length).toBe(0);
+ });
+
+ it('should not consider IPv6 hosts', function() {
+ const roomId = "!fake:example.org";
+ const room = mockRoom(roomId, [
+ makeMemberWithPL(roomId, "@alice:[::1]", 100),
+ ]);
+ const creator = new RoomPermalinkCreator(room);
+ creator.load();
+ expect(creator.serverCandidates).toBeTruthy();
+ expect(creator.serverCandidates.length).toBe(0);
+ });
+
+ it('should not consider IPv4 hostnames with ports', function() {
+ const roomId = "!fake:example.org";
+ const room = mockRoom(roomId, [
+ makeMemberWithPL(roomId, "@alice:127.0.0.1:8448", 100),
+ ]);
+ const creator = new RoomPermalinkCreator(room);
+ creator.load();
+ expect(creator.serverCandidates).toBeTruthy();
+ expect(creator.serverCandidates.length).toBe(0);
+ });
+
+ it('should not consider IPv6 hostnames with ports', function() {
+ const roomId = "!fake:example.org";
+ const room = mockRoom(roomId, [
+ makeMemberWithPL(roomId, "@alice:[::1]:8448", 100),
+ ]);
+ const creator = new RoomPermalinkCreator(room);
+ creator.load();
+ expect(creator.serverCandidates).toBeTruthy();
+ expect(creator.serverCandidates.length).toBe(0);
+ });
+
+ it('should work with hostnames with ports', function() {
+ const roomId = "!fake:example.org";
+ const room = mockRoom(roomId, [
+ makeMemberWithPL(roomId, "@alice:example.org:8448", 100),
+ ]);
+
+ const creator = new RoomPermalinkCreator(room);
+ creator.load();
+ expect(creator.serverCandidates).toBeTruthy();
+ expect(creator.serverCandidates.length).toBe(1);
+ expect(creator.serverCandidates[0]).toBe("example.org:8448");
+ });
+
+ it('should not consider servers explicitly denied by ACLs', function() {
+ const roomId = "!fake:example.org";
+ const room = mockRoom(roomId, [
+ makeMemberWithPL(roomId, "@alice:evilcorp.com", 100),
+ makeMemberWithPL(roomId, "@bob:chat.evilcorp.com", 0),
+ ], {
+ deny: ["evilcorp.com", "*.evilcorp.com"],
+ allow: ["*"],
+ });
+ const creator = new RoomPermalinkCreator(room);
+ creator.load();
+ expect(creator.serverCandidates).toBeTruthy();
+ expect(creator.serverCandidates.length).toBe(0);
+ });
+
+ it('should not consider servers not allowed by ACLs', function() {
+ const roomId = "!fake:example.org";
+ const room = mockRoom(roomId, [
+ makeMemberWithPL(roomId, "@alice:evilcorp.com", 100),
+ makeMemberWithPL(roomId, "@bob:chat.evilcorp.com", 0),
+ ], {
+ deny: [],
+ allow: [], // implies "ban everyone"
+ });
+ const creator = new RoomPermalinkCreator(room);
+ creator.load();
+ expect(creator.serverCandidates).toBeTruthy();
+ expect(creator.serverCandidates.length).toBe(0);
+ });
+
+ it('should consider servers not explicitly banned by ACLs', function() {
+ const roomId = "!fake:example.org";
+ const room = mockRoom(roomId, [
+ makeMemberWithPL(roomId, "@alice:evilcorp.com", 100),
+ makeMemberWithPL(roomId, "@bob:chat.evilcorp.com", 0),
+ ], {
+ deny: ["*.evilcorp.com"], // evilcorp.com is still good though
+ allow: ["*"],
+ });
+ const creator = new RoomPermalinkCreator(room);
+ creator.load();
+ expect(creator.serverCandidates).toBeTruthy();
+ expect(creator.serverCandidates.length).toBe(1);
+ expect(creator.serverCandidates[0]).toEqual("evilcorp.com");
+ });
+
+ it('should consider servers not disallowed by ACLs', function() {
+ const roomId = "!fake:example.org";
+ const room = mockRoom("!fake:example.org", [
+ makeMemberWithPL(roomId, "@alice:evilcorp.com", 100),
+ makeMemberWithPL(roomId, "@bob:chat.evilcorp.com", 0),
+ ], {
+ deny: [],
+ allow: ["evilcorp.com"], // implies "ban everyone else"
+ });
+ const creator = new RoomPermalinkCreator(room);
+ creator.load();
+ expect(creator.serverCandidates).toBeTruthy();
+ expect(creator.serverCandidates.length).toBe(1);
+ expect(creator.serverCandidates[0]).toEqual("evilcorp.com");
+ });
+
+ it('should generate an event permalink for room IDs with no candidate servers', function() {
+ const room = mockRoom("!somewhere:example.org", []);
+ const creator = new RoomPermalinkCreator(room);
+ creator.load();
+ const result = creator.forEvent("$something:example.com");
+ expect(result).toBe("https://matrix.to/#/!somewhere:example.org/$something:example.com");
+ });
+
+ it('should generate an event permalink for room IDs with some candidate servers', function() {
+ const roomId = "!somewhere:example.org";
+ const room = mockRoom(roomId, [
+ makeMemberWithPL(roomId, "@alice:first", 100),
+ makeMemberWithPL(roomId, "@bob:second", 0),
+ ]);
+ const creator = new RoomPermalinkCreator(room);
+ creator.load();
+ const result = creator.forEvent("$something:example.com");
+ expect(result).toBe("https://matrix.to/#/!somewhere:example.org/$something:example.com?via=first&via=second");
+ });
+
+ it('should generate a room permalink for room IDs with some candidate servers', function() {
+ mockClient.getRoom.mockImplementation((roomId: Room['roomId']) => {
+ return mockRoom(roomId, [
+ makeMemberWithPL(roomId, "@alice:first", 100),
+ makeMemberWithPL(roomId, "@bob:second", 0),
+ ]);
+ });
+ const result = makeRoomPermalink("!somewhere:example.org");
+ expect(result).toBe("https://matrix.to/#/!somewhere:example.org?via=first&via=second");
+ });
+
+ it('should generate a room permalink for room aliases with no candidate servers', function() {
+ mockClient.getRoom.mockReturnValue(null);
+ const result = makeRoomPermalink("#somewhere:example.org");
+ expect(result).toBe("https://matrix.to/#/#somewhere:example.org");
+ });
+
+ it('should generate a room permalink for room aliases without candidate servers', function() {
+ mockClient.getRoom.mockImplementation((roomId: Room['roomId']) => {
+ return mockRoom(roomId, [
+ makeMemberWithPL(roomId, "@alice:first", 100),
+ makeMemberWithPL(roomId, "@bob:second", 0),
+ ]);
+ });
+ const result = makeRoomPermalink("#somewhere:example.org");
+ expect(result).toBe("https://matrix.to/#/#somewhere:example.org");
+ });
+
+ it('should generate a user permalink', function() {
+ const result = makeUserPermalink("@someone:example.org");
+ expect(result).toBe("https://matrix.to/#/@someone:example.org");
+ });
+
+ it('should correctly parse room permalinks with a via argument', () => {
+ const result = parsePermalink("https://matrix.to/#/!room_id:server?via=some.org");
+ expect(result.roomIdOrAlias).toBe("!room_id:server");
+ expect(result.viaServers).toEqual(["some.org"]);
+ });
+
+ it('should correctly parse room permalink via arguments', () => {
+ const result = parsePermalink("https://matrix.to/#/!room_id:server?via=foo.bar&via=bar.foo");
+ expect(result.roomIdOrAlias).toBe("!room_id:server");
+ expect(result.viaServers).toEqual(["foo.bar", "bar.foo"]);
+ });
+
+ it('should correctly parse event permalink via arguments', () => {
+ const result = parsePermalink("https://matrix.to/#/!room_id:server/$event_id/some_thing_here/foobar" +
+ "?via=m1.org&via=m2.org");
+ expect(result.eventId).toBe("$event_id/some_thing_here/foobar");
+ expect(result.roomIdOrAlias).toBe("!room_id:server");
+ expect(result.viaServers).toEqual(["m1.org", "m2.org"]);
+ });
+});
diff --git a/test/utils/validate/numberInRange-test.ts b/test/utils/validate/numberInRange-test.ts
index f7ad2e8c1c2..dd28f0fc396 100644
--- a/test/utils/validate/numberInRange-test.ts
+++ b/test/utils/validate/numberInRange-test.ts
@@ -1,3 +1,19 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
import { validateNumberInRange } from '../../../src/utils/validate';
describe('validateNumberInRange', () => {
diff --git a/theme.sh b/theme.sh
index 84ee007117d..85f23d1bb10 100755
--- a/theme.sh
+++ b/theme.sh
@@ -63,6 +63,7 @@ replace_colors() {
sed -i 's|#61708b|#616161|gi' "$f"
sed -i 's|#616b7f|#616161|gi' "$f"
sed -i 's|#5c6470|#616161|gi' "$f"
+ sed -i 's|#545a66|#616161|gi' "$f" # pill hover bg color
sed -i 's|#737D8C|#757575|gi' "$f"
sed -i 's|#6F7882|#757575|gi' "$f"
sed -i 's|#91A1C0|#757575|gi' "$f" # icon in button color
@@ -89,6 +90,7 @@ replace_colors() {
sed -i 's|#f4f6fa|#f5f5f5|gi' "$f"
sed -i 's|#f6f7f8|#f5f5f5|gi' "$f"
sed -i 's|#f2f5f8|#f5f5f5|gi' "$f"
+ sed -i 's|#f5f8fa|#f5f5f5|gi' "$f"
sed -i 's|#f3f8fd|#fafafa|gi' "$f"
sed -i 's|rgba(33, 38, 34,|rgba(48, 48, 48,|gi' "$f"
sed -i 's|rgba(33, 38, 44,|rgba(48, 48, 48,|gi' "$f"
diff --git a/yarn.lock b/yarn.lock
index a58bb878794..5a896108f07 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3,9 +3,9 @@
"@actions/core@^1.4.0":
- version "1.6.0"
- resolved "https://registry.yarnpkg.com/@actions/core/-/core-1.6.0.tgz#0568e47039bfb6a9170393a73f3b7eb3b22462cb"
- integrity sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw==
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/@actions/core/-/core-1.7.0.tgz#f179a5a0bf5c1102d89b8cf1712825e763feaee4"
+ integrity sha512-7fPSS7yKOhTpgLMbw7lBLc1QJWvJBBAgyTX2PEhagWcKK8t0H8AKCoPMfnrHqIm5cRYH4QFPqD1/ruhuUE7YcQ==
dependencies:
"@actions/http-client" "^1.0.11"
@@ -27,25 +27,25 @@
tunnel "0.0.6"
"@ampproject/remapping@^2.1.0":
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.1.2.tgz#4edca94973ded9630d20101cd8559cedb8d8bd34"
- integrity sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg==
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d"
+ integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==
dependencies:
- "@jridgewell/trace-mapping" "^0.3.0"
+ "@jridgewell/gen-mapping" "^0.1.0"
+ "@jridgewell/trace-mapping" "^0.3.9"
"@babel/cli@^7.12.10":
- version "7.17.6"
- resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.17.6.tgz#169e5935f1795f0b62ded5a2accafeedfe5c5363"
- integrity sha512-l4w608nsDNlxZhiJ5tE3DbNmr61fIKMZ6fTBo171VEFuFMIYuJ3mHRhTLEkKKyvx2Mizkkv/0a8OJOnZqkKYNA==
+ version "7.17.10"
+ resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.17.10.tgz#5ea0bf6298bb78f3b59c7c06954f9bd1c79d5943"
+ integrity sha512-OygVO1M2J4yPMNOW9pb+I6kFGpQK77HmG44Oz3hg8xQIl5L/2zq+ZohwAdSaqYgVwM0SfmPHZHphH4wR8qzVYw==
dependencies:
- "@jridgewell/trace-mapping" "^0.3.4"
+ "@jridgewell/trace-mapping" "^0.3.8"
commander "^4.0.1"
convert-source-map "^1.1.0"
fs-readdir-recursive "^1.1.0"
glob "^7.0.0"
make-dir "^2.1.0"
slash "^2.0.0"
- source-map "^0.5.0"
optionalDependencies:
"@nicolo-ribaudo/chokidar-2" "2.1.8-no-fsevents.3"
chokidar "^3.4.0"
@@ -57,26 +57,26 @@
dependencies:
"@babel/highlight" "^7.16.7"
-"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.16.8", "@babel/compat-data@^7.17.0", "@babel/compat-data@^7.17.7":
- version "7.17.7"
- resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.7.tgz#078d8b833fbbcc95286613be8c716cef2b519fa2"
- integrity sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ==
+"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.17.0", "@babel/compat-data@^7.17.10":
+ version "7.17.10"
+ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.10.tgz#711dc726a492dfc8be8220028b1b92482362baab"
+ integrity sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw==
-"@babel/core@>=7.9.0", "@babel/core@^7.1.0", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.7.2", "@babel/core@^7.8.0":
- version "7.17.9"
- resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.9.tgz#6bae81a06d95f4d0dec5bb9d74bbc1f58babdcfe"
- integrity sha512-5ug+SfZCpDAkVp9SFIZAzlW18rlzsOcJGaetCjkySnrXXDUw9AR8cDUm1iByTmdWM6yxX6/zycaV76w3YTF2gw==
+"@babel/core@^7.1.0", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.17.9", "@babel/core@^7.7.2", "@babel/core@^7.8.0":
+ version "7.17.10"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.10.tgz#74ef0fbf56b7dfc3f198fc2d927f4f03e12f4b05"
+ integrity sha512-liKoppandF3ZcBnIYFjfSDHZLKdLHGJRkoWtG8zQyGJBQfIYobpnVGI5+pLBNtS6psFLDzyq8+h5HiVljW9PNA==
dependencies:
"@ampproject/remapping" "^2.1.0"
"@babel/code-frame" "^7.16.7"
- "@babel/generator" "^7.17.9"
- "@babel/helper-compilation-targets" "^7.17.7"
+ "@babel/generator" "^7.17.10"
+ "@babel/helper-compilation-targets" "^7.17.10"
"@babel/helper-module-transforms" "^7.17.7"
"@babel/helpers" "^7.17.9"
- "@babel/parser" "^7.17.9"
+ "@babel/parser" "^7.17.10"
"@babel/template" "^7.16.7"
- "@babel/traverse" "^7.17.9"
- "@babel/types" "^7.17.0"
+ "@babel/traverse" "^7.17.10"
+ "@babel/types" "^7.17.10"
convert-source-map "^1.7.0"
debug "^4.1.0"
gensync "^1.0.0-beta.2"
@@ -99,14 +99,14 @@
dependencies:
eslint-rule-composer "^0.3.0"
-"@babel/generator@^7.17.9", "@babel/generator@^7.7.2":
- version "7.17.9"
- resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.9.tgz#f4af9fd38fa8de143c29fce3f71852406fc1e2fc"
- integrity sha512-rAdDousTwxbIxbz5I7GEQ3lUip+xVCXooZNbsydCWs3xA7ZsYOv+CFRdzGxRX78BmQHu9B1Eso59AOZQOJDEdQ==
+"@babel/generator@^7.17.10", "@babel/generator@^7.7.2":
+ version "7.17.10"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.10.tgz#c281fa35b0c349bbe9d02916f4ae08fc85ed7189"
+ integrity sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==
dependencies:
- "@babel/types" "^7.17.0"
+ "@babel/types" "^7.17.10"
+ "@jridgewell/gen-mapping" "^0.1.0"
jsesc "^2.5.1"
- source-map "^0.5.0"
"@babel/helper-annotate-as-pure@^7.16.7":
version "7.16.7"
@@ -123,14 +123,14 @@
"@babel/helper-explode-assignable-expression" "^7.16.7"
"@babel/types" "^7.16.7"
-"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.16.7", "@babel/helper-compilation-targets@^7.17.7":
- version "7.17.7"
- resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.7.tgz#a3c2924f5e5f0379b356d4cfb313d1414dc30e46"
- integrity sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w==
+"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.16.7", "@babel/helper-compilation-targets@^7.17.10":
+ version "7.17.10"
+ resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.10.tgz#09c63106d47af93cf31803db6bc49fef354e2ebe"
+ integrity sha512-gh3RxjWbauw/dFiU/7whjd0qN9K6nPJMqe6+Er7rOavFh0CQUSwhAE3IcTho2rywPJFxej6TUUHDkWcYI6gGqQ==
dependencies:
- "@babel/compat-data" "^7.17.7"
+ "@babel/compat-data" "^7.17.10"
"@babel/helper-validator-option" "^7.16.7"
- browserslist "^4.17.5"
+ browserslist "^4.20.2"
semver "^6.3.0"
"@babel/helper-create-class-features-plugin@^7.16.10", "@babel/helper-create-class-features-plugin@^7.16.7", "@babel/helper-create-class-features-plugin@^7.17.6":
@@ -146,7 +146,7 @@
"@babel/helper-replace-supers" "^7.16.7"
"@babel/helper-split-export-declaration" "^7.16.7"
-"@babel/helper-create-regexp-features-plugin@^7.16.7":
+"@babel/helper-create-regexp-features-plugin@^7.16.7", "@babel/helper-create-regexp-features-plugin@^7.17.0":
version "7.17.0"
resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.17.0.tgz#1dcc7d40ba0c6b6b25618997c5dbfd310f186fe1"
integrity sha512-awO2So99wG6KnlE+TPs6rn83gCz5WlEePJDTnLEqbchMVrBeAujURVphRdigsk094VhvZehFoNOihSlcBjwsXA==
@@ -316,10 +316,10 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
-"@babel/parser@^7.1.0", "@babel/parser@^7.12.11", "@babel/parser@^7.13.16", "@babel/parser@^7.14.7", "@babel/parser@^7.16.7", "@babel/parser@^7.17.9":
- version "7.17.9"
- resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.9.tgz#9c94189a6062f0291418ca021077983058e171ef"
- integrity sha512-vqUSBLP8dQHFPdPi9bc5GK9vRkYHJ49fsZdtoJ8EQ8ibpwk5rPKfvNIwChB0KVXcIjcepEBBd2VHC5r9Gy8ueg==
+"@babel/parser@^7.1.0", "@babel/parser@^7.12.11", "@babel/parser@^7.13.16", "@babel/parser@^7.14.7", "@babel/parser@^7.16.7", "@babel/parser@^7.17.10":
+ version "7.17.10"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.10.tgz#873b16db82a8909e0fbd7f115772f4b739f6ce78"
+ integrity sha512-n2Q6i+fnJqzOaq2VkdXxy2TCPCWQZHiCo0XqmrCvDWcZQKRyZzYi4Z0yxlBuN0w+r2ZHmre+Q087DSrw3pbJDQ==
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.7":
version "7.16.7"
@@ -354,7 +354,7 @@
"@babel/helper-create-class-features-plugin" "^7.16.7"
"@babel/helper-plugin-utils" "^7.16.7"
-"@babel/plugin-proposal-class-static-block@^7.16.7":
+"@babel/plugin-proposal-class-static-block@^7.17.6":
version "7.17.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.17.6.tgz#164e8fd25f0d80fa48c5a4d1438a6629325ad83c"
integrity sha512-X/tididvL2zbs7jZCeeRJ8167U/+Ac135AM6jCAx6gYXDUviZV5Ku9UDvWS2NCuWlFjIRXklYhwo6HhAC7ETnA==
@@ -419,7 +419,7 @@
"@babel/helper-plugin-utils" "^7.16.7"
"@babel/plugin-syntax-numeric-separator" "^7.10.4"
-"@babel/plugin-proposal-object-rest-spread@^7.12.1", "@babel/plugin-proposal-object-rest-spread@^7.16.7":
+"@babel/plugin-proposal-object-rest-spread@^7.12.1", "@babel/plugin-proposal-object-rest-spread@^7.17.3":
version "7.17.3"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.17.3.tgz#d9eb649a54628a51701aef7e0ea3d17e2b9dd390"
integrity sha512-yuL5iQA/TbZn+RGAfxQXfi7CNLmKi1f8zInn4IgobuCWcAb7i+zj4TYzQ9l8cEzVyJ89PDGuqxK1xZpUDISesw==
@@ -600,9 +600,9 @@
"@babel/helper-plugin-utils" "^7.14.5"
"@babel/plugin-syntax-typescript@^7.16.7", "@babel/plugin-syntax-typescript@^7.7.2":
- version "7.16.7"
- resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.16.7.tgz#39c9b55ee153151990fb038651d58d3fd03f98f8"
- integrity sha512-YhUIJHHGkqPgEcMYkPCKTyGUdoGKWtopIycQyjJH8OjvRgOYsXsaKehLVPScKJWAULPxMa4N1vCe6szREFlZ7A==
+ version "7.17.10"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.17.10.tgz#80031e6042cad6a95ed753f672ebd23c30933195"
+ integrity sha512-xJefea1DWXW09pW4Tm9bjwVlPDyYA2it3fWlmEjpYz6alPvTUjL0EOzNzI/FEOyI3r4/J7uVH5UqKgl1TQ5hqQ==
dependencies:
"@babel/helper-plugin-utils" "^7.16.7"
@@ -657,7 +657,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.16.7"
-"@babel/plugin-transform-destructuring@^7.16.7":
+"@babel/plugin-transform-destructuring@^7.17.7":
version "7.17.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.17.7.tgz#49dc2675a7afa9a5e4c6bdee636061136c3408d1"
integrity sha512-XVh0r5yq9sLR4vZ6eVZe8FKfIcSgaTBxVBRSYokRj2qksf6QerYnTxz9/GTuKTH/n/HwLP7t6gtlybHetJ/6hQ==
@@ -726,7 +726,7 @@
"@babel/helper-plugin-utils" "^7.16.7"
babel-plugin-dynamic-import-node "^2.3.3"
-"@babel/plugin-transform-modules-commonjs@^7.16.8":
+"@babel/plugin-transform-modules-commonjs@^7.17.9":
version "7.17.9"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.17.9.tgz#274be1a2087beec0254d4abd4d86e52442e1e5b6"
integrity sha512-2TBFd/r2I6VlYn0YRTz2JdazS+FoUuQ2rIFHoAxtyP/0G3D82SBLaRq9rnUkpqlLg03Byfl/+M32mpxjO6KaPw==
@@ -736,7 +736,7 @@
"@babel/helper-simple-access" "^7.17.7"
babel-plugin-dynamic-import-node "^2.3.3"
-"@babel/plugin-transform-modules-systemjs@^7.16.7":
+"@babel/plugin-transform-modules-systemjs@^7.17.8":
version "7.17.8"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.17.8.tgz#81fd834024fae14ea78fbe34168b042f38703859"
integrity sha512-39reIkMTUVagzgA5x88zDYXPCMT6lcaRKs1+S9K6NKBPErbgO/w/kP8GlNQTC87b412ZTlmNgr3k2JrWgHH+Bw==
@@ -755,12 +755,12 @@
"@babel/helper-module-transforms" "^7.16.7"
"@babel/helper-plugin-utils" "^7.16.7"
-"@babel/plugin-transform-named-capturing-groups-regex@^7.16.8":
- version "7.16.8"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.16.8.tgz#7f860e0e40d844a02c9dcf9d84965e7dfd666252"
- integrity sha512-j3Jw+n5PvpmhRR+mrgIh04puSANCk/T/UA3m3P1MjJkhlK906+ApHhDIqBQDdOgL/r1UYpz4GNclTXxyZrYGSw==
+"@babel/plugin-transform-named-capturing-groups-regex@^7.17.10":
+ version "7.17.10"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.17.10.tgz#715dbcfafdb54ce8bccd3d12e8917296a4ba66a4"
+ integrity sha512-v54O6yLaJySCs6mGzaVOUw9T967GnH38T6CQSAtnzdNPwu84l2qAjssKzo/WSO8Yi7NF+7ekm5cVbF/5qiIgNA==
dependencies:
- "@babel/helper-create-regexp-features-plugin" "^7.16.7"
+ "@babel/helper-create-regexp-features-plugin" "^7.17.0"
"@babel/plugin-transform-new-target@^7.16.7":
version "7.16.7"
@@ -824,7 +824,7 @@
"@babel/helper-annotate-as-pure" "^7.16.7"
"@babel/helper-plugin-utils" "^7.16.7"
-"@babel/plugin-transform-regenerator@^7.16.7":
+"@babel/plugin-transform-regenerator@^7.17.9":
version "7.17.9"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.17.9.tgz#0a33c3a61cf47f45ed3232903683a0afd2d3460c"
integrity sha512-Lc2TfbxR1HOyn/c6b4Y/b6NHoTb67n/IoWLxTu4kC7h4KQnWlhCq2S8Tx0t2SVvv5Uu87Hs+6JEJ5kt2tYGylQ==
@@ -839,9 +839,9 @@
"@babel/helper-plugin-utils" "^7.16.7"
"@babel/plugin-transform-runtime@^7.12.10":
- version "7.17.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.17.0.tgz#0a2e08b5e2b2d95c4b1d3b3371a2180617455b70"
- integrity sha512-fr7zPWnKXNc1xoHfrIU9mN/4XKX4VLZ45Q+oMhfsYIaHvg7mHgmhfOy/ckRWqDK7XF3QDigRpkh5DKq6+clE8A==
+ version "7.17.10"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.17.10.tgz#b89d821c55d61b5e3d3c3d1d636d8d5a81040ae1"
+ integrity sha512-6jrMilUAJhktTr56kACL8LnWC5hx3Lf27BS0R0DSyW/OoJfb/iTHeE96V3b1dgKG3FSFdd/0culnYWMkjcKCig==
dependencies:
"@babel/helper-module-imports" "^7.16.7"
"@babel/helper-plugin-utils" "^7.16.7"
@@ -911,26 +911,26 @@
"@babel/helper-plugin-utils" "^7.16.7"
"@babel/preset-env@^7.12.11":
- version "7.16.11"
- resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.16.11.tgz#5dd88fd885fae36f88fd7c8342475c9f0abe2982"
- integrity sha512-qcmWG8R7ZW6WBRPZK//y+E3Cli151B20W1Rv7ln27vuPaXU/8TKms6jFdiJtF7UDTxcrb7mZd88tAeK9LjdT8g==
+ version "7.17.10"
+ resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.17.10.tgz#a81b093669e3eb6541bb81a23173c5963c5de69c"
+ integrity sha512-YNgyBHZQpeoBSRBg0xixsZzfT58Ze1iZrajvv0lJc70qDDGuGfonEnMGfWeSY0mQ3JTuCWFbMkzFRVafOyJx4g==
dependencies:
- "@babel/compat-data" "^7.16.8"
- "@babel/helper-compilation-targets" "^7.16.7"
+ "@babel/compat-data" "^7.17.10"
+ "@babel/helper-compilation-targets" "^7.17.10"
"@babel/helper-plugin-utils" "^7.16.7"
"@babel/helper-validator-option" "^7.16.7"
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.16.7"
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.16.7"
"@babel/plugin-proposal-async-generator-functions" "^7.16.8"
"@babel/plugin-proposal-class-properties" "^7.16.7"
- "@babel/plugin-proposal-class-static-block" "^7.16.7"
+ "@babel/plugin-proposal-class-static-block" "^7.17.6"
"@babel/plugin-proposal-dynamic-import" "^7.16.7"
"@babel/plugin-proposal-export-namespace-from" "^7.16.7"
"@babel/plugin-proposal-json-strings" "^7.16.7"
"@babel/plugin-proposal-logical-assignment-operators" "^7.16.7"
"@babel/plugin-proposal-nullish-coalescing-operator" "^7.16.7"
"@babel/plugin-proposal-numeric-separator" "^7.16.7"
- "@babel/plugin-proposal-object-rest-spread" "^7.16.7"
+ "@babel/plugin-proposal-object-rest-spread" "^7.17.3"
"@babel/plugin-proposal-optional-catch-binding" "^7.16.7"
"@babel/plugin-proposal-optional-chaining" "^7.16.7"
"@babel/plugin-proposal-private-methods" "^7.16.11"
@@ -956,7 +956,7 @@
"@babel/plugin-transform-block-scoping" "^7.16.7"
"@babel/plugin-transform-classes" "^7.16.7"
"@babel/plugin-transform-computed-properties" "^7.16.7"
- "@babel/plugin-transform-destructuring" "^7.16.7"
+ "@babel/plugin-transform-destructuring" "^7.17.7"
"@babel/plugin-transform-dotall-regex" "^7.16.7"
"@babel/plugin-transform-duplicate-keys" "^7.16.7"
"@babel/plugin-transform-exponentiation-operator" "^7.16.7"
@@ -965,15 +965,15 @@
"@babel/plugin-transform-literals" "^7.16.7"
"@babel/plugin-transform-member-expression-literals" "^7.16.7"
"@babel/plugin-transform-modules-amd" "^7.16.7"
- "@babel/plugin-transform-modules-commonjs" "^7.16.8"
- "@babel/plugin-transform-modules-systemjs" "^7.16.7"
+ "@babel/plugin-transform-modules-commonjs" "^7.17.9"
+ "@babel/plugin-transform-modules-systemjs" "^7.17.8"
"@babel/plugin-transform-modules-umd" "^7.16.7"
- "@babel/plugin-transform-named-capturing-groups-regex" "^7.16.8"
+ "@babel/plugin-transform-named-capturing-groups-regex" "^7.17.10"
"@babel/plugin-transform-new-target" "^7.16.7"
"@babel/plugin-transform-object-super" "^7.16.7"
"@babel/plugin-transform-parameters" "^7.16.7"
"@babel/plugin-transform-property-literals" "^7.16.7"
- "@babel/plugin-transform-regenerator" "^7.16.7"
+ "@babel/plugin-transform-regenerator" "^7.17.9"
"@babel/plugin-transform-reserved-words" "^7.16.7"
"@babel/plugin-transform-shorthand-properties" "^7.16.7"
"@babel/plugin-transform-spread" "^7.16.7"
@@ -983,11 +983,11 @@
"@babel/plugin-transform-unicode-escapes" "^7.16.7"
"@babel/plugin-transform-unicode-regex" "^7.16.7"
"@babel/preset-modules" "^0.1.5"
- "@babel/types" "^7.16.8"
+ "@babel/types" "^7.17.10"
babel-plugin-polyfill-corejs2 "^0.3.0"
babel-plugin-polyfill-corejs3 "^0.5.0"
babel-plugin-polyfill-regenerator "^0.3.0"
- core-js-compat "^3.20.2"
+ core-js-compat "^3.22.1"
semver "^6.3.0"
"@babel/preset-modules@^0.1.5":
@@ -1057,26 +1057,26 @@
"@babel/parser" "^7.16.7"
"@babel/types" "^7.16.7"
-"@babel/traverse@^7.12.12", "@babel/traverse@^7.13.0", "@babel/traverse@^7.13.17", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.3", "@babel/traverse@^7.17.9", "@babel/traverse@^7.7.2":
- version "7.17.9"
- resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.9.tgz#1f9b207435d9ae4a8ed6998b2b82300d83c37a0d"
- integrity sha512-PQO8sDIJ8SIwipTPiR71kJQCKQYB5NGImbOviK8K+kg5xkNSYXLBupuX9QhatFowrsvo9Hj8WgArg3W7ijNAQw==
+"@babel/traverse@^7.12.12", "@babel/traverse@^7.13.0", "@babel/traverse@^7.13.17", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.10", "@babel/traverse@^7.17.3", "@babel/traverse@^7.17.9", "@babel/traverse@^7.7.2":
+ version "7.17.10"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.10.tgz#1ee1a5ac39f4eac844e6cf855b35520e5eb6f8b5"
+ integrity sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==
dependencies:
"@babel/code-frame" "^7.16.7"
- "@babel/generator" "^7.17.9"
+ "@babel/generator" "^7.17.10"
"@babel/helper-environment-visitor" "^7.16.7"
"@babel/helper-function-name" "^7.17.9"
"@babel/helper-hoist-variables" "^7.16.7"
"@babel/helper-split-export-declaration" "^7.16.7"
- "@babel/parser" "^7.17.9"
- "@babel/types" "^7.17.0"
+ "@babel/parser" "^7.17.10"
+ "@babel/types" "^7.17.10"
debug "^4.1.0"
globals "^11.1.0"
-"@babel/types@^7.0.0", "@babel/types@^7.16.0", "@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.17.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4":
- version "7.17.0"
- resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.0.tgz#a826e368bccb6b3d84acd76acad5c0d87342390b"
- integrity sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==
+"@babel/types@^7.0.0", "@babel/types@^7.16.0", "@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.17.0", "@babel/types@^7.17.10", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4":
+ version "7.17.10"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.10.tgz#d35d7b4467e439fcf06d195f8100e0fea7fc82c4"
+ integrity sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==
dependencies:
"@babel/helper-validator-identifier" "^7.16.7"
to-fast-properties "^2.0.0"
@@ -1132,9 +1132,9 @@
lodash.once "^4.1.1"
"@eslint/eslintrc@^1.1.0":
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.1.tgz#8b5e1c49f4077235516bc9ec7d41378c0f69b8c6"
- integrity sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ==
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.2.tgz#4989b9e8c0216747ee7cca314ae73791bb281aae"
+ integrity sha512-lTVWHs7O2hjBFZunXTZYnYqtB9GakA1lnxIf+gKq2nY5gxkkNi/lQvveW6t8gFdOHTg6nG50Xs95PrLqVpcaLg==
dependencies:
ajv "^6.12.4"
debug "^4.3.2"
@@ -1377,20 +1377,33 @@
"@types/yargs" "^16.0.0"
chalk "^4.0.0"
+"@jridgewell/gen-mapping@^0.1.0":
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996"
+ integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==
+ dependencies:
+ "@jridgewell/set-array" "^1.0.0"
+ "@jridgewell/sourcemap-codec" "^1.4.10"
+
"@jridgewell/resolve-uri@^3.0.3":
- version "3.0.5"
- resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz#68eb521368db76d040a6315cdb24bf2483037b9c"
- integrity sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew==
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.6.tgz#4ac237f4dabc8dd93330386907b97591801f7352"
+ integrity sha512-R7xHtBSNm+9SyvpJkdQl+qrM3Hm2fea3Ef197M3mUug+v+yR+Rhfbs7PBtcBUVnIWJ4JcAdjvij+c8hXS9p5aw==
+
+"@jridgewell/set-array@^1.0.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.0.tgz#1179863356ac8fbea64a5a4bcde93a4871012c01"
+ integrity sha512-SfJxIxNVYLTsKwzB3MoOQ1yxf4w/E6MdkvTgrgAt1bfxjSrLUoHMKrDOykwN14q65waezZIdqDneUIPh4/sKxg==
"@jridgewell/sourcemap-codec@^1.4.10":
- version "1.4.11"
- resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz#771a1d8d744eeb71b6adb35808e1a6c7b9b8c8ec"
- integrity sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==
+ version "1.4.12"
+ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.12.tgz#7ed98f6fa525ffb7c56a2cbecb5f7bb91abd2baf"
+ integrity sha512-az/NhpIwP3K33ILr0T2bso+k2E/SLf8Yidd8mHl0n6sCQ4YdyC8qDhZA6kOPDNDBA56ZnIjngVl0U3jREA0BUA==
-"@jridgewell/trace-mapping@^0.3.0", "@jridgewell/trace-mapping@^0.3.4":
- version "0.3.4"
- resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz#f6a0832dffd5b8a6aaa633b7d9f8e8e94c83a0c3"
- integrity sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ==
+"@jridgewell/trace-mapping@^0.3.8", "@jridgewell/trace-mapping@^0.3.9":
+ version "0.3.9"
+ resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9"
+ integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==
dependencies:
"@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10"
@@ -1604,67 +1617,194 @@
tslib "^2.3.1"
webcrypto-core "^1.7.2"
+"@percy/cli-build@1.1.4":
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/@percy/cli-build/-/cli-build-1.1.4.tgz#98f93d427e116264cacde03bd06b5a8bb28029b8"
+ integrity sha512-ciifipdGEtBwEsMzUfOBDiVKiYRdGFs3vH3S3gn/3tTSxTp14uICJfTJ/J6vVPmxYEmaduEuVi/yJS4p3/O+SA==
+ dependencies:
+ "@percy/cli-command" "1.1.4"
+
+"@percy/cli-command@1.1.4":
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/@percy/cli-command/-/cli-command-1.1.4.tgz#5732d43b5506c9c22d39703ba6200a242897bad0"
+ integrity sha512-DooBkI0H3A1o/+NIr2diCgFmAoWCl7IZcoKasTCZnZYNVVLJhIqxNFtb/t8jpj+NC4ga0E4OGGEtNQ9ZcdtzHw==
+ dependencies:
+ "@percy/config" "1.1.4"
+ "@percy/core" "1.1.4"
+ "@percy/logger" "1.1.4"
+
+"@percy/cli-config@1.1.4":
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/@percy/cli-config/-/cli-config-1.1.4.tgz#13b2957f12e3383f2fe9b993c48aebf922ee1c76"
+ integrity sha512-dbkARKV/tXCa5pUB9jzdputfLMwjURwhwytDnh6Wwh9/GiY9RQW1ARGgJipwmZjcCp27ytbcRM1+zy0DXJ5nww==
+ dependencies:
+ "@percy/cli-command" "1.1.4"
+
+"@percy/cli-exec@1.1.4":
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/@percy/cli-exec/-/cli-exec-1.1.4.tgz#325c89526b522098bf98fcf155c2368759bcad50"
+ integrity sha512-2stWIHPMAlDzjRVb04pg2CUb/3h66S51ExBeUvjAY0CBKOhWQZX/PQidQLgZJy2pgFZnPQvk3Uesg8h5i6Vc7g==
+ dependencies:
+ "@percy/cli-command" "1.1.4"
+ cross-spawn "^7.0.3"
+ which "^2.0.2"
+
+"@percy/cli-snapshot@1.1.4":
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/@percy/cli-snapshot/-/cli-snapshot-1.1.4.tgz#42407a9568c90b656bb08fee7f1bdeb3e12a5c14"
+ integrity sha512-c6u9zJYZThyFIEnPWtqaiPfSgRXX+Ncpc4mObFRne8gQJX62OVji06keaa98wyxHDZyFqUe8NUr9t6pOzWjISw==
+ dependencies:
+ "@percy/cli-command" "1.1.4"
+ yaml "^2.0.0"
+
+"@percy/cli-upload@1.1.4":
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/@percy/cli-upload/-/cli-upload-1.1.4.tgz#d837f342acc1d000dd8250fdfe6d7e13eaba28d1"
+ integrity sha512-R07+U/DGn5T5pTuQ5vGETDmfhdQlZFeD8NDBYwdHOWlXN5gjnN4HfoZNfJ67hPwLGYOPiYXhjz83HkeyTsnn6w==
+ dependencies:
+ "@percy/cli-command" "1.1.4"
+ fast-glob "^3.2.11"
+ image-size "^1.0.0"
+
+"@percy/cli@^1.1.4":
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/@percy/cli/-/cli-1.1.4.tgz#fdc4858dec0d8e3404473c6b3de700215f1739e7"
+ integrity sha512-nKGwdI/ZvVuTNjf+Yl1m4ctcIAKcoxlD2nOcCT+VEi9Y9L7sXbreFtwsIQFmSNqyH9rgSxAXcNnPXAj3DpDZcw==
+ dependencies:
+ "@percy/cli-build" "1.1.4"
+ "@percy/cli-command" "1.1.4"
+ "@percy/cli-config" "1.1.4"
+ "@percy/cli-exec" "1.1.4"
+ "@percy/cli-snapshot" "1.1.4"
+ "@percy/cli-upload" "1.1.4"
+ "@percy/client" "1.1.4"
+ "@percy/logger" "1.1.4"
+
+"@percy/client@1.1.4":
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/@percy/client/-/client-1.1.4.tgz#21fa40a6f1d218b4c8567382af03d5d7f6d5ac1b"
+ integrity sha512-aJSDwQkMBiutJa7vbGZPup/wnA+EpKFVMKYyIfoAkqggqDHmHYTzHzg9C5TvH8DRzkc3xZG0vBQc1l7dgRth9A==
+ dependencies:
+ "@percy/env" "1.1.4"
+ "@percy/logger" "1.1.4"
+
+"@percy/config@1.1.4":
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/@percy/config/-/config-1.1.4.tgz#66ccf25dff9f02061f679b0af00061b37cd4be83"
+ integrity sha512-h1d6105dvV8pNMkohEauG/6I4xnzr2kjDEaaoVDsJazyMP0mIj/V7SLrM+KuQDTkn7vSmcty5rHPF+OjOgMhwA==
+ dependencies:
+ "@percy/logger" "1.1.4"
+ ajv "^8.6.2"
+ cosmiconfig "^7.0.0"
+ yaml "^2.0.0"
+
+"@percy/core@1.1.4":
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/@percy/core/-/core-1.1.4.tgz#705d632929480e9288675f629da1b0836ed8f68f"
+ integrity sha512-XefP+c/EAKH5ZHdxjVpY32ywLMIIm8sF87gZOMrRCxX29IX3epoctLBc7Ce0ZemXMVJPIVxdb0t/3qiOwe0PDg==
+ dependencies:
+ "@percy/client" "1.1.4"
+ "@percy/config" "1.1.4"
+ "@percy/dom" "1.1.4"
+ "@percy/logger" "1.1.4"
+ content-disposition "^0.5.4"
+ cross-spawn "^7.0.3"
+ extract-zip "^2.0.1"
+ fast-glob "^3.2.11"
+ micromatch "^4.0.4"
+ mime-types "^2.1.34"
+ path-to-regexp "^6.2.0"
+ rimraf "^3.0.2"
+ ws "^8.0.0"
+
+"@percy/cypress@^3.1.1":
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/@percy/cypress/-/cypress-3.1.1.tgz#4e7c5bdeccf1240b2150fc9d608df72c2f213d4b"
+ integrity sha512-khvWmCOJW7pxwDZPB5ovvbSe11FfNtH8Iyq8PHRYLD9ibAkiAWHZVs07bLK5wju1Q9X8s7zg5uj2yWxIlB1yjA==
+ dependencies:
+ "@percy/sdk-utils" "^1.0.0-beta.44"
+
+"@percy/dom@1.1.4":
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/@percy/dom/-/dom-1.1.4.tgz#b385960735e7c297b6e5930ce9e31992fa6eb9c6"
+ integrity sha512-5Z+2UtX0xcLNt/ECGdrVSesfZlawqj31YFpaEPq71RWKtzBjG/GxlymAX5lqhY2T+EFiKtCF7d/oLJAYcJhZPQ==
+
+"@percy/env@1.1.4":
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/@percy/env/-/env-1.1.4.tgz#9b069a80ec0d1f66acc746d0a33f48be6547babb"
+ integrity sha512-RdAcaXSAf7OPhiiXaoD/zQF9kYTi8E4P6uwEBQlRPjgk19oYwblpwQOGA8QJIyFXuJKvz5su+yyCynvsCdjMJA==
+
+"@percy/logger@1.1.4":
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/@percy/logger/-/logger-1.1.4.tgz#5fa8d823d643bdd8e298c50bebfe1942c869c599"
+ integrity sha512-ZaKW1WHkUq1Oiz9KgbKae5u6Zn33ZuWI8S2bwl6w5aRBdnaoy3vwPDeef0WaN7BHKPmG8n0BgCS9m5IOug/kxA==
+
+"@percy/sdk-utils@^1.0.0-beta.44":
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/@percy/sdk-utils/-/sdk-utils-1.1.4.tgz#67a98b3da424038a49fd33673b1f7f35d04dd170"
+ integrity sha512-fXpRt9Sgq3eerBobRAZ4No/e5LqHn4IHU9bpsxSExVMYridDBB3hy31ZyTRPjImXPDqNZs6BaYB5Yn+zq8mD+A==
+
"@sentry/browser@^6.11.0":
- version "6.19.6"
- resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.19.6.tgz#75be467667fffa1f4745382fc7a695568609c634"
- integrity sha512-V5QyY1cO1iuFCI78dOFbHV7vckbeQEPPq3a5dGSXlBQNYnd9Ec5xoxp5nRNpWQPOZ8/Ixt9IgRxdqVTkWib51g==
+ version "6.19.7"
+ resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.19.7.tgz#a40b6b72d911b5f1ed70ed3b4e7d4d4e625c0b5f"
+ integrity sha512-oDbklp4O3MtAM4mtuwyZLrgO1qDVYIujzNJQzXmi9YzymJCuzMLSRDvhY83NNDCRxf0pds4DShgYeZdbSyKraA==
dependencies:
- "@sentry/core" "6.19.6"
- "@sentry/types" "6.19.6"
- "@sentry/utils" "6.19.6"
+ "@sentry/core" "6.19.7"
+ "@sentry/types" "6.19.7"
+ "@sentry/utils" "6.19.7"
tslib "^1.9.3"
-"@sentry/core@6.19.6":
- version "6.19.6"
- resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.19.6.tgz#7d4649d0148b5d0be1358ab02e2f869bf7363e9a"
- integrity sha512-biEotGRr44/vBCOegkTfC9rwqaqRKIpFljKGyYU6/NtzMRooktqOhjmjmItNCMRknArdeaQwA8lk2jcZDXX3Og==
+"@sentry/core@6.19.7":
+ version "6.19.7"
+ resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.19.7.tgz#156aaa56dd7fad8c89c145be6ad7a4f7209f9785"
+ integrity sha512-tOfZ/umqB2AcHPGbIrsFLcvApdTm9ggpi/kQZFkej7kMphjT+SGBiQfYtjyg9jcRW+ilAR4JXC9BGKsdEQ+8Vw==
dependencies:
- "@sentry/hub" "6.19.6"
- "@sentry/minimal" "6.19.6"
- "@sentry/types" "6.19.6"
- "@sentry/utils" "6.19.6"
+ "@sentry/hub" "6.19.7"
+ "@sentry/minimal" "6.19.7"
+ "@sentry/types" "6.19.7"
+ "@sentry/utils" "6.19.7"
tslib "^1.9.3"
-"@sentry/hub@6.19.6":
- version "6.19.6"
- resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.19.6.tgz#ada83ceca0827c49534edfaba018221bc1eb75e1"
- integrity sha512-PuEOBZxvx3bjxcXmWWZfWXG+orojQiWzv9LQXjIgroVMKM/GG4QtZbnWl1hOckUj7WtKNl4hEGO2g/6PyCV/vA==
+"@sentry/hub@6.19.7":
+ version "6.19.7"
+ resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.19.7.tgz#58ad7776bbd31e9596a8ec46365b45cd8b9cfd11"
+ integrity sha512-y3OtbYFAqKHCWezF0EGGr5lcyI2KbaXW2Ik7Xp8Mu9TxbSTuwTe4rTntwg8ngPjUQU3SUHzgjqVB8qjiGqFXCA==
dependencies:
- "@sentry/types" "6.19.6"
- "@sentry/utils" "6.19.6"
+ "@sentry/types" "6.19.7"
+ "@sentry/utils" "6.19.7"
tslib "^1.9.3"
-"@sentry/minimal@6.19.6":
- version "6.19.6"
- resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.19.6.tgz#b6cced3708e25d322039e68ebdf8fadfa445bf7d"
- integrity sha512-T1NKcv+HTlmd8EbzUgnGPl4ySQGHWMCyZ8a8kXVMZOPDzphN3fVIzkYzWmSftCWp0rpabXPt9aRF2mfBKU+mAQ==
+"@sentry/minimal@6.19.7":
+ version "6.19.7"
+ resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.19.7.tgz#b3ee46d6abef9ef3dd4837ebcb6bdfd01b9aa7b4"
+ integrity sha512-wcYmSJOdvk6VAPx8IcmZgN08XTXRwRtB1aOLZm+MVHjIZIhHoBGZJYTVQS/BWjldsamj2cX3YGbGXNunaCfYJQ==
dependencies:
- "@sentry/hub" "6.19.6"
- "@sentry/types" "6.19.6"
+ "@sentry/hub" "6.19.7"
+ "@sentry/types" "6.19.7"
tslib "^1.9.3"
"@sentry/tracing@^6.11.0":
- version "6.19.6"
- resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.19.6.tgz#faa156886afe441730f03cf9ac9c4982044b7135"
- integrity sha512-STZdlEtTBqRmPw6Vjkzi/1kGkGPgiX0zdHaSOhSeA2HXHwx7Wnfu7veMKxtKWdO+0yW9QZGYOYqp0GVf4Swujg==
- dependencies:
- "@sentry/hub" "6.19.6"
- "@sentry/minimal" "6.19.6"
- "@sentry/types" "6.19.6"
- "@sentry/utils" "6.19.6"
+ version "6.19.7"
+ resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.19.7.tgz#54bb99ed5705931cd33caf71da347af769f02a4c"
+ integrity sha512-ol4TupNnv9Zd+bZei7B6Ygnr9N3Gp1PUrNI761QSlHtPC25xXC5ssSD3GMhBgyQrcvpuRcCFHVNNM97tN5cZiA==
+ dependencies:
+ "@sentry/hub" "6.19.7"
+ "@sentry/minimal" "6.19.7"
+ "@sentry/types" "6.19.7"
+ "@sentry/utils" "6.19.7"
tslib "^1.9.3"
-"@sentry/types@6.19.6", "@sentry/types@^6.10.0":
- version "6.19.6"
- resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.19.6.tgz#70513f9dca05d23d7ab9c2a6cb08d4db6763ca67"
- integrity sha512-QH34LMJidEUPZK78l+Frt3AaVFJhEmIi05Zf8WHd9/iTt+OqvCHBgq49DDr1FWFqyYWm/QgW/3bIoikFpfsXyQ==
+"@sentry/types@6.19.7", "@sentry/types@^6.10.0":
+ version "6.19.7"
+ resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.19.7.tgz#c6b337912e588083fc2896eb012526cf7cfec7c7"
+ integrity sha512-jH84pDYE+hHIbVnab3Hr+ZXr1v8QABfhx39KknxqKWr2l0oEItzepV0URvbEhB446lk/S/59230dlUUIBGsXbg==
-"@sentry/utils@6.19.6":
- version "6.19.6"
- resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.19.6.tgz#2ddc9ef036c3847084c43d0e5a55e4646bdf9021"
- integrity sha512-fAMWcsguL0632eWrROp/vhPgI7sBj/JROWVPzpabwVkm9z3m1rQm6iLFn4qfkZL8Ozy6NVZPXOQ7EXmeU24byg==
+"@sentry/utils@6.19.7":
+ version "6.19.7"
+ resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.19.7.tgz#6edd739f8185fd71afe49cbe351c1bbf5e7b7c79"
+ integrity sha512-z95ECmE3i9pbWoXQrD/7PgkBAzJYR+iXtPuTkpBjDKs86O3mT+PXOT3BAn79w2wkn7/i3vOGD2xVr1uiMl26dA==
dependencies:
- "@sentry/types" "6.19.6"
+ "@sentry/types" "6.19.7"
tslib "^1.9.3"
"@sinonjs/commons@^1.7.0":
@@ -1689,11 +1829,11 @@
"@sinonjs/commons" "^1.7.0"
"@stylelint/postcss-css-in-js@^0.37.2":
- version "0.37.2"
- resolved "https://registry.yarnpkg.com/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.37.2.tgz#7e5a84ad181f4234a2480803422a47b8749af3d2"
- integrity sha512-nEhsFoJurt8oUmieT8qy4nk81WRHmJynmVwn/Vts08PL9fhgIsMhk1GId5yAN643OzqEEb5S/6At2TZW7pqPDA==
+ version "0.37.3"
+ resolved "https://registry.yarnpkg.com/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.37.3.tgz#d149a385e07ae365b0107314c084cb6c11adbf49"
+ integrity sha512-scLk3cSH1H9KggSniseb2KNAU5D9FWc3H7BxCSAIdtU9OWIyw0zkEZ9qEKHryRM+SExYXRKNb7tOOVNAsQ3iwg==
dependencies:
- "@babel/core" ">=7.9.0"
+ "@babel/core" "^7.17.9"
"@stylelint/postcss-markdown@^0.36.2":
version "0.36.2"
@@ -1740,9 +1880,9 @@
"@babel/types" "^7.0.0"
"@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6":
- version "7.17.0"
- resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.17.0.tgz#7a9b80f712fe2052bc20da153ff1e552404d8e4b"
- integrity sha512-r8aveDbd+rzGP+ykSdF3oPuTVRWRfbBiHl0rVDM2yNEmSMXfkObQLV46b4RnCv3Lra51OlfnZhkkFaDl2MIRaA==
+ version "7.17.1"
+ resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.17.1.tgz#1a0e73e8c28c7e832656db372b779bfd2ef37314"
+ integrity sha512-kVzjari1s2YVi77D3w1yuvohV2idweYXMCDzqBiVNN63TcDWrIlTVOYpqVrvbbyOE/IyzBoTKF0fdnLPEORFxA==
dependencies:
"@babel/types" "^7.3.0"
@@ -1903,14 +2043,14 @@
integrity sha512-jhMOZSS0UGYTS9pqvt6q3wtT3uvOSve5piTEmTMx3zzTuBLvSIMxSIBIc3d5lajVD5h4xc41AMZD2M5orN3PxA==
"@types/node@*":
- version "17.0.25"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.25.tgz#527051f3c2f77aa52e5dc74e45a3da5fb2301448"
- integrity sha512-wANk6fBrUwdpY4isjWrKTufkrXdu1D2YHCot2fD/DfWxF5sMrVSA+KN7ydckvaTCh0HiqX9IVl0L5/ZoXg5M7w==
+ version "17.0.31"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.31.tgz#a5bb84ecfa27eec5e1c802c6bbf8139bdb163a5d"
+ integrity sha512-AR0x5HbXGqkEx9CadRH3EBYx/VkiUgZIhP4wvPn/+5KIsgpNoyFaRlVe0Zlx9gRtg8fA06a9tskE2MSN7TcG4Q==
"@types/node@^14.14.22", "@types/node@^14.14.31":
- version "14.18.13"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.13.tgz#6ad4d9db59e6b3faf98dcfe4ca9d2aec84443277"
- integrity sha512-Z6/KzgyWOga3pJNS42A+zayjhPbf2zM3hegRQaOPnLOzEi86VV++6FLDWgR1LGrVCRufP/ph2daa3tEa5br1zA==
+ version "14.18.16"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.16.tgz#878f670ba3f00482bf859b6550b6010610fc54b5"
+ integrity sha512-X3bUMdK/VmvrWdoTkz+VCn6nwKwrKCFTHtqwBIaQJNx4RUIBBUFXM00bqPz/DsDd+Icjmzm6/tyYZzeGVqb6/Q==
"@types/normalize-package-data@^2.4.0":
version "2.4.1"
@@ -1996,10 +2136,10 @@
"@types/scheduler" "*"
csstype "^3.0.2"
-"@types/retry@^0.12.0":
- version "0.12.1"
- resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.1.tgz#d8f1c0d0dc23afad6dc16a9e993a0865774b4065"
- integrity sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==
+"@types/retry@0.12.0":
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
+ integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==
"@types/sanitize-html@^2.3.1":
version "2.6.2"
@@ -2065,13 +2205,13 @@
integrity sha512-3NoqvZC2W5gAC5DZbTpCeJ251vGQmgcWIHQJGq2J240HY6ErQ9aWKkwfoKJlHLx+A83WPNTZ9+3cd2ILxbvr1w==
"@typescript-eslint/eslint-plugin@^5.6.0":
- version "5.20.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.20.0.tgz#022531a639640ff3faafaf251d1ce00a2ef000a1"
- integrity sha512-fapGzoxilCn3sBtC6NtXZX6+P/Hef7VDbyfGqTTpzYydwhlkevB+0vE0EnmHPVTVSy68GUncyJ/2PcrFBeCo5Q==
+ version "5.22.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.22.0.tgz#7b52a0de2e664044f28b36419210aea4ab619e2a"
+ integrity sha512-YCiy5PUzpAeOPGQ7VSGDEY2NeYUV1B0swde2e0HzokRsHBYjSdF6DZ51OuRZxVPHx0032lXGLvOMls91D8FXlg==
dependencies:
- "@typescript-eslint/scope-manager" "5.20.0"
- "@typescript-eslint/type-utils" "5.20.0"
- "@typescript-eslint/utils" "5.20.0"
+ "@typescript-eslint/scope-manager" "5.22.0"
+ "@typescript-eslint/type-utils" "5.22.0"
+ "@typescript-eslint/utils" "5.22.0"
debug "^4.3.2"
functional-red-black-tree "^1.0.1"
ignore "^5.1.8"
@@ -2080,68 +2220,68 @@
tsutils "^3.21.0"
"@typescript-eslint/parser@^5.6.0":
- version "5.20.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.20.0.tgz#4991c4ee0344315c2afc2a62f156565f689c8d0b"
- integrity sha512-UWKibrCZQCYvobmu3/N8TWbEeo/EPQbS41Ux1F9XqPzGuV7pfg6n50ZrFo6hryynD8qOTTfLHtHjjdQtxJ0h/w==
+ version "5.22.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.22.0.tgz#7bedf8784ef0d5d60567c5ba4ce162460e70c178"
+ integrity sha512-piwC4krUpRDqPaPbFaycN70KCP87+PC5WZmrWs+DlVOxxmF+zI6b6hETv7Quy4s9wbkV16ikMeZgXsvzwI3icQ==
dependencies:
- "@typescript-eslint/scope-manager" "5.20.0"
- "@typescript-eslint/types" "5.20.0"
- "@typescript-eslint/typescript-estree" "5.20.0"
+ "@typescript-eslint/scope-manager" "5.22.0"
+ "@typescript-eslint/types" "5.22.0"
+ "@typescript-eslint/typescript-estree" "5.22.0"
debug "^4.3.2"
-"@typescript-eslint/scope-manager@5.20.0":
- version "5.20.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.20.0.tgz#79c7fb8598d2942e45b3c881ced95319818c7980"
- integrity sha512-h9KtuPZ4D/JuX7rpp1iKg3zOH0WNEa+ZIXwpW/KWmEFDxlA/HSfCMhiyF1HS/drTICjIbpA6OqkAhrP/zkCStg==
+"@typescript-eslint/scope-manager@5.22.0":
+ version "5.22.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.22.0.tgz#590865f244ebe6e46dc3e9cab7976fc2afa8af24"
+ integrity sha512-yA9G5NJgV5esANJCO0oF15MkBO20mIskbZ8ijfmlKIvQKg0ynVKfHZ15/nhAJN5m8Jn3X5qkwriQCiUntC9AbA==
dependencies:
- "@typescript-eslint/types" "5.20.0"
- "@typescript-eslint/visitor-keys" "5.20.0"
+ "@typescript-eslint/types" "5.22.0"
+ "@typescript-eslint/visitor-keys" "5.22.0"
-"@typescript-eslint/type-utils@5.20.0":
- version "5.20.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.20.0.tgz#151c21cbe9a378a34685735036e5ddfc00223be3"
- integrity sha512-WxNrCwYB3N/m8ceyoGCgbLmuZwupvzN0rE8NBuwnl7APgjv24ZJIjkNzoFBXPRCGzLNkoU/WfanW0exvp/+3Iw==
+"@typescript-eslint/type-utils@5.22.0":
+ version "5.22.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.22.0.tgz#0c0e93b34210e334fbe1bcb7250c470f4a537c19"
+ integrity sha512-iqfLZIsZhK2OEJ4cQ01xOq3NaCuG5FQRKyHicA3xhZxMgaxQazLUHbH/B2k9y5i7l3+o+B5ND9Mf1AWETeMISA==
dependencies:
- "@typescript-eslint/utils" "5.20.0"
+ "@typescript-eslint/utils" "5.22.0"
debug "^4.3.2"
tsutils "^3.21.0"
-"@typescript-eslint/types@5.20.0":
- version "5.20.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.20.0.tgz#fa39c3c2aa786568302318f1cb51fcf64258c20c"
- integrity sha512-+d8wprF9GyvPwtoB4CxBAR/s0rpP25XKgnOvMf/gMXYDvlUC3rPFHupdTQ/ow9vn7UDe5rX02ovGYQbv/IUCbg==
+"@typescript-eslint/types@5.22.0":
+ version "5.22.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.22.0.tgz#50a4266e457a5d4c4b87ac31903b28b06b2c3ed0"
+ integrity sha512-T7owcXW4l0v7NTijmjGWwWf/1JqdlWiBzPqzAWhobxft0SiEvMJB56QXmeCQjrPuM8zEfGUKyPQr/L8+cFUBLw==
-"@typescript-eslint/typescript-estree@5.20.0":
- version "5.20.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.20.0.tgz#ab73686ab18c8781bbf249c9459a55dc9417d6b0"
- integrity sha512-36xLjP/+bXusLMrT9fMMYy1KJAGgHhlER2TqpUVDYUQg4w0q/NW/sg4UGAgVwAqb8V4zYg43KMUpM8vV2lve6w==
+"@typescript-eslint/typescript-estree@5.22.0":
+ version "5.22.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.22.0.tgz#e2116fd644c3e2fda7f4395158cddd38c0c6df97"
+ integrity sha512-EyBEQxvNjg80yinGE2xdhpDYm41so/1kOItl0qrjIiJ1kX/L/L8WWGmJg8ni6eG3DwqmOzDqOhe6763bF92nOw==
dependencies:
- "@typescript-eslint/types" "5.20.0"
- "@typescript-eslint/visitor-keys" "5.20.0"
+ "@typescript-eslint/types" "5.22.0"
+ "@typescript-eslint/visitor-keys" "5.22.0"
debug "^4.3.2"
globby "^11.0.4"
is-glob "^4.0.3"
semver "^7.3.5"
tsutils "^3.21.0"
-"@typescript-eslint/utils@5.20.0":
- version "5.20.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.20.0.tgz#b8e959ed11eca1b2d5414e12417fd94cae3517a5"
- integrity sha512-lHONGJL1LIO12Ujyx8L8xKbwWSkoUKFSO+0wDAqGXiudWB2EO7WEUT+YZLtVbmOmSllAjLb9tpoIPwpRe5Tn6w==
+"@typescript-eslint/utils@5.22.0":
+ version "5.22.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.22.0.tgz#1f2c4897e2cf7e44443c848a13c60407861babd8"
+ integrity sha512-HodsGb037iobrWSUMS7QH6Hl1kppikjA1ELiJlNSTYf/UdMEwzgj0WIp+lBNb6WZ3zTwb0tEz51j0Wee3iJ3wQ==
dependencies:
"@types/json-schema" "^7.0.9"
- "@typescript-eslint/scope-manager" "5.20.0"
- "@typescript-eslint/types" "5.20.0"
- "@typescript-eslint/typescript-estree" "5.20.0"
+ "@typescript-eslint/scope-manager" "5.22.0"
+ "@typescript-eslint/types" "5.22.0"
+ "@typescript-eslint/typescript-estree" "5.22.0"
eslint-scope "^5.1.1"
eslint-utils "^3.0.0"
-"@typescript-eslint/visitor-keys@5.20.0":
- version "5.20.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.20.0.tgz#70236b5c6b67fbaf8b2f58bf3414b76c1e826c2a"
- integrity sha512-1flRpNF+0CAQkMNlTJ6L/Z5jiODG/e5+7mk6XwtPOUS3UrTz3UOiAg9jG2VtKsWI6rZQfy4C6a232QNRZTRGlg==
+"@typescript-eslint/visitor-keys@5.22.0":
+ version "5.22.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.22.0.tgz#f49c0ce406944ffa331a1cfabeed451ea4d0909c"
+ integrity sha512-DbgTqn2Dv5RFWluG88tn0pP6Ex0ROF+dpDO1TNNZdRtLjUr6bdznjA6f/qNqJLjd2PgguAES2Zgxh/JzwzETDg==
dependencies:
- "@typescript-eslint/types" "5.20.0"
+ "@typescript-eslint/types" "5.22.0"
eslint-visitor-keys "^3.0.0"
"@wojtekmaj/enzyme-adapter-react-17@^0.6.1":
@@ -2195,9 +2335,9 @@ acorn@^7.1.1:
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
acorn@^8.2.4, acorn@^8.7.0:
- version "8.7.0"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf"
- integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==
+ version "8.7.1"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30"
+ integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==
agent-base@6:
version "6.0.2"
@@ -2229,7 +2369,7 @@ ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
-ajv@^8.0.1:
+ajv@^8.0.1, ajv@^8.6.2:
version "8.11.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f"
integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==
@@ -2356,13 +2496,13 @@ arr-union@^3.1.0:
integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
array-includes@^3.1.4:
- version "3.1.4"
- resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.4.tgz#f5b493162c760f3539631f005ba2bb46acb45ba9"
- integrity sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==
+ version "3.1.5"
+ resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.5.tgz#2c320010db8d31031fd2a5f6b3bbd4b1aad31bdb"
+ integrity sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==
dependencies:
call-bind "^1.0.2"
- define-properties "^1.1.3"
- es-abstract "^1.19.1"
+ define-properties "^1.1.4"
+ es-abstract "^1.19.5"
get-intrinsic "^1.1.1"
is-string "^1.0.7"
@@ -2425,11 +2565,11 @@ asn1@~0.2.3:
safer-buffer "~2.1.0"
asn1js@^2.3.1, asn1js@^2.3.2:
- version "2.3.2"
- resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-2.3.2.tgz#1864f859f6e5dfd7350c0543f411e18963f30592"
- integrity sha512-IYzujqcOk7fHaePpTyvD3KPAA0AjT3qZlaQAw76zmPPAV/XTjhO+tbHjbFbIQZIhw+fk9wCSfb0Z6K+JHe8Q2g==
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-2.4.0.tgz#9ca61dbdd7e4eb49b9ae95b36ab0615b77daff93"
+ integrity sha512-PvZC0FMyMut8aOnR2jAEGSkmRtHIUYPe9amUEnGjr9TdnUmsfoOkjrvUkOEU9mzpYBR1HyO9bF+8U1cLTMMHhQ==
dependencies:
- pvutils latest
+ pvutils "^1.1.3"
assert-plus@1.0.0, assert-plus@^1.0.0:
version "1.0.0"
@@ -2490,9 +2630,9 @@ available-typed-arrays@^1.0.5:
integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
await-lock@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/await-lock/-/await-lock-2.1.0.tgz#bc78c51d229a34d5d90965a1c94770e772c6145e"
- integrity sha512-t7Zm5YGgEEc/3eYAicF32m/TNvL+XOeYZy9CvBUeJY/szM7frLolFylhrlZNWV/ohWhcUXygrBGjYmoQdxF4CQ==
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/await-lock/-/await-lock-2.2.2.tgz#a95a9b269bfd2f69d22b17a321686f551152bcef"
+ integrity sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==
aws-sign2@~0.7.0:
version "0.7.0"
@@ -2766,15 +2906,15 @@ browser-request@^0.3.3:
resolved "https://registry.yarnpkg.com/browser-request/-/browser-request-0.3.3.tgz#9ece5b5aca89a29932242e18bf933def9876cc17"
integrity sha1-ns5bWsqJopkyJC4Yv5M975h2zBc=
-browserslist@^4.12.0, browserslist@^4.17.5, browserslist@^4.20.2:
- version "4.20.2"
- resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.2.tgz#567b41508757ecd904dab4d1c646c612cd3d4f88"
- integrity sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA==
+browserslist@^4.12.0, browserslist@^4.20.2, browserslist@^4.20.3:
+ version "4.20.3"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.3.tgz#eb7572f49ec430e054f56d52ff0ebe9be915f8bf"
+ integrity sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==
dependencies:
- caniuse-lite "^1.0.30001317"
- electron-to-chromium "^1.4.84"
+ caniuse-lite "^1.0.30001332"
+ electron-to-chromium "^1.4.118"
escalade "^3.1.1"
- node-releases "^2.0.2"
+ node-releases "^2.0.3"
picocolors "^1.0.0"
bs58@^4.0.1:
@@ -2879,10 +3019,10 @@ camelcase@^6.2.0:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
-caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001317:
- version "1.0.30001332"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001332.tgz#39476d3aa8d83ea76359c70302eafdd4a1d727dd"
- integrity sha512-10T30NYOEQtN6C11YGg411yebhvpnC6Z102+B95eAsN0oB6KUs01ivE8u+G6FMIRtIrVlYXhL+LUwQ3/hXwDWw==
+caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001332:
+ version "1.0.30001335"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001335.tgz#899254a0b70579e5a957c32dced79f0727c61f2a"
+ integrity sha512-ddP1Tgm7z2iIxu6QTtbZUv6HJxSaV/PZeSrWFZtbY4JZ69tOeNhBCl3HyRQgeNZKE5AOn1kpV7fhljigy0Ty3w==
capture-exit@^2.0.0:
version "2.0.0"
@@ -3180,6 +3320,13 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
+content-disposition@^0.5.4:
+ version "0.5.4"
+ resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
+ integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
+ dependencies:
+ safe-buffer "5.2.1"
+
content-type@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
@@ -3197,18 +3344,18 @@ copy-descriptor@^0.1.0:
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
-core-js-compat@^3.20.2, core-js-compat@^3.21.0:
- version "3.22.0"
- resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.22.0.tgz#7ce17ab57c378be2c717c7c8ed8f82a50a25b3e4"
- integrity sha512-WwA7xbfRGrk8BGaaHlakauVXrlYmAIkk8PNGb1FDQS+Rbrewc3pgFfwJFRw6psmJVAll7Px9UHRYE16oRQnwAQ==
+core-js-compat@^3.21.0, core-js-compat@^3.22.1:
+ version "3.22.4"
+ resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.22.4.tgz#d700f451e50f1d7672dcad0ac85d910e6691e579"
+ integrity sha512-dIWcsszDezkFZrfm1cnB4f/J85gyhiCpxbgBdohWCDtSVuAaChTSpPV7ldOQf/Xds2U5xCIJZOK82G4ZPAIswA==
dependencies:
- browserslist "^4.20.2"
+ browserslist "^4.20.3"
semver "7.0.0"
core-js-pure@^3.20.2:
- version "3.22.0"
- resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.22.0.tgz#0eaa54b6d1f4ebb4d19976bb4916dfad149a3747"
- integrity sha512-ylOC9nVy0ak1N+fPIZj00umoZHgUVqmucklP5RT5N+vJof38klKn8Ze6KGyvchdClvEBr6LcQqJpI216LUMqYA==
+ version "3.22.4"
+ resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.22.4.tgz#a992210f4cad8b32786b8654563776c56b0e0d0a"
+ integrity sha512-4iF+QZkpzIz0prAFuepmxwJ2h5t4agvE8WPYqs2mjLJMNNwJOnpch76w2Q7bUfCPEv/V7wpvOfog0w273M+ZSw==
core-js@^1.0.0:
version "1.2.7"
@@ -3339,10 +3486,10 @@ csstype@^3.0.2:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.11.tgz#d66700c5eacfac1940deb4e3ee5642792d85cd33"
integrity sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==
-cypress@^9.5.4:
- version "9.5.4"
- resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.5.4.tgz#49d9272f62eba12f2314faf29c2a865610e87550"
- integrity sha512-6AyJAD8phe7IMvOL4oBsI9puRNOWxZjl8z1lgixJMcgJ85JJmyKeP6uqNA0dI1z14lmJ7Qklf2MOgP/xdAqJ/Q==
+cypress@^9.6.1:
+ version "9.6.1"
+ resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.6.1.tgz#a7d6b5a53325b3dc4960181f5800a5ade0f085eb"
+ integrity sha512-ECzmV7pJSkk+NuAhEw6C3D+RIRATkSb2VAHXDY6qGZbca/F9mv5pPsj2LO6Ty6oIFVBTrwCyL9agl28MtJMe2g==
dependencies:
"@cypress/request" "^2.88.10"
"@cypress/xvfb" "^1.2.4"
@@ -3485,7 +3632,7 @@ deepmerge@^4.2.2:
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
-define-properties@^1.1.3, define-properties@~1.1.2:
+define-properties@^1.1.3, define-properties@^1.1.4, define-properties@~1.1.2:
version "1.1.4"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1"
integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==
@@ -3672,10 +3819,10 @@ ecc-jsbn@~0.1.1:
jsbn "~0.1.0"
safer-buffer "^2.1.0"
-electron-to-chromium@^1.4.84:
- version "1.4.113"
- resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.113.tgz#b3425c086e2f4fc31e9e53a724c6f239e3adb8b9"
- integrity sha512-s30WKxp27F3bBH6fA07FYL2Xm/FYnYrKpMjHr3XVCTUb9anAyZn/BeZfPWgTZGAbJeT4NxNwISSbLcYZvggPMA==
+electron-to-chromium@^1.4.118:
+ version "1.4.131"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.131.tgz#ca42d22eac0fe545860fbc636a6f4a7190ba70a9"
+ integrity sha512-oi3YPmaP87hiHn0c4ePB67tXaF+ldGhxvZnT19tW9zX6/Ej+pLN0Afja5rQ6S+TND7I9EuwQTT8JYn1k7R7rrw==
emittery@^0.8.1:
version "0.8.1"
@@ -3805,7 +3952,7 @@ error-ex@^1.3.1:
dependencies:
is-arrayish "^0.2.1"
-es-abstract@^1.18.5, es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2:
+es-abstract@^1.18.5, es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5:
version "1.19.5"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.5.tgz#a2cb01eb87f724e815b278b0dd0d00f36ca9a7f1"
integrity sha512-Aa2G2+Rd3b6kxEUKTF4TaW67czBLyAv3z7VOhYRU50YBx+bbsYZ9xQP4lMNazePuFlybXI0V4MruPos7qUo5fA==
@@ -3867,9 +4014,9 @@ es-to-primitive@^1.2.1:
is-symbol "^1.0.2"
es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@^0.10.53, es5-ext@^0.10.59, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46:
- version "0.10.60"
- resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.60.tgz#e8060a86472842b93019c31c34865012449883f4"
- integrity sha512-jpKNXIt60htYG59/9FGf2PYT3pwMpnEbNKysU+k/4FGwyGtMotOvcZOuW+EmXXYASRqYSXQfGL5cVIthOTgbkg==
+ version "0.10.61"
+ resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.61.tgz#311de37949ef86b6b0dcea894d1ffedb909d3269"
+ integrity sha512-yFhIqQAzu2Ca2I4SE2Au3rxVfmohU9Y7wqGR+s7+H7krk26NXhIRAZDgqd6xqjCEFUomDEA3/Bo/7fKmIkW1kA==
dependencies:
es6-iterator "^2.0.3"
es6-symbol "^3.1.3"
@@ -3997,15 +4144,15 @@ eslint-plugin-jsx-a11y@^6.5.1:
language-tags "^1.0.5"
minimatch "^3.0.4"
-eslint-plugin-matrix-org@^0.4.0:
- version "0.4.0"
- resolved "https://registry.yarnpkg.com/eslint-plugin-matrix-org/-/eslint-plugin-matrix-org-0.4.0.tgz#de2d2db1cd471d637728133ce9a2b921690e5cd1"
- integrity sha512-yVkNwtc33qtrQB4PPzpU+PUdFzdkENPan3JF4zhtAQJRUYXyvKEXnYSrXLUWYRXoYFxs9LbyI2CnhJL/RnHJaQ==
+eslint-plugin-matrix-org@^0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-matrix-org/-/eslint-plugin-matrix-org-0.5.2.tgz#eb355b1a81906ea814235d0b224e8162db7cbbf4"
+ integrity sha512-qJbyxp9cOi35Qpn3WCBqohCJaMSVp3ntOJ3WbjpREbCQdyrFze6MJAayl7GNidbNsdP7ejHTi0PtZzyKLcfLzQ==
eslint-plugin-react-hooks@^4.3.0:
- version "4.4.0"
- resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.4.0.tgz#71c39e528764c848d8253e1aa2c7024ed505f6c4"
- integrity sha512-U3RVIfdzJaeKDQKEJbz5p3NW8/L80PCATJAfuojwbaEL+gBjfGdhUcGde+WGUW46Q5sr/NgxevsIiDtNXrvZaQ==
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.5.0.tgz#5f762dfedf8b2cf431c689f533c9d3fa5dcf25ad"
+ integrity sha512-8k1gRt7D7h03kd+SAAlzXkQwWK22BnK6GKZG+FJA6BAGy22CFvl8kCIXKpVux0cCxMWDQUPqSok0LKaZ0aOcCw==
eslint-plugin-react@^7.28.0:
version "7.29.4"
@@ -4318,7 +4465,7 @@ extglob@^2.0.4:
snapdragon "^0.8.1"
to-regex "^3.0.1"
-extract-zip@2.0.1:
+extract-zip@2.0.1, extract-zip@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a"
integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
@@ -4344,7 +4491,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
-fast-glob@^3.2.5, fast-glob@^3.2.9:
+fast-glob@^3.2.11, fast-glob@^3.2.5, fast-glob@^3.2.9:
version "3.2.11"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9"
integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==
@@ -4365,11 +4512,6 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
-fast-memoize@^2.5.1:
- version "2.5.2"
- resolved "https://registry.yarnpkg.com/fast-memoize/-/fast-memoize-2.5.2.tgz#79e3bb6a4ec867ea40ba0e7146816f6cdce9b57e"
- integrity sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==
-
fastest-levenshtein@^1.0.12:
version "1.0.12"
resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2"
@@ -4524,10 +4666,10 @@ flux@2.1.1:
fbjs "0.1.0-alpha.7"
immutable "^3.7.4"
-focus-lock@^0.10.2:
- version "0.10.2"
- resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-0.10.2.tgz#561c62bae8387ecba1dd8e58a6df5ec29835c644"
- integrity sha512-DSaI/UHZ/02sg1P616aIWgToQcrKKBmcCvomDZ1PZvcJFj350PnWhSJxJ76T3e5/GbtQEARIACtbrdlrF9C5kA==
+focus-lock@^0.11.0:
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-0.11.0.tgz#72f9055d34fff59d54aec8e602adbb5438108709"
+ integrity sha512-7tCIkCdnMEnqwEWr3PktH8wA/SAcIPlhrDuLg+o20DjZ/fZW/rIy7Tc9BC2kJBOttH4vbzTXqte5PL8babatBw==
dependencies:
tslib "^2.0.3"
@@ -4636,9 +4778,9 @@ functional-red-black-tree@^1.0.1:
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
functions-have-names@^1.2.2:
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.2.tgz#98d93991c39da9361f8e50b337c4f6e41f120e21"
- integrity sha512-bLgc3asbWdwPbx2mNk2S49kmJCuQeu0nfmaOgbs8WIyzzkw3r4htszdIi9Q9EMezDPTYuJx2wvjZ/EwgAthpnA==
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
+ integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
gensync@^1.0.0-beta.2:
version "1.0.0-beta.2"
@@ -4848,10 +4990,10 @@ hard-rejection@^2.1.0:
resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883"
integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==
-has-bigints@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
- integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==
+has-bigints@^1.0.1, has-bigints@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"
+ integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==
has-flag@^3.0.0:
version "3.0.0"
@@ -5065,6 +5207,13 @@ ignore@^5.1.8, ignore@^5.2.0:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==
+image-size@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/image-size/-/image-size-1.0.1.tgz#86d6cfc2b1d19eab5d2b368d4b9194d9e48541c5"
+ integrity sha512-VAwkvNSNGClRw9mDHhc5Efax8PLlsOGcUTh0T/LIriC8vPA3U5PdqXWqkz406MoYHMKW8Uf9gWr05T/rYB44kQ==
+ dependencies:
+ queue "6.0.2"
+
immediate@~3.0.5:
version "3.0.6"
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
@@ -5254,9 +5403,9 @@ is-ci@^3.0.0:
ci-info "^3.2.0"
is-core-module@^2.2.0, is-core-module@^2.5.0, is-core-module@^2.8.1:
- version "2.8.1"
- resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211"
- integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==
+ version "2.9.0"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69"
+ integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==
dependencies:
has "^1.0.3"
@@ -5616,9 +5765,9 @@ istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0:
integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==
istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.1.0.tgz#7b49198b657b27a730b8e9cb601f1e1bff24c59a"
- integrity sha512-czwUz525rkOFDJxfKK6mYfIs9zBKILyrZQxjz3ABhjQXhbhFsSbo1HW/BFcsDnfJYJWA6thRR5/TUY2qs5W99Q==
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.0.tgz#31d18bdd127f825dd02ea7bfdfd906f8ab840e9f"
+ integrity sha512-6Lthe1hqXHBNsqvgDzGO6l03XNeu3CrG4RqQ1KM9+l5+jNGpEJfIELx1NS3SEHmJQA8np/u+E4EPRKRiu6m19A==
dependencies:
"@babel/core" "^7.12.3"
"@babel/parser" "^7.14.7"
@@ -5653,9 +5802,9 @@ istanbul-reports@^3.1.3:
istanbul-lib-report "^3.0.0"
jest-canvas-mock@^2.3.0:
- version "2.3.1"
- resolved "https://registry.yarnpkg.com/jest-canvas-mock/-/jest-canvas-mock-2.3.1.tgz#9535d14bc18ccf1493be36ac37dd349928387826"
- integrity sha512-5FnSZPrX3Q2ZfsbYNE3wqKR3+XorN8qFzDzB5o0golWgt6EOX1+emBnpOc9IAQ+NXFj8Nzm3h7ZdE/9H0ylBcg==
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/jest-canvas-mock/-/jest-canvas-mock-2.4.0.tgz#947b71442d7719f8e055decaecdb334809465341"
+ integrity sha512-mmMpZzpmLzn5vepIaHk5HoH3Ka4WykbSoLuG/EKoJd0x0ID/t+INo1l8ByfcUJuDM+RIsL4QDg/gDnBbrj2/IQ==
dependencies:
cssfontparser "^1.2.1"
moo-color "^1.0.2"
@@ -6295,9 +6444,9 @@ jsprim@^2.0.2:
verror "1.10.0"
"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.2.1:
- version "3.2.2"
- resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.2.tgz#6ab1e52c71dfc0c0707008a91729a9491fe9f76c"
- integrity sha512-HDAyJ4MNQBboGpUnHAVUNJs6X0lh058s6FuixsFGP7MgJYpD6Vasd6nzSG5iIfXu1zAYlHJ/zsOKNlrenTUBnw==
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.0.tgz#e624f259143b9062c92b6413ff92a164c80d3ccb"
+ integrity sha512-XzO9luP6L0xkxwhIJMTJQpZo/eeN60K08jHdexfD569AGxeNug6UketeHXEhROoM8aR7EcUoOQmIhcJQjcuq8Q==
dependencies:
array-includes "^3.1.4"
object.assign "^4.1.2"
@@ -6408,17 +6557,17 @@ lines-and-columns@^1.1.6:
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
-linkify-element@^4.0.0-beta.4:
+linkify-element@4.0.0-beta.4:
version "4.0.0-beta.4"
resolved "https://registry.yarnpkg.com/linkify-element/-/linkify-element-4.0.0-beta.4.tgz#31bb5dff7430c4debc34030466bd8f3e297793a7"
integrity sha512-dsu5qxk6MhQHxXUlPjul33JknQPx7Iv/N8zisH4JtV31qVk0qZg/5gn10Hr76GlMuixcdcxVvGHNfVcvbut13w==
-linkify-string@^4.0.0-beta.4:
+linkify-string@4.0.0-beta.4:
version "4.0.0-beta.4"
resolved "https://registry.yarnpkg.com/linkify-string/-/linkify-string-4.0.0-beta.4.tgz#0982509bc6ce81c554bff8d7121057193b84ea32"
integrity sha512-1U90tclSloCMAhbcuu4S+BN7ZisZkFB6ggKS1ofdYy1bmtgxdXGDppVUV+qRp5rcAudla7K0LBgOiwCQ0WzrYQ==
-linkifyjs@^4.0.0-beta.4:
+linkifyjs@4.0.0-beta.4:
version "4.0.0-beta.4"
resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.0.0-beta.4.tgz#8a03e7a999ed0b578a14d690585a32706525c45e"
integrity sha512-j8IUYMqyTT0aDrrkA5kf4hn6QurSKjGiQbqjNr4qc8dwEXIniCGp0JrdXmsGcTOEyhKG03GyRnJjp3NDTBBPDQ==
@@ -6636,9 +6785,9 @@ 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-analytics-events@github:matrix-org/matrix-analytics-events.git#4aef17b56798639906f26a8739043a3c5c5fde7e":
+"matrix-analytics-events@github:matrix-org/matrix-analytics-events.git#a0687ca6fbdb7258543d49b99fb88b9201e900b0":
version "0.0.1"
- resolved "https://codeload.github.com/matrix-org/matrix-analytics-events/tar.gz/4aef17b56798639906f26a8739043a3c5c5fde7e"
+ resolved "https://codeload.github.com/matrix-org/matrix-analytics-events/tar.gz/a0687ca6fbdb7258543d49b99fb88b9201e900b0"
matrix-encrypt-attachment@^1.0.3:
version "1.0.3"
@@ -6650,10 +6799,10 @@ matrix-events-sdk@^0.0.1-beta.7:
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1-beta.7.tgz#5ffe45eba1f67cc8d7c2377736c728b322524934"
integrity sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA==
-matrix-js-sdk@17.2.0:
- version "17.2.0"
- resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-17.2.0.tgz#b757eccbef8676dbb9131f7ee0c31fd8039cc1c6"
- integrity sha512-/IrgHCSVUZNVcKoPO20OF9Xog9X79a1ckmR7FwF5lSTNdmC7eQvU0XcFYCi5IXo57du+im69lEw8dLbPngZhoQ==
+matrix-js-sdk@18.0.0:
+ version "18.0.0"
+ resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-18.0.0.tgz#2452ca9e80e90180de4591960a3a338151c94446"
+ integrity sha512-P7PI2nQs7BfjkEATgVtQK3ix1DqIYBiDsVo9nSwJcG2vqq+Mf2PnnuPtU6/ZQBoUNhMbFCd8XCaxsPnE6XqnEA==
dependencies:
"@babel/runtime" "^7.12.5"
another-json "^0.2.0"
@@ -6817,7 +6966,7 @@ mime-db@1.52.0:
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
-mime-types@^2.1.12, mime-types@~2.1.19:
+mime-types@^2.1.12, mime-types@^2.1.34, mime-types@~2.1.19:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
@@ -6895,10 +7044,10 @@ murmurhash-js@^1.0.0:
resolved "https://registry.yarnpkg.com/murmurhash-js/-/murmurhash-js-1.0.0.tgz#b06278e21fc6c37fa5313732b0412bcb6ae15f51"
integrity sha1-sGJ44h/Gw3+lMTcysEEry2rhX1E=
-nanoid@^3.3.1:
- version "3.3.3"
- resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25"
- integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==
+nanoid@^3.3.3:
+ version "3.3.4"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
+ integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
nanomatch@^1.2.9:
version "1.2.13"
@@ -6962,10 +7111,10 @@ node-int64@^0.4.0:
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=
-node-releases@^2.0.2:
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.3.tgz#225ee7488e4a5e636da8da52854844f9d716ca96"
- integrity sha512-maHFz6OLqYxz+VQyCAtA3PTX4UP/53pa05fyDNc9CwjvJ0yEh6+xBwKsgCxMNhS8taUKBFYxfuiaD9U/55iFaw==
+node-releases@^2.0.3:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.4.tgz#f38252370c43854dc48aa431c766c6c398f40476"
+ integrity sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==
normalize-package-data@^2.5.0:
version "2.5.0"
@@ -7242,11 +7391,11 @@ p-map@^4.0.0:
aggregate-error "^3.0.0"
p-retry@^4.5.0:
- version "4.6.1"
- resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.1.tgz#8fcddd5cdf7a67a0911a9cf2ef0e5df7f602316c"
- integrity sha512-e2xXGNhZOZ0lfgR9kL34iGlU8N/KO0xZnQxVEwdeOvpqNDQfdnxIYizvWtK8RglUa3bGqI8g0R/BdfzLMxRkiA==
+ version "4.6.2"
+ resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16"
+ integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==
dependencies:
- "@types/retry" "^0.12.0"
+ "@types/retry" "0.12.0"
retry "^0.13.1"
p-try@^1.0.0:
@@ -7350,6 +7499,11 @@ path-parse@^1.0.6, path-parse@^1.0.7:
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+path-to-regexp@^6.2.0:
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.1.tgz#d54934d6798eb9e5ef14e7af7962c945906918e5"
+ integrity sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==
+
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
@@ -7512,11 +7666,11 @@ postcss@^7.0.14, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0.
source-map "^0.6.1"
postcss@^8.3.11:
- version "8.4.12"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.12.tgz#1e7de78733b28970fa4743f7da6f3763648b1905"
- integrity sha512-lg6eITwYe9v6Hr5CncVbK70SoioNQIq81nsaG86ev5hAidQvmOeETBqs7jm43K2F5/Ley3ytDtriImV6TpNiSg==
+ version "8.4.13"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.13.tgz#7c87bc268e79f7f86524235821dfdf9f73e5d575"
+ integrity sha512-jtL6eTBrza5MPzy8oJLFuUscHDXTV5KcLlqAWHl5q5WYRfnNRGSmOZmOZ1T6Gy7A99mOZfqungmZMpMmCVJ8ZA==
dependencies:
- nanoid "^3.3.1"
+ nanoid "^3.3.3"
picocolors "^1.0.0"
source-map-js "^1.0.2"
@@ -7634,13 +7788,13 @@ punycode@^2.1.0, punycode@^2.1.1:
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
pvtsutils@^1.2.1, pvtsutils@^1.2.2:
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.2.2.tgz#62ef6bc0513cbc255ee02574dedeaa41272d6101"
- integrity sha512-OALo5ZEdqiI127i64+CXwkCOyFHUA+tCQgaUO/MvRDFXWPr53f2sx28ECNztUEzuyu5xvuuD1EB/szg9mwJoGA==
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.0.tgz#3bb61f758c4c5ccd48a21a5b4943bfef643de47f"
+ integrity sha512-q/BKu90CcOTSxuaoUwfAbLReg2jwlXjKpUO5htADXdDmz/XG4rIkgvA5xkc24td2SCg3vcoH9182TEceK2Zn0g==
dependencies:
- tslib "^2.3.1"
+ tslib "^2.4.0"
-pvutils@latest:
+pvutils@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.1.3.tgz#f35fc1d27e7cd3dfbd39c0826d173e806a03f5a3"
integrity sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==
@@ -7680,6 +7834,13 @@ queue-microtask@^1.2.2:
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
+queue@6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/queue/-/queue-6.0.2.tgz#b91525283e2315c7553d2efa18d83e76432fed65"
+ integrity sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==
+ dependencies:
+ inherits "~2.0.3"
+
quick-lru@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
@@ -7724,11 +7885,9 @@ raw-loader@^4.0.2:
schema-utils "^3.0.0"
re-resizable@^6.9.0:
- version "6.9.6"
- resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.9.6.tgz#b95d37e3821481b56ddfb1e12862940a791e827d"
- integrity sha512-0xYKS5+Z0zk+vICQlcZW+g54CcJTTmHluA7JUUgvERDxnKAnytylcyPsA+BSFi759s5hPlHmBRegFrwXs2FuBQ==
- dependencies:
- fast-memoize "^2.5.1"
+ version "6.9.9"
+ resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.9.9.tgz#99e8b31c67a62115dc9c5394b7e55892265be216"
+ integrity sha512-l+MBlKZffv/SicxDySKEEh42hR6m5bAHfNu3Tvxks2c4Ah+ldnWjfnVRwxo/nxF27SsUsxDS0raAzFuJNKABXA==
react-beautiful-dnd@^13.1.0:
version "13.1.0"
@@ -7765,16 +7924,16 @@ react-dom@17.0.2:
scheduler "^0.20.2"
react-focus-lock@^2.5.1:
- version "2.8.1"
- resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.8.1.tgz#a28f06a4ef5eab7d4ef0d859512772ec1331d529"
- integrity sha512-4kb9I7JIiBm0EJ+CsIBQ+T1t5qtmwPRbFGYFQ0t2q2qIpbFbYTHDjnjJVFB7oMBtXityEOQehblJPjqSIf3Amg==
+ version "2.9.0"
+ resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.9.0.tgz#c148fcadb78cc86968c722b0ed7369aa45585f1c"
+ integrity sha512-MF4uqKm77jkz1gn5t2BAnHeWWsDevZofrCxp2utDls0FX7pW/F1cn7Xi7pSpnqxCP1JL2okS8tcFEFIfzjJcIw==
dependencies:
"@babel/runtime" "^7.0.0"
- focus-lock "^0.10.2"
+ focus-lock "^0.11.0"
prop-types "^15.6.2"
react-clientside-effect "^1.2.5"
- use-callback-ref "^1.2.5"
- use-sidecar "^1.0.5"
+ use-callback-ref "^1.3.0"
+ use-sidecar "^1.1.2"
react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
@@ -7782,9 +7941,9 @@ react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0:
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
"react-is@^16.12.0 || ^17.0.0 || ^18.0.0":
- version "18.0.0"
- resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.0.0.tgz#026f6c4a27dbe33bf4a35655b9e1327c4e55e3f5"
- integrity sha512-yUcBYdBBbo3QiPsgYDcfQcIkGZHfxOaoE6HLSnr1sPzMhdyxusbfKOSUbSd/ocGi32dxcj366PsTj+5oggeKKw==
+ version "18.1.0"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67"
+ integrity sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==
react-is@^17.0.0, react-is@^17.0.1, react-is@^17.0.2:
version "17.0.2"
@@ -8202,7 +8361,7 @@ rxjs@^7.5.1:
dependencies:
tslib "^2.1.0"
-safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
+safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
@@ -8459,7 +8618,7 @@ source-map-url@^0.4.0:
resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56"
integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==
-source-map@^0.5.0, source-map@^0.5.6:
+source-map@^0.5.6:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
@@ -8598,29 +8757,31 @@ string.prototype.repeat@^0.2.0:
integrity sha1-q6Nt4I3O5qWjN9SbLqHaGyj8Ds8=
string.prototype.trim@^1.2.1:
- version "1.2.5"
- resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.5.tgz#a587bcc8bfad8cb9829a577f5de30dd170c1682c"
- integrity sha512-Lnh17webJVsD6ECeovpVN17RlAKjmz4rF9S+8Y45CkMc/ufVpTkU3vZIyIC7sllQ1FCvObZnnCdNs/HXTUOTlg==
+ version "1.2.6"
+ resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.6.tgz#824960787db37a9e24711802ed0c1d1c0254f83e"
+ integrity sha512-8lMR2m+U0VJTPp6JjvJTtGyc4FIGq9CdRt7O9p6T0e6K4vjU+OP+SQJpbe/SBmRcCUIvNUnjsbmY6lnMp8MhsQ==
dependencies:
call-bind "^1.0.2"
- define-properties "^1.1.3"
- es-abstract "^1.19.1"
+ define-properties "^1.1.4"
+ es-abstract "^1.19.5"
string.prototype.trimend@^1.0.4:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80"
- integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz#914a65baaab25fbdd4ee291ca7dde57e869cb8d0"
+ integrity sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==
dependencies:
call-bind "^1.0.2"
- define-properties "^1.1.3"
+ define-properties "^1.1.4"
+ es-abstract "^1.19.5"
string.prototype.trimstart@^1.0.4:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed"
- integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz#5466d93ba58cfa2134839f81d7f42437e8c01fef"
+ integrity sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==
dependencies:
call-bind "^1.0.2"
- define-properties "^1.1.3"
+ define-properties "^1.1.4"
+ es-abstract "^1.19.5"
string_decoder@^1.1.1:
version "1.3.0"
@@ -9001,10 +9162,10 @@ tslib@^1.8.1, tslib@^1.9.3:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
-tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.1:
- version "2.3.1"
- resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
- integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
+tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.1, tslib@^2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
+ integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
tsutils@^3.21.0:
version "3.21.0"
@@ -9102,13 +9263,13 @@ ua-parser-js@^0.7.30:
integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==
unbox-primitive@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471"
- integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"
+ integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==
dependencies:
- function-bind "^1.1.1"
- has-bigints "^1.0.1"
- has-symbols "^1.0.2"
+ call-bind "^1.0.2"
+ has-bigints "^1.0.2"
+ has-symbols "^1.0.3"
which-boxed-primitive "^1.0.2"
unhomoglyph@^1.0.6:
@@ -9228,7 +9389,7 @@ url@^0.11.0:
punycode "1.3.2"
querystring "0.2.0"
-use-callback-ref@^1.2.5:
+use-callback-ref@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.0.tgz#772199899b9c9a50526fedc4993fc7fa1f7e32d5"
integrity sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==
@@ -9240,7 +9401,7 @@ use-memo-one@^1.1.1:
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.5:
+use-sidecar@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2"
integrity sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==
@@ -9381,9 +9542,9 @@ webidl-conversions@^6.1.0:
integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==
what-input@^5.2.10:
- version "5.2.10"
- resolved "https://registry.yarnpkg.com/what-input/-/what-input-5.2.10.tgz#f79f5b65cf95d75e55e6d580bb0a6b98174cad4e"
- integrity sha512-7AQoIMGq7uU8esmKniOtZG3A+pzlwgeyFpkS3f/yzRbxknSL68tvn5gjE6bZ4OMFxCPjpaBd2udUTqlZ0HwrXQ==
+ version "5.2.11"
+ resolved "https://registry.yarnpkg.com/what-input/-/what-input-5.2.11.tgz#09075570be5792ca542ebf34db6ba43790270e88"
+ integrity sha512-XmxIyHvy0vh+Gi/WB04encFUi1CapO6NE2eevts5qXroexlJXSKkFjkBW9HADmi7I72f8oRLhUZeCQOBEVJhQA==
whatwg-encoding@^1.0.5:
version "1.0.5"
@@ -9487,7 +9648,7 @@ which@^1.2.9, which@^1.3.1:
dependencies:
isexe "^2.0.0"
-which@^2.0.1:
+which@^2.0.1, which@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
@@ -9546,6 +9707,11 @@ ws@^7.4.6:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.7.tgz#9e0ac77ee50af70d58326ecff7e85eb3fa375e67"
integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==
+ws@^8.0.0:
+ version "8.6.0"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.6.0.tgz#e5e9f1d9e7ff88083d0c0dd8281ea662a42c9c23"
+ integrity sha512-AzmM3aH3gk0aX7/rZLYvjdvZooofDu3fFOzGqcSnQ1tOcTWwhM/o+q++E8mAyVVIyUdajrkzWUGftaVSDLn1bw==
+
xml-name-validator@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
@@ -9581,6 +9747,11 @@ yaml@^1.10.0:
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
+yaml@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.1.0.tgz#96ba62ff4dd990c0eb16bd96c6254a085d288b80"
+ integrity sha512-OuAINfTsoJrY5H7CBWnKZhX6nZciXBydrMtTHr1dC4nP40X5jyTIVlogZHxSlVZM8zSgXRfgZGsaHF4+pV+JRw==
+
yargs-parser@^13.1.2:
version "13.1.2"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"