Merge branch 'develop' into t3chguy/fix/27078
This commit is contained in:
commit
525cc6facd
473 changed files with 6113 additions and 19397 deletions
|
@ -10,24 +10,29 @@ inputs:
|
|||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Download current version for its old bundles
|
||||
id: current_download
|
||||
- name: Download release tarball
|
||||
uses: robinraju/release-downloader@a96f54c1b5f5e09e47d9504526e96febd949d4c2 # v1
|
||||
with:
|
||||
tag: steps.current_version.outputs.version
|
||||
tag: ${{ inputs.tag }}
|
||||
fileName: element-*.tar.gz*
|
||||
out-file-path: ${{ runner.temp }}/download-verify-element-tarball
|
||||
|
||||
- name: Verify tarball
|
||||
shell: bash
|
||||
run: gpg --verify element-*.tar.gz.asc element-*.tar.gz
|
||||
working-directory: ${{ runner.temp }}/download-verify-element-tarball
|
||||
|
||||
- name: Extract tarball
|
||||
run: tar xvzf element-*.tar.gz -C webapp --strip-components=1
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir webapp
|
||||
tar xvzf element-*.tar.gz -C webapp --strip-components=1
|
||||
working-directory: ${{ runner.temp }}/download-verify-element-tarball
|
||||
|
||||
- name: Move webapp to out-file-path
|
||||
shell: bash
|
||||
run: mv ${{ runner.temp }}/download-verify-element-tarball/webapp ${{ inputs.out-file-path }}
|
||||
|
||||
- name: Clean up temp directory
|
||||
shell: bash
|
||||
run: rm -R ${{ runner.temp }}/download-verify-element-tarball
|
||||
|
|
22
.github/workflows/deploy.yml
vendored
22
.github/workflows/deploy.yml
vendored
|
@ -1,7 +1,8 @@
|
|||
# Manual deploy workflow for deploying to app.element.io & staging.element.io
|
||||
# Runs automatically for staging.element.io when an RC or Release is published
|
||||
# Note: Does *NOT* run automatically for app.element.io so that it gets tested on staging.element.io beforehand
|
||||
name: Build and Deploy ${{ inputs.site || 'staging.element.io' }}
|
||||
name: Deploy release
|
||||
run-name: Deploy ${{ github.ref_name }} to ${{ inputs.site || 'staging.element.io' }}
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
@ -28,37 +29,40 @@ jobs:
|
|||
env:
|
||||
SITE: ${{ inputs.site || 'staging.element.io' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Load GPG key
|
||||
run: |
|
||||
curl https://packages.element.io/element-release-key.gpg | gpg --import
|
||||
gpg -k "$GPG_FINGERPRINT"
|
||||
env:
|
||||
GPG_FINGERPRINT: ${{ secrets.GPG_FINGERPRINT }}
|
||||
GPG_FINGERPRINT: ${{ vars.GPG_FINGERPRINT }}
|
||||
|
||||
- name: Check current version on deployment
|
||||
id: current_version
|
||||
run: |
|
||||
echo "version=$(curl -s https://$SITE/version)" >> $GITHUB_OUTPUT
|
||||
version=$(curl -s https://$SITE/version)
|
||||
echo "version=${version#v}" >> $GITHUB_OUTPUT
|
||||
|
||||
# The current version bundle melding dance is skipped if the version we're deploying is the same
|
||||
# as then we're just doing a re-deploy of the same version with potentially different configs.
|
||||
- name: Download current version for its old bundles
|
||||
id: current_download
|
||||
if: steps.current_version.outputs.version != github.ref_name
|
||||
uses: element-hq/element-web/.github/actions/download-verify-element-tarball@${{ github.ref_name }}
|
||||
uses: ./.github/actions/download-verify-element-tarball
|
||||
with:
|
||||
tag: steps.current_version.outputs.version
|
||||
out-file-path: current_version
|
||||
tag: v${{ steps.current_version.outputs.version }}
|
||||
out-file-path: _current_version
|
||||
|
||||
- name: Download target version
|
||||
uses: element-hq/element-web/.github/actions/download-verify-element-tarball@${{ github.ref_name }}
|
||||
uses: ./.github/actions/download-verify-element-tarball
|
||||
with:
|
||||
tag: ${{ github.ref_name }}
|
||||
out-file-path: _deploy
|
||||
|
||||
- name: Merge current bundles into target
|
||||
if: steps.current_download.outcome == 'success'
|
||||
run: cp -vnpr current_version/bundles/* _deploy/bundles/
|
||||
run: cp -vnpr _current_version/bundles/* _deploy/bundles/
|
||||
|
||||
- name: Copy config
|
||||
run: cp element.io/app/config.json _deploy/config.json
|
||||
|
@ -73,7 +77,7 @@ jobs:
|
|||
uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
running-workflow-name: "Build and Deploy ${{ env.SITE }}"
|
||||
running-workflow-name: "Deploy to Cloudflare Pages"
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
wait-interval: 10
|
||||
check-regexp: ^((?!SonarCloud|SonarQube|issue|board|label|Release|prepare|GitHub Pages).)*$
|
||||
|
|
4
.github/workflows/dockerhub.yaml
vendored
4
.github/workflows/dockerhub.yaml
vendored
|
@ -39,7 +39,7 @@ jobs:
|
|||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5
|
||||
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5
|
||||
with:
|
||||
images: |
|
||||
vectorim/element-web
|
||||
|
@ -51,7 +51,7 @@ jobs:
|
|||
|
||||
- name: Build and push
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6
|
||||
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
|
|
2
.github/workflows/pull_request.yaml
vendored
2
.github/workflows/pull_request.yaml
vendored
|
@ -9,6 +9,6 @@ jobs:
|
|||
action:
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/pull_request.yaml@develop
|
||||
permissions:
|
||||
pull-requests: read
|
||||
pull-requests: write
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
|
1
.github/workflows/release.yml
vendored
1
.github/workflows/release.yml
vendored
|
@ -18,6 +18,7 @@ jobs:
|
|||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: read
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
|
|
15
.github/workflows/release_prepare.yml
vendored
15
.github/workflows/release_prepare.yml
vendored
|
@ -19,8 +19,23 @@ on:
|
|||
default: true
|
||||
permissions: {} # Uses ELEMENT_BOT_TOKEN instead
|
||||
jobs:
|
||||
checks:
|
||||
name: Sanity checks
|
||||
strategy:
|
||||
matrix:
|
||||
repo:
|
||||
- matrix-org/matrix-js-sdk
|
||||
- element-hq/element-web
|
||||
- element-hq/element-desktop
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
with:
|
||||
repository: ${{ matrix.repo }}
|
||||
|
||||
prepare:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: checks
|
||||
env:
|
||||
# The order is specified bottom-up to avoid any races for allchange
|
||||
REPOS: matrix-js-sdk element-web element-desktop
|
||||
|
|
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
|
@ -104,7 +104,7 @@ jobs:
|
|||
|
||||
- name: Skip SonarCloud in merge queue
|
||||
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
|
||||
uses: guibranco/github-status-action-v2@1f26a0237cd1a57626fbb5a0eb2494c9b8797d07
|
||||
uses: guibranco/github-status-action-v2@66088c44e212a906c32a047529a213d81809ec1c
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
|
|
30
CHANGELOG.md
30
CHANGELOG.md
|
@ -1,3 +1,33 @@
|
|||
Changes in [1.11.87](https://github.com/element-hq/element-web/releases/tag/v1.11.87) (2024-12-03)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Send and respect MSC4230 is\_animated flag ([#28513](https://github.com/element-hq/element-web/pull/28513)). Contributed by @t3chguy.
|
||||
* Display a warning when an unverified user's identity changes ([#28211](https://github.com/element-hq/element-web/pull/28211)). Contributed by @uhoreg.
|
||||
* Swap out Twitter link for Mastodon on auth footer ([#28508](https://github.com/element-hq/element-web/pull/28508)). Contributed by @t3chguy.
|
||||
* Consider `org.matrix.msc3417.call` as video room in create room dialog ([#28497](https://github.com/element-hq/element-web/pull/28497)). Contributed by @t3chguy.
|
||||
* Standardise icons using Compound Design Tokens ([#28217](https://github.com/element-hq/element-web/pull/28217)). Contributed by @t3chguy.
|
||||
* Start sending stable `m.marked_unread` events ([#28478](https://github.com/element-hq/element-web/pull/28478)). Contributed by @tulir.
|
||||
* Upgrade to compound-design-tokens v2 ([#28471](https://github.com/element-hq/element-web/pull/28471)). Contributed by @t3chguy.
|
||||
* Standardise icons using Compound Design Tokens ([#28286](https://github.com/element-hq/element-web/pull/28286)). Contributed by @t3chguy.
|
||||
* Remove reply fallbacks as per merged MSC2781 ([#28406](https://github.com/element-hq/element-web/pull/28406)). Contributed by @t3chguy.
|
||||
* Use React Suspense when rendering async modals ([#28386](https://github.com/element-hq/element-web/pull/28386)). Contributed by @t3chguy.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Add spinner when room encryption is loading in room settings ([#28535](https://github.com/element-hq/element-web/pull/28535)). Contributed by @florianduros.
|
||||
* Fix getOidcCallbackUrl for Element Desktop ([#28521](https://github.com/element-hq/element-web/pull/28521)). Contributed by @t3chguy.
|
||||
* Filter out redacted poll votes to avoid crashing the Poll widget ([#28498](https://github.com/element-hq/element-web/pull/28498)). Contributed by @t3chguy.
|
||||
* Fix force tab complete not working since switching to React 18 createRoot API ([#28505](https://github.com/element-hq/element-web/pull/28505)). Contributed by @t3chguy.
|
||||
* Fix media captions in bubble layout ([#28480](https://github.com/element-hq/element-web/pull/28480)). Contributed by @tulir.
|
||||
* Reset cross-signing before backup when resetting both ([#28402](https://github.com/element-hq/element-web/pull/28402)). Contributed by @uhoreg.
|
||||
* Listen to events so that encryption icon updates when status changes ([#28407](https://github.com/element-hq/element-web/pull/28407)). Contributed by @uhoreg.
|
||||
* Check that the file the user chose has a MIME type of `image/*` ([#28467](https://github.com/element-hq/element-web/pull/28467)). Contributed by @t3chguy.
|
||||
* Fix download button size in message action bar ([#28472](https://github.com/element-hq/element-web/pull/28472)). Contributed by @t3chguy.
|
||||
* Allow tab completing users in brackets ([#28460](https://github.com/element-hq/element-web/pull/28460)). Contributed by @t3chguy.
|
||||
* Fix React 18 strict mode breaking spotlight dialog ([#28452](https://github.com/element-hq/element-web/pull/28452)). Contributed by @MidhunSureshR.
|
||||
|
||||
|
||||
Changes in [1.11.86](https://github.com/element-hq/element-web/releases/tag/v1.11.86) (2024-11-19)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
// Stub out FontManager for tests as it doesn't validate anything we don't already know given
|
||||
// our fixed test environment and it requires the installation of node-canvas.
|
||||
|
||||
module.exports = {
|
||||
fixupColorFonts: () => Promise.resolve(),
|
||||
};
|
|
@ -592,4 +592,3 @@ The following are undocumented or intended for developer use only.
|
|||
2. `sync_timeline_limit`
|
||||
3. `dangerously_allow_unsafe_and_insecure_passwords`
|
||||
4. `latex_maths_delims`: An optional setting to override the default delimiters used for maths parsing. See https://github.com/matrix-org/matrix-react-sdk/pull/5939 for details. Only used when `feature_latex_maths` is enabled.
|
||||
5. `voice_broadcast.chunk_length`: Target chunk length in seconds for the Voice Broadcast feature currently under development.
|
||||
|
|
|
@ -32,7 +32,6 @@ const config: Config = {
|
|||
"decoderWorker\\.min\\.wasm": "<rootDir>/__mocks__/empty.js",
|
||||
"waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
||||
"context-filter-polyfill": "<rootDir>/__mocks__/empty.js",
|
||||
"FontManager.ts": "<rootDir>/__mocks__/FontManager.js",
|
||||
"workers/(.+)Factory": "<rootDir>/__mocks__/workerFactoryMock.js",
|
||||
"^!!raw-loader!.*": "jest-raw-loader",
|
||||
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",
|
||||
|
|
17
package.json
17
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "element-web",
|
||||
"version": "1.11.86",
|
||||
"version": "1.11.87",
|
||||
"description": "A feature-rich client for Matrix.org",
|
||||
"author": "New Vector Ltd.",
|
||||
"repository": {
|
||||
|
@ -64,7 +64,7 @@
|
|||
"test:playwright:open": "yarn test:playwright --ui",
|
||||
"test:playwright:screenshots": "yarn test:playwright:screenshots:build && yarn test:playwright:screenshots:run",
|
||||
"test:playwright:screenshots:build": "docker build playwright -t element-web-playwright",
|
||||
"test:playwright:screenshots:run": "docker run --rm --network host -e BASE_URL -e CI -v $(pwd):/work/ -v $(node -e 'console.log(require(`path`).dirname(require.resolve(`matrix-js-sdk/package.json`)))'):/work/node_modules/matrix-js-sdk -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it element-web-playwright",
|
||||
"test:playwright:screenshots:run": "docker run --rm --network host -e BASE_URL -e CI -v $(pwd):/work/ -v $(node -e 'console.log(require(`path`).dirname(require.resolve(`matrix-js-sdk/package.json`)))'):/work/node_modules/matrix-js-sdk -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it element-web-playwright --grep @screenshot",
|
||||
"coverage": "yarn test --coverage",
|
||||
"analyse:unused-exports": "ts-node ./scripts/analyse_unused_exports.ts",
|
||||
"analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp",
|
||||
|
@ -73,7 +73,7 @@
|
|||
"resolutions": {
|
||||
"oidc-client-ts": "3.1.0",
|
||||
"jwt-decode": "4.0.0",
|
||||
"caniuse-lite": "1.0.30001679",
|
||||
"caniuse-lite": "1.0.30001684",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
|
||||
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
|
||||
},
|
||||
|
@ -114,10 +114,10 @@
|
|||
"jsrsasign": "^11.0.0",
|
||||
"jszip": "^3.7.0",
|
||||
"katex": "^0.16.0",
|
||||
"linkify-element": "4.1.3",
|
||||
"linkify-react": "4.1.3",
|
||||
"linkify-string": "4.1.3",
|
||||
"linkifyjs": "4.1.3",
|
||||
"linkify-element": "4.1.4",
|
||||
"linkify-react": "4.1.4",
|
||||
"linkify-string": "4.1.4",
|
||||
"linkifyjs": "4.1.4",
|
||||
"lodash": "^4.17.21",
|
||||
"maplibre-gl": "^4.0.0",
|
||||
"matrix-encrypt-attachment": "^1.0.3",
|
||||
|
@ -268,11 +268,12 @@
|
|||
"postcss-preset-env": "^10.0.0",
|
||||
"postcss-scss": "^4.0.4",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"prettier": "3.3.3",
|
||||
"prettier": "3.4.1",
|
||||
"process": "^0.11.10",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.0",
|
||||
"semver": "^7.5.2",
|
||||
"source-map-loader": "^5.0.0",
|
||||
"stylelint": "^16.1.0",
|
||||
"stylelint-config-standard": "^36.0.0",
|
||||
"stylelint-scss": "^6.0.0",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM mcr.microsoft.com/playwright:v1.48.2-jammy
|
||||
FROM mcr.microsoft.com/playwright:v1.49.0-jammy
|
||||
|
||||
WORKDIR /work
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
|||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
|
||||
test(`shows error page if browser lacks Intl support`, async ({ page }) => {
|
||||
test(`shows error page if browser lacks Intl support`, { tag: "@screenshot" }, async ({ page }) => {
|
||||
await page.addInitScript({ content: `delete window.Intl;` });
|
||||
await page.goto("/");
|
||||
|
||||
|
@ -21,7 +21,7 @@ test(`shows error page if browser lacks Intl support`, async ({ page }) => {
|
|||
await expect(page).toMatchScreenshot("unsupported-browser.png");
|
||||
});
|
||||
|
||||
test(`shows error page if browser lacks WebAssembly support`, async ({ page }) => {
|
||||
test(`shows error page if browser lacks WebAssembly support`, { tag: "@screenshot" }, async ({ page }) => {
|
||||
await page.addInitScript({ content: `delete window.WebAssembly;` });
|
||||
await page.goto("/");
|
||||
|
||||
|
|
|
@ -134,18 +134,22 @@ test.describe("Audio player", () => {
|
|||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("should be correctly rendered - light theme", async ({ page, app }) => {
|
||||
test("should be correctly rendered - light theme", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||
await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg");
|
||||
await takeSnapshots(page, app, "Selected EventTile of audio player (light theme)");
|
||||
});
|
||||
|
||||
test("should be correctly rendered - light theme with monospace font", async ({ page, app }) => {
|
||||
test(
|
||||
"should be correctly rendered - light theme with monospace font",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app }) => {
|
||||
await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg");
|
||||
|
||||
await takeSnapshots(page, app, "Selected EventTile of audio player (light theme, monospace font)", true); // Enable monospace
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("should be correctly rendered - high contrast theme", async ({ page, app }) => {
|
||||
test("should be correctly rendered - high contrast theme", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||
// Disable system theme in case ThemeWatcher enables the theme automatically,
|
||||
// so that the high contrast theme can be enabled
|
||||
await app.settings.setValue("use_system_theme", null, SettingLevel.DEVICE, false);
|
||||
|
@ -161,7 +165,7 @@ test.describe("Audio player", () => {
|
|||
await takeSnapshots(page, app, "Selected EventTile of audio player (high contrast)");
|
||||
});
|
||||
|
||||
test("should be correctly rendered - dark theme", async ({ page, app }) => {
|
||||
test("should be correctly rendered - dark theme", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||
// Enable dark theme
|
||||
await app.settings.setValue("theme", null, SettingLevel.ACCOUNT, "dark");
|
||||
|
||||
|
@ -207,7 +211,10 @@ test.describe("Audio player", () => {
|
|||
expect(download.suggestedFilename()).toBe("1sec.ogg");
|
||||
});
|
||||
|
||||
test("should support replying to audio file with another audio file", async ({ page, app }) => {
|
||||
test(
|
||||
"should support replying to audio file with another audio file",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app }) => {
|
||||
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
||||
|
||||
// Assert the audio player is rendered
|
||||
|
@ -230,9 +237,13 @@ test.describe("Audio player", () => {
|
|||
await expect(button.locator(".mx_MFileBody_info_filename")).toBeVisible();
|
||||
|
||||
await takeSnapshots(page, app, "Selected EventTile of audio player with a reply");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("should support creating a reply chain with multiple audio files", async ({ page, app, user }) => {
|
||||
test(
|
||||
"should support creating a reply chain with multiple audio files",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user }) => {
|
||||
// Note: "mx_ReplyChain" element is used not only for replies which
|
||||
// create a reply chain, but also for a single reply without a replied
|
||||
// message. This test checks whether a reply chain which consists of
|
||||
|
@ -293,7 +304,8 @@ test.describe("Audio player", () => {
|
|||
|
||||
// Take snapshots
|
||||
await takeSnapshots(page, app, "Selected EventTile of audio player with a reply chain");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("should be rendered, play, and support replying on a thread", async ({ page, app }) => {
|
||||
await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg");
|
||||
|
|
|
@ -89,7 +89,10 @@ test.describe("HTML Export", () => {
|
|||
},
|
||||
});
|
||||
|
||||
test("should export html successfully and match screenshot", async ({ page, app, room }) => {
|
||||
test(
|
||||
"should export html successfully and match screenshot",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, room }) => {
|
||||
// Set a fixed time rather than masking off the line with the time in it: we don't need to worry
|
||||
// about the width changing and we can actually test this line looks correct.
|
||||
page.clock.setSystemTime(new Date("2024-01-01T00:00:00Z"));
|
||||
|
@ -127,5 +130,6 @@ test.describe("HTML Export", () => {
|
|||
page.locator(".mx_MessageTimestamp"),
|
||||
],
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -204,12 +204,10 @@ test.describe("Cryptography", function () {
|
|||
await expect(page.locator(".mx_Dialog")).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("creating a DM should work, being e2e-encrypted / user verification", async ({
|
||||
page,
|
||||
app,
|
||||
bot: bob,
|
||||
user: aliceCredentials,
|
||||
}) => {
|
||||
test(
|
||||
"creating a DM should work, being e2e-encrypted / user verification",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, bot: bob, user: aliceCredentials }) => {
|
||||
await app.client.bootstrapCrossSigning(aliceCredentials);
|
||||
await startDMWithBob(page, bob);
|
||||
// send first message
|
||||
|
@ -227,7 +225,8 @@ test.describe("Cryptography", function () {
|
|||
|
||||
// Take a snapshot of RoomSummaryCard with a verified E2EE icon
|
||||
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("RoomSummaryCard-with-verified-e2ee.png");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("should allow verification when there is no existing DM", async ({
|
||||
page,
|
||||
|
|
|
@ -67,6 +67,9 @@ test.describe("Cryptography", function () {
|
|||
await page.locator(".mx_AuthPage").getByRole("button", { name: "I'll verify later" }).click();
|
||||
await app.viewRoomByName("Test room");
|
||||
|
||||
// In this case, the call to cryptoApi.isEncryptionEnabledInRoom is taking a long time to resolve
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// There should be two historical events in the timeline
|
||||
const tiles = await page.locator(".mx_EventTile").all();
|
||||
expect(tiles.length).toBeGreaterThanOrEqual(2);
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
logOutOfElement,
|
||||
verify,
|
||||
} from "./utils";
|
||||
import { bootstrapCrossSigningForClient } from "../../pages/client.ts";
|
||||
|
||||
test.describe("Cryptography", function () {
|
||||
test.use({
|
||||
|
@ -307,5 +308,30 @@ test.describe("Cryptography", function () {
|
|||
const penultimate = page.locator(".mx_EventTile").filter({ hasText: "test encrypted from verified" });
|
||||
await expect(penultimate.locator(".mx_EventTile_e2eIcon")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("should show correct shields on events sent by users with changed identity", async ({
|
||||
page,
|
||||
app,
|
||||
bot: bob,
|
||||
homeserver,
|
||||
}) => {
|
||||
// Verify Bob
|
||||
await verify(app, bob);
|
||||
|
||||
// Bob logs in a new device and resets cross-signing
|
||||
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
|
||||
await bootstrapCrossSigningForClient(await bobSecondDevice.prepareClient(), bob.credentials, true);
|
||||
|
||||
/* should show an error for a message from a previously verified device */
|
||||
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified");
|
||||
const last = page.locator(".mx_EventTile_last");
|
||||
await expect(last).toContainText("test encrypted from user that was previously verified");
|
||||
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
|
||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||
await lastE2eIcon.focus();
|
||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
||||
"Sender's verified identity has changed",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -66,7 +66,10 @@ test.describe("Editing", () => {
|
|||
botCreateOpts: { displayName: "Bob" },
|
||||
});
|
||||
|
||||
test("should render and interact with the message edit history dialog", async ({ page, user, app, room }) => {
|
||||
test(
|
||||
"should render and interact with the message edit history dialog",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, user, app, room }) => {
|
||||
// Click the "Remove" button on the message edit history dialog
|
||||
const clickButtonRemove = async (locator: Locator) => {
|
||||
const eventTileLine = locator.locator(".mx_EventTile_line");
|
||||
|
@ -185,7 +188,8 @@ test.describe("Editing", () => {
|
|||
.locator(".mx_RoomView_MessageList")
|
||||
.locator(".mx_EventTile_last .mx_RedactedBody", { hasText: "Message deleted" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("should render 'View Source' button in developer mode on the message edit history dialog", async ({
|
||||
page,
|
||||
|
|
|
@ -25,7 +25,7 @@ test.describe("Image Upload", () => {
|
|||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("should show image preview when uploading an image", async ({ page, app }) => {
|
||||
test("should show image preview when uploading an image", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||
await page
|
||||
.locator(".mx_MessageComposer_actions input[type='file']")
|
||||
.setInputFiles("playwright/sample-files/riot.png");
|
||||
|
|
|
@ -26,7 +26,7 @@ test.describe("Forgot Password", () => {
|
|||
}),
|
||||
});
|
||||
|
||||
test("renders properly", async ({ page, homeserver }) => {
|
||||
test("renders properly", { tag: "@screenshot" }, async ({ page, homeserver }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await page.getByRole("link", { name: "Sign in" }).click();
|
||||
|
@ -39,7 +39,7 @@ test.describe("Forgot Password", () => {
|
|||
await expect(page.getByRole("main")).toMatchScreenshot("forgot-password.png");
|
||||
});
|
||||
|
||||
test("renders email verification dialog properly", async ({ page, homeserver }) => {
|
||||
test("renders email verification dialog properly", { tag: "@screenshot" }, async ({ page, homeserver }) => {
|
||||
const user = await homeserver.registerUser(username, password);
|
||||
|
||||
await homeserver.setThreepid(user.userId, "email", email);
|
||||
|
|
|
@ -19,7 +19,7 @@ test.describe("Invite dialog", function () {
|
|||
|
||||
const botName = "BotAlice";
|
||||
|
||||
test("should support inviting a user to a room", async ({ page, app, user, bot }) => {
|
||||
test("should support inviting a user to a room", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||
// Create and view a room
|
||||
await app.client.createRoom({ name: "Test Room" });
|
||||
await app.viewRoomByName("Test Room");
|
||||
|
@ -73,12 +73,17 @@ test.describe("Invite dialog", function () {
|
|||
await expect(page.getByText(`${botName} joined the room`)).toBeVisible();
|
||||
});
|
||||
|
||||
test("should support inviting a user to Direct Messages", async ({ page, app, user, bot }) => {
|
||||
test(
|
||||
"should support inviting a user to Direct Messages",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user, bot }) => {
|
||||
await page.locator(".mx_RoomList").getByRole("button", { name: "Start chat" }).click();
|
||||
|
||||
const other = page.locator(".mx_InviteDialog_other");
|
||||
// Assert that the header is rendered
|
||||
await expect(other.locator(".mx_Dialog_header .mx_Dialog_title").getByText("Direct Messages")).toBeVisible();
|
||||
await expect(
|
||||
other.locator(".mx_Dialog_header .mx_Dialog_title").getByText("Direct Messages"),
|
||||
).toBeVisible();
|
||||
|
||||
// Assert that the bar is rendered
|
||||
await expect(other.locator(".mx_InviteDialog_addressBar")).toBeVisible();
|
||||
|
@ -88,7 +93,9 @@ test.describe("Invite dialog", function () {
|
|||
|
||||
await other.getByTestId("invite-dialog-input").fill(bot.credentials.userId);
|
||||
|
||||
await expect(other.locator(".mx_InviteDialog_tile_nameStack").getByText(bot.credentials.userId)).toBeVisible();
|
||||
await expect(
|
||||
other.locator(".mx_InviteDialog_tile_nameStack").getByText(bot.credentials.userId),
|
||||
).toBeVisible();
|
||||
await other.locator(".mx_InviteDialog_tile_nameStack").getByText(botName).click();
|
||||
|
||||
await expect(
|
||||
|
@ -108,7 +115,10 @@ test.describe("Invite dialog", function () {
|
|||
// TODO: implement the test on room-header.spec.ts
|
||||
const roomHeader = page.locator(".mx_RoomHeader");
|
||||
await roomHeader.locator(".mx_RoomHeader_heading").hover();
|
||||
await expect(roomHeader.locator(".mx_RoomHeader_heading")).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
|
||||
await expect(roomHeader.locator(".mx_RoomHeader_heading")).toHaveCSS(
|
||||
"background-color",
|
||||
"rgba(0, 0, 0, 0)",
|
||||
);
|
||||
|
||||
// Send a message to invite the bots
|
||||
const composer = app.getComposer().locator("[contenteditable]");
|
||||
|
@ -120,5 +130,6 @@ test.describe("Invite dialog", function () {
|
|||
|
||||
// Assert that the message is displayed at the bottom
|
||||
await expect(page.locator(".mx_EventTile_last").getByText("Hello")).toBeVisible();
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -63,7 +63,7 @@ test.describe("Message rendering", () => {
|
|||
{ direction: "ltr", displayName: "Quentin" },
|
||||
{ direction: "rtl", displayName: "كوينتين" },
|
||||
].forEach(({ direction, displayName }) => {
|
||||
test.describe(`with ${direction} display name`, () => {
|
||||
test.describe(`with ${direction} display name`, { tag: "@screenshot" }, () => {
|
||||
test.use({
|
||||
displayName,
|
||||
room: async ({ user, app }, use) => {
|
||||
|
@ -72,14 +72,18 @@ test.describe("Message rendering", () => {
|
|||
},
|
||||
});
|
||||
|
||||
test("should render a basic LTR text message", async ({ page, user, app, room }) => {
|
||||
test(
|
||||
"should render a basic LTR text message",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, user, app, room }) => {
|
||||
await page.goto(`#/room/${room.roomId}`);
|
||||
|
||||
const msgTile = await sendMessage(page, "Hello, world!");
|
||||
await expect(msgTile).toMatchScreenshot(`basic-message-ltr-${direction}displayname.png`, {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("should render an LTR emote", async ({ page, user, app, room }) => {
|
||||
await page.goto(`#/room/${room.roomId}`);
|
||||
|
|
|
@ -24,7 +24,7 @@ test.describe("permalinks", () => {
|
|||
displayName: "Alice",
|
||||
});
|
||||
|
||||
test("shoud render permalinks as expected", async ({ page, app, user, homeserver }) => {
|
||||
test("shoud render permalinks as expected", { tag: "@screenshot" }, async ({ page, app, user, homeserver }) => {
|
||||
const bob = new Bot(page, homeserver, { displayName: "Bob" });
|
||||
const charlotte = new Bot(page, homeserver, { displayName: "Charlotte" });
|
||||
await bob.prepareClient();
|
||||
|
|
|
@ -10,20 +10,22 @@ import { test } from "./index";
|
|||
import { expect } from "../../element-web-test";
|
||||
|
||||
test.describe("Pinned messages", () => {
|
||||
test("should show the empty state when there are no pinned messages", async ({ page, app, room1, util }) => {
|
||||
test(
|
||||
"should show the empty state when there are no pinned messages",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, room1, util }) => {
|
||||
await util.goTo(room1);
|
||||
await util.openRoomInfo();
|
||||
await util.assertPinnedCountInRoomInfo(0);
|
||||
await util.openPinnedMessagesList();
|
||||
await util.assertEmptyPinnedMessagesList();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("should pin one message and to have the pinned message badge in the timeline", async ({
|
||||
page,
|
||||
app,
|
||||
room1,
|
||||
util,
|
||||
}) => {
|
||||
test(
|
||||
"should pin one message and to have the pinned message badge in the timeline",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, room1, util }) => {
|
||||
await util.goTo(room1);
|
||||
await util.receiveMessages(room1, ["Msg1"]);
|
||||
await util.pinMessages(["Msg1"]);
|
||||
|
@ -38,7 +40,8 @@ test.describe("Pinned messages", () => {
|
|||
}
|
||||
`,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("should pin messages and show them in the room info panel", async ({ page, app, room1, util }) => {
|
||||
await util.goTo(room1);
|
||||
|
@ -73,7 +76,7 @@ test.describe("Pinned messages", () => {
|
|||
await util.assertPinnedCountInRoomInfo(2);
|
||||
});
|
||||
|
||||
test("should unpin all messages", async ({ page, app, room1, util }) => {
|
||||
test("should unpin all messages", { tag: "@screenshot" }, async ({ page, app, room1, util }) => {
|
||||
await util.goTo(room1);
|
||||
await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]);
|
||||
await util.pinMessages(["Msg1", "Msg2", "Msg4"]);
|
||||
|
@ -98,7 +101,7 @@ test.describe("Pinned messages", () => {
|
|||
await util.assertPinnedCountInRoomInfo(0);
|
||||
});
|
||||
|
||||
test("should display one message in the banner", async ({ page, app, room1, util }) => {
|
||||
test("should display one message in the banner", { tag: "@screenshot" }, async ({ page, app, room1, util }) => {
|
||||
await util.goTo(room1);
|
||||
await util.receiveMessages(room1, ["Msg1"]);
|
||||
await util.pinMessages(["Msg1"]);
|
||||
|
@ -106,7 +109,7 @@ test.describe("Pinned messages", () => {
|
|||
await expect(util.getBanner()).toMatchScreenshot("pinned-message-banner-1-Msg1.png");
|
||||
});
|
||||
|
||||
test("should display 2 messages in the banner", async ({ page, app, room1, util }) => {
|
||||
test("should display 2 messages in the banner", { tag: "@screenshot" }, async ({ page, app, room1, util }) => {
|
||||
await util.goTo(room1);
|
||||
await util.receiveMessages(room1, ["Msg1", "Msg2"]);
|
||||
await util.pinMessages(["Msg1", "Msg2"]);
|
||||
|
@ -123,7 +126,7 @@ test.describe("Pinned messages", () => {
|
|||
await expect(util.getBanner()).toMatchScreenshot("pinned-message-banner-2-Msg2.png");
|
||||
});
|
||||
|
||||
test("should display 4 messages in the banner", async ({ page, app, room1, util }) => {
|
||||
test("should display 4 messages in the banner", { tag: "@screenshot" }, async ({ page, app, room1, util }) => {
|
||||
await util.goTo(room1);
|
||||
await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]);
|
||||
await util.pinMessages(["Msg1", "Msg2", "Msg3", "Msg4"]);
|
||||
|
|
|
@ -93,7 +93,7 @@ test.describe("Polls", () => {
|
|||
});
|
||||
});
|
||||
|
||||
test("should be creatable and votable", async ({ page, app, bot, user }) => {
|
||||
test("should be creatable and votable", { tag: "@screenshot" }, async ({ page, app, bot, user }) => {
|
||||
const roomId: string = await app.client.createRoom({});
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await page.goto("/#/room/" + roomId);
|
||||
|
@ -219,7 +219,10 @@ test.describe("Polls", () => {
|
|||
await expect(page.locator(".mx_ErrorDialog")).toBeAttached();
|
||||
});
|
||||
|
||||
test("should be displayed correctly in thread panel", async ({ page, app, user, bot, homeserver }) => {
|
||||
test(
|
||||
"should be displayed correctly in thread panel",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user, bot, homeserver }) => {
|
||||
const botCharlie = new Bot(page, homeserver, { displayName: "BotCharlie" });
|
||||
await botCharlie.prepareClient();
|
||||
|
||||
|
@ -229,7 +232,9 @@ test.describe("Polls", () => {
|
|||
await page.goto("/#/room/" + roomId);
|
||||
|
||||
// wait until the bots joined
|
||||
await expect(page.getByText("BotBob and one other were invited and joined")).toBeAttached({ timeout: 10000 });
|
||||
await expect(page.getByText("BotBob and one other were invited and joined")).toBeAttached({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
const locator = await app.openMessageComposerOptions();
|
||||
await locator.getByRole("menuitem", { name: "Poll" }).click();
|
||||
|
@ -274,22 +279,30 @@ test.describe("Polls", () => {
|
|||
|
||||
// and thread view
|
||||
await expect(
|
||||
page.locator(".mx_ThreadView .mx_MPollBody_totalVotes").getByText("2 votes cast. Vote to see the results"),
|
||||
page
|
||||
.locator(".mx_ThreadView .mx_MPollBody_totalVotes")
|
||||
.getByText("2 votes cast. Vote to see the results"),
|
||||
).toBeAttached();
|
||||
|
||||
// Take snapshots of poll on ThreadView
|
||||
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
|
||||
await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='bubble']").first()).toBeVisible();
|
||||
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("ThreadView_with_a_poll_on_bubble_layout.png", {
|
||||
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
|
||||
"ThreadView_with_a_poll_on_bubble_layout.png",
|
||||
{
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group);
|
||||
await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='group']").first()).toBeVisible();
|
||||
|
||||
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("ThreadView_with_a_poll_on_group_layout.png", {
|
||||
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
|
||||
"ThreadView_with_a_poll_on_group_layout.png",
|
||||
{
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const roomViewLocator = page.locator(".mx_RoomView_body");
|
||||
// vote 'Maybe' in the main timeline poll
|
||||
|
@ -321,5 +334,6 @@ test.describe("Polls", () => {
|
|||
|
||||
// and in thread view tile
|
||||
await expectVoteCounts(page.locator(".mx_ThreadView"));
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -38,12 +38,10 @@ test.describe("Email Registration", async () => {
|
|||
await page.goto("/#/register");
|
||||
});
|
||||
|
||||
test("registers an account and lands on the use case selection screen", async ({
|
||||
page,
|
||||
mailhog,
|
||||
request,
|
||||
checkA11y,
|
||||
}) => {
|
||||
test(
|
||||
"registers an account and lands on the use case selection screen",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, mailhog, request, checkA11y }) => {
|
||||
await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible();
|
||||
// Hide the server text as it contains the randomly allocated Homeserver port
|
||||
const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")] };
|
||||
|
@ -67,5 +65,6 @@ test.describe("Email Registration", async () => {
|
|||
await request.get(emailLink); // "Click" the link in the email
|
||||
|
||||
await expect(page.locator(".mx_UseCaseSelection_skip")).toBeVisible();
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -15,7 +15,10 @@ test.describe("Registration", () => {
|
|||
await page.goto("/#/register");
|
||||
});
|
||||
|
||||
test("registers an account and lands on the home screen", async ({ homeserver, page, checkA11y, crypto }) => {
|
||||
test(
|
||||
"registers an account and lands on the home screen",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ homeserver, page, checkA11y, crypto }) => {
|
||||
await page.getByRole("button", { name: "Edit", exact: true }).click();
|
||||
await expect(page.getByRole("button", { name: "Continue", exact: true })).toBeVisible();
|
||||
|
||||
|
@ -29,7 +32,10 @@ test.describe("Registration", () => {
|
|||
|
||||
await expect(page.getByRole("textbox", { name: "Username", exact: true })).toBeVisible();
|
||||
// Hide the server text as it contains the randomly allocated Homeserver port
|
||||
const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")], includeDialogBackground: true };
|
||||
const screenshotOptions = {
|
||||
mask: [page.locator(".mx_ServerPicker_server")],
|
||||
includeDialogBackground: true,
|
||||
};
|
||||
await expect(page).toMatchScreenshot("registration.png", screenshotOptions);
|
||||
await checkA11y();
|
||||
|
||||
|
@ -68,13 +74,14 @@ test.describe("Registration", () => {
|
|||
await page.getByRole("button", { name: "User menu", exact: true }).click();
|
||||
await page.getByRole("menuitem", { name: "All settings", exact: true }).click();
|
||||
await page.getByRole("tab", { name: "Sessions", exact: true }).click();
|
||||
await expect(page.getByTestId("current-session-section").getByTestId("device-metadata-isVerified")).toHaveText(
|
||||
"Verified",
|
||||
);
|
||||
await expect(
|
||||
page.getByTestId("current-session-section").getByTestId("device-metadata-isVerified"),
|
||||
).toHaveText("Verified");
|
||||
|
||||
// check that cross-signing keys have been uploaded.
|
||||
await crypto.assertDeviceIsCrossSigned();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("should require username to fulfil requirements and be available", async ({ homeserver, page }) => {
|
||||
await page.getByRole("button", { name: "Edit", exact: true }).click();
|
||||
|
|
|
@ -18,7 +18,7 @@ test.describe("Release announcement", () => {
|
|||
labsFlags: ["threadsActivityCentre"],
|
||||
});
|
||||
|
||||
test("should display the release announcement process", async ({ page, app, util }) => {
|
||||
test("should display the release announcement process", { tag: "@screenshot" }, async ({ page, app, util }) => {
|
||||
// The TAC release announcement should be displayed
|
||||
await util.assertReleaseAnnouncementIsVisible("Threads Activity Centre");
|
||||
// Hide the release announcement
|
||||
|
|
|
@ -40,7 +40,7 @@ test.describe("FilePanel", () => {
|
|||
});
|
||||
|
||||
test.describe("render", () => {
|
||||
test("should render empty state", async ({ page }) => {
|
||||
test("should render empty state", { tag: "@screenshot" }, async ({ page }) => {
|
||||
// Wait until the information about the empty state is rendered
|
||||
await expect(page.locator(".mx_EmptyState")).toBeVisible();
|
||||
|
||||
|
@ -48,7 +48,7 @@ test.describe("FilePanel", () => {
|
|||
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("empty.png");
|
||||
});
|
||||
|
||||
test("should list tiles on the panel", async ({ page }) => {
|
||||
test("should list tiles on the panel", { tag: "@screenshot" }, async ({ page }) => {
|
||||
// Upload multiple files
|
||||
await uploadFile(page, "playwright/sample-files/riot.png"); // Image
|
||||
await uploadFile(page, "playwright/sample-files/1sec.ogg"); // Audio
|
||||
|
|
|
@ -21,7 +21,7 @@ test.describe("NotificationPanel", () => {
|
|||
await app.client.createRoom({ name: ROOM_NAME });
|
||||
});
|
||||
|
||||
test("should render empty state", async ({ page, app }) => {
|
||||
test("should render empty state", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||
await app.viewRoomByName(ROOM_NAME);
|
||||
|
||||
await page.getByRole("button", { name: "Notifications" }).click();
|
||||
|
|
|
@ -38,7 +38,7 @@ test.describe("RightPanel", () => {
|
|||
});
|
||||
|
||||
test.describe("in rooms", () => {
|
||||
test("should handle long room address and long room name", async ({ page, app }) => {
|
||||
test("should handle long room address and long room name", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||
await app.client.createRoom({ name: ROOM_NAME_LONG });
|
||||
await viewRoomSummaryByName(page, app, ROOM_NAME_LONG);
|
||||
|
||||
|
|
|
@ -47,7 +47,10 @@ test.describe("Room Directory", () => {
|
|||
expect(resp.chunk[0].room_id).toEqual(roomId);
|
||||
});
|
||||
|
||||
test("should allow finding published rooms in directory", async ({ page, app, user, bot }) => {
|
||||
test(
|
||||
"should allow finding published rooms in directory",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user, bot }) => {
|
||||
const name = "This is a public room";
|
||||
await bot.createRoom({
|
||||
visibility: "public" as Visibility,
|
||||
|
@ -60,7 +63,9 @@ test.describe("Room Directory", () => {
|
|||
const dialog = page.locator(".mx_SpotlightDialog");
|
||||
await dialog.getByRole("textbox", { name: "Search" }).fill("Unknown Room");
|
||||
await expect(
|
||||
dialog.getByText("If you can't find the room you're looking for, ask for an invite or create a new room."),
|
||||
dialog.getByText(
|
||||
"If you can't find the room you're looking for, ask for an invite or create a new room.",
|
||||
),
|
||||
).toHaveClass("mx_SpotlightDialog_otherSearches_messageSearchText");
|
||||
|
||||
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("filtered-no-results.png");
|
||||
|
@ -76,5 +81,6 @@ test.describe("Room Directory", () => {
|
|||
.click();
|
||||
|
||||
await expect(page).toHaveURL("/#/room/#test1234:localhost");
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -20,7 +20,7 @@ test.describe("Room Header", () => {
|
|||
test.use({
|
||||
labsFlags: ["feature_notifications"],
|
||||
});
|
||||
test("should render default buttons properly", async ({ page, app, user }) => {
|
||||
test("should render default buttons properly", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
await app.client.createRoom({ name: "Test Room" });
|
||||
await app.viewRoomByName("Test Room");
|
||||
|
||||
|
@ -51,7 +51,10 @@ test.describe("Room Header", () => {
|
|||
await expect(header).toMatchScreenshot("room-header.png");
|
||||
});
|
||||
|
||||
test("should render a very long room name without collapsing the buttons", async ({ page, app, user }) => {
|
||||
test(
|
||||
"should render a very long room name without collapsing the buttons",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user }) => {
|
||||
const LONG_ROOM_NAME =
|
||||
"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore " +
|
||||
"et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " +
|
||||
|
@ -78,7 +81,8 @@ test.describe("Room Header", () => {
|
|||
}
|
||||
|
||||
await expect(header).toMatchScreenshot("room-header-long-name.png");
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test.describe("with a video room", () => {
|
||||
|
@ -99,7 +103,10 @@ test.describe("Room Header", () => {
|
|||
test.describe("and with feature_notifications enabled", () => {
|
||||
test.use({ labsFlags: ["feature_video_rooms", "feature_notifications"] });
|
||||
|
||||
test("should render buttons for chat, room info, threads and facepile", async ({ page, app, user }) => {
|
||||
test(
|
||||
"should render buttons for chat, room info, threads and facepile",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user }) => {
|
||||
await createVideoRoom(page, app);
|
||||
|
||||
const header = page.locator(".mx_RoomHeader");
|
||||
|
@ -122,7 +129,8 @@ test.describe("Room Header", () => {
|
|||
await expect(header.getByRole("button")).toHaveCount(7);
|
||||
|
||||
await expect(header).toMatchScreenshot("room-header-video-room.png");
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("should render a working chat button which opens the timeline on a right panel", async ({
|
||||
|
|
|
@ -23,7 +23,7 @@ test.describe("Account user settings tab", () => {
|
|||
},
|
||||
});
|
||||
|
||||
test("should be rendered properly", async ({ uut, user }) => {
|
||||
test("should be rendered properly", { tag: "@screenshot" }, async ({ uut, user }) => {
|
||||
await expect(uut).toMatchScreenshot("account.png");
|
||||
|
||||
// Assert that the top heading is rendered
|
||||
|
@ -71,7 +71,7 @@ test.describe("Account user settings tab", () => {
|
|||
);
|
||||
});
|
||||
|
||||
test("should respond to small screen sizes", async ({ page, uut }) => {
|
||||
test("should respond to small screen sizes", { tag: "@screenshot" }, async ({ page, uut }) => {
|
||||
await page.setViewportSize({ width: 700, height: 600 });
|
||||
await expect(uut).toMatchScreenshot("account-smallscreen.png");
|
||||
});
|
||||
|
|
|
@ -13,7 +13,7 @@ test.describe("Appearance user settings tab", () => {
|
|||
displayName: "Hanako",
|
||||
});
|
||||
|
||||
test("should be rendered properly", async ({ page, user, app }) => {
|
||||
test("should be rendered properly", { tag: "@screenshot" }, async ({ page, user, app }) => {
|
||||
const tab = await app.settings.openUserSettings("Appearance");
|
||||
|
||||
// Click "Show advanced" link button
|
||||
|
@ -25,7 +25,10 @@ test.describe("Appearance user settings tab", () => {
|
|||
await expect(tab).toMatchScreenshot("appearance-tab.png");
|
||||
});
|
||||
|
||||
test("should support changing font size by using the font size dropdown", async ({ page, app, user }) => {
|
||||
test(
|
||||
"should support changing font size by using the font size dropdown",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user }) => {
|
||||
await app.settings.openUserSettings("Appearance");
|
||||
|
||||
const tab = page.getByTestId("mx_AppearanceUserSettingsTab");
|
||||
|
@ -37,7 +40,8 @@ test.describe("Appearance user settings tab", () => {
|
|||
await fontDropdown.getByLabel("Font size").selectOption({ value: "-4" });
|
||||
|
||||
await expect(page).toMatchScreenshot("window-12px.png", { includeDialogBackground: true });
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("should support enabling system font", async ({ page, app, user }) => {
|
||||
await app.settings.openUserSettings("Appearance");
|
||||
|
|
|
@ -20,7 +20,10 @@ test.describe("Appearance user settings tab", () => {
|
|||
await util.openAppearanceTab();
|
||||
});
|
||||
|
||||
test("should change the message layout from modern to bubble", async ({ page, app, user, util }) => {
|
||||
test(
|
||||
"should change the message layout from modern to bubble",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user, util }) => {
|
||||
await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-modern.png");
|
||||
|
||||
await util.getBubbleLayout().click();
|
||||
|
@ -33,7 +36,8 @@ test.describe("Appearance user settings tab", () => {
|
|||
// Assert that the room layout is set to bubble layout
|
||||
await util.assertBubbleLayout();
|
||||
await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-bubble.png");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("should enable compact layout when the modern layout is selected", async ({ page, app, user, util }) => {
|
||||
await expect(util.getCompactLayoutCheckbox()).not.toBeChecked();
|
||||
|
|
|
@ -20,7 +20,10 @@ test.describe("Appearance user settings tab", () => {
|
|||
await util.openAppearanceTab();
|
||||
});
|
||||
|
||||
test("should be rendered with the light theme selected", async ({ page, app, util }) => {
|
||||
test(
|
||||
"should be rendered with the light theme selected",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, util }) => {
|
||||
// Assert that 'Match system theme' is not checked
|
||||
await expect(util.getMatchSystemThemeCheckbox()).not.toBeChecked();
|
||||
|
||||
|
@ -31,9 +34,13 @@ test.describe("Appearance user settings tab", () => {
|
|||
await expect(util.getHighContrastTheme()).not.toBeChecked();
|
||||
|
||||
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-light.png");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("should disable the themes when the system theme is clicked", async ({ page, app, util }) => {
|
||||
test(
|
||||
"should disable the themes when the system theme is clicked",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, util }) => {
|
||||
await util.getMatchSystemThemeCheckbox().click();
|
||||
|
||||
// Assert that the themes are disabled
|
||||
|
@ -42,9 +49,10 @@ test.describe("Appearance user settings tab", () => {
|
|||
await expect(util.getHighContrastTheme()).toBeDisabled();
|
||||
|
||||
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-match-system-enabled.png");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("should change the theme to dark", async ({ page, app, util }) => {
|
||||
test("should change the theme to dark", { tag: "@screenshot" }, async ({ page, app, util }) => {
|
||||
// Assert that the light theme is selected
|
||||
await expect(util.getLightTheme()).toBeChecked();
|
||||
|
||||
|
@ -63,11 +71,14 @@ test.describe("Appearance user settings tab", () => {
|
|||
labsFlags: ["feature_custom_themes"],
|
||||
});
|
||||
|
||||
test("should render the custom theme section", async ({ page, app, util }) => {
|
||||
test("should render the custom theme section", { tag: "@screenshot" }, async ({ page, app, util }) => {
|
||||
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme.png");
|
||||
});
|
||||
|
||||
test("should be able to add and remove a custom theme", async ({ page, app, util }) => {
|
||||
test(
|
||||
"should be able to add and remove a custom theme",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, util }) => {
|
||||
await util.addCustomTheme();
|
||||
|
||||
await expect(util.getCustomTheme()).not.toBeChecked();
|
||||
|
@ -75,7 +86,8 @@ test.describe("Appearance user settings tab", () => {
|
|||
|
||||
await util.removeCustomTheme();
|
||||
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-removed.png");
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,7 +20,7 @@ test.describe("General room settings tab", () => {
|
|||
await app.viewRoomByName(roomName);
|
||||
});
|
||||
|
||||
test("should be rendered properly", async ({ page, app }) => {
|
||||
test("should be rendered properly", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||
const settings = await app.settings.openRoomSettings("General");
|
||||
|
||||
// Assert that "Show less" details element is rendered
|
||||
|
|
|
@ -23,7 +23,7 @@ test.describe("Preferences user settings tab", () => {
|
|||
},
|
||||
});
|
||||
|
||||
test("should be rendered properly", async ({ app, page, user }) => {
|
||||
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user }) => {
|
||||
page.setViewportSize({ width: 1024, height: 3300 });
|
||||
const tab = await app.settings.openUserSettings("Preferences");
|
||||
// Assert that the top heading is rendered
|
||||
|
|
|
@ -36,7 +36,7 @@ test.describe("Security user settings tab", () => {
|
|||
});
|
||||
|
||||
test.describe("AnalyticsLearnMoreDialog", () => {
|
||||
test("should be rendered properly", async ({ app, page }) => {
|
||||
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page }) => {
|
||||
const tab = await app.settings.openUserSettings("Security");
|
||||
await tab.getByRole("button", { name: "Learn more" }).click();
|
||||
await expect(page.locator(".mx_AnalyticsLearnMoreDialog_wrapper .mx_Dialog")).toMatchScreenshot(
|
||||
|
|
67
playwright/e2e/share-dialog/share-dialog.spec.ts
Normal file
67
playwright/e2e/share-dialog/share-dialog.spec.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
|
||||
test.describe("Share dialog", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
room: async ({ app, user, bot }, use) => {
|
||||
const roomId = await app.client.createRoom({ name: "Alice room" });
|
||||
await use({ roomId });
|
||||
},
|
||||
});
|
||||
|
||||
test("should share a room", { tag: "@screenshot" }, async ({ page, app, room }) => {
|
||||
await app.viewRoomById(room.roomId);
|
||||
await app.toggleRoomInfoPanel();
|
||||
await page.getByRole("menuitem", { name: "Copy link" }).click();
|
||||
|
||||
const dialog = page.getByRole("dialog", { name: "Share room" });
|
||||
await expect(dialog.getByText(`https://matrix.to/#/${room.roomId}`)).toBeVisible();
|
||||
expect(dialog).toMatchScreenshot("share-dialog-room.png", {
|
||||
// QRCode and url changes at every run
|
||||
mask: [page.locator(".mx_QRCode"), page.locator(".mx_ShareDialog_top > span")],
|
||||
});
|
||||
});
|
||||
|
||||
test("should share a room member", { tag: "@screenshot" }, async ({ page, app, room, user }) => {
|
||||
await app.viewRoomById(room.roomId);
|
||||
await app.client.sendMessage(room.roomId, { body: "hello", msgtype: "m.text" });
|
||||
|
||||
const rightPanel = await app.toggleRoomInfoPanel();
|
||||
await rightPanel.getByRole("menuitem", { name: "People" }).click();
|
||||
await rightPanel.getByRole("button", { name: `${user.userId} (power 100)` }).click();
|
||||
await rightPanel.getByRole("button", { name: "Share profile" }).click();
|
||||
|
||||
const dialog = page.getByRole("dialog", { name: "Share User" });
|
||||
await expect(dialog.getByText(`https://matrix.to/#/${user.userId}`)).toBeVisible();
|
||||
expect(dialog).toMatchScreenshot("share-dialog-user.png", {
|
||||
// QRCode changes at every run
|
||||
mask: [page.locator(".mx_QRCode")],
|
||||
});
|
||||
});
|
||||
|
||||
test("should share an event", { tag: "@screenshot" }, async ({ page, app, room }) => {
|
||||
await app.viewRoomById(room.roomId);
|
||||
await app.client.sendMessage(room.roomId, { body: "hello", msgtype: "m.text" });
|
||||
|
||||
const timelineMessage = page.locator(".mx_MTextBody", { hasText: "hello" });
|
||||
await timelineMessage.hover();
|
||||
await page.getByRole("button", { name: "Options", exact: true }).click();
|
||||
await page.getByRole("menuitem", { name: "Share" }).click();
|
||||
|
||||
const dialog = page.getByRole("dialog", { name: "Share Room Message" });
|
||||
await expect(dialog.getByRole("checkbox", { name: "Link to selected message" })).toBeChecked();
|
||||
expect(dialog).toMatchScreenshot("share-dialog-event.png", {
|
||||
// QRCode and url changes at every run
|
||||
mask: [page.locator(".mx_QRCode"), page.locator(".mx_ShareDialog_top > span")],
|
||||
});
|
||||
await dialog.getByRole("checkbox", { name: "Link to selected message" }).click();
|
||||
await expect(dialog.getByRole("checkbox", { name: "Link to selected message" })).not.toBeChecked();
|
||||
});
|
||||
});
|
|
@ -55,7 +55,7 @@ test.describe("Spaces", () => {
|
|||
botCreateOpts: { displayName: "BotBob" },
|
||||
});
|
||||
|
||||
test("should allow user to create public space", async ({ page, app, user }) => {
|
||||
test("should allow user to create public space", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const contextMenu = await openSpaceCreateMenu(page);
|
||||
await expect(contextMenu).toMatchScreenshot("space-create-menu.png");
|
||||
|
||||
|
@ -88,7 +88,7 @@ test.describe("Spaces", () => {
|
|||
await expect(page.getByRole("treeitem", { name: "Jokes" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should allow user to create private space", async ({ page, app, user }) => {
|
||||
test("should allow user to create private space", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const menu = await openSpaceCreateMenu(page);
|
||||
await menu.getByRole("button", { name: "Private" }).click();
|
||||
|
||||
|
@ -216,13 +216,10 @@ test.describe("Spaces", () => {
|
|||
await expect(hierarchyList.getByRole("treeitem", { name: "Gaming" }).getByRole("button")).toBeVisible();
|
||||
});
|
||||
|
||||
test("should render subspaces in the space panel only when expanded", async ({
|
||||
page,
|
||||
app,
|
||||
user,
|
||||
axe,
|
||||
checkA11y,
|
||||
}) => {
|
||||
test(
|
||||
"should render subspaces in the space panel only when expanded",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user, axe, checkA11y }) => {
|
||||
axe.disableRules([
|
||||
// Disable this check as it triggers on nested roving tab index elements which are in practice fine
|
||||
"nested-interactive",
|
||||
|
@ -258,7 +255,8 @@ test.describe("Spaces", () => {
|
|||
|
||||
await checkA11y();
|
||||
await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-expanded.png");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("should not soft crash when joining a room from space hierarchy which has a link in its topic", async ({
|
||||
page,
|
||||
|
|
|
@ -276,7 +276,7 @@ export class Helpers {
|
|||
* Assert that the threads activity centre button has no indicator
|
||||
*/
|
||||
async assertNoTacIndicator() {
|
||||
// Assert by checkng neither of the known indicators are visible first. This will wait
|
||||
// Assert by checking neither of the known indicators are visible first. This will wait
|
||||
// if it takes a little time to disappear, but the screenshot comparison won't.
|
||||
await expect(this.getTacButton().locator("[data-indicator='success']")).not.toBeVisible();
|
||||
await expect(this.getTacButton().locator("[data-indicator='critical']")).not.toBeVisible();
|
||||
|
@ -376,7 +376,7 @@ export class Helpers {
|
|||
* Clicks the button to mark all threads as read in the current room
|
||||
*/
|
||||
clickMarkAllThreadsRead() {
|
||||
return this.page.getByLabel("Mark all as read").click();
|
||||
return this.page.locator("#thread-panel").getByRole("button", { name: "Mark all as read" }).click();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,16 +16,18 @@ test.describe("Threads Activity Centre", () => {
|
|||
labsFlags: ["threadsActivityCentre"],
|
||||
});
|
||||
|
||||
test("should have the button correctly aligned and displayed in the space panel when expanded", async ({
|
||||
util,
|
||||
}) => {
|
||||
test(
|
||||
"should have the button correctly aligned and displayed in the space panel when expanded",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ util }) => {
|
||||
// Open the space panel
|
||||
await util.expandSpacePanel();
|
||||
// The buttons in the space panel should be aligned when expanded
|
||||
await expect(util.getSpacePanel()).toMatchScreenshot("tac-button-expanded.png");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("should not show indicator when there is no thread", async ({ room1, util }) => {
|
||||
test("should not show indicator when there is no thread", { tag: "@screenshot" }, async ({ room1, util }) => {
|
||||
// No indicator should be shown
|
||||
await util.assertNoTacIndicator();
|
||||
|
||||
|
@ -62,7 +64,7 @@ test.describe("Threads Activity Centre", () => {
|
|||
await util.assertHighlightIndicator();
|
||||
});
|
||||
|
||||
test("should show the rooms with unread threads", async ({ room1, room2, util, msg }) => {
|
||||
test("should show the rooms with unread threads", { tag: "@screenshot" }, async ({ room1, room2, util, msg }) => {
|
||||
await util.goTo(room2);
|
||||
await util.populateThreads(room1, room2, msg);
|
||||
// The indicator should be shown
|
||||
|
@ -79,7 +81,7 @@ test.describe("Threads Activity Centre", () => {
|
|||
await expect(util.getTacPanel()).toMatchScreenshot("tac-panel-mix-unread.png");
|
||||
});
|
||||
|
||||
test("should update with a thread is read", async ({ room1, room2, util, msg }) => {
|
||||
test("should update with a thread is read", { tag: "@screenshot" }, async ({ room1, room2, util, msg }) => {
|
||||
await util.goTo(room2);
|
||||
await util.populateThreads(room1, room2, msg);
|
||||
|
||||
|
@ -128,7 +130,7 @@ test.describe("Threads Activity Centre", () => {
|
|||
await expect(page.locator(".mx_SpotlightDialog")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("should have the correct hover state", async ({ util, page }) => {
|
||||
test("should have the correct hover state", { tag: "@screenshot" }, async ({ util, page }) => {
|
||||
await util.hoverTacButton();
|
||||
await expect(util.getSpacePanel()).toMatchScreenshot("tac-hovered.png");
|
||||
|
||||
|
@ -138,7 +140,7 @@ test.describe("Threads Activity Centre", () => {
|
|||
await expect(util.getSpacePanel()).toMatchScreenshot("tac-hovered-expanded.png");
|
||||
});
|
||||
|
||||
test("should mark all threads as read", async ({ room1, room2, util, msg, page }) => {
|
||||
test("should mark all threads as read", { tag: "@screenshot" }, async ({ room1, room2, util, msg, page }) => {
|
||||
await util.receiveMessages(room1, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
|
||||
|
||||
await util.assertNotificationTac();
|
||||
|
@ -146,7 +148,7 @@ test.describe("Threads Activity Centre", () => {
|
|||
await util.openTac();
|
||||
await util.clickRoomInTac(room1.name);
|
||||
|
||||
util.clickMarkAllThreadsRead();
|
||||
await util.clickMarkAllThreadsRead();
|
||||
|
||||
await util.assertNoTacIndicator();
|
||||
});
|
||||
|
|
|
@ -25,7 +25,7 @@ test.describe("Threads", () => {
|
|||
});
|
||||
|
||||
// Flaky: https://github.com/vector-im/element-web/issues/26452
|
||||
test.skip("should be usable for a conversation", async ({ page, app, bot }) => {
|
||||
test.skip("should be usable for a conversation", { tag: "@screenshot" }, async ({ page, app, bot }) => {
|
||||
const roomId = await app.client.createRoom({});
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
|
@ -150,7 +150,7 @@ test.describe("Threads", () => {
|
|||
).toHaveCSS("padding-inline-start", ThreadViewGroupSpacingStart);
|
||||
|
||||
// Take snapshot of group layout (IRC layout is not available on ThreadView)
|
||||
expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
|
||||
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
|
||||
"ThreadView_with_reaction_and_a_hidden_event_on_group_layout.png",
|
||||
{
|
||||
mask: mask,
|
||||
|
@ -174,7 +174,7 @@ test.describe("Threads", () => {
|
|||
.toHaveCSS("margin-inline-start", "0px");
|
||||
|
||||
// Take snapshot of bubble layout
|
||||
expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
|
||||
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
|
||||
"ThreadView_with_reaction_and_a_hidden_event_on_bubble_layout.png",
|
||||
{
|
||||
mask: mask,
|
||||
|
@ -351,7 +351,10 @@ test.describe("Threads", () => {
|
|||
});
|
||||
});
|
||||
|
||||
test("should send location and reply to the location on ThreadView", async ({ page, app, bot }) => {
|
||||
test(
|
||||
"should send location and reply to the location on ThreadView",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, bot }) => {
|
||||
const roomId = await app.client.createRoom({});
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
|
@ -401,7 +404,8 @@ test.describe("Threads", () => {
|
|||
// Take a snapshot of reply to the shared location
|
||||
await page.addStyleTag({ content: css });
|
||||
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("Reply_to_the_location_on_ThreadView.png");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("right panel behaves correctly", async ({ page, app, user }) => {
|
||||
// Create room
|
||||
|
|
|
@ -137,7 +137,10 @@ test.describe("Timeline", () => {
|
|||
});
|
||||
|
||||
test.describe("configure room", () => {
|
||||
test("should create and configure a room on IRC layout", async ({ page, app, room }) => {
|
||||
test(
|
||||
"should create and configure a room on IRC layout",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, room }) => {
|
||||
await page.goto(`/#/room/${room.roomId}`);
|
||||
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
|
||||
await expect(
|
||||
|
@ -151,9 +154,13 @@ test.describe("Timeline", () => {
|
|||
await expect(page.locator(".mx_TimelineSeparator")).toHaveText("today");
|
||||
|
||||
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("configured-room-irc-layout.png");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("should have an expanded generic event list summary (GELS) on IRC layout", async ({ page, app, room }) => {
|
||||
test(
|
||||
"should have an expanded generic event list summary (GELS) on IRC layout",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, room }) => {
|
||||
await page.goto(`/#/room/${room.roomId}`);
|
||||
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
|
||||
|
||||
|
@ -179,13 +186,13 @@ test.describe("Timeline", () => {
|
|||
}
|
||||
`,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("should have an expanded generic event list summary (GELS) on compact modern/group layout", async ({
|
||||
page,
|
||||
app,
|
||||
room,
|
||||
}) => {
|
||||
test(
|
||||
"should have an expanded generic event list summary (GELS) on compact modern/group layout",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, room }) => {
|
||||
await page.goto(`/#/room/${room.roomId}`);
|
||||
|
||||
// Set compact modern layout
|
||||
|
@ -213,13 +220,13 @@ test.describe("Timeline", () => {
|
|||
}
|
||||
`,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("should click 'collapse' on the first hovered info event line inside GELS on bubble layout", async ({
|
||||
page,
|
||||
app,
|
||||
room,
|
||||
}) => {
|
||||
test(
|
||||
"should click 'collapse' on the first hovered info event line inside GELS on bubble layout",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, room }) => {
|
||||
// This test checks clickability of the "Collapse" link button, which had been covered with
|
||||
// MessageActionBar's safe area - https://github.com/vector-im/element-web/issues/22864
|
||||
|
||||
|
@ -250,7 +257,9 @@ test.describe("Timeline", () => {
|
|||
});
|
||||
|
||||
// Click "collapse" link button on the first hovered info event line
|
||||
const firstTile = gels.locator(".mx_GenericEventListSummary_unstyledList .mx_EventTile_info:first-of-type");
|
||||
const firstTile = gels.locator(
|
||||
".mx_GenericEventListSummary_unstyledList .mx_EventTile_info:first-of-type",
|
||||
);
|
||||
await firstTile.hover();
|
||||
await expect(firstTile.getByRole("toolbar", { name: "Message Actions" })).toBeVisible();
|
||||
await gels.getByRole("button", { name: "Collapse" }).click();
|
||||
|
@ -262,15 +271,13 @@ test.describe("Timeline", () => {
|
|||
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("collapsed-gels-bubble-layout.png", {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("should add inline start margin to an event line on IRC layout", async ({
|
||||
page,
|
||||
app,
|
||||
room,
|
||||
axe,
|
||||
checkA11y,
|
||||
}) => {
|
||||
test(
|
||||
"should add inline start margin to an event line on IRC layout",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, room, axe, checkA11y }) => {
|
||||
axe.disableRules("color-contrast");
|
||||
|
||||
await page.goto(`/#/room/${room.roomId}`);
|
||||
|
@ -312,7 +319,8 @@ test.describe("Timeline", () => {
|
|||
},
|
||||
);
|
||||
await checkA11y();
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test.describe("message displaying", () => {
|
||||
|
@ -332,11 +340,10 @@ test.describe("Timeline", () => {
|
|||
).toBeVisible();
|
||||
};
|
||||
|
||||
test("should align generic event list summary with messages and emote on IRC layout", async ({
|
||||
page,
|
||||
app,
|
||||
room,
|
||||
}) => {
|
||||
test(
|
||||
"should align generic event list summary with messages and emote on IRC layout",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, room }) => {
|
||||
// This test aims to check:
|
||||
// 1. Alignment of collapsed GELS (generic event list summary) and messages
|
||||
// 2. Alignment of expanded GELS and messages
|
||||
|
@ -372,10 +379,9 @@ test.describe("Timeline", () => {
|
|||
// = var(--name-width) + var(--icon-width) + var(--MessageTimestamp-width) + 2 * var(--right-padding)
|
||||
// = 80 + 14 + 46 + 2 * 5
|
||||
// = 150px
|
||||
await expect(page.locator(".mx_GenericEventListSummary[data-layout=irc] > .mx_EventTile_line")).toHaveCSS(
|
||||
"padding-inline-start",
|
||||
"150px",
|
||||
);
|
||||
await expect(
|
||||
page.locator(".mx_GenericEventListSummary[data-layout=irc] > .mx_EventTile_line"),
|
||||
).toHaveCSS("padding-inline-start", "150px");
|
||||
// Check width and spacing values of elements in .mx_EventTile, which should be equal to 150px
|
||||
// --right-padding should be applied
|
||||
for (const locator of await page.locator(".mx_EventTile > a").all()) {
|
||||
|
@ -416,10 +422,13 @@ test.describe("Timeline", () => {
|
|||
page.locator(".mx_EventTile[data-layout=irc].mx_EventTile_info:first-of-type .mx_EventTile_line"),
|
||||
).toHaveCSS("margin-inline-start", "99px");
|
||||
// Record alignment of expanded GELS and messages on messagePanel
|
||||
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-and-messages-irc-layout.png", {
|
||||
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot(
|
||||
"expanded-gels-and-messages-irc-layout.png",
|
||||
{
|
||||
// Exclude timestamp from snapshot of mx_MainSplit
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// 3. Alignment of expanded GELS and placeholder of deleted message
|
||||
// Delete the second (last) message
|
||||
|
@ -431,15 +440,20 @@ test.describe("Timeline", () => {
|
|||
await page.locator(".mx_Dialog_buttons").getByRole("button", { name: "Remove" }).click();
|
||||
// Make sure the dialog was closed and the second (last) message was redacted
|
||||
await expect(page.locator(".mx_Dialog")).not.toBeVisible();
|
||||
await expect(page.locator(".mx_GenericEventListSummary .mx_EventTile_last .mx_RedactedBody")).toBeVisible();
|
||||
await expect(
|
||||
page.locator(".mx_GenericEventListSummary .mx_EventTile_last .mx_RedactedBody"),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.locator(".mx_GenericEventListSummary .mx_EventTile_last .mx_EventTile_receiptSent"),
|
||||
).toBeVisible();
|
||||
// Record alignment of expanded GELS and placeholder of deleted message on messagePanel
|
||||
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-redaction-placeholder.png", {
|
||||
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot(
|
||||
"expanded-gels-redaction-placeholder.png",
|
||||
{
|
||||
// Exclude timestamp from snapshot of mx_MainSplit
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// 4. Alignment of expanded GELS, placeholder of deleted message, and emote
|
||||
// Send a emote
|
||||
|
@ -447,7 +461,10 @@ test.describe("Timeline", () => {
|
|||
.locator(".mx_RoomView_body")
|
||||
.getByRole("textbox", { name: "Send a message…" })
|
||||
.fill("/me says hello to Mr. Bot");
|
||||
await page.locator(".mx_RoomView_body").getByRole("textbox", { name: "Send a message…" }).press("Enter");
|
||||
await page
|
||||
.locator(".mx_RoomView_body")
|
||||
.getByRole("textbox", { name: "Send a message…" })
|
||||
.press("Enter");
|
||||
// Check inline start margin of its avatar
|
||||
// Here --right-padding is for the avatar on the message line
|
||||
// See: _IRCLayout.pcss
|
||||
|
@ -456,15 +473,21 @@ test.describe("Timeline", () => {
|
|||
// = 80 + 14 + 1 * 5
|
||||
await expect(page.locator(".mx_EventTile_emote .mx_EventTile_avatar")).toHaveCSS("margin-left", "99px");
|
||||
// Make sure emote was sent
|
||||
await expect(page.locator(".mx_EventTile_last.mx_EventTile_emote .mx_EventTile_receiptSent")).toBeVisible();
|
||||
await expect(
|
||||
page.locator(".mx_EventTile_last.mx_EventTile_emote .mx_EventTile_receiptSent"),
|
||||
).toBeVisible();
|
||||
// Record alignment of expanded GELS, placeholder of deleted message, and emote
|
||||
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-emote-irc-layout.png", {
|
||||
// Exclude timestamp from snapshot of mx_MainSplit
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("should render EventTiles on IRC, modern (group), and bubble layout", async ({ page, app, room }) => {
|
||||
test(
|
||||
"should render EventTiles on IRC, modern (group), and bubble layout",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, room }) => {
|
||||
const screenshotOptions = {
|
||||
// Hide because flaky - See https://github.com/vector-im/element-web/issues/24957
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
|
@ -491,7 +514,9 @@ test.describe("Timeline", () => {
|
|||
await composer.fill("This message has an inline emoji 👒");
|
||||
await composer.press("Enter");
|
||||
|
||||
await expect(page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒")).toBeVisible();
|
||||
await expect(
|
||||
page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒"),
|
||||
).toBeVisible();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// IRC layout
|
||||
|
@ -558,9 +583,13 @@ test.describe("Timeline", () => {
|
|||
"event-tiles-bubble-layout.png",
|
||||
screenshotOptions,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("should set inline start padding to a hidden event line", async ({ page, app, room }) => {
|
||||
test(
|
||||
"should set inline start padding to a hidden event line",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, room }) => {
|
||||
test.skip(
|
||||
true,
|
||||
"Disabled due to screenshot test being flaky - https://github.com/element-hq/element-web/issues/26890",
|
||||
|
@ -612,9 +641,10 @@ test.describe("Timeline", () => {
|
|||
"hidden-event-line-padding-modern-layout.png",
|
||||
screenshotOptions,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("should click view source event toggle", async ({ page, app, room }) => {
|
||||
test("should click view source event toggle", { tag: "@screenshot" }, async ({ page, app, room }) => {
|
||||
// This test checks:
|
||||
// 1. clickability of top left of view source event toggle
|
||||
// 2. clickability of view source toggle on IRC layout
|
||||
|
@ -712,7 +742,10 @@ test.describe("Timeline", () => {
|
|||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("should render url previews", async ({ page, app, room, axe, checkA11y, context }) => {
|
||||
test(
|
||||
"should render url previews",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, room, axe, checkA11y, context }) => {
|
||||
axe.disableRules("color-contrast");
|
||||
|
||||
// Element Web uses a Service Worker to rewrite unauthenticated media requests to authenticated ones, but
|
||||
|
@ -769,10 +802,14 @@ test.describe("Timeline", () => {
|
|||
}
|
||||
`,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.describe("on search results panel", () => {
|
||||
test("should highlight search result words regardless of formatting", async ({ page, app, room }) => {
|
||||
test(
|
||||
"should highlight search result words regardless of formatting",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, room }) => {
|
||||
await sendEvent(app.client, room.roomId);
|
||||
await sendEvent(app.client, room.roomId, true);
|
||||
await page.goto(`/#/room/${room.roomId}`);
|
||||
|
@ -792,9 +829,10 @@ test.describe("Timeline", () => {
|
|||
await expect(page.locator(".mx_RoomView_searchResultsPanel")).toMatchScreenshot(
|
||||
"highlighted-search-results.png",
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("should render a fully opaque textual event", async ({ page, app, room }) => {
|
||||
test("should render a fully opaque textual event", { tag: "@screenshot" }, async ({ page, app, room }) => {
|
||||
const stringToSearch = "Message"; // Same with string sent with sendEvent()
|
||||
|
||||
await sendEvent(app.client, room.roomId);
|
||||
|
@ -918,7 +956,7 @@ test.describe("Timeline", () => {
|
|||
).toHaveCount(4);
|
||||
});
|
||||
|
||||
test("should display a reply chain", async ({ page, app, room, homeserver }) => {
|
||||
test("should display a reply chain", { tag: "@screenshot" }, async ({ page, app, room, homeserver }) => {
|
||||
const reply2 = "Reply again";
|
||||
|
||||
await page.goto(`/#/room/${room.roomId}`);
|
||||
|
@ -1031,12 +1069,10 @@ test.describe("Timeline", () => {
|
|||
);
|
||||
});
|
||||
|
||||
test("should send, reply, and display long strings without overflowing", async ({
|
||||
page,
|
||||
app,
|
||||
room,
|
||||
homeserver,
|
||||
}) => {
|
||||
test(
|
||||
"should send, reply, and display long strings without overflowing",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, room, homeserver }) => {
|
||||
// Max 256 characters for display name
|
||||
const LONG_STRING =
|
||||
"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut " +
|
||||
|
@ -1146,7 +1182,8 @@ test.describe("Timeline", () => {
|
|||
"long-strings-with-reply-bubble-layout.png",
|
||||
screenshotOptions,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
async function testImageRendering(page: Page, app: ElementAppPage, room: { roomId: string }) {
|
||||
await app.viewRoomById(room.roomId);
|
||||
|
@ -1176,7 +1213,7 @@ test.describe("Timeline", () => {
|
|||
);
|
||||
}
|
||||
|
||||
test("should render images in the timeline", async ({ page, app, room, context }) => {
|
||||
test("should render images in the timeline", { tag: "@screenshot" }, async ({ page, app, room, context }) => {
|
||||
await testImageRendering(page, app, room);
|
||||
});
|
||||
|
||||
|
@ -1188,7 +1225,10 @@ test.describe("Timeline", () => {
|
|||
// In practice, this means this test will *always* succeed because it ends up relying on fallback behaviour tested
|
||||
// above (unless of course the above tests are also broken).
|
||||
test.describe("MSC3916 - Authenticated Media", () => {
|
||||
test("should render authenticated images in the timeline", async ({ page, app, room, context }) => {
|
||||
test(
|
||||
"should render authenticated images in the timeline",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, room, context }) => {
|
||||
// Note: we have to use `context` instead of `page` for routing, otherwise we'll miss Service Worker events.
|
||||
// See https://playwright.dev/docs/service-workers-experimental#network-events-and-routing
|
||||
|
||||
|
@ -1231,7 +1271,8 @@ test.describe("Timeline", () => {
|
|||
|
||||
// We check the same screenshot because there should be no user-visible impact to using authentication.
|
||||
await testImageRendering(page, app, room);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,7 +11,7 @@ import { test, expect } from "../../element-web-test";
|
|||
test.describe("User Menu", () => {
|
||||
test.use({ displayName: "Jeff" });
|
||||
|
||||
test("should contain our name & userId", async ({ page, user }) => {
|
||||
test("should contain our name & userId", { tag: "@screenshot" }, async ({ page, user }) => {
|
||||
await page.getByRole("button", { name: "User menu", exact: true }).click();
|
||||
const menu = page.getByRole("menu");
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ test.describe("User Onboarding (new user)", () => {
|
|||
await expect(page.locator(".mx_UserOnboardingList")).toBeVisible();
|
||||
});
|
||||
|
||||
test("page is shown and preference exists", async ({ page, app }) => {
|
||||
test("page is shown and preference exists", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||
await expect(page.locator(".mx_UserOnboardingPage")).toMatchScreenshot(
|
||||
"User-Onboarding-new-user-page-is-shown-and-preference-exists-1.png",
|
||||
);
|
||||
|
@ -34,7 +34,7 @@ test.describe("User Onboarding (new user)", () => {
|
|||
await expect(page.getByText("Show shortcut to welcome checklist above the room list")).toBeVisible();
|
||||
});
|
||||
|
||||
test("app download dialog", async ({ page }) => {
|
||||
test("app download dialog", { tag: "@screenshot" }, async ({ page }) => {
|
||||
await page.getByRole("button", { name: "Download apps" }).click();
|
||||
await expect(
|
||||
page.getByRole("dialog").getByRole("heading", { level: 1, name: "Download Element" }),
|
||||
|
|
|
@ -14,7 +14,7 @@ test.describe("UserView", () => {
|
|||
botCreateOpts: { displayName: "Usman" },
|
||||
});
|
||||
|
||||
test("should render the user view as expected", async ({ page, homeserver, user, bot }) => {
|
||||
test("should render the user view as expected", { tag: "@screenshot" }, async ({ page, homeserver, user, bot }) => {
|
||||
await page.goto(`/#/user/${bot.credentials.userId}`);
|
||||
|
||||
const rightPanel = page.locator("#mx_RightPanel");
|
||||
|
|
|
@ -70,7 +70,7 @@ test.describe("Widget Layout", () => {
|
|||
await app.viewRoomByName(ROOM_NAME);
|
||||
});
|
||||
|
||||
test("should be set properly", async ({ page }) => {
|
||||
test("should be set properly", { tag: "@screenshot" }, async ({ page }) => {
|
||||
await expect(page.locator(".mx_AppsDrawer")).toMatchScreenshot("apps-drawer.png");
|
||||
});
|
||||
|
||||
|
|
|
@ -314,6 +314,10 @@ export const expect = baseExpect.extend({
|
|||
const testInfo = test.info();
|
||||
if (!testInfo) throw new Error(`toMatchScreenshot() must be called during the test`);
|
||||
|
||||
if (!testInfo.tags.includes("@screenshot")) {
|
||||
throw new Error("toMatchScreenshot() must be used in a test tagged with @screenshot");
|
||||
}
|
||||
|
||||
const page = "page" in receiver ? receiver.page() : receiver;
|
||||
|
||||
let css = `
|
||||
|
|
|
@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand";
|
|||
// Docker tag to use for synapse docker image.
|
||||
// We target a specific digest as every now and then a Synapse update will break our CI.
|
||||
// This digest is updated by the playwright-image-updates.yaml workflow periodically.
|
||||
const DOCKER_TAG = "develop@sha256:34da08a44994e0ad2def7ed5f28c3cc7a2f7ead9722f4ae87b23030f59384ea5";
|
||||
const DOCKER_TAG = "develop@sha256:7e5aa044c65d67382f29acd424fc5eae447a951114be8b650e322145ba65d75f";
|
||||
|
||||
async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> {
|
||||
const templateDir = path.join(__dirname, "templates", opts.template);
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
|
@ -596,7 +596,7 @@ legend {
|
|||
.mx_Dialog
|
||||
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
|
||||
.mx_UserProfileSettings button
|
||||
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button),
|
||||
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button),
|
||||
.mx_Dialog input[type="submit"],
|
||||
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton),
|
||||
.mx_Dialog_buttons input[type="submit"] {
|
||||
|
@ -616,14 +616,16 @@ legend {
|
|||
.mx_Dialog
|
||||
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
|
||||
.mx_UserProfileSettings button
|
||||
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):last-child {
|
||||
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(
|
||||
.mx_ShareDialog button
|
||||
):last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.mx_Dialog
|
||||
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
|
||||
.mx_UserProfileSettings button
|
||||
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):focus,
|
||||
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):focus,
|
||||
.mx_Dialog input[type="submit"]:focus,
|
||||
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):focus,
|
||||
.mx_Dialog_buttons input[type="submit"]:focus {
|
||||
|
@ -635,7 +637,7 @@ legend {
|
|||
.mx_Dialog_buttons
|
||||
button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(
|
||||
.mx_UserProfileSettings button
|
||||
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button),
|
||||
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button),
|
||||
.mx_Dialog_buttons input[type="submit"].mx_Dialog_primary {
|
||||
color: var(--cpd-color-text-on-solid-primary);
|
||||
background-color: var(--cpd-color-bg-action-primary-rest);
|
||||
|
@ -648,7 +650,7 @@ legend {
|
|||
.mx_Dialog_buttons
|
||||
button.danger:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(.mx_UserProfileSettings button):not(
|
||||
.mx_ThemeChoicePanel_CustomTheme button
|
||||
):not(.mx_UnpinAllDialog button),
|
||||
):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button),
|
||||
.mx_Dialog_buttons input[type="submit"].danger {
|
||||
background-color: var(--cpd-color-bg-critical-primary);
|
||||
border: solid 1px var(--cpd-color-bg-critical-primary);
|
||||
|
@ -664,7 +666,7 @@ legend {
|
|||
.mx_Dialog
|
||||
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
|
||||
.mx_UserProfileSettings button
|
||||
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):disabled,
|
||||
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):disabled,
|
||||
.mx_Dialog input[type="submit"]:disabled,
|
||||
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):disabled,
|
||||
.mx_Dialog_buttons input[type="submit"]:disabled {
|
||||
|
|
|
@ -393,9 +393,3 @@
|
|||
@import "./views/voip/_LegacyCallViewHeader.pcss";
|
||||
@import "./views/voip/_LegacyCallViewSidebar.pcss";
|
||||
@import "./views/voip/_VideoFeed.pcss";
|
||||
@import "./voice-broadcast/atoms/_LiveBadge.pcss";
|
||||
@import "./voice-broadcast/atoms/_VoiceBroadcastControl.pcss";
|
||||
@import "./voice-broadcast/atoms/_VoiceBroadcastHeader.pcss";
|
||||
@import "./voice-broadcast/atoms/_VoiceBroadcastRecordingConnectionError.pcss";
|
||||
@import "./voice-broadcast/atoms/_VoiceBroadcastRoomSubtitle.pcss";
|
||||
@import "./voice-broadcast/molecules/_VoiceBroadcastBody.pcss";
|
||||
|
|
|
@ -22,20 +22,6 @@ Please see LICENSE files in the repository root for full details.
|
|||
pointer-events: none; /* makes the avatar non-draggable */
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserMenu_userAvatarLive {
|
||||
align-items: center;
|
||||
background-color: $alert;
|
||||
border-radius: 6px;
|
||||
color: $live-badge-color;
|
||||
display: flex;
|
||||
height: 12px;
|
||||
justify-content: center;
|
||||
left: 25px;
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
width: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserMenu_contextMenuButton {
|
||||
|
|
|
@ -5,50 +5,73 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_ShareDialog hr {
|
||||
margin-top: 25px;
|
||||
margin-bottom: 25px;
|
||||
border-color: $light-fg-color;
|
||||
.mx_ShareDialog {
|
||||
/* Value from figma design */
|
||||
width: 416px;
|
||||
|
||||
.mx_Dialog_header {
|
||||
text-align: center;
|
||||
margin-bottom: var(--cpd-space-6x);
|
||||
/* Override dialog header padding to able to center it */
|
||||
padding-inline-end: 0;
|
||||
}
|
||||
|
||||
.mx_ShareDialog .mx_ShareDialog_content {
|
||||
margin: 10px 0;
|
||||
.mx_ShareDialog_content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--cpd-space-6x);
|
||||
align-items: center;
|
||||
|
||||
.mx_CopyableText {
|
||||
width: unset; /* full width */
|
||||
.mx_ShareDialog_top {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--cpd-space-4x);
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
> a {
|
||||
text-decoration: none;
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
span {
|
||||
text-align: center;
|
||||
font: var(--cpd-font-body-sm-semibold);
|
||||
color: var(--cpd-color-text-secondary);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_ShareDialog_split {
|
||||
label {
|
||||
display: inline-flex;
|
||||
gap: var(--cpd-space-3x);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font: var(--cpd-font-body-md-medium);
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mx_ShareDialog_social {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
gap: var(--cpd-space-3x);
|
||||
justify-content: center;
|
||||
|
||||
.mx_ShareDialog_qrcode_container {
|
||||
float: left;
|
||||
height: 256px;
|
||||
width: 256px;
|
||||
margin-right: 64px;
|
||||
}
|
||||
a {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 99px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--cpd-color-border-interactive-secondary);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.mx_ShareDialog_qrcode_container + .mx_ShareDialog_social_container {
|
||||
width: 299px;
|
||||
img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_ShareDialog_social_container {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.mx_ShareDialog_social_icon {
|
||||
display: inline-grid;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
|
|
@ -256,10 +256,6 @@ Please see LICENSE files in the repository root for full details.
|
|||
mask-image: url("@vector-im/compound-design-tokens/icons/mic-on-solid.svg");
|
||||
}
|
||||
|
||||
.mx_MessageComposer_voiceBroadcast::before {
|
||||
mask-image: url("$(res)/img/element-icons/live.svg");
|
||||
}
|
||||
|
||||
.mx_MessageComposer_plain_text::before {
|
||||
mask-image: url("$(res)/img/element-icons/room/composer/plain_text.svg");
|
||||
}
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_LiveBadge {
|
||||
align-items: center;
|
||||
background-color: $alert;
|
||||
border-radius: 2px;
|
||||
color: $live-badge-color;
|
||||
display: inline-flex;
|
||||
font-size: $font-12px;
|
||||
font-weight: var(--cpd-font-weight-semibold);
|
||||
gap: $spacing-4;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.mx_LiveBadge--grey {
|
||||
background-color: $quaternary-content;
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_VoiceBroadcastControl {
|
||||
align-items: center;
|
||||
background-color: $background;
|
||||
border-radius: 50%;
|
||||
color: $secondary-content;
|
||||
display: flex;
|
||||
flex: 0 0 32px;
|
||||
height: 32px;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.mx_VoiceBroadcastControl-recording {
|
||||
color: $alert;
|
||||
}
|
||||
|
||||
.mx_VoiceBroadcastControl-play .mx_Icon {
|
||||
left: 1px;
|
||||
position: relative;
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_VoiceBroadcastHeader {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
gap: $spacing-8;
|
||||
line-height: 20px;
|
||||
margin-bottom: $spacing-16;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mx_VoiceBroadcastHeader_content {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mx_VoiceBroadcastHeader_room_wrapper {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.mx_VoiceBroadcastHeader_room {
|
||||
font-size: $font-12px;
|
||||
font-weight: var(--cpd-font-weight-semibold);
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mx_VoiceBroadcastHeader_line {
|
||||
align-items: center;
|
||||
color: $secondary-content;
|
||||
font-size: $font-12px;
|
||||
display: flex;
|
||||
gap: $spacing-4;
|
||||
|
||||
.mx_Spinner {
|
||||
flex: 0 0 14px;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_VoiceBroadcastHeader_mic--clickable {
|
||||
cursor: pointer;
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_VoiceBroadcastRecordingConnectionError {
|
||||
align-items: center;
|
||||
color: $alert;
|
||||
display: flex;
|
||||
gap: $spacing-12;
|
||||
|
||||
svg path {
|
||||
fill: $alert;
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_RoomTile .mx_RoomTile_titleContainer .mx_RoomTile_subtitle.mx_RoomTile_subtitle--voice-broadcast {
|
||||
align-items: center;
|
||||
color: $alert;
|
||||
display: flex;
|
||||
gap: $spacing-4;
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_VoiceBroadcastBody {
|
||||
background-color: $quinary-content;
|
||||
border-radius: 8px;
|
||||
color: $secondary-content;
|
||||
display: inline-block;
|
||||
font-size: $font-12px;
|
||||
padding: $spacing-12;
|
||||
width: 271px;
|
||||
|
||||
.mx_Clock {
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_VoiceBroadcastBody--pip {
|
||||
background-color: $system;
|
||||
box-shadow: 0 2px 8px 0 #0000004a;
|
||||
}
|
||||
|
||||
.mx_VoiceBroadcastBody--small {
|
||||
display: flex;
|
||||
gap: $spacing-8;
|
||||
width: 192px;
|
||||
|
||||
.mx_VoiceBroadcastHeader {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mx_VoiceBroadcastControl {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.mx_LiveBadge {
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_VoiceBroadcastBody_divider {
|
||||
background-color: $quinary-content;
|
||||
border: 0;
|
||||
height: 1px;
|
||||
margin: $spacing-12 0;
|
||||
}
|
||||
|
||||
.mx_VoiceBroadcastBody_controls {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: $spacing-32;
|
||||
justify-content: center;
|
||||
margin-bottom: $spacing-8;
|
||||
}
|
||||
|
||||
.mx_VoiceBroadcastBody_timerow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton.mx_VoiceBroadcastBody_blockButton {
|
||||
display: flex;
|
||||
gap: $spacing-8;
|
||||
}
|
||||
|
||||
.mx_VoiceBroadcastBody__small-close {
|
||||
right: 8px;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
}
|
Binary file not shown.
|
@ -240,11 +240,6 @@ $location-live-secondary-color: #deddfd;
|
|||
}
|
||||
/* ******************** */
|
||||
|
||||
/* Voice Broadcast */
|
||||
/* ******************** */
|
||||
$live-badge-color: #ffffff;
|
||||
/* ******************** */
|
||||
|
||||
/* One-off colors */
|
||||
/* ******************** */
|
||||
$progressbar-bg-color: var(--cpd-color-gray-200);
|
||||
|
|
|
@ -226,11 +226,6 @@ $location-live-color: #5c56f5;
|
|||
$location-live-secondary-color: #deddfd;
|
||||
/* ******************** */
|
||||
|
||||
/* Voice Broadcast */
|
||||
/* ******************** */
|
||||
$live-badge-color: #ffffff;
|
||||
/* ******************** */
|
||||
|
||||
body {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
|
|
@ -325,11 +325,6 @@ $location-live-color: #5c56f5;
|
|||
$location-live-secondary-color: #deddfd;
|
||||
/* ******************** */
|
||||
|
||||
/* Voice Broadcast */
|
||||
/* ******************** */
|
||||
$live-badge-color: #ffffff;
|
||||
/* ******************** */
|
||||
|
||||
body {
|
||||
color-scheme: light;
|
||||
}
|
||||
|
|
|
@ -143,3 +143,21 @@ $inter-unicode-range: U+0000-20e2, U+20e4-23ce, U+23d0-24c1, U+24c3-259f, U+25c2
|
|||
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;
|
||||
}
|
||||
|
||||
/* Twemoji COLR */
|
||||
@font-face {
|
||||
font-family: "Twemoji";
|
||||
font-weight: 400;
|
||||
src: url("$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2") format("woff2");
|
||||
}
|
||||
/* For at least Chrome on Windows 10, we have to explictly add extra weights for the emoji to appear in bold messages, etc. */
|
||||
@font-face {
|
||||
font-family: "Twemoji";
|
||||
font-weight: 600;
|
||||
src: url("$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2") format("woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Twemoji";
|
||||
font-weight: 700;
|
||||
src: url("$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2") format("woff2");
|
||||
}
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
/* Noto Color Emoji contains digits, in fixed-width, therefore causing
|
||||
digits in flowed text to stand out.
|
||||
TODO: Consider putting all emoji fonts to the end rather than the front. */
|
||||
$font-family: "Inter", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Arial", "Helvetica", sans-serif,
|
||||
"Noto Color Emoji";
|
||||
$font-family: "Inter", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Arial", "Helvetica",
|
||||
sans-serif, "Noto Color Emoji";
|
||||
|
||||
$monospace-font-family: "Inconsolata", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Courier",
|
||||
monospace, "Noto Color Emoji";
|
||||
|
@ -355,11 +355,6 @@ $location-live-color: var(--cpd-color-purple-900);
|
|||
$location-live-secondary-color: var(--cpd-color-purple-600);
|
||||
/* ******************** */
|
||||
|
||||
/* Voice Broadcast */
|
||||
/* ******************** */
|
||||
$live-badge-color: var(--cpd-color-icon-on-solid-primary);
|
||||
/* ******************** */
|
||||
|
||||
body {
|
||||
color-scheme: light;
|
||||
}
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
if len(sys.argv) < 3:
|
||||
print "Usage: %s <source> <dest>" % (sys.argv[0],)
|
||||
print "eg. %s pt_BR.json pt.json" % (sys.argv[0],)
|
||||
print
|
||||
print "Adds any translations to <dest> that exist in <source> but not <dest>"
|
||||
sys.exit(1)
|
||||
|
||||
srcpath = sys.argv[1]
|
||||
dstpath = sys.argv[2]
|
||||
tmppath = dstpath + ".tmp"
|
||||
|
||||
with open(srcpath) as f:
|
||||
src = json.load(f)
|
||||
|
||||
with open(dstpath) as f:
|
||||
dst = json.load(f)
|
||||
|
||||
toAdd = {}
|
||||
for k,v in src.iteritems():
|
||||
if k not in dst:
|
||||
print "Adding %s" % (k,)
|
||||
toAdd[k] = v
|
||||
|
||||
# don't just json.dumps as we'll probably re-order all the keys (and they're
|
||||
# not in any given order so we can't just sort_keys). Append them to the end.
|
||||
with open(dstpath) as ifp:
|
||||
with open(tmppath, 'w') as ofp:
|
||||
for line in ifp:
|
||||
strippedline = line.strip()
|
||||
if strippedline in ('{', '}'):
|
||||
ofp.write(line)
|
||||
elif strippedline.endswith(','):
|
||||
ofp.write(line)
|
||||
else:
|
||||
ofp.write(' '+strippedline+',')
|
||||
toAddStr = json.dumps(toAdd, indent=4, separators=(',', ': '), ensure_ascii=False, encoding="utf8").strip("{}\n")
|
||||
ofp.write("\n")
|
||||
ofp.write(toAddStr.encode('utf8'))
|
||||
ofp.write("\n")
|
||||
|
||||
os.rename(tmppath, dstpath)
|
|
@ -1,84 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Fetches the js-sdk dependency for development or testing purposes
|
||||
# If there exists a branch of that dependency with the same name as
|
||||
# the branch the current checkout is on, use that branch. Otherwise,
|
||||
# use develop.
|
||||
|
||||
set -x
|
||||
|
||||
GIT_CLONE_ARGS=("$@")
|
||||
[ -z "$defbranch" ] && defbranch="develop"
|
||||
|
||||
# clone a specific branch of a github repo
|
||||
function clone() {
|
||||
org=$1
|
||||
repo=$2
|
||||
branch=$3
|
||||
|
||||
# Chop 'origin' off the start as jenkins ends up using
|
||||
# branches on the origin, but this doesn't work if we
|
||||
# specify the branch when cloning.
|
||||
branch=${branch#origin/}
|
||||
|
||||
if [ -n "$branch" ]
|
||||
then
|
||||
echo "Trying to use $org/$repo#$branch"
|
||||
# Disable auth prompts: https://serverfault.com/a/665959
|
||||
GIT_TERMINAL_PROMPT=0 git clone https://github.com/$org/$repo.git $repo --branch $branch \
|
||||
"${GIT_CLONE_ARGS[@]}"
|
||||
return $?
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
function dodep() {
|
||||
deforg=$1
|
||||
defrepo=$2
|
||||
rm -rf $defrepo
|
||||
|
||||
# Try the PR author's branch in case it exists on the deps as well.
|
||||
# Try the target branch of the push or PR.
|
||||
# Use the default branch as the last resort.
|
||||
if [[ "$BUILDKITE" == true ]]; then
|
||||
# If BUILDKITE_BRANCH is set, it will contain either:
|
||||
# * "branch" when the author's branch and target branch are in the same repo
|
||||
# * "author:branch" when the author's branch is in their fork
|
||||
# We can split on `:` into an array to check.
|
||||
BUILDKITE_BRANCH_ARRAY=(${BUILDKITE_BRANCH//:/ })
|
||||
if [[ "${#BUILDKITE_BRANCH_ARRAY[@]}" == "2" ]]; then
|
||||
prAuthor=${BUILDKITE_BRANCH_ARRAY[0]}
|
||||
prBranch=${BUILDKITE_BRANCH_ARRAY[1]}
|
||||
else
|
||||
prAuthor=$deforg
|
||||
prBranch=$BUILDKITE_BRANCH
|
||||
fi
|
||||
clone $prAuthor $defrepo $prBranch ||
|
||||
clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH ||
|
||||
clone $deforg $defrepo $defbranch ||
|
||||
return $?
|
||||
else
|
||||
clone $deforg $defrepo $ghprbSourceBranch ||
|
||||
clone $deforg $defrepo $GIT_BRANCH ||
|
||||
clone $deforg $defrepo `git rev-parse --abbrev-ref HEAD` ||
|
||||
clone $deforg $defrepo $defbranch ||
|
||||
return $?
|
||||
fi
|
||||
|
||||
echo "$defrepo set to branch "`git -C "$defrepo" rev-parse --abbrev-ref HEAD`
|
||||
}
|
||||
|
||||
##############################
|
||||
|
||||
echo 'Setting up matrix-js-sdk'
|
||||
|
||||
dodep matrix-org matrix-js-sdk
|
||||
|
||||
pushd matrix-js-sdk
|
||||
yarn link
|
||||
yarn install --frozen-lockfile
|
||||
popd
|
||||
|
||||
yarn link matrix-js-sdk
|
||||
|
||||
##############################
|
|
@ -1,64 +0,0 @@
|
|||
# Copyright 2017-2024 New Vector Ltd.
|
||||
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
# Please see LICENSE in the repository root for full details.
|
||||
|
||||
|
||||
# genflags.sh - Generates pngs for use with CountryDropdown.js
|
||||
#
|
||||
# Dependencies:
|
||||
# - imagemagick --with-rsvg (because default imagemagick SVG
|
||||
# renderer does not produce accurate results)
|
||||
#
|
||||
# on macOS, this is most easily done with:
|
||||
# brew install imagemagick --with-librsvg
|
||||
#
|
||||
# This will clone the googlei18n flag repo before converting
|
||||
# all phonenumber.js-supported country flags (as SVGs) into
|
||||
# PNGs that can be used by CountryDropdown.js.
|
||||
|
||||
set -e
|
||||
|
||||
# Allow CTRL+C to terminate the script
|
||||
trap "echo Exited!; exit;" SIGINT SIGTERM
|
||||
|
||||
# git clone the google repo to get flag SVGs
|
||||
git clone git@github.com:googlei18n/region-flags
|
||||
for f in region-flags/svg/*.svg; do
|
||||
# Skip state flags
|
||||
if [[ $f =~ [A-Z]{2}-[A-Z]{2,3}.svg ]] ; then
|
||||
echo "Skipping state flag "$f
|
||||
continue
|
||||
fi
|
||||
|
||||
# Skip countries not included in phonenumber.js
|
||||
if [[ $f =~ (AC|CP|DG|EA|EU|IC|TA|UM|UN|XK).svg ]] ; then
|
||||
echo "Skipping non-phonenumber supported flag "$f
|
||||
continue
|
||||
fi
|
||||
|
||||
# Run imagemagick convert
|
||||
# -background none : transparent background
|
||||
# -resize 50x30 : resize the flag to have a height of 15px (2x)
|
||||
# By default, aspect ratio is respected so the width will
|
||||
# be correct and not necessarily 25px.
|
||||
# -filter Lanczos : use sharper resampling to avoid muddiness
|
||||
# -gravity Center : keep the image central when adding an -extent
|
||||
# -border 1 : add a 1px border around the flag
|
||||
# -bordercolor : set the border colour
|
||||
# -extent 54x54 : surround the image with padding so that it
|
||||
# has the dimensions 27x27px (2x).
|
||||
convert $f -background none -filter Lanczos -resize 50x30 \
|
||||
-gravity Center -border 1 -bordercolor \#e0e0e0 \
|
||||
-extent 54x54 $f.png
|
||||
|
||||
# $f.png will be region-flags/svg/XX.svg.png at this point
|
||||
|
||||
# Extract filename from path $f
|
||||
newname=${f##*/}
|
||||
# Replace .svg with .png
|
||||
newname=${newname%.svg}.png
|
||||
# Move the file to flags directory
|
||||
mv $f.png ../res/flags/$newname
|
||||
echo "Generated res/flags/"$newname
|
||||
done
|
6
src/@types/matrix-js-sdk.d.ts
vendored
6
src/@types/matrix-js-sdk.d.ts
vendored
|
@ -10,7 +10,6 @@ import type { IWidget } from "matrix-widget-api";
|
|||
import type { BLURHASH_FIELD } from "../utils/image-media";
|
||||
import type { JitsiCallMemberEventType, JitsiCallMemberContent } from "../call-types";
|
||||
import type { ILayoutStateEvent, WIDGET_LAYOUT_EVENT_TYPE } from "../stores/widgets/types";
|
||||
import type { VoiceBroadcastInfoEventContent, VoiceBroadcastInfoEventType } from "../voice-broadcast/types";
|
||||
import type { EncryptedFile } from "matrix-js-sdk/src/types";
|
||||
|
||||
// Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types
|
||||
|
@ -37,9 +36,6 @@ declare module "matrix-js-sdk/src/types" {
|
|||
"im.vector.modular.widgets": IWidget | {};
|
||||
[WIDGET_LAYOUT_EVENT_TYPE]: ILayoutStateEvent;
|
||||
|
||||
// Unstable voice broadcast state events
|
||||
[VoiceBroadcastInfoEventType]: VoiceBroadcastInfoEventContent;
|
||||
|
||||
// Element custom state events
|
||||
"im.vector.web.settings": Record<string, any>;
|
||||
"org.matrix.room.preview_urls": { disable: boolean };
|
||||
|
@ -78,7 +74,5 @@ declare module "matrix-js-sdk/src/types" {
|
|||
waveform?: number[];
|
||||
};
|
||||
"org.matrix.msc3245.voice"?: {};
|
||||
|
||||
"io.element.voice_broadcast_chunk"?: { sequence: number };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -175,13 +175,6 @@ export interface IConfigOptions {
|
|||
sync_timeline_limit?: number;
|
||||
dangerously_allow_unsafe_and_insecure_passwords?: boolean; // developer option
|
||||
|
||||
voice_broadcast?: {
|
||||
// length per voice chunk in seconds
|
||||
chunk_length?: number;
|
||||
// max voice broadcast length in seconds
|
||||
max_length?: number;
|
||||
};
|
||||
|
||||
user_notice?: {
|
||||
title: string;
|
||||
description: string;
|
||||
|
|
|
@ -55,8 +55,6 @@ import { OpenInviteDialogPayload } from "./dispatcher/payloads/OpenInviteDialogP
|
|||
import { findDMForUser } from "./utils/dm/findDMForUser";
|
||||
import { getJoinedNonFunctionalMembers } from "./utils/room/getJoinedNonFunctionalMembers";
|
||||
import { localNotificationsAreSilenced } from "./utils/notifications";
|
||||
import { SdkContextClass } from "./contexts/SDKContext";
|
||||
import { showCantStartACallDialog } from "./voice-broadcast/utils/showCantStartACallDialog";
|
||||
import { isNotNull } from "./Typeguards";
|
||||
import { BackgroundAudio } from "./audio/BackgroundAudio";
|
||||
import { Jitsi } from "./widgets/Jitsi.ts";
|
||||
|
@ -859,15 +857,6 @@ export default class LegacyCallHandler extends EventEmitter {
|
|||
return;
|
||||
}
|
||||
|
||||
// Pause current broadcast, if any
|
||||
SdkContextClass.instance.voiceBroadcastPlaybacksStore.getCurrent()?.pause();
|
||||
|
||||
if (SdkContextClass.instance.voiceBroadcastRecordingsStore.getCurrent()) {
|
||||
// Do not start a call, if recording a broadcast
|
||||
showCantStartACallDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
// We might be using managed hybrid widgets
|
||||
if (isManagedHybridWidgetEnabled(room)) {
|
||||
await addManagedHybridWidget(room);
|
||||
|
|
|
@ -35,13 +35,11 @@ import IdentityAuthClient from "./IdentityAuthClient";
|
|||
import { crossSigningCallbacks } from "./SecurityManager";
|
||||
import { SlidingSyncManager } from "./SlidingSyncManager";
|
||||
import { _t, UserFriendlyError } from "./languageHandler";
|
||||
import { SettingLevel } from "./settings/SettingLevel";
|
||||
import MatrixClientBackedController from "./settings/controllers/MatrixClientBackedController";
|
||||
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
||||
import PlatformPeg from "./PlatformPeg";
|
||||
import { formatList } from "./utils/FormattingUtils";
|
||||
import SdkConfig from "./SdkConfig";
|
||||
import { Features } from "./settings/Settings";
|
||||
import { setDeviceIsolationMode } from "./settings/controllers/DeviceIsolationModeController.ts";
|
||||
|
||||
export interface IMatrixClientCreds {
|
||||
|
@ -333,11 +331,6 @@ class MatrixClientPegClass implements IMatrixClientPeg {
|
|||
logger.error("Warning! Not using an encryption key for rust crypto store.");
|
||||
}
|
||||
|
||||
// Record the fact that we used the Rust crypto stack with this client. This just guards against people
|
||||
// rolling back to versions of EW that did not default to Rust crypto (which would lead to an error, since
|
||||
// we cannot migrate from Rust to Legacy crypto).
|
||||
await SettingsStore.setValue(Features.RustCrypto, null, SettingLevel.DEVICE, true);
|
||||
|
||||
await this.matrixClient.initRustCrypto({
|
||||
storageKey: rustCryptoStoreKey,
|
||||
storagePassword: rustCryptoStorePassword,
|
||||
|
|
|
@ -49,8 +49,6 @@ import { SdkContextClass } from "./contexts/SDKContext";
|
|||
import { localNotificationsAreSilenced, createLocalNotificationSettingsIfNeeded } from "./utils/notifications";
|
||||
import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast";
|
||||
import ToastStore from "./stores/ToastStore";
|
||||
import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoEventType } from "./voice-broadcast";
|
||||
import { getSenderName } from "./utils/event/getSenderName";
|
||||
import { stripPlainReply } from "./utils/Reply";
|
||||
import { BackgroundAudio } from "./audio/BackgroundAudio";
|
||||
|
||||
|
@ -81,17 +79,6 @@ const msgTypeHandlers: Record<string, (event: MatrixEvent) => string | null> = {
|
|||
return TextForEvent.textForLocationEvent(event)();
|
||||
},
|
||||
[MsgType.Audio]: (event: MatrixEvent): string | null => {
|
||||
if (event.getContent()?.[VoiceBroadcastChunkEventType]) {
|
||||
if (event.getContent()?.[VoiceBroadcastChunkEventType]?.sequence === 1) {
|
||||
// Show a notification for the first broadcast chunk.
|
||||
// At this point a user received something to listen to.
|
||||
return _t("notifier|io.element.voice_broadcast_chunk", { senderName: getSenderName(event) });
|
||||
}
|
||||
|
||||
// Mute other broadcast chunks
|
||||
return null;
|
||||
}
|
||||
|
||||
return TextForEvent.textForEvent(event, MatrixClientPeg.safeGet());
|
||||
},
|
||||
};
|
||||
|
@ -460,8 +447,6 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
|
|||
|
||||
// XXX: exported for tests
|
||||
public evaluateEvent(ev: MatrixEvent): void {
|
||||
// Mute notifications for broadcast info events
|
||||
if (ev.getType() === VoiceBroadcastInfoEventType) return;
|
||||
let roomId = ev.getRoomId()!;
|
||||
if (LegacyCallHandler.instance.getSupportsVirtualRooms()) {
|
||||
// Attempt to translate a virtual room to a native one
|
||||
|
|
|
@ -46,10 +46,6 @@ export const DEFAULTS: DeepReadonly<IConfigOptions> = {
|
|||
logo: require("../res/img/element-desktop-logo.svg").default,
|
||||
url: "https://element.io/get-started",
|
||||
},
|
||||
voice_broadcast: {
|
||||
chunk_length: 2 * 60, // two minutes
|
||||
max_length: 4 * 60 * 60, // four hours
|
||||
},
|
||||
|
||||
feedback: {
|
||||
existing_issues_url:
|
||||
|
|
|
@ -7,8 +7,8 @@ Please see LICENSE files in the repository root for full details.
|
|||
*/
|
||||
|
||||
import { lazy } from "react";
|
||||
import { ICryptoCallbacks, SecretStorage } from "matrix-js-sdk/src/matrix";
|
||||
import { deriveRecoveryKeyFromPassphrase, decodeRecoveryKey } from "matrix-js-sdk/src/crypto-api";
|
||||
import { SecretStorage } from "matrix-js-sdk/src/matrix";
|
||||
import { deriveRecoveryKeyFromPassphrase, decodeRecoveryKey, CryptoCallbacks } from "matrix-js-sdk/src/crypto-api";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import Modal from "./Modal";
|
||||
|
@ -159,7 +159,7 @@ function cacheSecretStorageKey(
|
|||
}
|
||||
}
|
||||
|
||||
export const crossSigningCallbacks: ICryptoCallbacks = {
|
||||
export const crossSigningCallbacks: CryptoCallbacks = {
|
||||
getSecretStorageKey,
|
||||
cacheSecretStorageKey,
|
||||
};
|
||||
|
|
|
@ -49,7 +49,6 @@ import VoipUserMapper from "./VoipUserMapper";
|
|||
import { htmlSerializeFromMdIfNeeded } from "./editor/serialize";
|
||||
import { leaveRoomBehaviour } from "./utils/leave-behaviour";
|
||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||
import { getDeviceCryptoInfo } from "./utils/crypto/deviceInfo";
|
||||
import { isCurrentLocalRoom, reject, singleMxcUpload, success, successSync } from "./slash-commands/utils";
|
||||
import { deop, op } from "./slash-commands/op";
|
||||
import { CommandCategories } from "./slash-commands/interface";
|
||||
|
@ -658,69 +657,6 @@ export const Commands = [
|
|||
category: CommandCategories.admin,
|
||||
renderingTypes: [TimelineRenderingType.Room],
|
||||
}),
|
||||
new Command({
|
||||
command: "verify",
|
||||
args: "<user-id> <device-id> <device-signing-key>",
|
||||
description: _td("slash_command|verify"),
|
||||
runFn: function (cli, roomId, threadId, args) {
|
||||
if (args) {
|
||||
const matches = args.match(/^(\S+) +(\S+) +(\S+)$/);
|
||||
if (matches) {
|
||||
const userId = matches[1];
|
||||
const deviceId = matches[2];
|
||||
const fingerprint = matches[3];
|
||||
|
||||
return success(
|
||||
(async (): Promise<void> => {
|
||||
const device = await getDeviceCryptoInfo(cli, userId, deviceId);
|
||||
if (!device) {
|
||||
throw new UserFriendlyError("slash_command|verify_unknown_pair", {
|
||||
userId,
|
||||
deviceId,
|
||||
cause: undefined,
|
||||
});
|
||||
}
|
||||
const deviceTrust = await cli.getCrypto()?.getDeviceVerificationStatus(userId, deviceId);
|
||||
|
||||
if (deviceTrust?.isVerified()) {
|
||||
if (device.getFingerprint() === fingerprint) {
|
||||
throw new UserFriendlyError("slash_command|verify_nop");
|
||||
} else {
|
||||
throw new UserFriendlyError("slash_command|verify_nop_warning_mismatch");
|
||||
}
|
||||
}
|
||||
|
||||
if (device.getFingerprint() !== fingerprint) {
|
||||
const fprint = device.getFingerprint();
|
||||
throw new UserFriendlyError("slash_command|verify_mismatch", {
|
||||
fprint,
|
||||
userId,
|
||||
deviceId,
|
||||
fingerprint,
|
||||
cause: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
await cli.setDeviceVerified(userId, deviceId, true);
|
||||
|
||||
// Tell the user we verified everything
|
||||
Modal.createDialog(InfoDialog, {
|
||||
title: _t("slash_command|verify_success_title"),
|
||||
description: (
|
||||
<div>
|
||||
<p>{_t("slash_command|verify_success_description", { userId, deviceId })}</p>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
})(),
|
||||
);
|
||||
}
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
},
|
||||
category: CommandCategories.advanced,
|
||||
renderingTypes: [TimelineRenderingType.Room],
|
||||
}),
|
||||
new Command({
|
||||
command: "discardsession",
|
||||
description: _td("slash_command|discardsession"),
|
||||
|
|
|
@ -36,7 +36,6 @@ import AccessibleButton from "./components/views/elements/AccessibleButton";
|
|||
import RightPanelStore from "./stores/right-panel/RightPanelStore";
|
||||
import { highlightEvent, isLocationEvent } from "./utils/EventUtils";
|
||||
import { ElementCall } from "./models/Call";
|
||||
import { textForVoiceBroadcastStoppedEvent, VoiceBroadcastInfoEventType } from "./voice-broadcast";
|
||||
import { getSenderName } from "./utils/event/getSenderName";
|
||||
import PosthogTrackers from "./PosthogTrackers.ts";
|
||||
|
||||
|
@ -906,7 +905,6 @@ const stateHandlers: IHandlers = {
|
|||
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
||||
"im.vector.modular.widgets": textForWidgetEvent,
|
||||
[WIDGET_LAYOUT_EVENT_TYPE]: textForWidgetLayoutEvent,
|
||||
[VoiceBroadcastInfoEventType]: textForVoiceBroadcastStoppedEvent,
|
||||
};
|
||||
|
||||
// Add all the Mjolnir stuff to the renderer
|
||||
|
|
|
@ -36,7 +36,7 @@ interface IState {
|
|||
|
||||
export default class EmbeddedPage extends React.PureComponent<IProps, IState> {
|
||||
public static contextType = MatrixClientContext;
|
||||
public declare context: React.ContextType<typeof MatrixClientContext>;
|
||||
declare public context: React.ContextType<typeof MatrixClientContext>;
|
||||
private unmounted = false;
|
||||
private dispatcherRef?: string;
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ import { Layout } from "../../settings/enums/Layout";
|
|||
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||
import Measured from "../views/elements/Measured";
|
||||
import EmptyState from "../views/right_panel/EmptyState";
|
||||
import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx";
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
|
@ -51,7 +52,7 @@ interface IState {
|
|||
*/
|
||||
class FilePanel extends React.Component<IProps, IState> {
|
||||
public static contextType = RoomContext;
|
||||
public declare context: React.ContextType<typeof RoomContext>;
|
||||
declare public context: React.ContextType<typeof RoomContext>;
|
||||
|
||||
// This is used to track if a decrypted event was a live event and should be
|
||||
// added to the timeline.
|
||||
|
@ -104,7 +105,11 @@ class FilePanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
if (!this.state.timelineSet.eventIdToTimeline(ev.getId()!)) {
|
||||
this.state.timelineSet.addEventToTimeline(ev, timeline, false);
|
||||
this.state.timelineSet.addEventToTimeline(ev, timeline, {
|
||||
fromCache: false,
|
||||
addToState: false,
|
||||
toStartOfTimeline: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -269,12 +274,10 @@ class FilePanel extends React.Component<IProps, IState> {
|
|||
|
||||
if (this.state.timelineSet) {
|
||||
return (
|
||||
<RoomContext.Provider
|
||||
value={{
|
||||
...this.context,
|
||||
timelineRenderingType: TimelineRenderingType.File,
|
||||
narrow: this.state.narrow,
|
||||
}}
|
||||
<ScopedRoomContextProvider
|
||||
{...this.context}
|
||||
timelineRenderingType={TimelineRenderingType.File}
|
||||
narrow={this.state.narrow}
|
||||
>
|
||||
<BaseCard
|
||||
className="mx_FilePanel"
|
||||
|
@ -298,16 +301,11 @@ class FilePanel extends React.Component<IProps, IState> {
|
|||
layout={Layout.Group}
|
||||
/>
|
||||
</BaseCard>
|
||||
</RoomContext.Provider>
|
||||
</ScopedRoomContextProvider>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<RoomContext.Provider
|
||||
value={{
|
||||
...this.context,
|
||||
timelineRenderingType: TimelineRenderingType.File,
|
||||
}}
|
||||
>
|
||||
<ScopedRoomContextProvider {...this.context} timelineRenderingType={TimelineRenderingType.File}>
|
||||
<BaseCard
|
||||
className="mx_FilePanel"
|
||||
onClose={this.props.onClose}
|
||||
|
@ -315,7 +313,7 @@ class FilePanel extends React.Component<IProps, IState> {
|
|||
>
|
||||
<Spinner />
|
||||
</BaseCard>
|
||||
</RoomContext.Provider>
|
||||
</ScopedRoomContextProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ import classNames from "classnames";
|
|||
import { isOnlyCtrlOrCmdKeyEvent, Key } from "../../Keyboard";
|
||||
import PageTypes from "../../PageTypes";
|
||||
import MediaDeviceHandler from "../../MediaDeviceHandler";
|
||||
import { fixupColorFonts } from "../../utils/FontManager";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import { IMatrixClientCreds } from "../../MatrixClientPeg";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
@ -149,8 +148,6 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
|
||||
MediaDeviceHandler.loadDevices();
|
||||
|
||||
fixupColorFonts();
|
||||
|
||||
this._roomView = React.createRef();
|
||||
this._resizeContainer = React.createRef();
|
||||
this.resizeHandler = React.createRef();
|
||||
|
|
|
@ -119,7 +119,6 @@ import { ValidatedServerConfig } from "../../utils/ValidatedServerConfig";
|
|||
import { isLocalRoom } from "../../utils/localRoom/isLocalRoom";
|
||||
import { SDKContext, SdkContextClass } from "../../contexts/SDKContext";
|
||||
import { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSettings";
|
||||
import { cleanUpBroadcasts, VoiceBroadcastResumer } from "../../voice-broadcast";
|
||||
import GenericToast from "../views/toasts/GenericToast";
|
||||
import RovingSpotlightDialog from "../views/dialogs/spotlight/SpotlightDialog";
|
||||
import { findDMForUser } from "../../utils/dm/findDMForUser";
|
||||
|
@ -227,7 +226,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
private focusNext: FocusNextType;
|
||||
private subTitleStatus: string;
|
||||
private prevWindowWidth: number;
|
||||
private voiceBroadcastResumer?: VoiceBroadcastResumer;
|
||||
|
||||
private readonly loggedInView = createRef<LoggedInViewType>();
|
||||
private dispatcherRef?: string;
|
||||
|
@ -501,7 +499,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
window.removeEventListener("resize", this.onWindowResized);
|
||||
|
||||
this.stores.accountPasswordStore.clearPassword();
|
||||
this.voiceBroadcastResumer?.destroy();
|
||||
}
|
||||
|
||||
private onWindowResized = (): void => {
|
||||
|
@ -651,10 +648,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
break;
|
||||
case "logout":
|
||||
LegacyCallHandler.instance.hangupAllCalls();
|
||||
Promise.all([
|
||||
...[...CallStore.instance.connectedCalls].map((call) => call.disconnect()),
|
||||
cleanUpBroadcasts(this.stores),
|
||||
]).finally(() => Lifecycle.logout(this.stores.oidcClientStore));
|
||||
Promise.all([...[...CallStore.instance.connectedCalls].map((call) => call.disconnect())]).finally(() =>
|
||||
Lifecycle.logout(this.stores.oidcClientStore),
|
||||
);
|
||||
break;
|
||||
case "require_registration":
|
||||
startAnyRegistrationFlow(payload as any);
|
||||
|
@ -1679,8 +1675,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.voiceBroadcastResumer = new VoiceBroadcastResumer(cli);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -196,7 +196,7 @@ interface IReadReceiptForUser {
|
|||
*/
|
||||
export default class MessagePanel extends React.Component<IProps, IState> {
|
||||
public static contextType = RoomContext;
|
||||
public declare context: React.ContextType<typeof RoomContext>;
|
||||
declare public context: React.ContextType<typeof RoomContext>;
|
||||
|
||||
public static defaultProps = {
|
||||
disableGrouping: false,
|
||||
|
|
|
@ -19,6 +19,7 @@ import { Layout } from "../../settings/enums/Layout";
|
|||
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||
import Measured from "../views/elements/Measured";
|
||||
import EmptyState from "../views/right_panel/EmptyState";
|
||||
import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx";
|
||||
|
||||
interface IProps {
|
||||
onClose(): void;
|
||||
|
@ -33,7 +34,7 @@ interface IState {
|
|||
*/
|
||||
export default class NotificationPanel extends React.PureComponent<IProps, IState> {
|
||||
public static contextType = RoomContext;
|
||||
public declare context: React.ContextType<typeof RoomContext>;
|
||||
declare public context: React.ContextType<typeof RoomContext>;
|
||||
|
||||
private card = React.createRef<HTMLDivElement>();
|
||||
|
||||
|
@ -79,12 +80,10 @@ export default class NotificationPanel extends React.PureComponent<IProps, IStat
|
|||
}
|
||||
|
||||
return (
|
||||
<RoomContext.Provider
|
||||
value={{
|
||||
...this.context,
|
||||
timelineRenderingType: TimelineRenderingType.Notification,
|
||||
narrow: this.state.narrow,
|
||||
}}
|
||||
<ScopedRoomContextProvider
|
||||
{...this.context}
|
||||
timelineRenderingType={TimelineRenderingType.Notification}
|
||||
narrow={this.state.narrow}
|
||||
>
|
||||
<BaseCard
|
||||
header={_t("notifications|enable_prompt_toast_title")}
|
||||
|
@ -99,7 +98,7 @@ export default class NotificationPanel extends React.PureComponent<IProps, IStat
|
|||
{this.card.current && <Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />}
|
||||
{content}
|
||||
</BaseCard>
|
||||
</RoomContext.Provider>
|
||||
</ScopedRoomContextProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { MutableRefObject, ReactNode, useContext, useRef } from "react";
|
||||
import React, { MutableRefObject, ReactNode, useRef } from "react";
|
||||
import { CallEvent, CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { Optional } from "matrix-events-sdk";
|
||||
|
@ -21,19 +21,7 @@ import { WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
|
|||
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../stores/ActiveWidgetStore";
|
||||
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import { SDKContext, SdkContextClass } from "../../contexts/SDKContext";
|
||||
import {
|
||||
useCurrentVoiceBroadcastPreRecording,
|
||||
useCurrentVoiceBroadcastRecording,
|
||||
VoiceBroadcastPlayback,
|
||||
VoiceBroadcastPlaybackBody,
|
||||
VoiceBroadcastPreRecording,
|
||||
VoiceBroadcastPreRecordingPip,
|
||||
VoiceBroadcastRecording,
|
||||
VoiceBroadcastRecordingPip,
|
||||
VoiceBroadcastSmallPlaybackBody,
|
||||
} from "../../voice-broadcast";
|
||||
import { useCurrentVoiceBroadcastPlayback } from "../../voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback";
|
||||
import { SdkContextClass } from "../../contexts/SDKContext";
|
||||
import { WidgetPip } from "../views/pips/WidgetPip";
|
||||
|
||||
const SHOW_CALL_IN_STATES = [
|
||||
|
@ -46,9 +34,6 @@ const SHOW_CALL_IN_STATES = [
|
|||
];
|
||||
|
||||
interface IProps {
|
||||
voiceBroadcastRecording: Optional<VoiceBroadcastRecording>;
|
||||
voiceBroadcastPreRecording: Optional<VoiceBroadcastPreRecording>;
|
||||
voiceBroadcastPlayback: Optional<VoiceBroadcastPlayback>;
|
||||
movePersistedElement: MutableRefObject<(() => void) | undefined>;
|
||||
}
|
||||
|
||||
|
@ -245,52 +230,9 @@ class PipContainerInner extends React.Component<IProps, IState> {
|
|||
this.setState({ showWidgetInPip, persistentWidgetId, persistentRoomId });
|
||||
}
|
||||
|
||||
private createVoiceBroadcastPlaybackPipContent(voiceBroadcastPlayback: VoiceBroadcastPlayback): CreatePipChildren {
|
||||
const content =
|
||||
this.state.viewedRoomId === voiceBroadcastPlayback.infoEvent.getRoomId() ? (
|
||||
<VoiceBroadcastPlaybackBody playback={voiceBroadcastPlayback} pip={true} />
|
||||
) : (
|
||||
<VoiceBroadcastSmallPlaybackBody playback={voiceBroadcastPlayback} />
|
||||
);
|
||||
|
||||
return ({ onStartMoving }) => (
|
||||
<div key={`vb-playback-${voiceBroadcastPlayback.infoEvent.getId()}`} onMouseDown={onStartMoving}>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private createVoiceBroadcastPreRecordingPipContent(
|
||||
voiceBroadcastPreRecording: VoiceBroadcastPreRecording,
|
||||
): CreatePipChildren {
|
||||
return ({ onStartMoving }) => (
|
||||
<div key="vb-pre-recording" onMouseDown={onStartMoving}>
|
||||
<VoiceBroadcastPreRecordingPip voiceBroadcastPreRecording={voiceBroadcastPreRecording} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private createVoiceBroadcastRecordingPipContent(
|
||||
voiceBroadcastRecording: VoiceBroadcastRecording,
|
||||
): CreatePipChildren {
|
||||
return ({ onStartMoving }) => (
|
||||
<div key={`vb-recording-${voiceBroadcastRecording.infoEvent.getId()}`} onMouseDown={onStartMoving}>
|
||||
<VoiceBroadcastRecordingPip recording={voiceBroadcastRecording} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public render(): ReactNode {
|
||||
const pipMode = true;
|
||||
let pipContent: Array<CreatePipChildren> = [];
|
||||
|
||||
if (this.props.voiceBroadcastRecording) {
|
||||
pipContent = [this.createVoiceBroadcastRecordingPipContent(this.props.voiceBroadcastRecording)];
|
||||
} else if (this.props.voiceBroadcastPreRecording) {
|
||||
pipContent = [this.createVoiceBroadcastPreRecordingPipContent(this.props.voiceBroadcastPreRecording)];
|
||||
} else if (this.props.voiceBroadcastPlayback) {
|
||||
pipContent = [this.createVoiceBroadcastPlaybackPipContent(this.props.voiceBroadcastPlayback)];
|
||||
}
|
||||
const pipContent: Array<CreatePipChildren> = [];
|
||||
|
||||
if (this.state.primaryCall) {
|
||||
// get a ref to call inside the current scope
|
||||
|
@ -338,24 +280,7 @@ class PipContainerInner extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
export const PipContainer: React.FC = () => {
|
||||
const sdkContext = useContext(SDKContext);
|
||||
const voiceBroadcastPreRecordingStore = sdkContext.voiceBroadcastPreRecordingStore;
|
||||
const { currentVoiceBroadcastPreRecording } = useCurrentVoiceBroadcastPreRecording(voiceBroadcastPreRecordingStore);
|
||||
|
||||
const voiceBroadcastRecordingsStore = sdkContext.voiceBroadcastRecordingsStore;
|
||||
const { currentVoiceBroadcastRecording } = useCurrentVoiceBroadcastRecording(voiceBroadcastRecordingsStore);
|
||||
|
||||
const voiceBroadcastPlaybacksStore = sdkContext.voiceBroadcastPlaybacksStore;
|
||||
const { currentVoiceBroadcastPlayback } = useCurrentVoiceBroadcastPlayback(voiceBroadcastPlaybacksStore);
|
||||
|
||||
const movePersistedElement = useRef<() => void>();
|
||||
|
||||
return (
|
||||
<PipContainerInner
|
||||
voiceBroadcastPlayback={currentVoiceBroadcastPlayback}
|
||||
voiceBroadcastPreRecording={currentVoiceBroadcastPreRecording}
|
||||
voiceBroadcastRecording={currentVoiceBroadcastRecording}
|
||||
movePersistedElement={movePersistedElement}
|
||||
/>
|
||||
);
|
||||
return <PipContainerInner movePersistedElement={movePersistedElement} />;
|
||||
};
|
||||
|
|
|
@ -63,7 +63,7 @@ interface IState {
|
|||
|
||||
export default class RightPanel extends React.Component<Props, IState> {
|
||||
public static contextType = MatrixClientContext;
|
||||
public declare context: React.ContextType<typeof MatrixClientContext>;
|
||||
declare public context: React.ContextType<typeof MatrixClientContext>;
|
||||
|
||||
public constructor(props: Props, context: React.ContextType<typeof MatrixClientContext>) {
|
||||
super(props, context);
|
||||
|
@ -109,10 +109,10 @@ export default class RightPanel extends React.Component<Props, IState> {
|
|||
}
|
||||
|
||||
// redraw the badge on the membership list
|
||||
if (this.state.phase === RightPanelPhases.RoomMemberList) {
|
||||
if (this.state.phase === RightPanelPhases.MemberList) {
|
||||
this.delayedUpdate();
|
||||
} else if (
|
||||
this.state.phase === RightPanelPhases.RoomMemberInfo &&
|
||||
this.state.phase === RightPanelPhases.MemberInfo &&
|
||||
member.userId === this.state.cardState?.member?.userId
|
||||
) {
|
||||
// refresh the member info (e.g. new power level)
|
||||
|
@ -157,7 +157,7 @@ export default class RightPanel extends React.Component<Props, IState> {
|
|||
const phase = this.props.overwriteCard?.phase ?? this.state.phase;
|
||||
const cardState = this.props.overwriteCard?.state ?? this.state.cardState;
|
||||
switch (phase) {
|
||||
case RightPanelPhases.RoomMemberList:
|
||||
case RightPanelPhases.MemberList:
|
||||
if (!!roomId) {
|
||||
card = (
|
||||
<MemberList
|
||||
|
@ -170,22 +170,8 @@ export default class RightPanel extends React.Component<Props, IState> {
|
|||
);
|
||||
}
|
||||
break;
|
||||
case RightPanelPhases.SpaceMemberList:
|
||||
if (!!cardState?.spaceId || !!roomId) {
|
||||
card = (
|
||||
<MemberList
|
||||
roomId={cardState?.spaceId ?? roomId!}
|
||||
key={cardState?.spaceId ?? roomId!}
|
||||
onClose={this.onClose}
|
||||
searchQuery={this.state.searchQuery}
|
||||
onSearchQueryChanged={this.onSearchQueryChanged}
|
||||
/>
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case RightPanelPhases.RoomMemberInfo:
|
||||
case RightPanelPhases.SpaceMemberInfo:
|
||||
case RightPanelPhases.MemberInfo:
|
||||
case RightPanelPhases.EncryptionPanel: {
|
||||
if (!!cardState?.member) {
|
||||
const roomMember = cardState.member instanceof RoomMember ? cardState.member : undefined;
|
||||
|
@ -203,8 +189,7 @@ export default class RightPanel extends React.Component<Props, IState> {
|
|||
}
|
||||
break;
|
||||
}
|
||||
case RightPanelPhases.Room3pidMemberInfo:
|
||||
case RightPanelPhases.Space3pidMemberInfo:
|
||||
case RightPanelPhases.ThreePidMemberInfo:
|
||||
if (!!cardState?.memberInfoEvent) {
|
||||
card = (
|
||||
<ThirdPartyMemberInfo event={cardState.memberInfoEvent} key={roomId} onClose={this.onClose} />
|
||||
|
|
|
@ -26,7 +26,7 @@ import ErrorDialog from "../views/dialogs/ErrorDialog";
|
|||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
|
||||
import RoomContext from "../../contexts/RoomContext";
|
||||
import { useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx";
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function (msg: string): void {};
|
||||
|
@ -53,7 +53,7 @@ interface Props {
|
|||
export const RoomSearchView = forwardRef<ScrollPanel, Props>(
|
||||
({ term, scope, promise, abortController, resizeNotifier, className, onUpdate, inProgress }: Props, ref) => {
|
||||
const client = useContext(MatrixClientContext);
|
||||
const roomContext = useContext(RoomContext);
|
||||
const roomContext = useScopedRoomContext("showHiddenEvents");
|
||||
const [highlights, setHighlights] = useState<string[] | null>(null);
|
||||
const [results, setResults] = useState<ISearchResults | null>(null);
|
||||
const aborted = useRef(false);
|
||||
|
|
|
@ -89,7 +89,7 @@ interface IState {
|
|||
export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
|
||||
private unmounted = false;
|
||||
public static contextType = MatrixClientContext;
|
||||
public declare context: React.ContextType<typeof MatrixClientContext>;
|
||||
declare public context: React.ContextType<typeof MatrixClientContext>;
|
||||
|
||||
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
|
||||
super(props, context);
|
||||
|
|
|
@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { ChangeEvent, ComponentProps, createRef, ReactElement, ReactNode, RefObject, useContext } from "react";
|
||||
import React, { ChangeEvent, ComponentProps, createRef, ReactElement, ReactNode, RefObject, JSX } from "react";
|
||||
import classNames from "classnames";
|
||||
import {
|
||||
IRecommendedVersion,
|
||||
|
@ -29,6 +29,7 @@ import {
|
|||
MatrixError,
|
||||
ISearchResults,
|
||||
THREAD_RELATION_TYPE,
|
||||
MatrixClient,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
@ -54,7 +55,7 @@ import WidgetEchoStore from "../../stores/WidgetEchoStore";
|
|||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import { Layout } from "../../settings/enums/Layout";
|
||||
import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
|
||||
import RoomContext, { TimelineRenderingType, MainSplitContentType } from "../../contexts/RoomContext";
|
||||
import { TimelineRenderingType, MainSplitContentType } from "../../contexts/RoomContext";
|
||||
import { E2EStatus, shieldStatusForRoom } from "../../utils/ShieldUtils";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { IMatrixClientCreds } from "../../MatrixClientPeg";
|
||||
|
@ -126,6 +127,7 @@ import RightPanelStore from "../../stores/right-panel/RightPanelStore";
|
|||
import { onView3pidInvite } from "../../stores/right-panel/action-handlers";
|
||||
import RoomSearchAuxPanel from "../views/rooms/RoomSearchAuxPanel";
|
||||
import { PinnedMessageBanner } from "../views/rooms/PinnedMessageBanner";
|
||||
import { ScopedRoomContextProvider, useScopedRoomContext } from "../../contexts/ScopedRoomContext";
|
||||
|
||||
const DEBUG = false;
|
||||
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
|
||||
|
@ -233,6 +235,11 @@ export interface IRoomState {
|
|||
liveTimeline?: EventTimeline;
|
||||
narrow: boolean;
|
||||
msc3946ProcessDynamicPredecessor: boolean;
|
||||
/**
|
||||
* Whether the room is encrypted or not.
|
||||
* If null, we are still determining the encryption status.
|
||||
*/
|
||||
isRoomEncrypted: boolean | null;
|
||||
|
||||
canAskToJoin: boolean;
|
||||
promptAskToJoin: boolean;
|
||||
|
@ -246,6 +253,7 @@ interface LocalRoomViewProps {
|
|||
permalinkCreator: RoomPermalinkCreator;
|
||||
roomView: RefObject<HTMLElement>;
|
||||
onFileDrop: (dataTransfer: DataTransfer) => Promise<void>;
|
||||
mainSplitContentType: MainSplitContentType;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -255,7 +263,7 @@ interface LocalRoomViewProps {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
function LocalRoomView(props: LocalRoomViewProps): ReactElement {
|
||||
const context = useContext(RoomContext);
|
||||
const context = useScopedRoomContext("room");
|
||||
const room = context.room as LocalRoom;
|
||||
const encryptionEvent = props.localRoom.currentState.getStateEvents(EventType.RoomEncryption)[0];
|
||||
let encryptionTile: ReactNode;
|
||||
|
@ -323,6 +331,7 @@ interface ILocalRoomCreateLoaderProps {
|
|||
localRoom: LocalRoom;
|
||||
names: string;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
mainSplitContentType: MainSplitContentType;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -363,7 +372,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
private roomViewBody = createRef<HTMLDivElement>();
|
||||
|
||||
public static contextType = SDKContext;
|
||||
public declare context: React.ContextType<typeof SDKContext>;
|
||||
declare public context: React.ContextType<typeof SDKContext>;
|
||||
|
||||
public constructor(props: IRoomProps, context: React.ContextType<typeof SDKContext>) {
|
||||
super(props, context);
|
||||
|
@ -417,6 +426,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
canAskToJoin: this.askToJoinEnabled,
|
||||
promptAskToJoin: false,
|
||||
viewRoomOpts: { buttons: [] },
|
||||
isRoomEncrypted: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -655,6 +665,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
// the RoomView instance
|
||||
if (initial) {
|
||||
newState.room = this.context.client!.getRoom(newState.roomId) || undefined;
|
||||
newState.isRoomEncrypted = null;
|
||||
if (newState.room) {
|
||||
newState.showApps = this.shouldShowApps(newState.room);
|
||||
this.onRoomLoaded(newState.room);
|
||||
|
@ -697,6 +708,14 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
if (initial) {
|
||||
this.setupRoom(newState.room, newState.roomId, !!newState.joining, !!newState.shouldPeek);
|
||||
}
|
||||
|
||||
// We don't block the initial setup but we want to make it early to not block the timeline rendering
|
||||
const isRoomEncrypted = await this.getIsRoomEncrypted(newState.roomId);
|
||||
this.setState({
|
||||
isRoomEncrypted,
|
||||
...(isRoomEncrypted &&
|
||||
newState.roomId && { e2eStatus: RoomView.e2eStatusCache.get(newState.roomId) ?? E2EStatus.Warning }),
|
||||
});
|
||||
};
|
||||
|
||||
private onConnectedCalls = (): void => {
|
||||
|
@ -1214,18 +1233,18 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
if (payload.member) {
|
||||
if (payload.push) {
|
||||
RightPanelStore.instance.pushCard({
|
||||
phase: RightPanelPhases.RoomMemberInfo,
|
||||
phase: RightPanelPhases.MemberInfo,
|
||||
state: { member: payload.member },
|
||||
});
|
||||
} else {
|
||||
RightPanelStore.instance.setCards([
|
||||
{ phase: RightPanelPhases.RoomSummary },
|
||||
{ phase: RightPanelPhases.RoomMemberList },
|
||||
{ phase: RightPanelPhases.RoomMemberInfo, state: { member: payload.member } },
|
||||
{ phase: RightPanelPhases.MemberList },
|
||||
{ phase: RightPanelPhases.MemberInfo, state: { member: payload.member } },
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomMemberList);
|
||||
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.MemberList);
|
||||
}
|
||||
break;
|
||||
case Action.View3pidInvite:
|
||||
|
@ -1342,13 +1361,12 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
this.context.widgetLayoutStore.on(WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange);
|
||||
|
||||
this.calculatePeekRules(room);
|
||||
this.updatePreviewUrlVisibility(room);
|
||||
this.loadMembersIfJoined(room);
|
||||
this.calculateRecommendedVersion(room);
|
||||
this.updateE2EStatus(room);
|
||||
this.updatePermissions(room);
|
||||
this.checkWidgets(room);
|
||||
this.loadVirtualRoom(room);
|
||||
this.updateRoomEncrypted(room);
|
||||
|
||||
if (
|
||||
this.getMainSplitContentType(room) !== MainSplitContentType.Timeline &&
|
||||
|
@ -1377,6 +1395,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
return room?.currentState.getStateEvents(EventType.RoomTombstone, "") ?? undefined;
|
||||
}
|
||||
|
||||
private async getIsRoomEncrypted(roomId = this.state.roomId): Promise<boolean> {
|
||||
const crypto = this.context.client?.getCrypto();
|
||||
if (!crypto || !roomId) return false;
|
||||
|
||||
return await crypto.isEncryptionEnabledInRoom(roomId);
|
||||
}
|
||||
|
||||
private async calculateRecommendedVersion(room: Room): Promise<void> {
|
||||
const upgradeRecommendation = await room.getRecommendedVersion();
|
||||
if (this.unmounted) return;
|
||||
|
@ -1409,12 +1434,15 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
});
|
||||
}
|
||||
|
||||
private updatePreviewUrlVisibility({ roomId }: Room): void {
|
||||
// URL Previews in E2EE rooms can be a privacy leak so use a different setting which is per-room explicit
|
||||
const key = this.context.client?.isRoomEncrypted(roomId) ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled";
|
||||
this.setState({
|
||||
showUrlPreview: SettingsStore.getValue(key, roomId),
|
||||
});
|
||||
private updatePreviewUrlVisibility(room: Room): void {
|
||||
this.setState(({ isRoomEncrypted }) => ({
|
||||
showUrlPreview: this.getPreviewUrlVisibility(room, isRoomEncrypted),
|
||||
}));
|
||||
}
|
||||
|
||||
private getPreviewUrlVisibility({ roomId }: Room, isRoomEncrypted: boolean | null): boolean {
|
||||
const key = isRoomEncrypted ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled";
|
||||
return SettingsStore.getValue(key, roomId);
|
||||
}
|
||||
|
||||
private onRoom = (room: Room): void => {
|
||||
|
@ -1456,22 +1484,20 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
};
|
||||
|
||||
private async updateE2EStatus(room: Room): Promise<void> {
|
||||
if (!this.context.client?.isRoomEncrypted(room.roomId)) return;
|
||||
|
||||
// If crypto is not currently enabled, we aren't tracking devices at all,
|
||||
// so we don't know what the answer is. Let's error on the safe side and show
|
||||
// a warning for this case.
|
||||
let e2eStatus = RoomView.e2eStatusCache.get(room.roomId) ?? E2EStatus.Warning;
|
||||
// set the state immediately then update, so we don't scare the user into thinking the room is unencrypted
|
||||
this.setState({ e2eStatus });
|
||||
|
||||
if (this.context.client.getCrypto()) {
|
||||
/* At this point, the user has encryption on and cross-signing on */
|
||||
e2eStatus = await shieldStatusForRoom(this.context.client, room);
|
||||
RoomView.e2eStatusCache.set(room.roomId, e2eStatus);
|
||||
if (!this.context.client || !this.state.isRoomEncrypted) return;
|
||||
const e2eStatus = await this.cacheAndGetE2EStatus(room, this.context.client);
|
||||
if (this.unmounted) return;
|
||||
this.setState({ e2eStatus });
|
||||
}
|
||||
|
||||
private async cacheAndGetE2EStatus(room: Room, client: MatrixClient): Promise<E2EStatus> {
|
||||
let e2eStatus = RoomView.e2eStatusCache.get(room.roomId);
|
||||
// set the state immediately then update, so we don't scare the user into thinking the room is unencrypted
|
||||
if (e2eStatus) this.setState({ e2eStatus });
|
||||
|
||||
e2eStatus = await shieldStatusForRoom(client, room);
|
||||
RoomView.e2eStatusCache.set(room.roomId, e2eStatus);
|
||||
return e2eStatus;
|
||||
}
|
||||
|
||||
private onUrlPreviewsEnabledChange = (): void => {
|
||||
|
@ -1480,20 +1506,36 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onRoomStateEvents = (ev: MatrixEvent, state: RoomState): void => {
|
||||
private onRoomStateEvents = async (ev: MatrixEvent, state: RoomState): Promise<void> => {
|
||||
// ignore if we don't have a room yet
|
||||
if (!this.state.room || this.state.room.roomId !== state.roomId) return;
|
||||
if (!this.state.room || this.state.room.roomId !== state.roomId || !this.context.client) return;
|
||||
|
||||
switch (ev.getType()) {
|
||||
case EventType.RoomTombstone:
|
||||
this.setState({ tombstone: this.getRoomTombstone() });
|
||||
break;
|
||||
|
||||
case EventType.RoomEncryption: {
|
||||
await this.updateRoomEncrypted();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
this.updatePermissions(this.state.room);
|
||||
}
|
||||
};
|
||||
|
||||
private async updateRoomEncrypted(room = this.state.room): Promise<void> {
|
||||
if (!room || !this.context.client) return;
|
||||
|
||||
const isRoomEncrypted = await this.getIsRoomEncrypted(room.roomId);
|
||||
const newE2EStatus = isRoomEncrypted ? await this.cacheAndGetE2EStatus(room, this.context.client) : null;
|
||||
|
||||
this.setState({
|
||||
isRoomEncrypted,
|
||||
showUrlPreview: this.getPreviewUrlVisibility(room, isRoomEncrypted),
|
||||
...(newE2EStatus && { e2eStatus: newE2EStatus }),
|
||||
});
|
||||
}
|
||||
|
||||
private onRoomStateUpdate = (state: RoomState): void => {
|
||||
// ignore members in other rooms
|
||||
if (state.roomId !== this.state.room?.roomId) {
|
||||
|
@ -1959,35 +2001,41 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
if (!this.state.room || !this.context?.client) return null;
|
||||
const names = this.state.room.getDefaultRoomName(this.context.client.getSafeUserId());
|
||||
return (
|
||||
<RoomContext.Provider value={this.state}>
|
||||
<LocalRoomCreateLoader localRoom={localRoom} names={names} resizeNotifier={this.props.resizeNotifier} />
|
||||
</RoomContext.Provider>
|
||||
<ScopedRoomContextProvider {...this.state}>
|
||||
<LocalRoomCreateLoader
|
||||
localRoom={localRoom}
|
||||
names={names}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
mainSplitContentType={this.state.mainSplitContentType}
|
||||
/>
|
||||
</ScopedRoomContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
private renderLocalRoomView(localRoom: LocalRoom): ReactNode {
|
||||
return (
|
||||
<RoomContext.Provider value={this.state}>
|
||||
<ScopedRoomContextProvider {...this.state}>
|
||||
<LocalRoomView
|
||||
localRoom={localRoom}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
permalinkCreator={this.permalinkCreator}
|
||||
roomView={this.roomView}
|
||||
onFileDrop={this.onFileDrop}
|
||||
mainSplitContentType={this.state.mainSplitContentType}
|
||||
/>
|
||||
</RoomContext.Provider>
|
||||
</ScopedRoomContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
private renderWaitingForThirdPartyRoomView(inviteEvent: MatrixEvent): ReactNode {
|
||||
return (
|
||||
<RoomContext.Provider value={this.state}>
|
||||
<ScopedRoomContextProvider {...this.state}>
|
||||
<WaitingForThirdPartyRoomView
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
roomView={this.roomView}
|
||||
inviteEvent={inviteEvent}
|
||||
/>
|
||||
</RoomContext.Provider>
|
||||
</ScopedRoomContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -2027,6 +2075,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
|
||||
public render(): ReactNode {
|
||||
if (!this.context.client) return null;
|
||||
const { isRoomEncrypted } = this.state;
|
||||
const isRoomEncryptionLoading = isRoomEncrypted === null;
|
||||
|
||||
if (this.state.room instanceof LocalRoom) {
|
||||
if (this.state.room.state === LocalRoomState.CREATING) {
|
||||
|
@ -2242,14 +2292,16 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
let aux: JSX.Element | undefined;
|
||||
let previewBar;
|
||||
if (this.state.timelineRenderingType === TimelineRenderingType.Search) {
|
||||
if (!isRoomEncryptionLoading) {
|
||||
aux = (
|
||||
<RoomSearchAuxPanel
|
||||
searchInfo={this.state.search}
|
||||
onCancelClick={this.onCancelSearchClick}
|
||||
onSearchScopeChange={this.onSearchScopeChange}
|
||||
isRoomEncrypted={this.context.client.isRoomEncrypted(this.state.room.roomId)}
|
||||
isRoomEncrypted={isRoomEncrypted}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else if (showRoomUpgradeBar) {
|
||||
aux = <RoomUpgradeWarningBar room={this.state.room} />;
|
||||
} else if (myMembership !== KnownMembership.Join) {
|
||||
|
@ -2325,8 +2377,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
|
||||
let messageComposer;
|
||||
const showComposer =
|
||||
!isRoomEncryptionLoading &&
|
||||
// joined and not showing search results
|
||||
myMembership === KnownMembership.Join && !this.state.search;
|
||||
myMembership === KnownMembership.Join &&
|
||||
!this.state.search;
|
||||
if (showComposer) {
|
||||
messageComposer = (
|
||||
<MessageComposer
|
||||
|
@ -2367,7 +2421,9 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
highlightedEventId = this.state.initialEventId;
|
||||
}
|
||||
|
||||
const messagePanel = (
|
||||
let messagePanel: JSX.Element | undefined;
|
||||
if (!isRoomEncryptionLoading) {
|
||||
messagePanel = (
|
||||
<TimelinePanel
|
||||
ref={this.gatherTimelinePanelRef}
|
||||
timelineSet={this.state.room.getUnfilteredTimelineSet()}
|
||||
|
@ -2395,6 +2451,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
editState={this.state.editState}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let topUnreadMessagesBar: JSX.Element | undefined;
|
||||
// Do not show TopUnreadMessagesBar if we have search results showing, it makes no sense
|
||||
|
@ -2415,7 +2472,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
);
|
||||
}
|
||||
|
||||
const showRightPanel = this.state.room && this.state.showRightPanel;
|
||||
const showRightPanel = !isRoomEncryptionLoading && this.state.room && this.state.showRightPanel;
|
||||
|
||||
const rightPanel = showRightPanel ? (
|
||||
<RightPanel
|
||||
|
@ -2516,7 +2573,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
}
|
||||
|
||||
return (
|
||||
<RoomContext.Provider value={this.state}>
|
||||
<ScopedRoomContextProvider {...this.state}>
|
||||
<div className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
|
||||
{showChatEffects && this.roomView.current && (
|
||||
<EffectsOverlay roomWidth={this.roomView.current.offsetWidth} />
|
||||
|
@ -2543,7 +2600,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
</MainSplit>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</RoomContext.Provider>
|
||||
</ScopedRoomContextProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -208,7 +208,7 @@ const SpaceLanding: React.FC<{ space: Room }> = ({ space }) => {
|
|||
const storeIsShowingSpaceMembers = useCallback(
|
||||
() =>
|
||||
RightPanelStore.instance.isOpenForRoom(space.roomId) &&
|
||||
RightPanelStore.instance.currentCardForRoom(space.roomId)?.phase === RightPanelPhases.SpaceMemberList,
|
||||
RightPanelStore.instance.currentCardForRoom(space.roomId)?.phase === RightPanelPhases.MemberList,
|
||||
[space.roomId],
|
||||
);
|
||||
const isShowingMembers = useEventEmitterState(RightPanelStore.instance, UPDATE_EVENT, storeIsShowingSpaceMembers);
|
||||
|
@ -251,7 +251,7 @@ const SpaceLanding: React.FC<{ space: Room }> = ({ space }) => {
|
|||
}
|
||||
|
||||
const onMembersClick = (): void => {
|
||||
RightPanelStore.instance.setCard({ phase: RightPanelPhases.SpaceMemberList });
|
||||
RightPanelStore.instance.setCard({ phase: RightPanelPhases.MemberList });
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -597,7 +597,7 @@ const SpaceSetupPrivateInvite: React.FC<{
|
|||
|
||||
export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
|
||||
public static contextType = MatrixClientContext;
|
||||
public declare context: React.ContextType<typeof MatrixClientContext>;
|
||||
declare public context: React.ContextType<typeof MatrixClientContext>;
|
||||
|
||||
private dispatcherRef?: string;
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ import MatrixClientContext, { useMatrixClientContext } from "../../contexts/Matr
|
|||
import { _t } from "../../languageHandler";
|
||||
import { ContextMenuButton } from "../../accessibility/context_menu/ContextMenuButton";
|
||||
import ContextMenu, { ChevronFace, MenuItemRadio, useContextMenu } from "./ContextMenu";
|
||||
import RoomContext, { TimelineRenderingType, useRoomContext } from "../../contexts/RoomContext";
|
||||
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||
import TimelinePanel from "./TimelinePanel";
|
||||
import { Layout } from "../../settings/enums/Layout";
|
||||
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
|
||||
|
@ -30,6 +30,7 @@ import { ButtonEvent } from "../views/elements/AccessibleButton";
|
|||
import Spinner from "../views/elements/Spinner";
|
||||
import { clearRoomNotification } from "../../utils/notifications";
|
||||
import EmptyState from "../views/right_panel/EmptyState";
|
||||
import { ScopedRoomContextProvider, useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx";
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
|
@ -68,7 +69,7 @@ export const ThreadPanelHeader: React.FC<{
|
|||
setFilterOption: (filterOption: ThreadFilterType) => void;
|
||||
}> = ({ filterOption, setFilterOption }) => {
|
||||
const mxClient = useMatrixClientContext();
|
||||
const roomContext = useRoomContext();
|
||||
const roomContext = useScopedRoomContext("room");
|
||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu<HTMLElement>();
|
||||
const options: readonly ThreadPanelHeaderOption[] = [
|
||||
{
|
||||
|
@ -184,13 +185,11 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
|
|||
}, [timelineSet, timelinePanel]);
|
||||
|
||||
return (
|
||||
<RoomContext.Provider
|
||||
value={{
|
||||
...roomContext,
|
||||
timelineRenderingType: TimelineRenderingType.ThreadsList,
|
||||
showHiddenEvents: true,
|
||||
narrow,
|
||||
}}
|
||||
<ScopedRoomContextProvider
|
||||
{...roomContext}
|
||||
timelineRenderingType={TimelineRenderingType.ThreadsList}
|
||||
showHiddenEvents={true}
|
||||
narrow={narrow}
|
||||
>
|
||||
<BaseCard
|
||||
header={
|
||||
|
@ -241,7 +240,7 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
|
|||
</div>
|
||||
)}
|
||||
</BaseCard>
|
||||
</RoomContext.Provider>
|
||||
</ScopedRoomContextProvider>
|
||||
);
|
||||
};
|
||||
export default ThreadPanel;
|
||||
|
|
|
@ -51,6 +51,7 @@ import { ComposerInsertPayload, ComposerType } from "../../dispatcher/payloads/C
|
|||
import Heading from "../views/typography/Heading";
|
||||
import { SdkContextClass } from "../../contexts/SDKContext";
|
||||
import { ThreadPayload } from "../../dispatcher/payloads/ThreadPayload";
|
||||
import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -75,7 +76,7 @@ interface IState {
|
|||
|
||||
export default class ThreadView extends React.Component<IProps, IState> {
|
||||
public static contextType = RoomContext;
|
||||
public declare context: React.ContextType<typeof RoomContext>;
|
||||
declare public context: React.ContextType<typeof RoomContext>;
|
||||
|
||||
private dispatcherRef?: string;
|
||||
private layoutWatcherRef?: string;
|
||||
|
@ -422,14 +423,12 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
return (
|
||||
<RoomContext.Provider
|
||||
value={{
|
||||
...this.context,
|
||||
timelineRenderingType: TimelineRenderingType.Thread,
|
||||
threadId: this.state.thread?.id,
|
||||
liveTimeline: this.state?.thread?.timelineSet?.getLiveTimeline(),
|
||||
narrow: this.state.narrow,
|
||||
}}
|
||||
<ScopedRoomContextProvider
|
||||
{...this.context}
|
||||
timelineRenderingType={TimelineRenderingType.Thread}
|
||||
threadId={this.state.thread?.id}
|
||||
liveTimeline={this.state?.thread?.timelineSet?.getLiveTimeline()}
|
||||
narrow={this.state.narrow}
|
||||
>
|
||||
<BaseCard
|
||||
className={classNames("mx_ThreadView mx_ThreadPanel", {
|
||||
|
@ -463,7 +462,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
|||
/>
|
||||
)}
|
||||
</BaseCard>
|
||||
</RoomContext.Provider>
|
||||
</ScopedRoomContextProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue