Merge remote-tracking branch 'origin/develop' into staging
14
.eslintrc.js
|
@ -78,6 +78,11 @@ module.exports = {
|
||||||
name: "matrix-react-sdk/",
|
name: "matrix-react-sdk/",
|
||||||
message: "Please use matrix-react-sdk/src/index instead",
|
message: "Please use matrix-react-sdk/src/index instead",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "emojibase-regex",
|
||||||
|
message:
|
||||||
|
"This regex doesn't actually test for emoji. See the docs at https://emojibase.dev/docs/regex/ and prefer our own EMOJI_REGEX from HtmlUtils.",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
patterns: [
|
patterns: [
|
||||||
{
|
{
|
||||||
|
@ -115,13 +120,9 @@ module.exports = {
|
||||||
"!matrix-js-sdk/src/extensible_events_v1/InvalidEventError",
|
"!matrix-js-sdk/src/extensible_events_v1/InvalidEventError",
|
||||||
"!matrix-js-sdk/src/crypto",
|
"!matrix-js-sdk/src/crypto",
|
||||||
"!matrix-js-sdk/src/crypto/aes",
|
"!matrix-js-sdk/src/crypto/aes",
|
||||||
"!matrix-js-sdk/src/crypto/olmlib",
|
|
||||||
"!matrix-js-sdk/src/crypto/crypto",
|
|
||||||
"!matrix-js-sdk/src/crypto/keybackup",
|
"!matrix-js-sdk/src/crypto/keybackup",
|
||||||
"!matrix-js-sdk/src/crypto/RoomList",
|
|
||||||
"!matrix-js-sdk/src/crypto/deviceinfo",
|
"!matrix-js-sdk/src/crypto/deviceinfo",
|
||||||
"!matrix-js-sdk/src/crypto/key_passphrase",
|
"!matrix-js-sdk/src/crypto/key_passphrase",
|
||||||
"!matrix-js-sdk/src/crypto/CrossSigning",
|
|
||||||
"!matrix-js-sdk/src/crypto/recoverykey",
|
"!matrix-js-sdk/src/crypto/recoverykey",
|
||||||
"!matrix-js-sdk/src/crypto/dehydration",
|
"!matrix-js-sdk/src/crypto/dehydration",
|
||||||
"!matrix-js-sdk/src/oidc",
|
"!matrix-js-sdk/src/oidc",
|
||||||
|
@ -144,6 +145,11 @@ module.exports = {
|
||||||
],
|
],
|
||||||
message: "Please use matrix-js-sdk/src/matrix instead",
|
message: "Please use matrix-js-sdk/src/matrix instead",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
group: ["emojibase-regex/emoji*"],
|
||||||
|
message:
|
||||||
|
"This regex doesn't actually test for emoji. See the docs at https://emojibase.dev/docs/regex/ and prefer our own EMOJI_REGEX from HtmlUtils.",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
7
.github/workflows/end-to-end-tests.yaml
vendored
|
@ -56,6 +56,7 @@ jobs:
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
|
node-version: "lts/*"
|
||||||
|
|
||||||
- name: Fetch layered build
|
- name: Fetch layered build
|
||||||
id: layered_build
|
id: layered_build
|
||||||
|
@ -103,7 +104,7 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
# Run multiple instances in parallel to speed up the tests
|
# Run multiple instances in parallel to speed up the tests
|
||||||
runner: [1, 2, 3, 4, 5, 6, 7, 8]
|
runner: [1, 2, 3, 4, 5, 6]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
|
@ -121,6 +122,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
cache-dependency-path: matrix-react-sdk/yarn.lock
|
cache-dependency-path: matrix-react-sdk/yarn.lock
|
||||||
|
node-version: "lts/*"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
working-directory: matrix-react-sdk
|
working-directory: matrix-react-sdk
|
||||||
|
@ -145,8 +147,6 @@ jobs:
|
||||||
run: yarn playwright install --with-deps
|
run: yarn playwright install --with-deps
|
||||||
|
|
||||||
- name: Run Playwright tests
|
- name: Run Playwright tests
|
||||||
uses: coactions/setup-xvfb@6b00cf1889f4e1d5a48635647013c0508128ee1a
|
|
||||||
with:
|
|
||||||
run: yarn playwright test --shard ${{ matrix.runner }}/${{ strategy.job-total }}
|
run: yarn playwright test --shard ${{ matrix.runner }}/${{ strategy.job-total }}
|
||||||
working-directory: matrix-react-sdk
|
working-directory: matrix-react-sdk
|
||||||
|
|
||||||
|
@ -174,6 +174,7 @@ jobs:
|
||||||
if: inputs.skip != true
|
if: inputs.skip != true
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
|
node-version: "lts/*"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
if: inputs.skip != true
|
if: inputs.skip != true
|
||||||
|
|
|
@ -7,7 +7,7 @@ jobs:
|
||||||
update:
|
update:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Update matrixdotorg/synapse image
|
- name: Update matrixdotorg/synapse image
|
||||||
run: |
|
run: |
|
||||||
|
@ -20,7 +20,7 @@ jobs:
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
id: cpr
|
id: cpr
|
||||||
uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 # v5
|
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
branch: actions/playwright-image-updates
|
branch: actions/playwright-image-updates
|
||||||
|
|
|
@ -7,7 +7,7 @@ jobs:
|
||||||
name: Check PR base branch
|
name: Check PR base branch
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@v3
|
- uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const baseBranch = context.payload.pull_request.base.ref;
|
const baseBranch = context.payload.pull_request.base.ref;
|
||||||
|
|
5
.github/workflows/static_analysis.yaml
vendored
|
@ -25,6 +25,7 @@ jobs:
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
|
node-version: "lts/*"
|
||||||
|
|
||||||
- name: Install Deps
|
- name: Install Deps
|
||||||
run: "./scripts/ci/install-deps.sh"
|
run: "./scripts/ci/install-deps.sh"
|
||||||
|
@ -83,6 +84,7 @@ jobs:
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
|
node-version: "lts/*"
|
||||||
|
|
||||||
# Does not need branch matching as only analyses this layer
|
# Does not need branch matching as only analyses this layer
|
||||||
- name: Install Deps
|
- name: Install Deps
|
||||||
|
@ -100,6 +102,7 @@ jobs:
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
|
node-version: "lts/*"
|
||||||
|
|
||||||
# Does not need branch matching as only analyses this layer
|
# Does not need branch matching as only analyses this layer
|
||||||
- name: Install Deps
|
- name: Install Deps
|
||||||
|
@ -117,6 +120,7 @@ jobs:
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
|
node-version: "lts/*"
|
||||||
|
|
||||||
# Does not need branch matching as only analyses this layer
|
# Does not need branch matching as only analyses this layer
|
||||||
- name: Install Deps
|
- name: Install Deps
|
||||||
|
@ -134,6 +138,7 @@ jobs:
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
|
node-version: "lts/*"
|
||||||
|
|
||||||
- name: Install Deps
|
- name: Install Deps
|
||||||
run: "scripts/ci/layered.sh"
|
run: "scripts/ci/layered.sh"
|
||||||
|
|
2
.github/workflows/tests.yml
vendored
|
@ -44,6 +44,7 @@ jobs:
|
||||||
- name: Yarn cache
|
- name: Yarn cache
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
|
node-version: "lts/*"
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
|
|
||||||
- name: Install Deps
|
- name: Install Deps
|
||||||
|
@ -115,6 +116,7 @@ jobs:
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
|
node-version: "lts/*"
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: "./scripts/ci/app-tests.sh"
|
run: "./scripts/ci/app-tests.sh"
|
||||||
|
|
|
@ -22,7 +22,7 @@ const config: Config = {
|
||||||
testEnvironment: "jsdom",
|
testEnvironment: "jsdom",
|
||||||
testMatch: ["<rootDir>/test/**/*-test.[jt]s?(x)"],
|
testMatch: ["<rootDir>/test/**/*-test.[jt]s?(x)"],
|
||||||
globalSetup: "<rootDir>/test/globalSetup.ts",
|
globalSetup: "<rootDir>/test/globalSetup.ts",
|
||||||
setupFiles: ["jest-canvas-mock"],
|
setupFiles: ["jest-canvas-mock", "web-streams-polyfill/polyfill"],
|
||||||
setupFilesAfterEnv: ["<rootDir>/test/setupTests.ts"],
|
setupFilesAfterEnv: ["<rootDir>/test/setupTests.ts"],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
"\\.(gif|png|ttf|woff2)$": "<rootDir>/__mocks__/imageMock.js",
|
"\\.(gif|png|ttf|woff2)$": "<rootDir>/__mocks__/imageMock.js",
|
||||||
|
|
22
package.json
|
@ -55,7 +55,7 @@
|
||||||
"test:playwright:open": "yarn test:playwright --ui",
|
"test:playwright:open": "yarn test:playwright --ui",
|
||||||
"test:playwright:screenshots": "yarn test:playwright:screenshots:build && yarn test:playwright:screenshots:run",
|
"test:playwright:screenshots": "yarn test:playwright:screenshots:build && yarn test:playwright:screenshots:run",
|
||||||
"test:playwright:screenshots:build": "docker build playwright -t matrix-react-sdk-playwright",
|
"test:playwright:screenshots:build": "docker build playwright -t matrix-react-sdk-playwright",
|
||||||
"test:playwright:screenshots:run": "docker run --rm --network host -v $(pwd)/../:/work/ -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it matrix-react-sdk-playwright",
|
"test:playwright:screenshots:run": "docker run --rm --network host -e BASE_URL -v $(pwd)/../:/work/ -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it matrix-react-sdk-playwright",
|
||||||
"coverage": "yarn test --coverage",
|
"coverage": "yarn test --coverage",
|
||||||
"lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'"
|
"lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'"
|
||||||
},
|
},
|
||||||
|
@ -65,20 +65,20 @@
|
||||||
"@types/seedrandom": "3.0.8",
|
"@types/seedrandom": "3.0.8",
|
||||||
"oidc-client-ts": "3.0.1",
|
"oidc-client-ts": "3.0.1",
|
||||||
"jwt-decode": "4.0.0",
|
"jwt-decode": "4.0.0",
|
||||||
"@floating-ui/react": "0.26.11"
|
"@floating-ui/react": "0.26.11",
|
||||||
|
"@radix-ui/react-id": "1.1.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@matrix-org/analytics-events": "^0.23.0",
|
"@matrix-org/analytics-events": "^0.23.0",
|
||||||
"@matrix-org/emojibase-bindings": "^1.1.2",
|
"@matrix-org/emojibase-bindings": "^1.1.2",
|
||||||
"@matrix-org/matrix-wysiwyg": "2.37.3",
|
"@matrix-org/matrix-wysiwyg": "2.37.4",
|
||||||
"@matrix-org/olm": "3.2.15",
|
|
||||||
"@matrix-org/react-sdk-module-api": "^2.4.0",
|
"@matrix-org/react-sdk-module-api": "^2.4.0",
|
||||||
"@matrix-org/spec": "^1.7.0",
|
"@matrix-org/spec": "^1.7.0",
|
||||||
"@sentry/browser": "^8.0.0",
|
"@sentry/browser": "^8.0.0",
|
||||||
"@testing-library/react-hooks": "^8.0.1",
|
"@testing-library/react-hooks": "^8.0.1",
|
||||||
"@vector-im/compound-design-tokens": "^1.2.0",
|
"@vector-im/compound-design-tokens": "^1.2.0",
|
||||||
"@vector-im/compound-web": "^4.9.0",
|
"@vector-im/compound-web": "^5.2.3",
|
||||||
"@zxcvbn-ts/core": "^3.0.4",
|
"@zxcvbn-ts/core": "^3.0.4",
|
||||||
"@zxcvbn-ts/language-common": "^3.0.4",
|
"@zxcvbn-ts/language-common": "^3.0.4",
|
||||||
"@zxcvbn-ts/language-en": "^3.0.2",
|
"@zxcvbn-ts/language-en": "^3.0.2",
|
||||||
|
@ -96,7 +96,6 @@
|
||||||
"filesize": "10.1.2",
|
"filesize": "10.1.2",
|
||||||
"github-markdown-css": "^5.5.1",
|
"github-markdown-css": "^5.5.1",
|
||||||
"glob-to-regexp": "^0.4.1",
|
"glob-to-regexp": "^0.4.1",
|
||||||
"graphemer": "^1.4.0",
|
|
||||||
"highlight.js": "^11.3.1",
|
"highlight.js": "^11.3.1",
|
||||||
"html-entities": "^2.0.0",
|
"html-entities": "^2.0.0",
|
||||||
"is-ip": "^3.1.0",
|
"is-ip": "^3.1.0",
|
||||||
|
@ -119,8 +118,7 @@
|
||||||
"opus-recorder": "^8.0.3",
|
"opus-recorder": "^8.0.3",
|
||||||
"pako": "^2.0.3",
|
"pako": "^2.0.3",
|
||||||
"png-chunks-extract": "^1.0.0",
|
"png-chunks-extract": "^1.0.0",
|
||||||
"posthog-js": "1.139.2",
|
"posthog-js": "1.141.3",
|
||||||
"proposal-temporal": "^0.9.0",
|
|
||||||
"qrcode": "1.5.3",
|
"qrcode": "1.5.3",
|
||||||
"re-resizable": "^6.9.0",
|
"re-resizable": "^6.9.0",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
|
@ -133,6 +131,7 @@
|
||||||
"sanitize-filename": "^1.6.3",
|
"sanitize-filename": "^1.6.3",
|
||||||
"sanitize-html": "2.13.0",
|
"sanitize-html": "2.13.0",
|
||||||
"tar-js": "^0.3.0",
|
"tar-js": "^0.3.0",
|
||||||
|
"temporal-polyfill": "^0.2.5",
|
||||||
"ua-parser-js": "^1.0.2",
|
"ua-parser-js": "^1.0.2",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^10.0.0",
|
||||||
"what-input": "^5.2.10"
|
"what-input": "^5.2.10"
|
||||||
|
@ -188,7 +187,7 @@
|
||||||
"@types/seedrandom": "3.0.8",
|
"@types/seedrandom": "3.0.8",
|
||||||
"@types/tar-js": "^0.3.2",
|
"@types/tar-js": "^0.3.2",
|
||||||
"@types/ua-parser-js": "^0.7.36",
|
"@types/ua-parser-js": "^0.7.36",
|
||||||
"@types/uuid": "^9.0.2",
|
"@types/uuid": "^10.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||||
"@typescript-eslint/parser": "^7.0.0",
|
"@typescript-eslint/parser": "^7.0.0",
|
||||||
"axe-core": "4.9.1",
|
"axe-core": "4.9.1",
|
||||||
|
@ -204,7 +203,7 @@
|
||||||
"eslint-plugin-matrix-org": "1.2.1",
|
"eslint-plugin-matrix-org": "1.2.1",
|
||||||
"eslint-plugin-react": "^7.28.0",
|
"eslint-plugin-react": "^7.28.0",
|
||||||
"eslint-plugin-react-hooks": "^4.3.0",
|
"eslint-plugin-react-hooks": "^4.3.0",
|
||||||
"eslint-plugin-unicorn": "^53.0.0",
|
"eslint-plugin-unicorn": "^54.0.0",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"fake-indexeddb": "^6.0.0",
|
"fake-indexeddb": "^6.0.0",
|
||||||
"fetch-mock-jest": "^1.5.1",
|
"fetch-mock-jest": "^1.5.1",
|
||||||
|
@ -227,7 +226,8 @@
|
||||||
"stylelint-config-standard": "^36.0.0",
|
"stylelint-config-standard": "^36.0.0",
|
||||||
"stylelint-scss": "^6.0.0",
|
"stylelint-scss": "^6.0.0",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "5.4.5"
|
"typescript": "5.5.2",
|
||||||
|
"web-streams-polyfill": "^4.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"postcss": "^8.4.19",
|
"postcss": "^8.4.19",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM mcr.microsoft.com/playwright:v1.44.1-jammy
|
FROM mcr.microsoft.com/playwright:v1.45.0-jammy
|
||||||
|
|
||||||
WORKDIR /work/matrix-react-sdk
|
WORKDIR /work/matrix-react-sdk
|
||||||
VOLUME ["/work/element-web/node_modules"]
|
VOLUME ["/work/element-web/node_modules"]
|
||||||
|
|
|
@ -160,7 +160,7 @@ test.describe("Audio player", () => {
|
||||||
|
|
||||||
// Enable high contrast manually
|
// Enable high contrast manually
|
||||||
const settings = await app.settings.openUserSettings("Appearance");
|
const settings = await app.settings.openUserSettings("Appearance");
|
||||||
await settings.getByTestId("mx_ThemeChoicePanel").getByText("Use high contrast").click();
|
await settings.getByRole("radio", { name: "High contrast" }).click();
|
||||||
|
|
||||||
await app.closeDialog();
|
await app.closeDialog();
|
||||||
|
|
||||||
|
|
|
@ -103,7 +103,7 @@ const verify = async (page: Page, bob: Bot) => {
|
||||||
const bobsVerificationRequestPromise = waitForVerificationRequest(bob);
|
const bobsVerificationRequestPromise = waitForVerificationRequest(bob);
|
||||||
|
|
||||||
const roomInfo = await openRoomInfo(page);
|
const roomInfo = await openRoomInfo(page);
|
||||||
await roomInfo.getByRole("menuitem", { name: "People" }).click();
|
await page.locator(".mx_RightPanelTabs").getByText("People").click();
|
||||||
await roomInfo.getByText("Bob").click();
|
await roomInfo.getByText("Bob").click();
|
||||||
await roomInfo.getByRole("button", { name: "Verify" }).click();
|
await roomInfo.getByRole("button", { name: "Verify" }).click();
|
||||||
await roomInfo.getByRole("button", { name: "Start Verification" }).click();
|
await roomInfo.getByRole("button", { name: "Start Verification" }).click();
|
||||||
|
@ -279,7 +279,7 @@ test.describe("Cryptography", function () {
|
||||||
|
|
||||||
// Assert that verified icon is rendered
|
// Assert that verified icon is rendered
|
||||||
await page.getByRole("button", { name: "Room members" }).click();
|
await page.getByRole("button", { name: "Room members" }).click();
|
||||||
await page.getByRole("button", { name: "Room information" }).click();
|
await page.locator(".mx_RightPanelTabs").getByText("Info").click();
|
||||||
await expect(page.locator('.mx_RoomSummaryCard_badges [data-kind="success"]')).toContainText("Encrypted");
|
await expect(page.locator('.mx_RoomSummaryCard_badges [data-kind="success"]')).toContainText("Encrypted");
|
||||||
|
|
||||||
// Take a snapshot of RoomSummaryCard with a verified E2EE icon
|
// Take a snapshot of RoomSummaryCard with a verified E2EE icon
|
||||||
|
|
|
@ -102,7 +102,7 @@ test.describe("Dehydration", () => {
|
||||||
|
|
||||||
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
||||||
|
|
||||||
await page.getByRole("menuitem", { name: "People" }).click();
|
await page.locator(".mx_RightPanelTabs").getByText("People").click();
|
||||||
await expect(page.locator(".mx_MemberList")).toBeVisible();
|
await expect(page.locator(".mx_MemberList")).toBeVisible();
|
||||||
|
|
||||||
await getMemberTileByName(page, NAME).click();
|
await getMemberTileByName(page, NAME).click();
|
||||||
|
|
|
@ -45,7 +45,6 @@ test.describe("Device verification", () => {
|
||||||
|
|
||||||
// Create a new device for alice
|
// Create a new device for alice
|
||||||
aliceBotClient = new Bot(page, homeserver, {
|
aliceBotClient = new Bot(page, homeserver, {
|
||||||
rustCrypto: true,
|
|
||||||
bootstrapCrossSigning: true,
|
bootstrapCrossSigning: true,
|
||||||
bootstrapSecretStorage: true,
|
bootstrapSecretStorage: true,
|
||||||
});
|
});
|
||||||
|
|
77
playwright/e2e/forgot-password/forgot-password.spec.ts
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { expect, test } from "../../element-web-test";
|
||||||
|
import { selectHomeserver } from "../utils";
|
||||||
|
|
||||||
|
const username = "user1234";
|
||||||
|
// this has to be password-like enough to please zxcvbn. Needless to say it's just from pwgen.
|
||||||
|
const password = "oETo7MPf0o";
|
||||||
|
const email = "user@nowhere.dummy";
|
||||||
|
|
||||||
|
test.describe("Forgot Password", () => {
|
||||||
|
test.use({
|
||||||
|
startHomeserverOpts: ({ mailhog }, use) =>
|
||||||
|
use({
|
||||||
|
template: "email",
|
||||||
|
variables: {
|
||||||
|
SMTP_HOST: "host.containers.internal",
|
||||||
|
SMTP_PORT: mailhog.instance.smtpPort,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders properly", async ({ page, homeserver }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
await page.getByRole("link", { name: "Sign in" }).click();
|
||||||
|
|
||||||
|
// need to select a homeserver at this stage, before entering the forgot password flow
|
||||||
|
await selectHomeserver(page, homeserver.config.baseUrl);
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Forgot password?" }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole("main")).toMatchScreenshot("forgot-password.png");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders email verification dialog properly", async ({ page, homeserver }) => {
|
||||||
|
const user = await homeserver.registerUser(username, password);
|
||||||
|
|
||||||
|
await homeserver.setThreepid(user.userId, "email", email);
|
||||||
|
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
await page.getByRole("link", { name: "Sign in" }).click();
|
||||||
|
await selectHomeserver(page, homeserver.config.baseUrl);
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Forgot password?" }).click();
|
||||||
|
|
||||||
|
await page.getByRole("textbox", { name: "Email address" }).fill(email);
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Send email" }).click();
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Next" }).click();
|
||||||
|
|
||||||
|
await page.getByRole("textbox", { name: "New Password", exact: true }).fill(password);
|
||||||
|
await page.getByRole("textbox", { name: "Confirm new password", exact: true }).fill(password);
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Reset password" }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole("button", { name: "Resend" })).toBeInViewport();
|
||||||
|
|
||||||
|
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("forgot-password-verify-email.png");
|
||||||
|
});
|
||||||
|
});
|
|
@ -80,7 +80,7 @@ test.describe("Lazy Loading", () => {
|
||||||
|
|
||||||
async function openMemberlist(page: Page): Promise<void> {
|
async function openMemberlist(page: Page): Promise<void> {
|
||||||
await page.locator(".mx_LegacyRoomHeader").getByRole("button", { name: "Room info" }).click();
|
await page.locator(".mx_LegacyRoomHeader").getByRole("button", { name: "Room info" }).click();
|
||||||
await page.locator(".mx_RoomSummaryCard").getByRole("menuitem", { name: "People" }).click(); // \d represents the number of the room members
|
await page.locator(".mx_RightPanelTabs").getByText("People").click();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMemberInMemberlist(page: Page, name: string): Locator {
|
function getMemberInMemberlist(page: Page, name: string): Locator {
|
||||||
|
|
|
@ -14,11 +14,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Page } from "@playwright/test";
|
|
||||||
|
|
||||||
import { expect, test } from "../../element-web-test";
|
import { expect, test } from "../../element-web-test";
|
||||||
import { doTokenRegistration } from "./utils";
|
import { doTokenRegistration } from "./utils";
|
||||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||||
|
import { selectHomeserver } from "../utils";
|
||||||
|
|
||||||
test.describe("Login", () => {
|
test.describe("Login", () => {
|
||||||
test.describe("Password login", () => {
|
test.describe("Password login", () => {
|
||||||
|
@ -85,17 +84,6 @@ test.describe("Login", () => {
|
||||||
await expect(page).toHaveURL(/\/#\/room\/!room:id$/);
|
await expect(page).toHaveURL(/\/#\/room\/!room:id$/);
|
||||||
await expect(page.getByRole("button", { name: "Join the discussion" })).toBeVisible();
|
await expect(page.getByRole("button", { name: "Join the discussion" })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function selectHomeserver(page: Page, homeserverUrl: string) {
|
|
||||||
await page.getByRole("button", { name: "Edit" }).click();
|
|
||||||
await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserverUrl);
|
|
||||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
|
||||||
// wait for the dialog to go away
|
|
||||||
await expect(page.locator(".mx_ServerPickerDialog")).toHaveCount(0);
|
|
||||||
|
|
||||||
await expect(page.locator(".mx_Spinner")).toHaveCount(0);
|
|
||||||
await expect(page.locator(".mx_ServerPicker_server")).toHaveText(homeserverUrl);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// tests for old-style SSO login, in which we exchange tokens with Synapse, and Synapse talks to an auth server
|
// tests for old-style SSO login, in which we exchange tokens with Synapse, and Synapse talks to an auth server
|
||||||
|
|
|
@ -399,11 +399,10 @@ class Helpers {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close the threads panel. (Actually, close any right panel, but for these
|
* Close the threads panel.
|
||||||
* tests we only open the threads panel.)
|
|
||||||
*/
|
*/
|
||||||
async closeThreadsPanel() {
|
async closeThreadsPanel() {
|
||||||
await this.page.locator(".mx_RightPanel").getByLabel("Close").click();
|
await this.page.locator(".mx_LegacyRoomHeader").getByLabel("Threads").click();
|
||||||
await expect(this.page.locator(".mx_RightPanel")).not.toBeVisible();
|
await expect(this.page.locator(".mx_RightPanel")).not.toBeVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -411,7 +410,7 @@ class Helpers {
|
||||||
* Return to the list of threads, given we are viewing a single thread.
|
* Return to the list of threads, given we are viewing a single thread.
|
||||||
*/
|
*/
|
||||||
async backToThreadsList() {
|
async backToThreadsList() {
|
||||||
await this.page.locator(".mx_RightPanel").getByLabel("Threads").click();
|
await this.page.locator(".mx_LegacyRoomHeader").getByLabel("Threads").click();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -113,7 +113,7 @@ test.describe("RightPanel", () => {
|
||||||
test("should handle viewing room member", async ({ page, app }) => {
|
test("should handle viewing room member", async ({ page, app }) => {
|
||||||
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
||||||
|
|
||||||
await page.getByRole("menuitem", { name: "People" }).click();
|
await page.locator(".mx_RightPanelTabs").getByText("People").click();
|
||||||
await expect(page.locator(".mx_MemberList")).toBeVisible();
|
await expect(page.locator(".mx_MemberList")).toBeVisible();
|
||||||
|
|
||||||
await getMemberTileByName(page, NAME).click();
|
await getMemberTileByName(page, NAME).click();
|
||||||
|
@ -123,7 +123,7 @@ test.describe("RightPanel", () => {
|
||||||
await page.getByRole("button", { name: "Room members" }).click();
|
await page.getByRole("button", { name: "Room members" }).click();
|
||||||
await expect(page.locator(".mx_MemberList")).toBeVisible();
|
await expect(page.locator(".mx_MemberList")).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Room information" }).click();
|
await page.locator(".mx_RightPanelTabs").getByText("Info").click();
|
||||||
await checkRoomSummaryCard(page, ROOM_NAME);
|
await checkRoomSummaryCard(page, ROOM_NAME);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,219 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2023 Suguru Hirahara
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
|
||||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
|
||||||
|
|
||||||
test.describe("Appearance user settings tab", () => {
|
|
||||||
test.use({
|
|
||||||
displayName: "Hanako",
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should be rendered properly", async ({ page, user, app }) => {
|
|
||||||
const tab = await app.settings.openUserSettings("Appearance");
|
|
||||||
|
|
||||||
// Click "Show advanced" link button
|
|
||||||
await tab.getByRole("button", { name: "Show advanced" }).click();
|
|
||||||
|
|
||||||
// Assert that "Hide advanced" link button is rendered
|
|
||||||
await expect(tab.getByRole("button", { name: "Hide advanced" })).toBeVisible();
|
|
||||||
|
|
||||||
await expect(tab).toMatchScreenshot("appearance-tab.png");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should support switching layouts", async ({ page, user, app }) => {
|
|
||||||
// Create and view a room first
|
|
||||||
await app.client.createRoom({ name: "Test Room" });
|
|
||||||
await app.viewRoomByName("Test Room");
|
|
||||||
|
|
||||||
await app.settings.openUserSettings("Appearance");
|
|
||||||
|
|
||||||
const buttons = page.locator(".mx_LayoutSwitcher_RadioButton");
|
|
||||||
|
|
||||||
// Assert that the layout selected by default is "Modern"
|
|
||||||
await expect(
|
|
||||||
buttons.locator(".mx_StyledRadioButton_enabled", {
|
|
||||||
hasText: "Modern",
|
|
||||||
}),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
// Assert that the room layout is set to group (modern) layout
|
|
||||||
await expect(page.locator(".mx_RoomView_body[data-layout='group']")).toBeVisible();
|
|
||||||
|
|
||||||
// Select the first layout
|
|
||||||
await buttons.first().click();
|
|
||||||
// Assert that the layout selected is "IRC (Experimental)"
|
|
||||||
await expect(buttons.locator(".mx_StyledRadioButton_enabled", { hasText: "IRC (Experimental)" })).toBeVisible();
|
|
||||||
|
|
||||||
// Assert that the room layout is set to IRC layout
|
|
||||||
await expect(page.locator(".mx_RoomView_body[data-layout='irc']")).toBeVisible();
|
|
||||||
|
|
||||||
// Select the last layout
|
|
||||||
await buttons.last().click();
|
|
||||||
|
|
||||||
// Assert that the layout selected is "Message bubbles"
|
|
||||||
await expect(buttons.locator(".mx_StyledRadioButton_enabled", { hasText: "Message bubbles" })).toBeVisible();
|
|
||||||
|
|
||||||
// Assert that the room layout is set to bubble layout
|
|
||||||
await expect(page.locator(".mx_RoomView_body[data-layout='bubble']")).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should support changing font size by using the font size dropdown", async ({ page, app, user }) => {
|
|
||||||
await app.settings.openUserSettings("Appearance");
|
|
||||||
|
|
||||||
const tab = page.getByTestId("mx_AppearanceUserSettingsTab");
|
|
||||||
const fontDropdown = tab.locator(".mx_FontScalingPanel_Dropdown");
|
|
||||||
await expect(fontDropdown.getByLabel("Font size")).toBeVisible();
|
|
||||||
|
|
||||||
// Default browser font size is 16px and the select value is 0
|
|
||||||
// -4 value is 12px
|
|
||||||
await fontDropdown.getByLabel("Font size").selectOption({ value: "-4" });
|
|
||||||
|
|
||||||
await expect(page).toMatchScreenshot("window-12px.png");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should support enabling compact group (modern) layout", async ({ page, app, user }) => {
|
|
||||||
// Create and view a room first
|
|
||||||
await app.client.createRoom({ name: "Test Room" });
|
|
||||||
await app.viewRoomByName("Test Room");
|
|
||||||
|
|
||||||
await app.settings.openUserSettings("Appearance");
|
|
||||||
|
|
||||||
// Click "Show advanced" link button
|
|
||||||
const tab = page.getByTestId("mx_AppearanceUserSettingsTab");
|
|
||||||
await tab.getByRole("button", { name: "Show advanced" }).click();
|
|
||||||
|
|
||||||
await tab.locator("label", { hasText: "Use a more compact 'Modern' layout" }).click();
|
|
||||||
|
|
||||||
// Assert that the room layout is set to compact group (modern) layout
|
|
||||||
await expect(page.locator("#matrixchat .mx_MatrixChat_wrapper.mx_MatrixChat_useCompactLayout")).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should disable compact group (modern) layout option on IRC layout and bubble layout", async ({
|
|
||||||
page,
|
|
||||||
app,
|
|
||||||
user,
|
|
||||||
}) => {
|
|
||||||
await app.settings.openUserSettings("Appearance");
|
|
||||||
const tab = page.getByTestId("mx_AppearanceUserSettingsTab");
|
|
||||||
|
|
||||||
const checkDisabled = async () => {
|
|
||||||
await expect(tab.getByRole("checkbox", { name: "Use a more compact 'Modern' layout" })).toBeDisabled();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Click "Show advanced" link button
|
|
||||||
await tab.getByRole("button", { name: "Show advanced" }).click();
|
|
||||||
|
|
||||||
const buttons = page.locator(".mx_LayoutSwitcher_RadioButton");
|
|
||||||
|
|
||||||
// Enable IRC layout
|
|
||||||
await buttons.first().click();
|
|
||||||
|
|
||||||
// Assert that the layout selected is "IRC (Experimental)"
|
|
||||||
await expect(buttons.locator(".mx_StyledRadioButton_enabled", { hasText: "IRC (Experimental)" })).toBeVisible();
|
|
||||||
|
|
||||||
await checkDisabled();
|
|
||||||
|
|
||||||
// Enable bubble layout
|
|
||||||
await buttons.last().click();
|
|
||||||
|
|
||||||
// Assert that the layout selected is "IRC (Experimental)"
|
|
||||||
await expect(buttons.locator(".mx_StyledRadioButton_enabled", { hasText: "Message bubbles" })).toBeVisible();
|
|
||||||
|
|
||||||
await checkDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should support enabling system font", async ({ page, app, user }) => {
|
|
||||||
await app.settings.openUserSettings("Appearance");
|
|
||||||
const tab = page.getByTestId("mx_AppearanceUserSettingsTab");
|
|
||||||
|
|
||||||
// Click "Show advanced" link button
|
|
||||||
await tab.getByRole("button", { name: "Show advanced" }).click();
|
|
||||||
|
|
||||||
await tab.locator(".mx_Checkbox", { hasText: "Use bundled emoji font" }).click();
|
|
||||||
await tab.locator(".mx_Checkbox", { hasText: "Use a system font" }).click();
|
|
||||||
|
|
||||||
// Assert that the font-family value was removed
|
|
||||||
await expect(page.locator("body")).toHaveCSS("font-family", '""');
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe("Theme Choice Panel", () => {
|
|
||||||
test.beforeEach(async ({ app, user }) => {
|
|
||||||
// Disable the default theme for consistency in case ThemeWatcher automatically chooses it
|
|
||||||
await app.settings.setValue("use_system_theme", null, SettingLevel.DEVICE, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should be rendered with the light theme selected", async ({ page, app }) => {
|
|
||||||
await app.settings.openUserSettings("Appearance");
|
|
||||||
const themePanel = page.getByTestId("mx_ThemeChoicePanel");
|
|
||||||
|
|
||||||
const useSystemTheme = themePanel.getByTestId("checkbox-use-system-theme");
|
|
||||||
await expect(useSystemTheme.getByText("Match system theme")).toBeVisible();
|
|
||||||
// Assert that 'Match system theme' is not checked
|
|
||||||
// Note that mx_Checkbox_checkmark exists and is hidden by CSS if it is not checked
|
|
||||||
await expect(useSystemTheme.locator(".mx_Checkbox_checkmark")).not.toBeVisible();
|
|
||||||
|
|
||||||
const selectors = themePanel.getByTestId("theme-choice-panel-selectors");
|
|
||||||
await expect(selectors.locator(".mx_ThemeSelector_light")).toBeVisible();
|
|
||||||
await expect(selectors.locator(".mx_ThemeSelector_dark")).toBeVisible();
|
|
||||||
// Assert that the light theme is selected
|
|
||||||
await expect(selectors.locator(".mx_ThemeSelector_light.mx_StyledRadioButton_enabled")).toBeVisible();
|
|
||||||
// Assert that the buttons for the light and dark theme are not enabled
|
|
||||||
await expect(selectors.locator(".mx_ThemeSelector_light.mx_StyledRadioButton_disabled")).not.toBeVisible();
|
|
||||||
await expect(selectors.locator(".mx_ThemeSelector_dark.mx_StyledRadioButton_disabled")).not.toBeVisible();
|
|
||||||
|
|
||||||
// Assert that the checkbox for the high contrast theme is rendered
|
|
||||||
await expect(themePanel.locator(".mx_Checkbox", { hasText: "Use high contrast" })).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should disable the labels for themes and the checkbox for the high contrast theme if the checkbox for the system theme is clicked", async ({
|
|
||||||
page,
|
|
||||||
app,
|
|
||||||
}) => {
|
|
||||||
await app.settings.openUserSettings("Appearance");
|
|
||||||
const themePanel = page.getByTestId("mx_ThemeChoicePanel");
|
|
||||||
|
|
||||||
await themePanel.locator(".mx_Checkbox", { hasText: "Match system theme" }).click();
|
|
||||||
|
|
||||||
// Assert that the labels for the light theme and dark theme are disabled
|
|
||||||
await expect(themePanel.locator(".mx_ThemeSelector_light.mx_StyledRadioButton_disabled")).toBeVisible();
|
|
||||||
await expect(themePanel.locator(".mx_ThemeSelector_dark.mx_StyledRadioButton_disabled")).toBeVisible();
|
|
||||||
|
|
||||||
// Assert that there does not exist a label for an enabled theme
|
|
||||||
await expect(themePanel.locator("label.mx_StyledRadioButton_enabled")).not.toBeVisible();
|
|
||||||
|
|
||||||
// Assert that the checkbox and label to enable the high contrast theme should not exist
|
|
||||||
await expect(themePanel.locator(".mx_Checkbox", { hasText: "Use high contrast" })).not.toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should not render the checkbox and the label for the high contrast theme if the dark theme is selected", async ({
|
|
||||||
page,
|
|
||||||
app,
|
|
||||||
}) => {
|
|
||||||
await app.settings.openUserSettings("Appearance");
|
|
||||||
const themePanel = page.getByTestId("mx_ThemeChoicePanel");
|
|
||||||
|
|
||||||
// Assert that the checkbox and the label to enable the high contrast theme should exist
|
|
||||||
await expect(themePanel.locator(".mx_Checkbox", { hasText: "Use high contrast" })).toBeVisible();
|
|
||||||
|
|
||||||
// Enable the dark theme
|
|
||||||
await themePanel.locator(".mx_ThemeSelector_dark").click();
|
|
||||||
|
|
||||||
// Assert that the checkbox and the label should not exist
|
|
||||||
await expect(themePanel.locator(".mx_Checkbox", { hasText: "Use high contrast" })).not.toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -0,0 +1,172 @@
|
||||||
|
/*
|
||||||
|
Copyright 2023 Suguru Hirahara
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { expect, test } from ".";
|
||||||
|
|
||||||
|
test.describe("Appearance user settings tab", () => {
|
||||||
|
test.use({
|
||||||
|
displayName: "Hanako",
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should be rendered properly", async ({ page, user, app }) => {
|
||||||
|
const tab = await app.settings.openUserSettings("Appearance");
|
||||||
|
|
||||||
|
// Click "Show advanced" link button
|
||||||
|
await tab.getByRole("button", { name: "Show advanced" }).click();
|
||||||
|
|
||||||
|
// Assert that "Hide advanced" link button is rendered
|
||||||
|
await expect(tab.getByRole("button", { name: "Hide advanced" })).toBeVisible();
|
||||||
|
|
||||||
|
await expect(tab).toMatchScreenshot("appearance-tab.png");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should support changing font size by using the font size dropdown", async ({ page, app, user }) => {
|
||||||
|
await app.settings.openUserSettings("Appearance");
|
||||||
|
|
||||||
|
const tab = page.getByTestId("mx_AppearanceUserSettingsTab");
|
||||||
|
const fontDropdown = tab.locator(".mx_FontScalingPanel_Dropdown");
|
||||||
|
await expect(fontDropdown.getByLabel("Font size")).toBeVisible();
|
||||||
|
|
||||||
|
// Default browser font size is 16px and the select value is 0
|
||||||
|
// -4 value is 12px
|
||||||
|
await fontDropdown.getByLabel("Font size").selectOption({ value: "-4" });
|
||||||
|
|
||||||
|
await expect(page).toMatchScreenshot("window-12px.png");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should support enabling system font", async ({ page, app, user }) => {
|
||||||
|
await app.settings.openUserSettings("Appearance");
|
||||||
|
const tab = page.getByTestId("mx_AppearanceUserSettingsTab");
|
||||||
|
|
||||||
|
// Click "Show advanced" link button
|
||||||
|
await tab.getByRole("button", { name: "Show advanced" }).click();
|
||||||
|
|
||||||
|
await tab.locator(".mx_Checkbox", { hasText: "Use bundled emoji font" }).click();
|
||||||
|
await tab.locator(".mx_Checkbox", { hasText: "Use a system font" }).click();
|
||||||
|
|
||||||
|
// Assert that the font-family value was removed
|
||||||
|
await expect(page.locator("body")).toHaveCSS("font-family", '""');
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Message Layout Panel", () => {
|
||||||
|
test.beforeEach(async ({ app, user, util }) => {
|
||||||
|
await util.createAndDisplayRoom();
|
||||||
|
await util.assertModernLayout();
|
||||||
|
await util.openAppearanceTab();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should change the message layout from modern to bubble", async ({ page, app, user, util }) => {
|
||||||
|
await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-modern.png");
|
||||||
|
|
||||||
|
await util.getBubbleLayout().click();
|
||||||
|
|
||||||
|
// Assert that modern are irc layout are not selected
|
||||||
|
await expect(util.getBubbleLayout()).toBeChecked();
|
||||||
|
await expect(util.getModernLayout()).not.toBeChecked();
|
||||||
|
await expect(util.getIRCLayout()).not.toBeChecked();
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
await util.getCompactLayoutCheckbox().click();
|
||||||
|
await util.assertCompactLayout();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should disable compact layout when the modern layout is not selected", async ({
|
||||||
|
page,
|
||||||
|
app,
|
||||||
|
user,
|
||||||
|
util,
|
||||||
|
}) => {
|
||||||
|
await expect(util.getCompactLayoutCheckbox()).not.toBeDisabled();
|
||||||
|
|
||||||
|
// Select the bubble layout, which should disable the compact layout checkbox
|
||||||
|
await util.getBubbleLayout().click();
|
||||||
|
await expect(util.getCompactLayoutCheckbox()).toBeDisabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Theme Choice Panel", () => {
|
||||||
|
test.beforeEach(async ({ app, user, util }) => {
|
||||||
|
// Disable the default theme for consistency in case ThemeWatcher automatically chooses it
|
||||||
|
await util.disableSystemTheme();
|
||||||
|
await util.openAppearanceTab();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should be rendered with the light theme selected", async ({ page, app, util }) => {
|
||||||
|
// Assert that 'Match system theme' is not checked
|
||||||
|
await expect(util.getMatchSystemThemeCheckbox()).not.toBeChecked();
|
||||||
|
|
||||||
|
// Assert that the light theme is selected
|
||||||
|
await expect(util.getLightTheme()).toBeChecked();
|
||||||
|
// Assert that the dark and high contrast themes are not selected
|
||||||
|
await expect(util.getDarkTheme()).not.toBeChecked();
|
||||||
|
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 }) => {
|
||||||
|
await util.getMatchSystemThemeCheckbox().click();
|
||||||
|
|
||||||
|
// Assert that the themes are disabled
|
||||||
|
await expect(util.getLightTheme()).toBeDisabled();
|
||||||
|
await expect(util.getDarkTheme()).toBeDisabled();
|
||||||
|
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 }) => {
|
||||||
|
// Assert that the light theme is selected
|
||||||
|
await expect(util.getLightTheme()).toBeChecked();
|
||||||
|
|
||||||
|
await util.getDarkTheme().click();
|
||||||
|
|
||||||
|
// Assert that the light and high contrast themes are not selected
|
||||||
|
await expect(util.getLightTheme()).not.toBeChecked();
|
||||||
|
await expect(util.getDarkTheme()).toBeChecked();
|
||||||
|
await expect(util.getHighContrastTheme()).not.toBeChecked();
|
||||||
|
|
||||||
|
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-dark.png");
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("custom theme", () => {
|
||||||
|
test.use({
|
||||||
|
labsFlags: ["feature_custom_themes"],
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should render the custom theme section", 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 }) => {
|
||||||
|
await util.addCustomTheme();
|
||||||
|
|
||||||
|
await expect(util.getCustomTheme()).not.toBeChecked();
|
||||||
|
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-added.png");
|
||||||
|
|
||||||
|
await util.removeCustomTheme();
|
||||||
|
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme.png");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
241
playwright/e2e/settings/appearance-user-settings-tab/index.ts
Normal file
|
@ -0,0 +1,241 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Locator, Page } from "@playwright/test";
|
||||||
|
|
||||||
|
import { ElementAppPage } from "../../../pages/ElementAppPage";
|
||||||
|
import { test as base, expect } from "../../../element-web-test";
|
||||||
|
import { SettingLevel } from "../../../../src/settings/SettingLevel";
|
||||||
|
import { Layout } from "../../../../src/settings/enums/Layout";
|
||||||
|
|
||||||
|
export { expect };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up for the appearance tab test
|
||||||
|
*/
|
||||||
|
export const test = base.extend<{
|
||||||
|
util: Helpers;
|
||||||
|
}>({
|
||||||
|
util: async ({ page, app }, use) => {
|
||||||
|
await use(new Helpers(page, app));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A collection of helper functions for the appearance tab test
|
||||||
|
* The goal is to make easier to get and interact with the button, input, or other elements of the appearance tab
|
||||||
|
*/
|
||||||
|
class Helpers {
|
||||||
|
private CUSTOM_THEME_URL = "http://custom.theme";
|
||||||
|
private CUSTOM_THEME = {
|
||||||
|
name: "Custom theme",
|
||||||
|
isDark: false,
|
||||||
|
colors: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private page: Page,
|
||||||
|
private app: ElementAppPage,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the appearance tab
|
||||||
|
*/
|
||||||
|
openAppearanceTab() {
|
||||||
|
return this.app.settings.openUserSettings("Appearance");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare screenshot and hide the matrix chat
|
||||||
|
* @param locator
|
||||||
|
* @param screenshot
|
||||||
|
*/
|
||||||
|
assertScreenshot(locator: Locator, screenshot: `${string}.png`) {
|
||||||
|
return expect(locator).toMatchScreenshot(screenshot, {
|
||||||
|
css: `
|
||||||
|
#matrixchat {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Theme Panel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable in the settings the system theme
|
||||||
|
*/
|
||||||
|
disableSystemTheme() {
|
||||||
|
return this.app.settings.setValue("use_system_theme", null, SettingLevel.DEVICE, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the theme section
|
||||||
|
*/
|
||||||
|
getThemePanel() {
|
||||||
|
return this.page.getByTestId("themePanel");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the system theme toggle
|
||||||
|
*/
|
||||||
|
getMatchSystemThemeCheckbox() {
|
||||||
|
return this.getThemePanel().getByRole("checkbox", { name: "Match system theme" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the theme radio button
|
||||||
|
* @param theme - the theme to select
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private getThemeRadio(theme: string) {
|
||||||
|
return this.getThemePanel().getByRole("radio", { name: theme });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the light theme radio button
|
||||||
|
*/
|
||||||
|
getLightTheme() {
|
||||||
|
return this.getThemeRadio("Light");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the dark theme radio button
|
||||||
|
*/
|
||||||
|
getDarkTheme() {
|
||||||
|
return this.getThemeRadio("Dark");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the custom theme radio button
|
||||||
|
*/
|
||||||
|
getCustomTheme() {
|
||||||
|
return this.getThemeRadio(this.CUSTOM_THEME.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the high contrast theme radio button
|
||||||
|
*/
|
||||||
|
getHighContrastTheme() {
|
||||||
|
return this.getThemeRadio("High contrast");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a custom theme
|
||||||
|
* Mock the request to the custom and return a fake local custom theme
|
||||||
|
*/
|
||||||
|
async addCustomTheme() {
|
||||||
|
await this.page.route(this.CUSTOM_THEME_URL, (route) =>
|
||||||
|
route.fulfill({ body: JSON.stringify(this.CUSTOM_THEME) }),
|
||||||
|
);
|
||||||
|
await this.page.getByRole("textbox", { name: "Add custom theme" }).fill(this.CUSTOM_THEME_URL);
|
||||||
|
await this.page.getByRole("button", { name: "Add custom theme" }).click();
|
||||||
|
await this.page.unroute(this.CUSTOM_THEME_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the custom theme
|
||||||
|
*/
|
||||||
|
removeCustomTheme() {
|
||||||
|
return this.getThemePanel().getByRole("listitem", { name: this.CUSTOM_THEME.name }).getByRole("button").click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message layout Panel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and display a room named Test Room
|
||||||
|
*/
|
||||||
|
async createAndDisplayRoom() {
|
||||||
|
await this.app.client.createRoom({ name: "Test Room" });
|
||||||
|
await this.app.viewRoomByName("Test Room");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert the room layout
|
||||||
|
* @param layout
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private assertRoomLayout(layout: Layout) {
|
||||||
|
return expect(this.page.locator(`.mx_RoomView_body[data-layout=${layout}]`)).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert the room layout is modern
|
||||||
|
*/
|
||||||
|
assertModernLayout() {
|
||||||
|
return this.assertRoomLayout(Layout.Group);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert the room layout is bubble
|
||||||
|
*/
|
||||||
|
assertBubbleLayout() {
|
||||||
|
return this.assertRoomLayout(Layout.Bubble);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the layout panel
|
||||||
|
*/
|
||||||
|
getMessageLayoutPanel() {
|
||||||
|
return this.page.getByTestId("layoutPanel");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the layout radio button
|
||||||
|
* @param layoutName
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private getLayout(layoutName: string) {
|
||||||
|
return this.getMessageLayoutPanel().getByRole("radio", { name: layoutName });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the message bubbles layout radio button
|
||||||
|
*/
|
||||||
|
getBubbleLayout() {
|
||||||
|
return this.getLayout("Message bubbles");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the modern layout radio button
|
||||||
|
*/
|
||||||
|
getModernLayout() {
|
||||||
|
return this.getLayout("Modern");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the IRC layout radio button
|
||||||
|
*/
|
||||||
|
getIRCLayout() {
|
||||||
|
return this.getLayout("IRC (experimental)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the compact layout checkbox
|
||||||
|
*/
|
||||||
|
getCompactLayoutCheckbox() {
|
||||||
|
return this.getMessageLayoutPanel().getByRole("checkbox", { name: "Show compact text and messages" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert the compact layout is enabled
|
||||||
|
*/
|
||||||
|
assertCompactLayout() {
|
||||||
|
return expect(
|
||||||
|
this.page.locator("#matrixchat .mx_MatrixChat_wrapper.mx_MatrixChat_useCompactLayout"),
|
||||||
|
).toBeVisible();
|
||||||
|
}
|
||||||
|
}
|
|
@ -73,29 +73,6 @@ test.describe("General user settings tab", () => {
|
||||||
// Assert that the add button is rendered
|
// Assert that the add button is rendered
|
||||||
await expect(phoneNumbers.getByRole("button", { name: "Add" })).toBeVisible();
|
await expect(phoneNumbers.getByRole("button", { name: "Add" })).toBeVisible();
|
||||||
|
|
||||||
// Check language and region setting dropdown
|
|
||||||
const languageInput = uut.locator(".mx_GeneralUserSettingsTab_section_languageInput");
|
|
||||||
await languageInput.scrollIntoViewIfNeeded();
|
|
||||||
// Check the default value
|
|
||||||
await expect(languageInput.getByText("English")).toBeVisible();
|
|
||||||
// Click the button to display the dropdown menu
|
|
||||||
await languageInput.getByRole("button", { name: "Language Dropdown" }).click();
|
|
||||||
// Assert that the default option is rendered and highlighted
|
|
||||||
languageInput.getByRole("option", { name: /Albanian/ });
|
|
||||||
await expect(languageInput.getByRole("option", { name: /Albanian/ })).toHaveClass(
|
|
||||||
/mx_Dropdown_option_highlight/,
|
|
||||||
);
|
|
||||||
await expect(languageInput.getByRole("option", { name: /Deutsch/ })).toBeVisible();
|
|
||||||
// Click again to close the dropdown
|
|
||||||
await languageInput.getByRole("button", { name: "Language Dropdown" }).click();
|
|
||||||
// Assert that the default value is rendered again
|
|
||||||
await expect(languageInput.getByText("English")).toBeVisible();
|
|
||||||
|
|
||||||
const setIdServer = uut.locator(".mx_SetIdServer");
|
|
||||||
await setIdServer.scrollIntoViewIfNeeded();
|
|
||||||
// Assert that an input area for identity server exists
|
|
||||||
await expect(setIdServer.getByRole("textbox", { name: "Enter a new identity server" })).toBeVisible();
|
|
||||||
|
|
||||||
const setIntegrationManager = uut.locator(".mx_SetIntegrationManager");
|
const setIntegrationManager = uut.locator(".mx_SetIntegrationManager");
|
||||||
await setIntegrationManager.scrollIntoViewIfNeeded();
|
await setIntegrationManager.scrollIntoViewIfNeeded();
|
||||||
await expect(
|
await expect(
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2023 Suguru Hirahara
|
Copyright 2023 Suguru Hirahara
|
||||||
|
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -19,6 +20,10 @@ import { test, expect } from "../../element-web-test";
|
||||||
test.describe("Preferences user settings tab", () => {
|
test.describe("Preferences user settings tab", () => {
|
||||||
test.use({
|
test.use({
|
||||||
displayName: "Bob",
|
displayName: "Bob",
|
||||||
|
uut: async ({ app, user }, use) => {
|
||||||
|
const locator = await app.settings.openUserSettings("Preferences");
|
||||||
|
await use(locator);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should be rendered properly", async ({ app, user }) => {
|
test("should be rendered properly", async ({ app, user }) => {
|
||||||
|
@ -28,4 +33,24 @@ test.describe("Preferences user settings tab", () => {
|
||||||
await expect(tab.getByRole("heading", { name: "Preferences" })).toBeVisible();
|
await expect(tab.getByRole("heading", { name: "Preferences" })).toBeVisible();
|
||||||
await expect(tab).toMatchScreenshot();
|
await expect(tab).toMatchScreenshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should be able to change the app language", async ({ uut, user }) => {
|
||||||
|
// Check language and region setting dropdown
|
||||||
|
const languageInput = uut.locator(".mx_GeneralUserSettingsTab_section_languageInput");
|
||||||
|
await languageInput.scrollIntoViewIfNeeded();
|
||||||
|
// Check the default value
|
||||||
|
await expect(languageInput.getByText("English")).toBeVisible();
|
||||||
|
// Click the button to display the dropdown menu
|
||||||
|
await languageInput.getByRole("button", { name: "Language Dropdown" }).click();
|
||||||
|
// Assert that the default option is rendered and highlighted
|
||||||
|
languageInput.getByRole("option", { name: /Albanian/ });
|
||||||
|
await expect(languageInput.getByRole("option", { name: /Albanian/ })).toHaveClass(
|
||||||
|
/mx_Dropdown_option_highlight/,
|
||||||
|
);
|
||||||
|
await expect(languageInput.getByRole("option", { name: /Deutsch/ })).toBeVisible();
|
||||||
|
// Click again to close the dropdown
|
||||||
|
await languageInput.getByRole("button", { name: "Language Dropdown" }).click();
|
||||||
|
// Assert that the default value is rendered again
|
||||||
|
await expect(languageInput.getByText("English")).toBeVisible();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -47,5 +47,14 @@ test.describe("Security user settings tab", () => {
|
||||||
await expect(page.locator(".mx_AnalyticsLearnMoreDialog_wrapper .mx_Dialog")).toMatchScreenshot();
|
await expect(page.locator(".mx_AnalyticsLearnMoreDialog_wrapper .mx_Dialog")).toMatchScreenshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should contain section to set ID server", async ({ app }) => {
|
||||||
|
const tab = await app.settings.openUserSettings("Security");
|
||||||
|
|
||||||
|
const setIdServer = tab.locator(".mx_SetIdServer");
|
||||||
|
await setIdServer.scrollIntoViewIfNeeded();
|
||||||
|
// Assert that an input area for identity server exists
|
||||||
|
await expect(setIdServer.getByRole("textbox", { name: "Enter a new identity server" })).toBeVisible();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -337,12 +337,10 @@ export class Helpers {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert that the thread panel is focused (actually the 'close' button, specifically)
|
* Assert that the thread tab is focused
|
||||||
*/
|
*/
|
||||||
assertThreadPanelFocused() {
|
assertThreadTabFocused() {
|
||||||
return expect(
|
return expect(this.page.locator("#thread-panel-tab")).toBeFocused();
|
||||||
this.page.locator(".mx_ThreadPanel").locator(".mx_BaseCard_header").getByLabel("Close"),
|
|
||||||
).toBeFocused();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -161,17 +161,12 @@ test.describe("Threads Activity Centre", () => {
|
||||||
await util.assertNoTacIndicator();
|
await util.assertNoTacIndicator();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should focus the thread panel close button when clicking an item in the TAC", async ({
|
test("should focus the thread tab when clicking an item in the TAC", async ({ room1, room2, util, msg }) => {
|
||||||
room1,
|
|
||||||
room2,
|
|
||||||
util,
|
|
||||||
msg,
|
|
||||||
}) => {
|
|
||||||
await util.receiveMessages(room1, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
|
await util.receiveMessages(room1, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
|
||||||
|
|
||||||
await util.openTac();
|
await util.openTac();
|
||||||
await util.clickRoomInTac(room1.name);
|
await util.clickRoomInTac(room1.name);
|
||||||
|
|
||||||
await util.assertThreadPanelFocused();
|
await util.assertThreadTabFocused();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -781,10 +781,10 @@ test.describe("Timeline", () => {
|
||||||
|
|
||||||
await page.locator(".mx_LegacyRoomHeader").getByRole("button", { name: "Search" }).click();
|
await page.locator(".mx_LegacyRoomHeader").getByRole("button", { name: "Search" }).click();
|
||||||
|
|
||||||
await expect(page.locator(".mx_SearchBar")).toMatchScreenshot("search-bar-on-timeline.png");
|
await page.locator(".mx_RoomSummaryCard_search").getByRole("searchbox").fill("Message");
|
||||||
|
await page.locator(".mx_RoomSummaryCard_search").getByRole("searchbox").press("Enter");
|
||||||
|
|
||||||
await page.locator(".mx_SearchBar_input").getByRole("textbox").fill("Message");
|
await expect(page.locator(".mx_RoomSearchAuxPanel")).toMatchScreenshot("search-aux-panel.png");
|
||||||
await page.locator(".mx_SearchBar_input").getByRole("textbox").press("Enter");
|
|
||||||
|
|
||||||
for (const locator of await page
|
for (const locator of await page
|
||||||
.locator(".mx_EventTile:not(.mx_EventTile_contextual) .mx_EventTile_searchHighlight")
|
.locator(".mx_EventTile:not(.mx_EventTile_contextual) .mx_EventTile_searchHighlight")
|
||||||
|
@ -822,8 +822,8 @@ test.describe("Timeline", () => {
|
||||||
await page.locator(".mx_LegacyRoomHeader").getByRole("button", { name: "Search" }).click();
|
await page.locator(".mx_LegacyRoomHeader").getByRole("button", { name: "Search" }).click();
|
||||||
|
|
||||||
// Search the string to display both the message and TextualEvent on search results panel
|
// Search the string to display both the message and TextualEvent on search results panel
|
||||||
await page.locator(".mx_SearchBar").getByRole("textbox").fill(stringToSearch);
|
await page.locator(".mx_RoomSummaryCard_search").getByRole("searchbox").fill(stringToSearch);
|
||||||
await page.locator(".mx_SearchBar").getByRole("textbox").press("Enter");
|
await page.locator(".mx_RoomSummaryCard_search").getByRole("searchbox").press("Enter");
|
||||||
|
|
||||||
// On search results panel
|
// On search results panel
|
||||||
const resultsPanel = page.locator(".mx_RoomView_searchResultsPanel");
|
const resultsPanel = page.locator(".mx_RoomView_searchResultsPanel");
|
||||||
|
|
|
@ -17,8 +17,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { uniqueId } from "lodash";
|
import { uniqueId } from "lodash";
|
||||||
|
import { expect, type Page } from "@playwright/test";
|
||||||
|
|
||||||
import type { Page } from "@playwright/test";
|
|
||||||
import type { ClientEvent, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
import type { ClientEvent, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||||
import { Client } from "../pages/client";
|
import { Client } from "../pages/client";
|
||||||
|
|
||||||
|
@ -63,4 +63,15 @@ export async function waitForRoom(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function selectHomeserver(page: Page, homeserverUrl: string) {
|
||||||
|
await page.getByRole("button", { name: "Edit" }).click();
|
||||||
|
await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserverUrl);
|
||||||
|
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
||||||
|
// wait for the dialog to go away
|
||||||
|
await expect(page.locator(".mx_ServerPickerDialog")).toHaveCount(0);
|
||||||
|
|
||||||
|
await expect(page.locator(".mx_Spinner")).toHaveCount(0);
|
||||||
|
await expect(page.locator(".mx_ServerPicker_server")).toHaveText(homeserverUrl);
|
||||||
|
}
|
||||||
|
|
||||||
export const CommandOrControl = process.platform === "darwin" ? "Meta" : "Control";
|
export const CommandOrControl = process.platform === "darwin" ? "Meta" : "Control";
|
||||||
|
|
|
@ -53,7 +53,10 @@ class FlakyReporter implements Reporter {
|
||||||
|
|
||||||
const headers = { Authorization: `Bearer ${GITHUB_TOKEN}` };
|
const headers = { Authorization: `Bearer ${GITHUB_TOKEN}` };
|
||||||
// Fetch all existing issues with the flaky-test label.
|
// Fetch all existing issues with the flaky-test label.
|
||||||
const issuesRequest = await fetch(`${GITHUB_API_URL}/repos/${REPO}/issues?labels=${LABEL}`, { headers });
|
const issuesRequest = await fetch(
|
||||||
|
`${GITHUB_API_URL}/repos/${REPO}/issues?labels=${LABEL}&state=all&per_page=100&sort=created`,
|
||||||
|
{ headers },
|
||||||
|
);
|
||||||
const issues = await issuesRequest.json();
|
const issues = await issuesRequest.json();
|
||||||
for (const flake of this.flakes) {
|
for (const flake of this.flakes) {
|
||||||
const title = ISSUE_TITLE_PREFIX + "`" + flake + "`";
|
const title = ISSUE_TITLE_PREFIX + "`" + flake + "`";
|
||||||
|
@ -61,6 +64,12 @@ class FlakyReporter implements Reporter {
|
||||||
|
|
||||||
if (existingIssue) {
|
if (existingIssue) {
|
||||||
console.log(`Found issue ${existingIssue.number} for ${flake}, adding comment...`);
|
console.log(`Found issue ${existingIssue.number} for ${flake}, adding comment...`);
|
||||||
|
// Ensure that the test is open
|
||||||
|
await fetch(existingIssue.url, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify({ state: "open" }),
|
||||||
|
});
|
||||||
await fetch(`${existingIssue.url}/comments`, {
|
await fetch(`${existingIssue.url}/comments`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers,
|
headers,
|
||||||
|
|
|
@ -45,10 +45,6 @@ export interface CreateBotOpts {
|
||||||
* Whether to generate cross-signing keys
|
* Whether to generate cross-signing keys
|
||||||
*/
|
*/
|
||||||
bootstrapCrossSigning?: boolean;
|
bootstrapCrossSigning?: boolean;
|
||||||
/**
|
|
||||||
* Whether to use the rust crypto impl. Defaults to false (for now!)
|
|
||||||
*/
|
|
||||||
rustCrypto?: boolean;
|
|
||||||
/**
|
/**
|
||||||
* Whether to bootstrap the secret storage
|
* Whether to bootstrap the secret storage
|
||||||
*/
|
*/
|
||||||
|
@ -188,11 +184,7 @@ export class Bot extends Client {
|
||||||
return cli;
|
return cli;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.rustCrypto) {
|
|
||||||
await cli.initRustCrypto({ useIndexedDB: false });
|
await cli.initRustCrypto({ useIndexedDB: false });
|
||||||
} else {
|
|
||||||
await cli.initCrypto();
|
|
||||||
}
|
|
||||||
cli.setGlobalErrorOnUnknownDevices(false);
|
cli.setGlobalErrorOnUnknownDevices(false);
|
||||||
await cli.startClient();
|
await cli.startClient();
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,15 @@ export interface HomeserverInstance {
|
||||||
* @param password login password
|
* @param password login password
|
||||||
*/
|
*/
|
||||||
loginUser(userId: string, password: string): Promise<Credentials>;
|
loginUser(userId: string, password: string): Promise<Credentials>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a third party identifier for the given user. This only supports setting a single 3pid and will
|
||||||
|
* replace any others.
|
||||||
|
* @param userId The full ID of the user to edit (as returned from registerUser)
|
||||||
|
* @param medium The medium of the 3pid to set
|
||||||
|
* @param address The address of the 3pid to set
|
||||||
|
*/
|
||||||
|
setThreepid(userId: string, medium: string, address: string): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StartHomeserverOpts {
|
export interface StartHomeserverOpts {
|
||||||
|
|
|
@ -28,7 +28,7 @@ import { randB64Bytes } from "../../utils/rand";
|
||||||
// Docker tag to use for `matrixdotorg/synapse` image.
|
// Docker tag to use for `matrixdotorg/synapse` image.
|
||||||
// We target a specific digest as every now and then a Synapse update will break our CI.
|
// 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.
|
// This digest is updated by the playwright-image-updates.yaml workflow periodically.
|
||||||
const DOCKER_TAG = "develop@sha256:38bdd185e32dbfb40d11a69a26c5b04c0ccf1cb7d4078a14d6fdb16620bd4b3c";
|
const DOCKER_TAG = "develop@sha256:db5f8e8ca4a903379ea18b010ac3360bd843c9ac7eb2e73ad89f5059d01f8104";
|
||||||
|
|
||||||
async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> {
|
async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> {
|
||||||
const templateDir = path.join(__dirname, "templates", opts.template);
|
const templateDir = path.join(__dirname, "templates", opts.template);
|
||||||
|
@ -94,6 +94,8 @@ export class Synapse implements Homeserver, HomeserverInstance {
|
||||||
protected docker: Docker = new Docker();
|
protected docker: Docker = new Docker();
|
||||||
public config: HomeserverConfig & { serverId: string };
|
public config: HomeserverConfig & { serverId: string };
|
||||||
|
|
||||||
|
private adminToken?: string;
|
||||||
|
|
||||||
public constructor(private readonly request: APIRequestContext) {}
|
public constructor(private readonly request: APIRequestContext) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -152,12 +154,17 @@ export class Synapse implements Homeserver, HomeserverInstance {
|
||||||
return [path.join(synapseLogsPath, "stdout.log"), path.join(synapseLogsPath, "stderr.log")];
|
return [path.join(synapseLogsPath, "stdout.log"), path.join(synapseLogsPath, "stderr.log")];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async registerUser(username: string, password: string, displayName?: string): Promise<Credentials> {
|
private async registerUserInternal(
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
displayName?: string,
|
||||||
|
admin = false,
|
||||||
|
): Promise<Credentials> {
|
||||||
const url = `${this.config.baseUrl}/_synapse/admin/v1/register`;
|
const url = `${this.config.baseUrl}/_synapse/admin/v1/register`;
|
||||||
const { nonce } = await this.request.get(url).then((r) => r.json());
|
const { nonce } = await this.request.get(url).then((r) => r.json());
|
||||||
const mac = crypto
|
const mac = crypto
|
||||||
.createHmac("sha1", this.config.registrationSecret)
|
.createHmac("sha1", this.config.registrationSecret)
|
||||||
.update(`${nonce}\0${username}\0${password}\0notadmin`)
|
.update(`${nonce}\0${username}\0${password}\0${admin ? "" : "not"}admin`)
|
||||||
.digest("hex");
|
.digest("hex");
|
||||||
const res = await this.request.post(url, {
|
const res = await this.request.post(url, {
|
||||||
data: {
|
data: {
|
||||||
|
@ -165,7 +172,7 @@ export class Synapse implements Homeserver, HomeserverInstance {
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
mac,
|
mac,
|
||||||
admin: false,
|
admin,
|
||||||
displayname: displayName,
|
displayname: displayName,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -185,6 +192,10 @@ export class Synapse implements Homeserver, HomeserverInstance {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public registerUser(username: string, password: string, displayName?: string): Promise<Credentials> {
|
||||||
|
return this.registerUserInternal(username, password, displayName, false);
|
||||||
|
}
|
||||||
|
|
||||||
public async loginUser(userId: string, password: string): Promise<Credentials> {
|
public async loginUser(userId: string, password: string): Promise<Credentials> {
|
||||||
const url = `${this.config.baseUrl}/_matrix/client/v3/login`;
|
const url = `${this.config.baseUrl}/_matrix/client/v3/login`;
|
||||||
const res = await this.request.post(url, {
|
const res = await this.request.post(url, {
|
||||||
|
@ -207,4 +218,30 @@ export class Synapse implements Homeserver, HomeserverInstance {
|
||||||
homeServer: json.home_server,
|
homeServer: json.home_server,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async setThreepid(userId: string, medium: string, address: string): Promise<void> {
|
||||||
|
if (this.adminToken === undefined) {
|
||||||
|
const result = await this.registerUserInternal("admin", "totalyinsecureadminpassword", undefined, true);
|
||||||
|
this.adminToken = result.accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${this.config.baseUrl}/_synapse/admin/v2/users/${userId}`;
|
||||||
|
const res = await this.request.put(url, {
|
||||||
|
data: {
|
||||||
|
threepids: [
|
||||||
|
{
|
||||||
|
medium,
|
||||||
|
address,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.adminToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok()) {
|
||||||
|
throw await res.json();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.1 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 8.5 KiB |
After Width: | Height: | Size: 9 KiB |
After Width: | Height: | Size: 8.2 KiB |
After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 5 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 19 KiB |
|
@ -177,9 +177,9 @@ a:visited {
|
||||||
color: $accent-alt;
|
color: $accent-alt;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"],
|
:not(.mx_no_textinput):not(.mx_textinput):not(.mx_Field) > input[type="text"],
|
||||||
input[type="search"],
|
:not(.mx_no_textinput):not(.mx_textinput):not(.mx_Field) > input[type="search"],
|
||||||
input[type="password"] {
|
:not(.mx_no_textinput):not(.mx_textinput):not(.mx_Field) > input[type="password"] {
|
||||||
padding: 9px;
|
padding: 9px;
|
||||||
font: var(--cpd-font-body-md-semibold);
|
font: var(--cpd-font-body-md-semibold);
|
||||||
font-weight: var(--cpd-font-weight-semibold);
|
font-weight: var(--cpd-font-weight-semibold);
|
||||||
|
@ -522,6 +522,8 @@ legend {
|
||||||
content: "";
|
content: "";
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
mask-image: url("@vector-im/compound-design-tokens/icons/close.svg");
|
mask-image: url("@vector-im/compound-design-tokens/icons/close.svg");
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
|
@ -604,7 +606,7 @@ legend {
|
||||||
.mx_Dialog
|
.mx_Dialog
|
||||||
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
|
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
|
||||||
.mx_UserProfileSettings button
|
.mx_UserProfileSettings button
|
||||||
),
|
):not(.mx_ThemeChoicePanel_CustomTheme button),
|
||||||
.mx_Dialog input[type="submit"],
|
.mx_Dialog input[type="submit"],
|
||||||
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton),
|
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton),
|
||||||
.mx_Dialog_buttons input[type="submit"] {
|
.mx_Dialog_buttons input[type="submit"] {
|
||||||
|
@ -624,14 +626,14 @@ legend {
|
||||||
.mx_Dialog
|
.mx_Dialog
|
||||||
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
|
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
|
||||||
.mx_UserProfileSettings button
|
.mx_UserProfileSettings button
|
||||||
):last-child {
|
):not(.mx_ThemeChoicePanel_CustomTheme button):last-child {
|
||||||
margin-right: 0px;
|
margin-right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Dialog
|
.mx_Dialog
|
||||||
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
|
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
|
||||||
.mx_UserProfileSettings button
|
.mx_UserProfileSettings button
|
||||||
):focus,
|
):not(.mx_ThemeChoicePanel_CustomTheme button):focus,
|
||||||
.mx_Dialog input[type="submit"]:focus,
|
.mx_Dialog input[type="submit"]:focus,
|
||||||
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):focus,
|
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):focus,
|
||||||
.mx_Dialog_buttons input[type="submit"]:focus {
|
.mx_Dialog_buttons input[type="submit"]:focus {
|
||||||
|
@ -643,7 +645,7 @@ legend {
|
||||||
.mx_Dialog_buttons
|
.mx_Dialog_buttons
|
||||||
button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(
|
button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(
|
||||||
.mx_UserProfileSettings button
|
.mx_UserProfileSettings button
|
||||||
),
|
):not(.mx_ThemeChoicePanel_CustomTheme button),
|
||||||
.mx_Dialog_buttons input[type="submit"].mx_Dialog_primary {
|
.mx_Dialog_buttons input[type="submit"].mx_Dialog_primary {
|
||||||
color: var(--cpd-color-text-on-solid-primary);
|
color: var(--cpd-color-text-on-solid-primary);
|
||||||
background-color: var(--cpd-color-bg-action-primary-rest);
|
background-color: var(--cpd-color-bg-action-primary-rest);
|
||||||
|
@ -654,7 +656,9 @@ legend {
|
||||||
.mx_Dialog button.danger:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]),
|
.mx_Dialog button.danger:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]),
|
||||||
.mx_Dialog input[type="submit"].danger,
|
.mx_Dialog input[type="submit"].danger,
|
||||||
.mx_Dialog_buttons
|
.mx_Dialog_buttons
|
||||||
button.danger:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(.mx_UserProfileSettings button),
|
button.danger:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(.mx_UserProfileSettings button):not(
|
||||||
|
.mx_ThemeChoicePanel_CustomTheme button
|
||||||
|
),
|
||||||
.mx_Dialog_buttons input[type="submit"].danger {
|
.mx_Dialog_buttons input[type="submit"].danger {
|
||||||
background-color: var(--cpd-color-bg-critical-primary);
|
background-color: var(--cpd-color-bg-critical-primary);
|
||||||
border: solid 1px var(--cpd-color-bg-critical-primary);
|
border: solid 1px var(--cpd-color-bg-critical-primary);
|
||||||
|
@ -670,7 +674,7 @@ legend {
|
||||||
.mx_Dialog
|
.mx_Dialog
|
||||||
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
|
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
|
||||||
.mx_UserProfileSettings button
|
.mx_UserProfileSettings button
|
||||||
):disabled,
|
):not(.mx_ThemeChoicePanel_CustomTheme button):disabled,
|
||||||
.mx_Dialog input[type="submit"]:disabled,
|
.mx_Dialog input[type="submit"]:disabled,
|
||||||
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):disabled,
|
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):disabled,
|
||||||
.mx_Dialog_buttons input[type="submit"]:disabled {
|
.mx_Dialog_buttons input[type="submit"]:disabled {
|
||||||
|
|
|
@ -261,6 +261,7 @@
|
||||||
@import "./views/right_panel/_BaseCard.pcss";
|
@import "./views/right_panel/_BaseCard.pcss";
|
||||||
@import "./views/right_panel/_EncryptionInfo.pcss";
|
@import "./views/right_panel/_EncryptionInfo.pcss";
|
||||||
@import "./views/right_panel/_PinnedMessagesCard.pcss";
|
@import "./views/right_panel/_PinnedMessagesCard.pcss";
|
||||||
|
@import "./views/right_panel/_RightPanelTabs.pcss";
|
||||||
@import "./views/right_panel/_RoomSummaryCard.pcss";
|
@import "./views/right_panel/_RoomSummaryCard.pcss";
|
||||||
@import "./views/right_panel/_ThreadPanel.pcss";
|
@import "./views/right_panel/_ThreadPanel.pcss";
|
||||||
@import "./views/right_panel/_TimelineCard.pcss";
|
@import "./views/right_panel/_TimelineCard.pcss";
|
||||||
|
@ -306,10 +307,10 @@
|
||||||
@import "./views/rooms/_RoomListHeader.pcss";
|
@import "./views/rooms/_RoomListHeader.pcss";
|
||||||
@import "./views/rooms/_RoomPreviewBar.pcss";
|
@import "./views/rooms/_RoomPreviewBar.pcss";
|
||||||
@import "./views/rooms/_RoomPreviewCard.pcss";
|
@import "./views/rooms/_RoomPreviewCard.pcss";
|
||||||
|
@import "./views/rooms/_RoomSearchAuxPanel.pcss";
|
||||||
@import "./views/rooms/_RoomSublist.pcss";
|
@import "./views/rooms/_RoomSublist.pcss";
|
||||||
@import "./views/rooms/_RoomTile.pcss";
|
@import "./views/rooms/_RoomTile.pcss";
|
||||||
@import "./views/rooms/_RoomUpgradeWarningBar.pcss";
|
@import "./views/rooms/_RoomUpgradeWarningBar.pcss";
|
||||||
@import "./views/rooms/_SearchBar.pcss";
|
|
||||||
@import "./views/rooms/_SendMessageComposer.pcss";
|
@import "./views/rooms/_SendMessageComposer.pcss";
|
||||||
@import "./views/rooms/_SpaceScopeHeader.pcss";
|
@import "./views/rooms/_SpaceScopeHeader.pcss";
|
||||||
@import "./views/rooms/_Stickers.pcss";
|
@import "./views/rooms/_Stickers.pcss";
|
||||||
|
|
|
@ -17,6 +17,12 @@ limitations under the License.
|
||||||
.mx_SettingsSubsection {
|
.mx_SettingsSubsection {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&.mx_SettingsSubsection_newUi {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--cpd-space-8x);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SettingsSubsection_description {
|
.mx_SettingsSubsection_description {
|
||||||
|
@ -54,4 +60,8 @@ limitations under the License.
|
||||||
&.mx_SettingsSubsection_noHeading {
|
&.mx_SettingsSubsection_noHeading {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
&.mx_SettingsSubsection_content_newUi {
|
||||||
|
gap: var(--cpd-space-6x);
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
25
res/css/views/right_panel/_RightPanelTabs.pcss
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_RightPanelTabs {
|
||||||
|
margin: 0;
|
||||||
|
height: 64px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -235,28 +235,15 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomSummaryCard_header {
|
.mx_RoomSummaryCard_header {
|
||||||
padding: 15px 12px;
|
padding: 24px 12px 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomSummaryCard_search input {
|
.mx_RoomSummaryCard_search {
|
||||||
/* Overriding very broad CSS rules */
|
flex-grow: 1;
|
||||||
border: 0 !important;
|
min-width: 0;
|
||||||
margin: 0 !important;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomSummaryCard_searchBtn {
|
input[type="search"]::-webkit-search-cancel-button {
|
||||||
background: var(--cpd-color-bg-canvas-default);
|
display: unset; /* override _common.pcss which inhibits this */
|
||||||
color: var(--cpd-color-icon-primary);
|
|
||||||
border: 1px solid var(--cpd-color-gray-400);
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
padding: var(--cpd-space-2x);
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--cpd-color-bg-subtle-primary);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
margin-top: 24px;
|
||||||
|
|
||||||
.mx_Spinner {
|
.mx_Spinner {
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
|
|
|
@ -20,7 +20,7 @@ limitations under the License.
|
||||||
padding: 0 var(--cpd-space-3x);
|
padding: 0 var(--cpd-space-3x);
|
||||||
border-bottom: 1px solid $separator;
|
border-bottom: 1px solid $separator;
|
||||||
background-color: $background;
|
background-color: $background;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomHeader:hover {
|
.mx_RoomHeader:hover {
|
||||||
|
@ -74,7 +74,9 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomHeader:hover .mx_RoomHeader_topic {
|
.mx_RoomHeader:hover,
|
||||||
|
.mx_RoomHeader:focus-within {
|
||||||
|
.mx_RoomHeader_topic {
|
||||||
/* height needed to compute the transition, it equals to the `line-height`
|
/* height needed to compute the transition, it equals to the `line-height`
|
||||||
value in pixels */
|
value in pixels */
|
||||||
height: calc($font-13px * 1.5);
|
height: calc($font-13px * 1.5);
|
||||||
|
@ -83,6 +85,7 @@ limitations under the License.
|
||||||
a:hover {
|
a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomHeader_icon {
|
.mx_RoomHeader_icon {
|
||||||
|
|
72
res/css/views/rooms/_RoomSearchAuxPanel.pcss
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_RoomSearchAuxPanel {
|
||||||
|
/* use `min-height` rather than height, to allow room for the text to wrap if the window is narrow */
|
||||||
|
min-height: 84px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-color: var(--cpd-color-bg-canvas-default);
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px 0;
|
||||||
|
padding: var(--cpd-space-3x);
|
||||||
|
box-sizing: border-box;
|
||||||
|
gap: var(--cpd-space-2x);
|
||||||
|
|
||||||
|
.mx_RoomSearchAuxPanel_summary {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: inherit; /* flex */
|
||||||
|
gap: var(--cpd-space-2x);
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
> svg {
|
||||||
|
padding: var(--cpd-space-2x);
|
||||||
|
border-radius: var(--cpd-space-2x);
|
||||||
|
background-color: var(--cpd-color-bg-subtle-secondary);
|
||||||
|
color: var(--cpd-color-icon-secondary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomSearchAuxPanel_summary_text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-22px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
span {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SearchWarning {
|
||||||
|
display: contents;
|
||||||
|
font-size: $font-13px;
|
||||||
|
line-height: $font-20px;
|
||||||
|
color: var(--cpd-color-text-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomSearchAuxPanel_buttons {
|
||||||
|
display: inherit; /* flex */
|
||||||
|
gap: var(--cpd-space-6x);
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,83 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.mx_SearchBar {
|
|
||||||
/* use `min-height` rather than height, to allow room for the text to wrap if the window is narrow */
|
|
||||||
min-height: 56px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border-bottom: 1px solid $primary-hairline-color;
|
|
||||||
|
|
||||||
.mx_SearchBar_input {
|
|
||||||
--size-button-search: 37px; /* size of the search button inside `input` element */
|
|
||||||
|
|
||||||
/* border: 1px solid $input-border-color; */
|
|
||||||
/* font-size: $font-15px; */
|
|
||||||
flex: 1 1 0;
|
|
||||||
margin-left: 22px;
|
|
||||||
|
|
||||||
/* do not allow the input element to shrink below the width needed for the placeholder 'Search…'
|
|
||||||
and the search button */
|
|
||||||
min-width: calc(7em + var(--size-button-search));
|
|
||||||
|
|
||||||
input {
|
|
||||||
box-sizing: border-box; /* include padding value into width calculation */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_SearchBar_searchButton {
|
|
||||||
cursor: pointer;
|
|
||||||
width: var(--size-button-search);
|
|
||||||
height: var(--size-button-search);
|
|
||||||
background-color: $accent;
|
|
||||||
mask: url("$(res)/img/feather-customised/search-input.svg");
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-position: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_SearchBar_buttons {
|
|
||||||
display: inherit; /* flex */
|
|
||||||
min-width: 0; /* have the close button displayed even on a very narrow timeline */
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_SearchBar_button {
|
|
||||||
border: 0;
|
|
||||||
margin: 0 0 0 22px;
|
|
||||||
padding: 5px;
|
|
||||||
font-size: $font-15px;
|
|
||||||
cursor: pointer;
|
|
||||||
color: $primary-content;
|
|
||||||
border-bottom: 2px solid $accent;
|
|
||||||
font-weight: var(--cpd-font-weight-semibold);
|
|
||||||
word-break: break-all; /* prevent the input area and cancel button from being overlapped by BaseCard */
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_SearchBar_unselected {
|
|
||||||
color: $input-darker-fg-color;
|
|
||||||
border-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_SearchBar_cancel {
|
|
||||||
background-color: $alert;
|
|
||||||
mask: url("$(res)/img/cancel.svg");
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-position: center;
|
|
||||||
mask-size: 14px;
|
|
||||||
padding: 9px;
|
|
||||||
margin: 0 12px 0 3px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -15,79 +15,80 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_LayoutSwitcher_RadioButtons {
|
.mx_LayoutSwitcher_LayoutSelector {
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 24px;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
color: $primary-content;
|
|
||||||
|
|
||||||
> .mx_LayoutSwitcher_RadioButton {
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 1;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
/**
|
||||||
|
* The settings form has a default gap of 10px
|
||||||
|
* We want to have a bigger gap between the layout options
|
||||||
|
*/
|
||||||
|
gap: var(--cpd-space-4x) !important;
|
||||||
|
|
||||||
flex-basis: 33%;
|
.mxLayoutSwitcher_LayoutSelector_LayoutRadio {
|
||||||
min-width: 0;
|
border: 1px solid var(--cpd-color-border-interactive-primary);
|
||||||
|
border-radius: var(--cpd-space-2x);
|
||||||
|
|
||||||
border: 1px solid $quinary-content;
|
.mxLayoutSwitcher_LayoutSelector_LayoutRadio_inline {
|
||||||
border-radius: 10px;
|
display: flex;
|
||||||
|
/*
|
||||||
|
* 10px
|
||||||
|
*/
|
||||||
|
gap: calc(var(--cpd-space-2x) + var(--cpd-space-0-5x));
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mxLayoutSwitcher_LayoutSelector_LayoutRadio_inline,
|
||||||
|
.mxLayoutSwitcher_LayoutSelector_LayoutRadio_EventTilePreview {
|
||||||
|
margin: var(--cpd-space-3x);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override the event tile style to make it fit in the selector
|
||||||
|
* Tweak also hover style and remove action bar
|
||||||
|
*/
|
||||||
|
.mxLayoutSwitcher_LayoutSelector_LayoutRadio_EventTilePreview {
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
.mx_EventTile {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide the message options and message action bar in the preview
|
||||||
|
*/
|
||||||
.mx_EventTile_msgOption,
|
.mx_EventTile_msgOption,
|
||||||
.mx_MessageActionBar {
|
.mx_MessageActionBar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_LayoutSwitcher_RadioButton_preview {
|
|
||||||
flex-grow: 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 10px;
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
.mx_EventTile[data-layout="bubble"] .mx_EventTile_line {
|
|
||||||
padding-right: 11px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_StyledRadioButton {
|
|
||||||
flex-grow: 0;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_content {
|
.mx_EventTile_content {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_LayoutSwitcher_RadioButton_selected {
|
&[data-layout="group"] {
|
||||||
border-color: var(--cpd-color-bg-accent-rest);
|
margin-top: calc(var(--cpd-space-3x) * -1);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_StyledRadioButton {
|
/**
|
||||||
border-top: 1px solid $quinary-content;
|
* Add margin to center the bubble
|
||||||
}
|
*/
|
||||||
|
|
||||||
.mx_StyledRadioButton_checked {
|
|
||||||
background-color: var(--cpd-color-bg-subtle-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile {
|
|
||||||
margin: 0;
|
|
||||||
&[data-layout="bubble"] {
|
&[data-layout="bubble"] {
|
||||||
margin-right: 40px;
|
/**
|
||||||
|
* Add the layout margin and the margin to vertically center the bubble
|
||||||
|
*/
|
||||||
|
margin-top: var(--cpd-space-6x);
|
||||||
|
margin-right: 34px;
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
}
|
}
|
||||||
&[data-layout="irc"] {
|
|
||||||
> a {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.mx_EventTile_line {
|
.mx_EventTile_line {
|
||||||
max-width: 90%;
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mxLayoutSwitcher_LayoutSelector_LayoutRadio_separator {
|
||||||
|
border-top: 0;
|
||||||
|
border-bottom: 1px solid var(--cpd-color-border-interactive-secondary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,48 +14,72 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_ThemeChoicePanel_themeSelectors {
|
.mx_ThemeChoicePanel_ThemeSelectors {
|
||||||
color: $primary-content;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
/* Override form default style */
|
||||||
|
flex-direction: row !important;
|
||||||
|
gap: var(--cpd-space-4x) !important;
|
||||||
|
|
||||||
> .mx_StyledRadioButton {
|
.mx_ThemeChoicePanel_themeSelector {
|
||||||
align-items: center;
|
border: 1px solid var(--cpd-color-border-interactive-secondary);
|
||||||
padding: $font-16px;
|
border-radius: var(--cpd-space-1-5x);
|
||||||
box-sizing: border-box;
|
padding: var(--cpd-space-3x) var(--cpd-space-5x) var(--cpd-space-3x) var(--cpd-space-3x);
|
||||||
border-radius: 10px;
|
gap: var(--cpd-space-2x);
|
||||||
width: 180px;
|
background-color: var(--cpd-color-bg-canvas-default);
|
||||||
|
|
||||||
background: $accent-200;
|
&.mx_ThemeChoicePanel_themeSelector_enabled {
|
||||||
opacity: 0.4;
|
border-color: var(--cpd-color-border-interactive-primary);
|
||||||
|
|
||||||
flex-shrink: 1;
|
|
||||||
flex-grow: 0;
|
|
||||||
|
|
||||||
margin-right: 15px;
|
|
||||||
margin-top: 10px;
|
|
||||||
|
|
||||||
font-weight: var(--cpd-font-weight-semibold);
|
|
||||||
|
|
||||||
> span {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> .mx_StyledRadioButton_enabled {
|
&.mx_ThemeChoicePanel_themeSelector_disabled {
|
||||||
opacity: 1;
|
border-color: var(--cpd-color-border-disabled);
|
||||||
|
|
||||||
/* These colors need to be hardcoded because they don't change with the theme */
|
|
||||||
&.mx_ThemeSelector_light {
|
|
||||||
background-color: #f3f8fd;
|
|
||||||
color: #2e2f32;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_ThemeSelector_dark {
|
.mx_ThemeChoicePanel_themeSelector_Label {
|
||||||
/* 5% lightened version of 181b21 */
|
color: var(--cpd-color-text-primary);
|
||||||
background-color: #25282e;
|
font: var(--cpd-font-body-md-semibold);
|
||||||
color: #f3f8fd;
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ThemeChoicePanel_CustomTheme {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--cpd-space-4x);
|
||||||
|
|
||||||
|
.mx_ThemeChoicePanel_CustomTheme_EditInPlace input:focus {
|
||||||
|
/*
|
||||||
|
* When the input is focused, the border is growing
|
||||||
|
* We need to move it a bit to avoid the left border to be under the left panel
|
||||||
|
*/
|
||||||
|
margin-left: var(--cpd-space-0-5x);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_ThemeChoicePanel_CustomThemeList {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--cpd-space-4x);
|
||||||
|
/*
|
||||||
|
* Override the default padding/margin of the list
|
||||||
|
*/
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
.mx_ThemeChoicePanel_CustomThemeList_theme {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--cpd-color-gray-200);
|
||||||
|
padding: var(--cpd-space-2x) var(--cpd-space-2x) var(--cpd-space-2x) var(--cpd-space-4x);
|
||||||
|
|
||||||
|
.mx_ThemeChoicePanel_CustomThemeList_name {
|
||||||
|
font: var(--cpd-font-body-sm-semibold);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_UserProfileSettings_profile_controls_userId {
|
.mx_UserProfileSettings_profile_controls_userId {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
margin-top: var(--cpd-space-4x);
|
||||||
.mx_CopyableText {
|
.mx_CopyableText {
|
||||||
margin-top: var(--cpd-space-1x);
|
margin-top: var(--cpd-space-1x);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -46,6 +47,15 @@ limitations under the License.
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_UserProfileSettings_profile_buttons {
|
||||||
|
margin-top: var(--cpd-space-8x);
|
||||||
|
margin-bottom: var(--cpd-space-8x);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_UserProfileSettings_accountmanageIcon {
|
||||||
|
margin-right: var(--cpd-space-2x);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
|
|
@ -34,3 +34,8 @@ limitations under the License.
|
||||||
margin-right: $spacing-8;
|
margin-right: $spacing-8;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_GeneralUserSettingsTab_section_hint {
|
||||||
|
font: var(--cpd-font-body-sm-regular);
|
||||||
|
color: var(--cpd-color-text-secondary);
|
||||||
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_InlineTermsAgreement_cbContainer {
|
.mx_InlineTermsAgreement_cbContainer {
|
||||||
|
margin-top: var(--cpd-space-4x);
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
font: var(--cpd-font-body-md-regular);
|
font: var(--cpd-font-body-md-regular);
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { Error as ErrorEvent } from "@matrix-org/analytics-events/types/typescri
|
||||||
import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api";
|
import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api";
|
||||||
|
|
||||||
import { PosthogAnalytics } from "./PosthogAnalytics";
|
import { PosthogAnalytics } from "./PosthogAnalytics";
|
||||||
|
import { MEGOLM_ENCRYPTION_ALGORITHM } from "./utils/crypto";
|
||||||
|
|
||||||
/** The key that we use to store the `reportedEvents` bloom filter in localstorage */
|
/** The key that we use to store the `reportedEvents` bloom filter in localstorage */
|
||||||
const DECRYPTION_FAILURE_STORAGE_KEY = "mx_decryption_failure_event_ids";
|
const DECRYPTION_FAILURE_STORAGE_KEY = "mx_decryption_failure_event_ids";
|
||||||
|
@ -207,7 +208,7 @@ export class DecryptionFailureTracker {
|
||||||
*/
|
*/
|
||||||
private eventDecrypted(e: MatrixEvent, nowTs: number): void {
|
private eventDecrypted(e: MatrixEvent, nowTs: number): void {
|
||||||
// for now we only track megolm decryption failures
|
// for now we only track megolm decryption failures
|
||||||
if (e.getWireContent().algorithm != "m.megolm.v1.aes-sha2") {
|
if (e.getWireContent().algorithm != MEGOLM_ENCRYPTION_ALGORITHM) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const errCode = e.decryptionFailureReason;
|
const errCode = e.decryptionFailureReason;
|
||||||
|
|
|
@ -20,13 +20,11 @@ limitations under the License.
|
||||||
import React, { LegacyRef, ReactNode } from "react";
|
import React, { LegacyRef, ReactNode } from "react";
|
||||||
import sanitizeHtml from "sanitize-html";
|
import sanitizeHtml from "sanitize-html";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import EMOJIBASE_REGEX from "emojibase-regex";
|
|
||||||
import katex from "katex";
|
import katex from "katex";
|
||||||
import { decode } from "html-entities";
|
import { decode } from "html-entities";
|
||||||
import { IContent } from "matrix-js-sdk/src/matrix";
|
import { IContent } from "matrix-js-sdk/src/matrix";
|
||||||
import { Optional } from "matrix-events-sdk";
|
import { Optional } from "matrix-events-sdk";
|
||||||
import escapeHtml from "escape-html";
|
import escapeHtml from "escape-html";
|
||||||
import GraphemeSplitter from "graphemer";
|
|
||||||
import { getEmojiFromUnicode } from "@matrix-org/emojibase-bindings";
|
import { getEmojiFromUnicode } from "@matrix-org/emojibase-bindings";
|
||||||
|
|
||||||
import { IExtendedSanitizeOptions } from "./@types/sanitize-html";
|
import { IExtendedSanitizeOptions } from "./@types/sanitize-html";
|
||||||
|
@ -34,6 +32,7 @@ import SettingsStore from "./settings/SettingsStore";
|
||||||
import { stripHTMLReply, stripPlainReply } from "./utils/Reply";
|
import { stripHTMLReply, stripPlainReply } from "./utils/Reply";
|
||||||
import { PERMITTED_URL_SCHEMES } from "./utils/UrlUtils";
|
import { PERMITTED_URL_SCHEMES } from "./utils/UrlUtils";
|
||||||
import { sanitizeHtmlParams, transformTags } from "./Linkify";
|
import { sanitizeHtmlParams, transformTags } from "./Linkify";
|
||||||
|
import { graphemeSegmenter } from "./utils/strings";
|
||||||
|
|
||||||
export { Linkify, linkifyElement, linkifyAndSanitizeHtml } from "./Linkify";
|
export { Linkify, linkifyElement, linkifyAndSanitizeHtml } from "./Linkify";
|
||||||
|
|
||||||
|
@ -46,10 +45,35 @@ const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/;
|
||||||
const SYMBOL_PATTERN = /([\u2100-\u2bff])/;
|
const SYMBOL_PATTERN = /([\u2100-\u2bff])/;
|
||||||
|
|
||||||
// Regex pattern for non-emoji characters that can appear in an "all-emoji" message
|
// Regex pattern for non-emoji characters that can appear in an "all-emoji" message
|
||||||
// (Zero-Width Joiner, Zero-Width Space, Emoji presentation character, other whitespace)
|
// (Zero-Width Space, other whitespace)
|
||||||
const EMOJI_SEPARATOR_REGEX = /[\u200D\u200B\s]|\uFE0F/g;
|
const EMOJI_SEPARATOR_REGEX = /[\u200B\s]/g;
|
||||||
|
|
||||||
const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, "i");
|
// Regex for emoji. This includes any RGI_Emoji sequence followed by an optional
|
||||||
|
// emoji presentation VS (U+FE0F), but not those sequences that are followed by
|
||||||
|
// a text presentation VS (U+FE0E). We also count lone regional indicators
|
||||||
|
// (U+1F1E6-U+1F1FF). Technically this regex produces false negatives for emoji
|
||||||
|
// followed by U+FE0E when the emoji doesn't have a text variant, but in
|
||||||
|
// practice this doesn't matter.
|
||||||
|
export const EMOJI_REGEX = (() => {
|
||||||
|
try {
|
||||||
|
// Per our support policy, v mode is available to us, but we still don't
|
||||||
|
// want the app to completely crash on older platforms. We use the
|
||||||
|
// constructor here to avoid a syntax error on such platforms.
|
||||||
|
return new RegExp("\\p{RGI_Emoji}(?!\\uFE0E)(?:(?<!\\uFE0F)\\uFE0F)?|[\\u{1f1e6}-\\u{1f1ff}]", "v");
|
||||||
|
} catch (_e) {
|
||||||
|
// v mode not supported; fall back to matching nothing
|
||||||
|
return /(?!)/;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
const BIGEMOJI_REGEX = (() => {
|
||||||
|
try {
|
||||||
|
return new RegExp(`^(${EMOJI_REGEX.source})+$`, "iv");
|
||||||
|
} catch (_e) {
|
||||||
|
// Fall back, just like for EMOJI_REGEX
|
||||||
|
return /(?!)/;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Return true if the given string contains emoji
|
* Return true if the given string contains emoji
|
||||||
|
@ -265,17 +289,16 @@ export function formatEmojis(message: string | undefined, isHtmlMessage?: boolea
|
||||||
let text = "";
|
let text = "";
|
||||||
let key = 0;
|
let key = 0;
|
||||||
|
|
||||||
const splitter = new GraphemeSplitter();
|
for (const data of graphemeSegmenter.segment(message)) {
|
||||||
for (const char of splitter.iterateGraphemes(message)) {
|
if (EMOJI_REGEX.test(data.segment)) {
|
||||||
if (EMOJIBASE_REGEX.test(char)) {
|
|
||||||
if (text) {
|
if (text) {
|
||||||
result.push(text);
|
result.push(text);
|
||||||
text = "";
|
text = "";
|
||||||
}
|
}
|
||||||
result.push(emojiToSpan(char, key));
|
result.push(emojiToSpan(data.segment, key));
|
||||||
key++;
|
key++;
|
||||||
} else {
|
} else {
|
||||||
text += char;
|
text += data.segment;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (text) {
|
if (text) {
|
||||||
|
|
|
@ -66,6 +66,7 @@ import { localNotificationsAreSilenced } from "./utils/notifications";
|
||||||
import { SdkContextClass } from "./contexts/SDKContext";
|
import { SdkContextClass } from "./contexts/SDKContext";
|
||||||
import { showCantStartACallDialog } from "./voice-broadcast/utils/showCantStartACallDialog";
|
import { showCantStartACallDialog } from "./voice-broadcast/utils/showCantStartACallDialog";
|
||||||
import { isNotNull } from "./Typeguards";
|
import { isNotNull } from "./Typeguards";
|
||||||
|
import { BackgroundAudio } from "./audio/BackgroundAudio";
|
||||||
|
|
||||||
export const PROTOCOL_PSTN = "m.protocol.pstn";
|
export const PROTOCOL_PSTN = "m.protocol.pstn";
|
||||||
export const PROTOCOL_PSTN_PREFIXED = "im.vector.protocol.pstn";
|
export const PROTOCOL_PSTN_PREFIXED = "im.vector.protocol.pstn";
|
||||||
|
@ -157,8 +158,6 @@ export default class LegacyCallHandler extends EventEmitter {
|
||||||
// Calls started as an attended transfer, ie. with the intention of transferring another
|
// Calls started as an attended transfer, ie. with the intention of transferring another
|
||||||
// call with a different party to this one.
|
// call with a different party to this one.
|
||||||
private transferees = new Map<string, MatrixCall>(); // callId (target) -> call (transferee)
|
private transferees = new Map<string, MatrixCall>(); // callId (target) -> call (transferee)
|
||||||
private audioPromises = new Map<AudioID, Promise<void>>();
|
|
||||||
private audioElementsWithListeners = new Map<HTMLMediaElement, boolean>();
|
|
||||||
private supportsPstnProtocol: boolean | null = null;
|
private supportsPstnProtocol: boolean | null = null;
|
||||||
private pstnSupportPrefixed: boolean | null = null; // True if the server only support the prefixed pstn protocol
|
private pstnSupportPrefixed: boolean | null = null; // True if the server only support the prefixed pstn protocol
|
||||||
private supportsSipNativeVirtual: boolean | null = null; // im.vector.protocol.sip_virtual and im.vector.protocol.sip_native
|
private supportsSipNativeVirtual: boolean | null = null; // im.vector.protocol.sip_virtual and im.vector.protocol.sip_native
|
||||||
|
@ -170,6 +169,9 @@ export default class LegacyCallHandler extends EventEmitter {
|
||||||
|
|
||||||
private silencedCalls = new Set<string>(); // callIds
|
private silencedCalls = new Set<string>(); // callIds
|
||||||
|
|
||||||
|
private backgroundAudio = new BackgroundAudio();
|
||||||
|
private playingSources: Record<string, AudioBufferSourceNode> = {}; // Record them for stopping
|
||||||
|
|
||||||
public static get instance(): LegacyCallHandler {
|
public static get instance(): LegacyCallHandler {
|
||||||
if (!window.mxLegacyCallHandler) {
|
if (!window.mxLegacyCallHandler) {
|
||||||
window.mxLegacyCallHandler = new LegacyCallHandler();
|
window.mxLegacyCallHandler = new LegacyCallHandler();
|
||||||
|
@ -199,33 +201,11 @@ export default class LegacyCallHandler extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
public start(): void {
|
public start(): void {
|
||||||
// add empty handlers for media actions, otherwise the media keys
|
|
||||||
// end up causing the audio elements with our ring/ringback etc
|
|
||||||
// audio clips in to play.
|
|
||||||
if (navigator.mediaSession) {
|
|
||||||
navigator.mediaSession.setActionHandler("play", function () {});
|
|
||||||
navigator.mediaSession.setActionHandler("pause", function () {});
|
|
||||||
navigator.mediaSession.setActionHandler("seekbackward", function () {});
|
|
||||||
navigator.mediaSession.setActionHandler("seekforward", function () {});
|
|
||||||
navigator.mediaSession.setActionHandler("previoustrack", function () {});
|
|
||||||
navigator.mediaSession.setActionHandler("nexttrack", function () {});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SettingsStore.getValue(UIFeature.Voip)) {
|
if (SettingsStore.getValue(UIFeature.Voip)) {
|
||||||
MatrixClientPeg.safeGet().on(CallEventHandlerEvent.Incoming, this.onCallIncoming);
|
MatrixClientPeg.safeGet().on(CallEventHandlerEvent.Incoming, this.onCallIncoming);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.checkProtocols(CHECK_PROTOCOLS_ATTEMPTS);
|
this.checkProtocols(CHECK_PROTOCOLS_ATTEMPTS);
|
||||||
|
|
||||||
// Add event listeners for the <audio> elements
|
|
||||||
Object.values(AudioID).forEach((audioId) => {
|
|
||||||
const audioElement = document.getElementById(audioId) as HTMLMediaElement;
|
|
||||||
if (audioElement) {
|
|
||||||
this.addEventListenersForAudioElement(audioElement);
|
|
||||||
} else {
|
|
||||||
logger.warn(`LegacyCallHandler: missing <audio id="${audioId}"> from page`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public stop(): void {
|
public stop(): void {
|
||||||
|
@ -233,27 +213,6 @@ export default class LegacyCallHandler extends EventEmitter {
|
||||||
if (cli) {
|
if (cli) {
|
||||||
cli.removeListener(CallEventHandlerEvent.Incoming, this.onCallIncoming);
|
cli.removeListener(CallEventHandlerEvent.Incoming, this.onCallIncoming);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove event listeners for the <audio> elements
|
|
||||||
Array.from(this.audioElementsWithListeners.keys()).forEach((audioElement) => {
|
|
||||||
this.removeEventListenersForAudioElement(audioElement);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private addEventListenersForAudioElement(audioElement: HTMLMediaElement): void {
|
|
||||||
// Only need to setup the listeners once
|
|
||||||
if (!this.audioElementsWithListeners.get(audioElement)) {
|
|
||||||
MEDIA_EVENT_TYPES.forEach((errorEventType) => {
|
|
||||||
audioElement.addEventListener(errorEventType, this);
|
|
||||||
this.audioElementsWithListeners.set(audioElement, true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private removeEventListenersForAudioElement(audioElement: HTMLMediaElement): void {
|
|
||||||
MEDIA_EVENT_TYPES.forEach((errorEventType) => {
|
|
||||||
audioElement.removeEventListener(errorEventType, this);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* istanbul ignore next (remove if we start using this function for things other than debug logging) */
|
/* istanbul ignore next (remove if we start using this function for things other than debug logging) */
|
||||||
|
@ -465,74 +424,46 @@ export default class LegacyCallHandler extends EventEmitter {
|
||||||
return this.transferees.get(callId);
|
return this.transferees.get(callId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public play(audioId: AudioID): void {
|
public async play(audioId: AudioID): Promise<void> {
|
||||||
const logPrefix = `LegacyCallHandler.play(${audioId}):`;
|
const logPrefix = `LegacyCallHandler.play(${audioId}):`;
|
||||||
logger.debug(`${logPrefix} beginning of function`);
|
logger.debug(`${logPrefix} beginning of function`);
|
||||||
// TODO: Attach an invisible element for this instead
|
|
||||||
// which listens?
|
|
||||||
const audio = document.getElementById(audioId) as HTMLMediaElement;
|
|
||||||
if (audio) {
|
|
||||||
this.addEventListenersForAudioElement(audio);
|
|
||||||
const playAudio = async (): Promise<void> => {
|
|
||||||
try {
|
|
||||||
if (audio.muted) {
|
|
||||||
logger.error(
|
|
||||||
`${logPrefix} <audio> element was unexpectedly muted but we recovered ` +
|
|
||||||
`gracefully by unmuting it`,
|
|
||||||
);
|
|
||||||
// Recover gracefully
|
|
||||||
audio.muted = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This still causes the chrome debugger to break on promise rejection if
|
const audioInfo: Record<AudioID, [prefix: string, loop: boolean]> = {
|
||||||
// the promise is rejected, even though we're catching the exception.
|
[AudioID.Ring]: [`./media/ring`, true],
|
||||||
logger.debug(`${logPrefix} attempting to play audio at volume=${audio.volume}`);
|
[AudioID.Ringback]: [`./media/ringback`, true],
|
||||||
await audio.play();
|
[AudioID.CallEnd]: [`./media/callend`, false],
|
||||||
logger.debug(`${logPrefix} playing audio successfully`);
|
[AudioID.Busy]: [`./media/busy`, false],
|
||||||
} catch (e) {
|
|
||||||
// This is usually because the user hasn't interacted with the document,
|
|
||||||
// or chrome doesn't think so and is denying the request. Not sure what
|
|
||||||
// we can really do here...
|
|
||||||
// https://github.com/vector-im/element-web/issues/7657
|
|
||||||
logger.warn(`${logPrefix} unable to play audio clip`, e);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
if (this.audioPromises.has(audioId)) {
|
|
||||||
this.audioPromises.set(
|
const [urlPrefix, loop] = audioInfo[audioId];
|
||||||
audioId,
|
const source = await this.backgroundAudio.pickFormatAndPlay(urlPrefix, ["mp3", "ogg"], loop);
|
||||||
this.audioPromises.get(audioId)!.then(() => {
|
this.playingSources[audioId] = source;
|
||||||
audio.load();
|
logger.debug(`${logPrefix} playing audio successfully`);
|
||||||
return playAudio();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.audioPromises.set(audioId, playAudio());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.warn(`${logPrefix} unable to find <audio> element for ${audioId}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public pause(audioId: AudioID): void {
|
public pause(audioId: AudioID): void {
|
||||||
const logPrefix = `LegacyCallHandler.pause(${audioId}):`;
|
const logPrefix = `LegacyCallHandler.pause(${audioId}):`;
|
||||||
logger.debug(`${logPrefix} beginning of function`);
|
logger.debug(`${logPrefix} beginning of function`);
|
||||||
// TODO: Attach an invisible element for this instead
|
|
||||||
// which listens?
|
const source = this.playingSources[audioId];
|
||||||
const audio = document.getElementById(audioId) as HTMLMediaElement;
|
if (!source) {
|
||||||
const pauseAudio = (): void => {
|
logger.debug(`${logPrefix} audio not playing`);
|
||||||
logger.debug(`${logPrefix} pausing audio`);
|
return;
|
||||||
// pause doesn't return a promise, so just do it
|
|
||||||
audio.pause();
|
|
||||||
};
|
|
||||||
if (audio) {
|
|
||||||
if (this.audioPromises.has(audioId)) {
|
|
||||||
this.audioPromises.set(audioId, this.audioPromises.get(audioId)!.then(pauseAudio));
|
|
||||||
} else {
|
|
||||||
pauseAudio();
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
logger.warn(`${logPrefix} unable to find <audio> element for ${audioId}`);
|
source.stop();
|
||||||
|
delete this.playingSources[audioId];
|
||||||
|
|
||||||
|
logger.debug(`${logPrefix} paused audio`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the given audio is currently playing
|
||||||
|
* Only supported for looping audio tracks
|
||||||
|
* @param audioId the ID of the audio to query for playing state
|
||||||
|
*/
|
||||||
|
public isPlaying(audioId: AudioID.Ring | AudioID.Ringback): boolean {
|
||||||
|
return !!this.playingSources[audioId];
|
||||||
}
|
}
|
||||||
|
|
||||||
private matchesCallForThisRoom(call: MatrixCall): boolean {
|
private matchesCallForThisRoom(call: MatrixCall): boolean {
|
||||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { defer, sleep } from "matrix-js-sdk/src/utils";
|
import { IDeferred, defer, sleep } from "matrix-js-sdk/src/utils";
|
||||||
import { TypedEventEmitter } from "matrix-js-sdk/src/matrix";
|
import { TypedEventEmitter } from "matrix-js-sdk/src/matrix";
|
||||||
import { Glass } from "@vector-im/compound-web";
|
import { Glass } from "@vector-im/compound-web";
|
||||||
|
|
||||||
|
@ -47,11 +47,12 @@ export interface IModal<C extends ComponentType> {
|
||||||
elem: React.ReactNode;
|
elem: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
beforeClosePromise?: Promise<boolean>;
|
beforeClosePromise?: Promise<boolean>;
|
||||||
closeReason?: string;
|
closeReason?: ModalCloseReason;
|
||||||
onBeforeClose?(reason?: string): Promise<boolean>;
|
onBeforeClose?(reason?: ModalCloseReason): Promise<boolean>;
|
||||||
onFinished: ComponentProps<C>["onFinished"];
|
onFinished: ComponentProps<C>["onFinished"];
|
||||||
close(...args: Parameters<ComponentProps<C>["onFinished"]>): void;
|
close(...args: Parameters<ComponentProps<C>["onFinished"]>): void;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
|
deferred?: IDeferred<Parameters<ComponentProps<C>["onFinished"]>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IHandle<C extends ComponentType> {
|
export interface IHandle<C extends ComponentType> {
|
||||||
|
@ -73,6 +74,8 @@ type HandlerMap = {
|
||||||
[ModalManagerEvent.Closed]: () => void;
|
[ModalManagerEvent.Closed]: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ModalCloseReason = "backgroundClick";
|
||||||
|
|
||||||
export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMap> {
|
export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMap> {
|
||||||
private counter = 0;
|
private counter = 0;
|
||||||
// The modal to prioritise over all others. If this is set, only show
|
// The modal to prioritise over all others. If this is set, only show
|
||||||
|
@ -148,10 +151,14 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* DEPRECATED.
|
||||||
|
* This is used only for tests. They should be using forceCloseAllModals but that
|
||||||
|
* caused a chunk of tests to fail, so for now they continue to use this.
|
||||||
|
*
|
||||||
* @param reason either "backgroundClick" or undefined
|
* @param reason either "backgroundClick" or undefined
|
||||||
* @return whether a modal was closed
|
* @return whether a modal was closed
|
||||||
*/
|
*/
|
||||||
public closeCurrentModal(reason?: string): boolean {
|
public closeCurrentModal(reason?: ModalCloseReason): boolean {
|
||||||
const modal = this.getCurrentModal();
|
const modal = this.getCurrentModal();
|
||||||
if (!modal) {
|
if (!modal) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -161,6 +168,22 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forces closes all open modals. The modals onBeforeClose function will not be
|
||||||
|
* run and the modal will not have a chance to prevent closing. Intended for
|
||||||
|
* situations like the user logging out of the app.
|
||||||
|
*/
|
||||||
|
public forceCloseAllModals(): void {
|
||||||
|
for (const modal of this.modals) {
|
||||||
|
modal.deferred?.resolve([]);
|
||||||
|
if (modal.onFinished) modal.onFinished.apply(null);
|
||||||
|
this.emitClosed();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.modals = [];
|
||||||
|
this.reRender();
|
||||||
|
}
|
||||||
|
|
||||||
private buildModal<C extends ComponentType>(
|
private buildModal<C extends ComponentType>(
|
||||||
prom: Promise<C>,
|
prom: Promise<C>,
|
||||||
props?: ComponentProps<C>,
|
props?: ComponentProps<C>,
|
||||||
|
@ -199,7 +222,7 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
|
||||||
modal: IModal<C>,
|
modal: IModal<C>,
|
||||||
props?: ComponentProps<C>,
|
props?: ComponentProps<C>,
|
||||||
): [IHandle<C>["close"], IHandle<C>["finished"]] {
|
): [IHandle<C>["close"], IHandle<C>["finished"]] {
|
||||||
const deferred = defer<Parameters<ComponentProps<C>["onFinished"]>>();
|
modal.deferred = defer<Parameters<ComponentProps<C>["onFinished"]>>();
|
||||||
return [
|
return [
|
||||||
async (...args: Parameters<ComponentProps<C>["onFinished"]>): Promise<void> => {
|
async (...args: Parameters<ComponentProps<C>["onFinished"]>): Promise<void> => {
|
||||||
if (modal.beforeClosePromise) {
|
if (modal.beforeClosePromise) {
|
||||||
|
@ -212,7 +235,7 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
deferred.resolve(args);
|
modal.deferred?.resolve(args);
|
||||||
if (props?.onFinished) props.onFinished.apply(null, args);
|
if (props?.onFinished) props.onFinished.apply(null, args);
|
||||||
const i = this.modals.indexOf(modal);
|
const i = this.modals.indexOf(modal);
|
||||||
if (i >= 0) {
|
if (i >= 0) {
|
||||||
|
@ -236,7 +259,7 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
|
||||||
this.reRender();
|
this.reRender();
|
||||||
this.emitClosed();
|
this.emitClosed();
|
||||||
},
|
},
|
||||||
deferred.promise,
|
modal.deferred.promise,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,6 +58,7 @@ import ToastStore from "./stores/ToastStore";
|
||||||
import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoEventType } from "./voice-broadcast";
|
import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoEventType } from "./voice-broadcast";
|
||||||
import { getSenderName } from "./utils/event/getSenderName";
|
import { getSenderName } from "./utils/event/getSenderName";
|
||||||
import { stripPlainReply } from "./utils/Reply";
|
import { stripPlainReply } from "./utils/Reply";
|
||||||
|
import { BackgroundAudio } from "./audio/BackgroundAudio";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Dispatches:
|
* Dispatches:
|
||||||
|
@ -112,6 +113,8 @@ class NotifierClass {
|
||||||
private toolbarHidden?: boolean;
|
private toolbarHidden?: boolean;
|
||||||
private isSyncing?: boolean;
|
private isSyncing?: boolean;
|
||||||
|
|
||||||
|
private backgroundAudio = new BackgroundAudio();
|
||||||
|
|
||||||
public notificationMessageForEvent(ev: MatrixEvent): string | null {
|
public notificationMessageForEvent(ev: MatrixEvent): string | null {
|
||||||
const msgType = ev.getContent().msgtype;
|
const msgType = ev.getContent().msgtype;
|
||||||
if (msgType && msgTypeHandlers.hasOwnProperty(msgType)) {
|
if (msgType && msgTypeHandlers.hasOwnProperty(msgType)) {
|
||||||
|
@ -226,28 +229,14 @@ class NotifierClass {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Play notification sound here
|
||||||
const sound = this.getSoundForRoom(room.roomId);
|
const sound = this.getSoundForRoom(room.roomId);
|
||||||
logger.log(`Got sound ${(sound && sound.name) || "default"} for ${room.roomId}`);
|
logger.log(`Got sound ${(sound && sound.name) || "default"} for ${room.roomId}`);
|
||||||
|
|
||||||
try {
|
if (sound) {
|
||||||
const selector = document.querySelector<HTMLAudioElement>(
|
await this.backgroundAudio.play(sound.url);
|
||||||
sound ? `audio[src='${sound.url}']` : "#messageAudio",
|
} else {
|
||||||
);
|
await this.backgroundAudio.pickFormatAndPlay("media/message", ["mp3", "ogg"]);
|
||||||
let audioElement = selector;
|
|
||||||
if (!audioElement) {
|
|
||||||
if (!sound) {
|
|
||||||
logger.error("No audio element or sound to play for notification");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
audioElement = new Audio(sound.url);
|
|
||||||
if (sound.type) {
|
|
||||||
audioElement.type = sound.type;
|
|
||||||
}
|
|
||||||
document.body.appendChild(audioElement);
|
|
||||||
}
|
|
||||||
await audioElement.play();
|
|
||||||
} catch (ex) {
|
|
||||||
logger.warn("Caught error when trying to fetch room notification sound:", ex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -681,3 +681,49 @@ export default function eventSearch(
|
||||||
return eventIndexSearch(client, term, roomId, abortSignal);
|
return eventIndexSearch(client, term, roomId, abortSignal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The scope for a message search, either in the current room or across all rooms.
|
||||||
|
*/
|
||||||
|
export enum SearchScope {
|
||||||
|
Room = "Room",
|
||||||
|
All = "All",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about a message search in progress.
|
||||||
|
*/
|
||||||
|
export interface SearchInfo {
|
||||||
|
/**
|
||||||
|
* Opaque ID for this search.
|
||||||
|
*/
|
||||||
|
searchId: number;
|
||||||
|
/**
|
||||||
|
* The room ID being searched, or undefined if searching all rooms.
|
||||||
|
*/
|
||||||
|
roomId?: string;
|
||||||
|
/**
|
||||||
|
* The search term.
|
||||||
|
*/
|
||||||
|
term: string;
|
||||||
|
/**
|
||||||
|
* The scope of the search.
|
||||||
|
*/
|
||||||
|
scope: SearchScope;
|
||||||
|
/**
|
||||||
|
* The promise for the search results.
|
||||||
|
*/
|
||||||
|
promise: Promise<ISearchResults>;
|
||||||
|
/**
|
||||||
|
* Controller for aborting the search.
|
||||||
|
*/
|
||||||
|
abortController?: AbortController;
|
||||||
|
/**
|
||||||
|
* Whether the search is currently awaiting data from the backend.
|
||||||
|
*/
|
||||||
|
inProgress?: boolean;
|
||||||
|
/**
|
||||||
|
* The total count of matching results as returned by the backend.
|
||||||
|
*/
|
||||||
|
count?: number;
|
||||||
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
|
import { KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||||
|
|
||||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||||
import dis from "../../../../dispatcher/dispatcher";
|
import dis from "../../../../dispatcher/dispatcher";
|
||||||
|
@ -28,7 +28,7 @@ import DialogButtons from "../../../../components/views/elements/DialogButtons";
|
||||||
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
|
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
newVersionInfo: IKeyBackupInfo;
|
newVersionInfo: KeyBackupInfo;
|
||||||
onFinished(): void;
|
onFinished(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
74
src/audio/BackgroundAudio.ts
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
|
import { createAudioContext } from "./compat";
|
||||||
|
|
||||||
|
const formatMap = {
|
||||||
|
mp3: "audio/mpeg",
|
||||||
|
ogg: "audio/ogg",
|
||||||
|
};
|
||||||
|
|
||||||
|
export class BackgroundAudio {
|
||||||
|
private audioContext = createAudioContext();
|
||||||
|
private sounds: Record<string, AudioBuffer> = {};
|
||||||
|
|
||||||
|
public async pickFormatAndPlay<F extends Array<keyof typeof formatMap>>(
|
||||||
|
urlPrefix: string,
|
||||||
|
formats: F,
|
||||||
|
loop = false,
|
||||||
|
): Promise<AudioBufferSourceNode> {
|
||||||
|
const format = this.pickFormat(...formats);
|
||||||
|
if (!format) {
|
||||||
|
console.log("Browser doesn't support any of the formats", formats);
|
||||||
|
// Will probably never happen. If happened, format="" and will fail to load audio. Who cares...
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.play(`${urlPrefix}.${format}`, loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async play(url: string, loop = false): Promise<AudioBufferSourceNode> {
|
||||||
|
if (!this.sounds.hasOwnProperty(url)) {
|
||||||
|
// No cache, fetch it
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (response.status != 200) {
|
||||||
|
logger.warn("Failed to fetch error audio");
|
||||||
|
}
|
||||||
|
const buffer = await response.arrayBuffer();
|
||||||
|
const sound = await this.audioContext.decodeAudioData(buffer);
|
||||||
|
this.sounds[url] = sound;
|
||||||
|
}
|
||||||
|
const source = this.audioContext.createBufferSource();
|
||||||
|
source.buffer = this.sounds[url];
|
||||||
|
source.loop = loop;
|
||||||
|
source.connect(this.audioContext.destination);
|
||||||
|
source.start();
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
private pickFormat<F extends Array<keyof typeof formatMap>>(...formats: F): F[number] | null {
|
||||||
|
// Detect supported formats
|
||||||
|
const audioElement = document.createElement("audio");
|
||||||
|
|
||||||
|
for (const format of formats) {
|
||||||
|
if (audioElement.canPlayType(formatMap[format])) {
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -99,6 +99,8 @@ export const AutocompleteInput: React.FC<AutocompleteInputProps> = ({
|
||||||
|
|
||||||
onSelectionChange(newSelection);
|
onSelectionChange(newSelection);
|
||||||
focusEditor();
|
focusEditor();
|
||||||
|
setQuery("");
|
||||||
|
setSuggestions([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeSelection = (completion: ICompletion): void => {
|
const removeSelection = (completion: ICompletion): void => {
|
||||||
|
|
|
@ -458,9 +458,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
handled = true;
|
handled = true;
|
||||||
break;
|
break;
|
||||||
case KeyBindingAction.SearchInRoom:
|
case KeyBindingAction.SearchInRoom:
|
||||||
dis.dispatch({
|
dis.fire(Action.FocusMessageSearch);
|
||||||
action: "focus_search",
|
|
||||||
});
|
|
||||||
handled = true;
|
handled = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -490,11 +488,15 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
handled = true;
|
handled = true;
|
||||||
break;
|
break;
|
||||||
case KeyBindingAction.GoToHome:
|
case KeyBindingAction.GoToHome:
|
||||||
|
// even if we cancel because there are modals open, we still
|
||||||
|
// handled it: nothing else should happen.
|
||||||
|
handled = true;
|
||||||
|
if (Modal.hasDialogs()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: Action.ViewHomePage,
|
action: Action.ViewHomePage,
|
||||||
});
|
});
|
||||||
Modal.closeCurrentModal("homeKeyboardShortcut");
|
|
||||||
handled = true;
|
|
||||||
break;
|
break;
|
||||||
case KeyBindingAction.ToggleSpacePanel:
|
case KeyBindingAction.ToggleSpacePanel:
|
||||||
dis.fire(Action.ToggleSpacePanel);
|
dis.fire(Action.ToggleSpacePanel);
|
||||||
|
|
|
@ -31,7 +31,7 @@ import { defer, IDeferred, QueryDict } from "matrix-js-sdk/src/utils";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { throttle } from "lodash";
|
import { throttle } from "lodash";
|
||||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
||||||
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
|
import { KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||||
|
|
||||||
// what-input helps improve keyboard accessibility
|
// what-input helps improve keyboard accessibility
|
||||||
import "what-input";
|
import "what-input";
|
||||||
|
@ -1544,7 +1544,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
if (Lifecycle.isLoggingOut()) return;
|
if (Lifecycle.isLoggingOut()) return;
|
||||||
|
|
||||||
// A modal might have been open when we were logged out by the server
|
// A modal might have been open when we were logged out by the server
|
||||||
Modal.closeCurrentModal("Session.logged_out");
|
Modal.forceCloseAllModals();
|
||||||
|
|
||||||
if (errObj.httpStatus === 401 && errObj.data && errObj.data["soft_logout"]) {
|
if (errObj.httpStatus === 401 && errObj.data && errObj.data["soft_logout"]) {
|
||||||
logger.warn("Soft logout issued by server - avoiding data deletion");
|
logger.warn("Soft logout issued by server - avoiding data deletion");
|
||||||
|
@ -1614,7 +1614,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
});
|
});
|
||||||
cli.on(CryptoEvent.KeyBackupFailed, async (errcode): Promise<void> => {
|
cli.on(CryptoEvent.KeyBackupFailed, async (errcode): Promise<void> => {
|
||||||
let haveNewVersion: boolean | undefined;
|
let haveNewVersion: boolean | undefined;
|
||||||
let newVersionInfo: IKeyBackupInfo | null = null;
|
let newVersionInfo: KeyBackupInfo | null = null;
|
||||||
// if key backup is still enabled, there must be a new backup in place
|
// if key backup is still enabled, there must be a new backup in place
|
||||||
if (cli.getKeyBackupEnabled()) {
|
if (cli.getKeyBackupEnabled()) {
|
||||||
haveNewVersion = true;
|
haveNewVersion = true;
|
||||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { ChangeEvent } from "react";
|
||||||
import { Room, RoomState, RoomStateEvent, RoomMember, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
import { Room, RoomState, RoomStateEvent, RoomMember, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
import { throttle } from "lodash";
|
import { throttle } from "lodash";
|
||||||
|
|
||||||
|
@ -42,6 +42,7 @@ import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||||
import { IRightPanelCard, IRightPanelCardState } from "../../stores/right-panel/RightPanelStoreIPanelState";
|
import { IRightPanelCard, IRightPanelCardState } from "../../stores/right-panel/RightPanelStoreIPanelState";
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import { XOR } from "../../@types/common";
|
import { XOR } from "../../@types/common";
|
||||||
|
import { RightPanelTabs } from "../views/right_panel/RightPanelTabs";
|
||||||
|
|
||||||
interface BaseProps {
|
interface BaseProps {
|
||||||
overwriteCard?: IRightPanelCard; // used to display a custom card and ignoring the RightPanelStore (used for UserView)
|
overwriteCard?: IRightPanelCard; // used to display a custom card and ignoring the RightPanelStore (used for UserView)
|
||||||
|
@ -57,7 +58,8 @@ interface RoomlessProps extends BaseProps {
|
||||||
interface RoomProps extends BaseProps {
|
interface RoomProps extends BaseProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
permalinkCreator: RoomPermalinkCreator;
|
permalinkCreator: RoomPermalinkCreator;
|
||||||
onSearchClick?: () => void;
|
onSearchChange?: (e: ChangeEvent) => void;
|
||||||
|
onSearchCancel?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = XOR<RoomlessProps, RoomProps>;
|
type Props = XOR<RoomlessProps, RoomProps>;
|
||||||
|
@ -170,6 +172,7 @@ export default class RightPanel extends React.Component<Props, IState> {
|
||||||
<MemberList
|
<MemberList
|
||||||
roomId={roomId}
|
roomId={roomId}
|
||||||
key={roomId}
|
key={roomId}
|
||||||
|
hideHeaderButtons
|
||||||
onClose={this.onClose}
|
onClose={this.onClose}
|
||||||
searchQuery={this.state.searchQuery}
|
searchQuery={this.state.searchQuery}
|
||||||
onSearchQueryChanged={this.onSearchQueryChanged}
|
onSearchQueryChanged={this.onSearchQueryChanged}
|
||||||
|
@ -293,10 +296,11 @@ export default class RightPanel extends React.Component<Props, IState> {
|
||||||
card = (
|
card = (
|
||||||
<RoomSummaryCard
|
<RoomSummaryCard
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
onClose={this.onClose}
|
|
||||||
// whenever RightPanel is passed a room it is passed a permalinkcreator
|
// whenever RightPanel is passed a room it is passed a permalinkcreator
|
||||||
permalinkCreator={this.props.permalinkCreator!}
|
permalinkCreator={this.props.permalinkCreator!}
|
||||||
onSearchClick={this.props.onSearchClick}
|
onSearchChange={this.props.onSearchChange}
|
||||||
|
onSearchCancel={this.props.onSearchCancel}
|
||||||
|
focusRoomSearch={cardState?.focusRoomSearch}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -311,6 +315,7 @@ export default class RightPanel extends React.Component<Props, IState> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="mx_RightPanel" id="mx_RightPanel">
|
<aside className="mx_RightPanel" id="mx_RightPanel">
|
||||||
|
{phase && <RightPanelTabs phase={phase} />}
|
||||||
{card}
|
{card}
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
|
|
|
@ -24,12 +24,11 @@ import {
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import ScrollPanel from "./ScrollPanel";
|
import ScrollPanel from "./ScrollPanel";
|
||||||
import { SearchScope } from "../views/rooms/SearchBar";
|
|
||||||
import Spinner from "../views/elements/Spinner";
|
import Spinner from "../views/elements/Spinner";
|
||||||
import { _t } from "../../languageHandler";
|
import { _t } from "../../languageHandler";
|
||||||
import { haveRendererForEvent } from "../../events/EventTileFactory";
|
import { haveRendererForEvent } from "../../events/EventTileFactory";
|
||||||
import SearchResultTile from "../views/rooms/SearchResultTile";
|
import SearchResultTile from "../views/rooms/SearchResultTile";
|
||||||
import { searchPagination } from "../../Searching";
|
import { searchPagination, SearchScope } from "../../Searching";
|
||||||
import Modal from "../../Modal";
|
import Modal from "../../Modal";
|
||||||
import ErrorDialog from "../views/dialogs/ErrorDialog";
|
import ErrorDialog from "../views/dialogs/ErrorDialog";
|
||||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||||
|
@ -49,6 +48,7 @@ if (DEBUG) {
|
||||||
interface Props {
|
interface Props {
|
||||||
term: string;
|
term: string;
|
||||||
scope: SearchScope;
|
scope: SearchScope;
|
||||||
|
inProgress: boolean;
|
||||||
promise: Promise<ISearchResults>;
|
promise: Promise<ISearchResults>;
|
||||||
abortController?: AbortController;
|
abortController?: AbortController;
|
||||||
resizeNotifier: ResizeNotifier;
|
resizeNotifier: ResizeNotifier;
|
||||||
|
@ -59,10 +59,9 @@ interface Props {
|
||||||
// XXX: todo: merge overlapping results somehow?
|
// XXX: todo: merge overlapping results somehow?
|
||||||
// XXX: why doesn't searching on name work?
|
// XXX: why doesn't searching on name work?
|
||||||
export const RoomSearchView = forwardRef<ScrollPanel, Props>(
|
export const RoomSearchView = forwardRef<ScrollPanel, Props>(
|
||||||
({ term, scope, promise, abortController, resizeNotifier, className, onUpdate }: Props, ref) => {
|
({ term, scope, promise, abortController, resizeNotifier, className, onUpdate, inProgress }: Props, ref) => {
|
||||||
const client = useContext(MatrixClientContext);
|
const client = useContext(MatrixClientContext);
|
||||||
const roomContext = useContext(RoomContext);
|
const roomContext = useContext(RoomContext);
|
||||||
const [inProgress, setInProgress] = useState(true);
|
|
||||||
const [highlights, setHighlights] = useState<string[] | null>(null);
|
const [highlights, setHighlights] = useState<string[] | null>(null);
|
||||||
const [results, setResults] = useState<ISearchResults | null>(null);
|
const [results, setResults] = useState<ISearchResults | null>(null);
|
||||||
const aborted = useRef(false);
|
const aborted = useRef(false);
|
||||||
|
@ -79,10 +78,9 @@ export const RoomSearchView = forwardRef<ScrollPanel, Props>(
|
||||||
|
|
||||||
const handleSearchResult = useCallback(
|
const handleSearchResult = useCallback(
|
||||||
(searchPromise: Promise<ISearchResults>): Promise<boolean> => {
|
(searchPromise: Promise<ISearchResults>): Promise<boolean> => {
|
||||||
setInProgress(true);
|
onUpdate(true, null);
|
||||||
|
|
||||||
return searchPromise
|
return searchPromise.then(
|
||||||
.then(
|
|
||||||
async (results): Promise<boolean> => {
|
async (results): Promise<boolean> => {
|
||||||
debuglog("search complete");
|
debuglog("search complete");
|
||||||
if (aborted.current) {
|
if (aborted.current) {
|
||||||
|
@ -126,6 +124,7 @@ export const RoomSearchView = forwardRef<ScrollPanel, Props>(
|
||||||
|
|
||||||
setHighlights(highlights);
|
setHighlights(highlights);
|
||||||
setResults({ ...results }); // copy to force a refresh
|
setResults({ ...results }); // copy to force a refresh
|
||||||
|
onUpdate(false, results);
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
|
@ -138,14 +137,12 @@ export const RoomSearchView = forwardRef<ScrollPanel, Props>(
|
||||||
title: _t("error_dialog|search_failed|title"),
|
title: _t("error_dialog|search_failed|title"),
|
||||||
description: error?.message ?? _t("error_dialog|search_failed|server_unavailable"),
|
description: error?.message ?? _t("error_dialog|search_failed|server_unavailable"),
|
||||||
});
|
});
|
||||||
|
onUpdate(false, null);
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
)
|
);
|
||||||
.finally(() => {
|
|
||||||
setInProgress(false);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[client, term],
|
[client, term, onUpdate],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Mount & unmount effect
|
// Mount & unmount effect
|
||||||
|
|
|
@ -17,7 +17,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef, ReactElement, ReactNode, RefObject, useContext } from "react";
|
import React, { ChangeEvent, createRef, ReactElement, ReactNode, RefObject, useContext } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import {
|
import {
|
||||||
IRecommendedVersion,
|
IRecommendedVersion,
|
||||||
|
@ -41,7 +41,7 @@ import {
|
||||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
import { CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||||
import { throttle } from "lodash";
|
import { debounce, throttle } from "lodash";
|
||||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
||||||
import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
||||||
|
|
||||||
|
@ -70,10 +70,9 @@ import TimelinePanel from "./TimelinePanel";
|
||||||
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||||
import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
|
import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
|
||||||
import RoomPreviewCard from "../views/rooms/RoomPreviewCard";
|
import RoomPreviewCard from "../views/rooms/RoomPreviewCard";
|
||||||
import SearchBar, { SearchScope } from "../views/rooms/SearchBar";
|
|
||||||
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
||||||
import AuxPanel from "../views/rooms/AuxPanel";
|
import AuxPanel from "../views/rooms/AuxPanel";
|
||||||
import LegacyRoomHeader, { ISearchInfo } from "../views/rooms/LegacyRoomHeader";
|
import LegacyRoomHeader from "../views/rooms/LegacyRoomHeader";
|
||||||
import RoomHeader from "../views/rooms/RoomHeader";
|
import RoomHeader from "../views/rooms/RoomHeader";
|
||||||
import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||||
import EffectsOverlay from "../views/elements/EffectsOverlay";
|
import EffectsOverlay from "../views/elements/EffectsOverlay";
|
||||||
|
@ -121,7 +120,7 @@ import { SDKContext } from "../../contexts/SDKContext";
|
||||||
import { CallStore, CallStoreEvent } from "../../stores/CallStore";
|
import { CallStore, CallStoreEvent } from "../../stores/CallStore";
|
||||||
import { Call } from "../../models/Call";
|
import { Call } from "../../models/Call";
|
||||||
import { RoomSearchView } from "./RoomSearchView";
|
import { RoomSearchView } from "./RoomSearchView";
|
||||||
import eventSearch from "../../Searching";
|
import eventSearch, { SearchInfo, SearchScope } from "../../Searching";
|
||||||
import VoipUserMapper from "../../VoipUserMapper";
|
import VoipUserMapper from "../../VoipUserMapper";
|
||||||
import { isCallEvent } from "./LegacyCallEventGrouper";
|
import { isCallEvent } from "./LegacyCallEventGrouper";
|
||||||
import { WidgetType } from "../../widgets/WidgetType";
|
import { WidgetType } from "../../widgets/WidgetType";
|
||||||
|
@ -133,6 +132,7 @@ import { CancelAskToJoinPayload } from "../../dispatcher/payloads/CancelAskToJoi
|
||||||
import { SubmitAskToJoinPayload } from "../../dispatcher/payloads/SubmitAskToJoinPayload";
|
import { SubmitAskToJoinPayload } from "../../dispatcher/payloads/SubmitAskToJoinPayload";
|
||||||
import RightPanelStore from "../../stores/right-panel/RightPanelStore";
|
import RightPanelStore from "../../stores/right-panel/RightPanelStore";
|
||||||
import { onView3pidInvite } from "../../stores/right-panel/action-handlers";
|
import { onView3pidInvite } from "../../stores/right-panel/action-handlers";
|
||||||
|
import RoomSearchAuxPanel from "../views/rooms/RoomSearchAuxPanel";
|
||||||
|
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
|
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
|
||||||
|
@ -190,7 +190,7 @@ export interface IRoomState {
|
||||||
/**
|
/**
|
||||||
* The state of an ongoing search if there is one.
|
* The state of an ongoing search if there is one.
|
||||||
*/
|
*/
|
||||||
search?: ISearchInfo;
|
search?: SearchInfo;
|
||||||
callState?: CallState;
|
callState?: CallState;
|
||||||
activeCall: Call | null;
|
activeCall: Call | null;
|
||||||
canPeek: boolean;
|
canPeek: boolean;
|
||||||
|
@ -1196,9 +1196,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "focus_search":
|
|
||||||
this.onSearchClick();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "local_room_event":
|
case "local_room_event":
|
||||||
this.onLocalRoomEvent(payload.roomId);
|
this.onLocalRoomEvent(payload.roomId);
|
||||||
|
@ -1290,7 +1287,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.RoomMemberList);
|
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomMemberList);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Action.View3pidInvite:
|
case Action.View3pidInvite:
|
||||||
|
@ -1725,13 +1722,14 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private onSearch = (term: string, scope: SearchScope): void => {
|
private onSearch = (term: string, scope = SearchScope.Room): void => {
|
||||||
const roomId = scope === SearchScope.Room ? this.getRoomId() : undefined;
|
const roomId = scope === SearchScope.Room ? this.getRoomId() : undefined;
|
||||||
debuglog("sending search request");
|
debuglog("sending search request");
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
const promise = eventSearch(this.context.client!, term, roomId, abortController.signal);
|
const promise = eventSearch(this.context.client!, term, roomId, abortController.signal);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
|
timelineRenderingType: TimelineRenderingType.Search,
|
||||||
search: {
|
search: {
|
||||||
// make sure that we don't end up showing results from
|
// make sure that we don't end up showing results from
|
||||||
// an aborted search by keeping a unique id.
|
// an aborted search by keeping a unique id.
|
||||||
|
@ -1745,6 +1743,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onSearchScopeChange = (scope: SearchScope): void => {
|
||||||
|
this.onSearch(this.state.search?.term ?? "", scope);
|
||||||
|
};
|
||||||
|
|
||||||
private onSearchUpdate = (inProgress: boolean, searchResults: ISearchResults | null): void => {
|
private onSearchUpdate = (inProgress: boolean, searchResults: ISearchResults | null): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
search: {
|
search: {
|
||||||
|
@ -1839,15 +1841,14 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private onSearchClick = (): void => {
|
private onSearchClick = (): void => {
|
||||||
if (this.state.timelineRenderingType === TimelineRenderingType.Search) {
|
dis.fire(Action.FocusMessageSearch);
|
||||||
this.onCancelSearchClick();
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
timelineRenderingType: TimelineRenderingType.Search,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onSearchChange = debounce((e: ChangeEvent): void => {
|
||||||
|
const term = (e.target as HTMLInputElement).value;
|
||||||
|
this.onSearch(term);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
private onCancelSearchClick = (): Promise<void> => {
|
private onCancelSearchClick = (): Promise<void> => {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
this.setState(
|
this.setState(
|
||||||
|
@ -2328,10 +2329,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
let previewBar;
|
let previewBar;
|
||||||
if (this.state.timelineRenderingType === TimelineRenderingType.Search) {
|
if (this.state.timelineRenderingType === TimelineRenderingType.Search) {
|
||||||
aux = (
|
aux = (
|
||||||
<SearchBar
|
<RoomSearchAuxPanel
|
||||||
searchInProgress={this.state.search?.inProgress}
|
searchInfo={this.state.search}
|
||||||
onCancelClick={this.onCancelSearchClick}
|
onCancelClick={this.onCancelSearchClick}
|
||||||
onSearch={this.onSearch}
|
onSearchScopeChange={this.onSearchScopeChange}
|
||||||
isRoomEncrypted={this.context.client.isRoomEncrypted(this.state.room.roomId)}
|
isRoomEncrypted={this.context.client.isRoomEncrypted(this.state.room.roomId)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -2438,6 +2439,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
scope={this.state.search.scope}
|
scope={this.state.search.scope}
|
||||||
promise={this.state.search.promise}
|
promise={this.state.search.promise}
|
||||||
abortController={this.state.search.abortController}
|
abortController={this.state.search.abortController}
|
||||||
|
inProgress={!!this.state.search.inProgress}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
className={this.messagePanelClassNames}
|
className={this.messagePanelClassNames}
|
||||||
onUpdate={this.onSearchUpdate}
|
onUpdate={this.onSearchUpdate}
|
||||||
|
@ -2507,7 +2509,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
permalinkCreator={this.permalinkCreator}
|
permalinkCreator={this.permalinkCreator}
|
||||||
e2eStatus={this.state.e2eStatus}
|
e2eStatus={this.state.e2eStatus}
|
||||||
onSearchClick={this.onSearchClick}
|
onSearchChange={this.onSearchChange}
|
||||||
|
onSearchCancel={this.onCancelSearchClick}
|
||||||
/>
|
/>
|
||||||
) : undefined;
|
) : undefined;
|
||||||
|
|
||||||
|
|
|
@ -37,9 +37,6 @@ import { ButtonEvent } from "../views/elements/AccessibleButton";
|
||||||
import Spinner from "../views/elements/Spinner";
|
import Spinner from "../views/elements/Spinner";
|
||||||
import Heading from "../views/typography/Heading";
|
import Heading from "../views/typography/Heading";
|
||||||
import { clearRoomNotification } from "../../utils/notifications";
|
import { clearRoomNotification } from "../../utils/notifications";
|
||||||
import { useDispatcher } from "../../hooks/useDispatcher";
|
|
||||||
import dis from "../../dispatcher/dispatcher";
|
|
||||||
import { Action } from "../../dispatcher/actions";
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
|
@ -259,14 +256,6 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
|
||||||
}
|
}
|
||||||
}, [timelineSet, timelinePanel]);
|
}, [timelineSet, timelinePanel]);
|
||||||
|
|
||||||
useDispatcher(dis, (payload) => {
|
|
||||||
// This actually foucses the close button on the threads panel, as its the only interactive element,
|
|
||||||
// but at least it puts the user in the right area of the app.
|
|
||||||
if (payload.action === Action.FocusThreadsPanel) {
|
|
||||||
closeButonRef.current?.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RoomContext.Provider
|
<RoomContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
@ -277,6 +266,7 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BaseCard
|
<BaseCard
|
||||||
|
hideHeaderButtons
|
||||||
header={
|
header={
|
||||||
<ThreadPanelHeader
|
<ThreadPanelHeader
|
||||||
filterOption={filterOption}
|
filterOption={filterOption}
|
||||||
|
@ -284,7 +274,10 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
|
||||||
empty={!hasThreads}
|
empty={!hasThreads}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
id="thread-panel"
|
||||||
className="mx_ThreadPanel"
|
className="mx_ThreadPanel"
|
||||||
|
ariaLabelledBy="thread-panel-tab"
|
||||||
|
role="tabpanel"
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
withoutScrollContainer={true}
|
withoutScrollContainer={true}
|
||||||
ref={card}
|
ref={card}
|
||||||
|
|
|
@ -55,7 +55,6 @@ export const WaitingForThirdPartyRoomView: React.FC<Props> = ({ roomView, resize
|
||||||
<LegacyRoomHeader
|
<LegacyRoomHeader
|
||||||
room={context.room}
|
room={context.room}
|
||||||
inRoom={true}
|
inRoom={true}
|
||||||
onSearchClick={null}
|
|
||||||
onInviteClick={null}
|
onInviteClick={null}
|
||||||
onForgetClick={null}
|
onForgetClick={null}
|
||||||
e2eStatus={E2EStatus.Normal}
|
e2eStatus={E2EStatus.Normal}
|
||||||
|
|
|
@ -15,8 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
|
import { KeyBackupInfo, VerificationRequest } from "matrix-js-sdk/src/crypto-api";
|
||||||
import { VerificationRequest } from "matrix-js-sdk/src/crypto-api";
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { SecretStorageKeyDescription } from "matrix-js-sdk/src/secret-storage";
|
import { SecretStorageKeyDescription } from "matrix-js-sdk/src/secret-storage";
|
||||||
|
|
||||||
|
@ -40,7 +39,7 @@ interface IProps {
|
||||||
interface IState {
|
interface IState {
|
||||||
phase?: Phase;
|
phase?: Phase;
|
||||||
verificationRequest: VerificationRequest | null;
|
verificationRequest: VerificationRequest | null;
|
||||||
backupInfo: IKeyBackupInfo | null;
|
backupInfo: KeyBackupInfo | null;
|
||||||
lostKeys: boolean;
|
lostKeys: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { HTMLProps } from "react";
|
import React, { HTMLProps } from "react";
|
||||||
import { Temporal } from "proposal-temporal";
|
import { Temporal } from "temporal-polyfill";
|
||||||
|
|
||||||
import { formatSeconds } from "../../../DateUtils";
|
import { formatSeconds } from "../../../DateUtils";
|
||||||
|
|
||||||
|
@ -45,8 +45,9 @@ export default class Clock extends React.Component<Props> {
|
||||||
return currentFloor !== nextFloor;
|
return currentFloor !== nextFloor;
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateDuration(seconds: number): string {
|
private calculateDuration(seconds: number): string | undefined {
|
||||||
return new Temporal.Duration(0, 0, 0, 0, 0, 0, seconds)
|
if (isNaN(seconds)) return undefined;
|
||||||
|
return new Temporal.Duration(0, 0, 0, 0, 0, 0, Math.round(seconds))
|
||||||
.round({ smallestUnit: "seconds", largestUnit: "hours" })
|
.round({ smallestUnit: "seconds", largestUnit: "hours" })
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { ComponentProps, ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { Playback, PlaybackState } from "../../../audio/Playback";
|
import { Playback, PlaybackState } from "../../../audio/Playback";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton, { ButtonProps } from "../elements/AccessibleButton";
|
||||||
|
|
||||||
type Props = Omit<ComponentProps<typeof AccessibleButton>, "title" | "onClick" | "disabled" | "element" | "ref"> & {
|
type Props = Omit<ButtonProps<"div">, "title" | "onClick" | "disabled" | "element" | "ref"> & {
|
||||||
// Playback instance to manipulate. Cannot change during the component lifecycle.
|
// Playback instance to manipulate. Cannot change during the component lifecycle.
|
||||||
playback: Playback;
|
playback: Playback;
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ import React from "react";
|
||||||
|
|
||||||
import { Icon as ContextMenuIcon } from "../../../../res/img/element-icons/context-menu.svg";
|
import { Icon as ContextMenuIcon } from "../../../../res/img/element-icons/context-menu.svg";
|
||||||
import { ChevronFace, ContextMenuButton, MenuProps, useContextMenu } from "../../structures/ContextMenu";
|
import { ChevronFace, ContextMenuButton, MenuProps, useContextMenu } from "../../structures/ContextMenu";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import { ButtonProps } from "../elements/AccessibleButton";
|
||||||
import IconizedContextMenu, { IconizedContextMenuOptionList } from "./IconizedContextMenu";
|
import IconizedContextMenu, { IconizedContextMenuOptionList } from "./IconizedContextMenu";
|
||||||
|
|
||||||
const contextMenuBelow = (elementRect: DOMRect): MenuProps => {
|
const contextMenuBelow = (elementRect: DOMRect): MenuProps => {
|
||||||
|
@ -29,10 +29,10 @@ const contextMenuBelow = (elementRect: DOMRect): MenuProps => {
|
||||||
return { left, top, chevronFace };
|
return { left, top, chevronFace };
|
||||||
};
|
};
|
||||||
|
|
||||||
interface KebabContextMenuProps extends Partial<React.ComponentProps<typeof AccessibleButton>> {
|
type KebabContextMenuProps = Partial<ButtonProps<any>> & {
|
||||||
options: React.ReactNode[];
|
options: React.ReactNode[];
|
||||||
title: string;
|
title: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const KebabContextMenu: React.FC<KebabContextMenuProps> = ({ options, title, ...props }) => {
|
export const KebabContextMenu: React.FC<KebabContextMenuProps> = ({ options, title, ...props }) => {
|
||||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
||||||
|
|
|
@ -128,7 +128,8 @@ export default class BaseDialog extends React.Component<IProps> {
|
||||||
onClick={this.onCancelClick}
|
onClick={this.onCancelClick}
|
||||||
className="mx_Dialog_cancelButton"
|
className="mx_Dialog_cancelButton"
|
||||||
aria-label={_t("dialog_close_label")}
|
aria-label={_t("dialog_close_label")}
|
||||||
title={_t("dialog_close_label")}
|
title={_t("action|close")}
|
||||||
|
placement="bottom"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,9 @@ limitations under the License.
|
||||||
|
|
||||||
import React, { ChangeEvent } from "react";
|
import React, { ChangeEvent } from "react";
|
||||||
import { MatrixClient, MatrixError, SecretStorage } from "matrix-js-sdk/src/matrix";
|
import { MatrixClient, MatrixError, SecretStorage } from "matrix-js-sdk/src/matrix";
|
||||||
import { IKeyBackupInfo, IKeyBackupRestoreResult } from "matrix-js-sdk/src/crypto/keybackup";
|
import { IKeyBackupRestoreResult } from "matrix-js-sdk/src/crypto/keybackup";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||||
|
|
||||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||||
import { _t } from "../../../../languageHandler";
|
import { _t } from "../../../../languageHandler";
|
||||||
|
@ -51,7 +52,7 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
backupInfo: IKeyBackupInfo | null;
|
backupInfo: KeyBackupInfo | null;
|
||||||
backupKeyStored: Record<string, SecretStorage.SecretStorageKeyDescription> | null;
|
backupKeyStored: Record<string, SecretStorage.SecretStorageKeyDescription> | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
loadError: boolean | null;
|
loadError: boolean | null;
|
||||||
|
@ -246,7 +247,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent<IProps,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async restoreWithCachedKey(backupInfo: IKeyBackupInfo | null): Promise<boolean> {
|
private async restoreWithCachedKey(backupInfo: KeyBackupInfo | null): Promise<boolean> {
|
||||||
if (!backupInfo) return false;
|
if (!backupInfo) return false;
|
||||||
try {
|
try {
|
||||||
const recoverInfo = await MatrixClientPeg.safeGet().restoreKeyBackupWithCache(
|
const recoverInfo = await MatrixClientPeg.safeGet().restoreKeyBackupWithCache(
|
||||||
|
|
|
@ -15,18 +15,23 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import React, { ComponentProps, ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
|
|
||||||
import { useRovingTabIndex } from "../../../../accessibility/RovingTabIndex";
|
import { useRovingTabIndex } from "../../../../accessibility/RovingTabIndex";
|
||||||
import AccessibleButton from "../../elements/AccessibleButton";
|
import AccessibleButton, { ButtonProps } from "../../elements/AccessibleButton";
|
||||||
import { Ref } from "../../../../accessibility/roving/types";
|
import { Ref } from "../../../../accessibility/roving/types";
|
||||||
|
|
||||||
interface TooltipOptionProps extends ComponentProps<typeof AccessibleButton> {
|
type TooltipOptionProps<T extends keyof JSX.IntrinsicElements> = ButtonProps<T> & {
|
||||||
endAdornment?: ReactNode;
|
endAdornment?: ReactNode;
|
||||||
inputRef?: Ref;
|
inputRef?: Ref;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const TooltipOption: React.FC<TooltipOptionProps> = ({ inputRef, className, ...props }) => {
|
export const TooltipOption = <T extends keyof JSX.IntrinsicElements>({
|
||||||
|
inputRef,
|
||||||
|
className,
|
||||||
|
element,
|
||||||
|
...props
|
||||||
|
}: TooltipOptionProps<T>): JSX.Element => {
|
||||||
const [onFocus, isActive, ref] = useRovingTabIndex(inputRef);
|
const [onFocus, isActive, ref] = useRovingTabIndex(inputRef);
|
||||||
return (
|
return (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
|
@ -37,6 +42,7 @@ export const TooltipOption: React.FC<TooltipOptionProps> = ({ inputRef, classNam
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
aria-selected={isActive}
|
aria-selected={isActive}
|
||||||
role="option"
|
role="option"
|
||||||
|
element={element as keyof JSX.IntrinsicElements}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -113,6 +113,8 @@ type Props<T extends keyof JSX.IntrinsicElements> = DynamicHtmlElementProps<T> &
|
||||||
disableTooltip?: TooltipProps["disabled"];
|
disableTooltip?: TooltipProps["disabled"];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ButtonProps<T extends keyof JSX.IntrinsicElements> = Props<T>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type of the props passed to the element that is rendered by AccessibleButton.
|
* Type of the props passed to the element that is rendered by AccessibleButton.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -37,7 +37,7 @@ interface IProps {
|
||||||
/**
|
/**
|
||||||
* classnames to apply to the wrapper of the preview
|
* classnames to apply to the wrapper of the preview
|
||||||
*/
|
*/
|
||||||
className: string;
|
className?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ID of the displayed user
|
* The ID of the displayed user
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { AvatarStack, Tooltip } from "@vector-im/compound-web";
|
||||||
import MemberAvatar from "../avatars/MemberAvatar";
|
import MemberAvatar from "../avatars/MemberAvatar";
|
||||||
import AccessibleButton, { ButtonEvent } from "./AccessibleButton";
|
import AccessibleButton, { ButtonEvent } from "./AccessibleButton";
|
||||||
|
|
||||||
interface IProps extends HTMLAttributes<HTMLSpanElement> {
|
interface IProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
|
||||||
members: RoomMember[];
|
members: RoomMember[];
|
||||||
size: string;
|
size: string;
|
||||||
overflow: boolean;
|
overflow: boolean;
|
||||||
|
@ -32,6 +32,11 @@ interface IProps extends HTMLAttributes<HTMLSpanElement> {
|
||||||
onClick?: (e: ButtonEvent) => void | Promise<void>;
|
onClick?: (e: ButtonEvent) => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A component which displays a list of avatars in a row, with a tooltip showing the names of the users.
|
||||||
|
*
|
||||||
|
* Any additional props, not named explicitly here, are passed to the underlying {@link AccessibleButton}.
|
||||||
|
*/
|
||||||
const FacePile: FC<IProps> = ({
|
const FacePile: FC<IProps> = ({
|
||||||
members,
|
members,
|
||||||
size,
|
size,
|
||||||
|
@ -40,6 +45,7 @@ const FacePile: FC<IProps> = ({
|
||||||
tooltipShortcut,
|
tooltipShortcut,
|
||||||
children,
|
children,
|
||||||
viewUserOnClick = true,
|
viewUserOnClick = true,
|
||||||
|
onClick,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const faces = members.map(
|
const faces = members.map(
|
||||||
|
@ -47,12 +53,7 @@ const FacePile: FC<IProps> = ({
|
||||||
? (m) => <MemberAvatar key={m.userId} member={m} size={size} hideTitle />
|
? (m) => <MemberAvatar key={m.userId} member={m} size={size} hideTitle />
|
||||||
: (m) => (
|
: (m) => (
|
||||||
<Tooltip key={m.userId} label={m.name} caption={tooltipShortcut}>
|
<Tooltip key={m.userId} label={m.name} caption={tooltipShortcut}>
|
||||||
<MemberAvatar
|
<MemberAvatar member={m} size={size} viewUserOnClick={!onClick && viewUserOnClick} hideTitle />
|
||||||
member={m}
|
|
||||||
size={size}
|
|
||||||
viewUserOnClick={!props.onClick && viewUserOnClick}
|
|
||||||
hideTitle
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -65,7 +66,7 @@ const FacePile: FC<IProps> = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<AccessibleButton className="mx_FacePile" onClick={props.onClick ?? null}>
|
<AccessibleButton {...props} className="mx_FacePile" onClick={onClick ?? null}>
|
||||||
<AvatarStack>{pileContents}</AvatarStack>
|
<AvatarStack>{pileContents}</AvatarStack>
|
||||||
{children}
|
{children}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
|
|
@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { ComponentProps } from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import InfoDialog from "../dialogs/InfoDialog";
|
import InfoDialog from "../dialogs/InfoDialog";
|
||||||
import AccessibleButton from "./AccessibleButton";
|
import AccessibleButton, { ButtonProps } from "./AccessibleButton";
|
||||||
|
|
||||||
type Props = Omit<ComponentProps<typeof AccessibleButton>, "kind" | "onClick" | "className"> & {
|
type Props = Omit<ButtonProps<"div">, "element" | "kind" | "onClick" | "className"> & {
|
||||||
title: string;
|
title: string;
|
||||||
description: string | React.ReactNode;
|
description: string | React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
|
@ -33,9 +33,10 @@ export enum WarningKind {
|
||||||
interface IProps {
|
interface IProps {
|
||||||
isRoomEncrypted?: boolean;
|
isRoomEncrypted?: boolean;
|
||||||
kind: WarningKind;
|
kind: WarningKind;
|
||||||
|
showLogo?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SearchWarning({ isRoomEncrypted, kind }: IProps): JSX.Element {
|
export default function SearchWarning({ isRoomEncrypted, kind, showLogo = true }: IProps): JSX.Element {
|
||||||
if (!isRoomEncrypted) return <></>;
|
if (!isRoomEncrypted) return <></>;
|
||||||
if (EventIndexPeg.get()) return <></>;
|
if (EventIndexPeg.get()) return <></>;
|
||||||
|
|
||||||
|
@ -121,7 +122,7 @@ export default function SearchWarning({ isRoomEncrypted, kind }: IProps): JSX.El
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_SearchWarning">
|
<div className="mx_SearchWarning">
|
||||||
{logo}
|
{showLogo ? logo : null}
|
||||||
<span>{text}</span>
|
<span>{text}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -16,8 +16,8 @@ limitations under the License.
|
||||||
|
|
||||||
import React, { forwardRef, useContext } from "react";
|
import React, { forwardRef, useContext } from "react";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
import { IRoomEncryption } from "matrix-js-sdk/src/crypto/RoomList";
|
|
||||||
|
|
||||||
|
import type { RoomEncryptionEventContent } from "matrix-js-sdk/src/types";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import EventTileBubble from "./EventTileBubble";
|
import EventTileBubble from "./EventTileBubble";
|
||||||
|
@ -25,30 +25,29 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||||
import { objectHasDiff } from "../../../utils/objects";
|
import { objectHasDiff } from "../../../utils/objects";
|
||||||
import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom";
|
import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom";
|
||||||
|
import { MEGOLM_ENCRYPTION_ALGORITHM } from "../../../utils/crypto";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
timestamp?: JSX.Element;
|
timestamp?: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ALGORITHM = "m.megolm.v1.aes-sha2";
|
|
||||||
|
|
||||||
const EncryptionEvent = forwardRef<HTMLDivElement, IProps>(({ mxEvent, timestamp }, ref) => {
|
const EncryptionEvent = forwardRef<HTMLDivElement, IProps>(({ mxEvent, timestamp }, ref) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
const roomId = mxEvent.getRoomId()!;
|
const roomId = mxEvent.getRoomId()!;
|
||||||
const isRoomEncrypted = MatrixClientPeg.safeGet().isRoomEncrypted(roomId);
|
const isRoomEncrypted = MatrixClientPeg.safeGet().isRoomEncrypted(roomId);
|
||||||
|
|
||||||
const prevContent = mxEvent.getPrevContent() as IRoomEncryption;
|
const prevContent = mxEvent.getPrevContent() as RoomEncryptionEventContent;
|
||||||
const content = mxEvent.getContent<IRoomEncryption>();
|
const content = mxEvent.getContent<RoomEncryptionEventContent>();
|
||||||
|
|
||||||
// if no change happened then skip rendering this, a shallow check is enough as all known fields are top-level.
|
// if no change happened then skip rendering this, a shallow check is enough as all known fields are top-level.
|
||||||
if (!objectHasDiff(prevContent, content)) return null; // nop
|
if (!objectHasDiff(prevContent, content)) return null; // nop
|
||||||
|
|
||||||
if (content.algorithm === ALGORITHM && isRoomEncrypted) {
|
if (content.algorithm === MEGOLM_ENCRYPTION_ALGORITHM && isRoomEncrypted) {
|
||||||
let subtitle: string;
|
let subtitle: string;
|
||||||
const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||||
const room = cli?.getRoom(roomId);
|
const room = cli?.getRoom(roomId);
|
||||||
if (prevContent.algorithm === ALGORITHM) {
|
if (prevContent.algorithm === MEGOLM_ENCRYPTION_ALGORITHM) {
|
||||||
subtitle = _t("timeline|m.room.encryption|parameters_changed");
|
subtitle = _t("timeline|m.room.encryption|parameters_changed");
|
||||||
} else if (dmPartner) {
|
} else if (dmPartner) {
|
||||||
const displayName = room?.getMember(dmPartner)?.rawDisplayName || dmPartner;
|
const displayName = room?.getMember(dmPartner)?.rawDisplayName || dmPartner;
|
||||||
|
|
|
@ -26,8 +26,12 @@ import { CardContext } from "./context";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
header?: ReactNode | null;
|
header?: ReactNode | null;
|
||||||
|
hideHeaderButtons?: boolean;
|
||||||
footer?: ReactNode;
|
footer?: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
id?: string;
|
||||||
|
role?: "tabpanel";
|
||||||
|
ariaLabelledBy?: string;
|
||||||
withoutScrollContainer?: boolean;
|
withoutScrollContainer?: boolean;
|
||||||
closeLabel?: string;
|
closeLabel?: string;
|
||||||
onClose?(ev: ButtonEvent): void;
|
onClose?(ev: ButtonEvent): void;
|
||||||
|
@ -62,6 +66,10 @@ const BaseCard: React.FC<IProps> = forwardRef<HTMLDivElement, IProps>(
|
||||||
onClose,
|
onClose,
|
||||||
onBack,
|
onBack,
|
||||||
className,
|
className,
|
||||||
|
id,
|
||||||
|
ariaLabelledBy,
|
||||||
|
role,
|
||||||
|
hideHeaderButtons,
|
||||||
header,
|
header,
|
||||||
footer,
|
footer,
|
||||||
withoutScrollContainer,
|
withoutScrollContainer,
|
||||||
|
@ -100,13 +108,31 @@ const BaseCard: React.FC<IProps> = forwardRef<HTMLDivElement, IProps>(
|
||||||
children = <AutoHideScrollbar>{children}</AutoHideScrollbar>;
|
children = <AutoHideScrollbar>{children}</AutoHideScrollbar>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
let headerButtons: React.ReactElement | undefined;
|
||||||
<CardContext.Provider value={{ isCard: true }}>
|
if (!hideHeaderButtons) {
|
||||||
<div className={classNames("mx_BaseCard", className)} ref={ref} onKeyDown={onKeyDown}>
|
headerButtons = (
|
||||||
{header !== null && (
|
<>
|
||||||
<div className="mx_BaseCard_header">
|
|
||||||
{backButton}
|
{backButton}
|
||||||
{closeButton}
|
{closeButton}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldRenderHeader = header || !hideHeaderButtons;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardContext.Provider value={{ isCard: true }}>
|
||||||
|
<div
|
||||||
|
id={id}
|
||||||
|
aria-labelledby={ariaLabelledBy}
|
||||||
|
role={role}
|
||||||
|
className={classNames("mx_BaseCard", className)}
|
||||||
|
ref={ref}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
>
|
||||||
|
{shouldRenderHeader && (
|
||||||
|
<div className="mx_BaseCard_header">
|
||||||
|
{headerButtons}
|
||||||
<div className="mx_BaseCard_headerProp">{header}</div>
|
<div className="mx_BaseCard_headerProp">{header}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -214,27 +214,27 @@ export default class LegacyRoomHeaderButtons extends HeaderButtons<IProps> {
|
||||||
const currentPhase = RightPanelStore.instance.currentCard.phase;
|
const currentPhase = RightPanelStore.instance.currentCard.phase;
|
||||||
if (currentPhase && ROOM_INFO_PHASES.includes(currentPhase)) {
|
if (currentPhase && ROOM_INFO_PHASES.includes(currentPhase)) {
|
||||||
if (this.state.phase === currentPhase) {
|
if (this.state.phase === currentPhase) {
|
||||||
RightPanelStore.instance.showOrHidePanel(currentPhase);
|
RightPanelStore.instance.showOrHidePhase(currentPhase);
|
||||||
} else {
|
} else {
|
||||||
RightPanelStore.instance.showOrHidePanel(currentPhase, RightPanelStore.instance.currentCard.state);
|
RightPanelStore.instance.showOrHidePhase(currentPhase, RightPanelStore.instance.currentCard.state);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// This toggles for us, if needed
|
// This toggles for us, if needed
|
||||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.RoomSummary);
|
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomSummary);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onNotificationsClicked = (): void => {
|
private onNotificationsClicked = (): void => {
|
||||||
// This toggles for us, if needed
|
// This toggles for us, if needed
|
||||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.NotificationPanel);
|
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.NotificationPanel);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onPinnedMessagesClicked = (): void => {
|
private onPinnedMessagesClicked = (): void => {
|
||||||
// This toggles for us, if needed
|
// This toggles for us, if needed
|
||||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.PinnedMessages);
|
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.PinnedMessages);
|
||||||
};
|
};
|
||||||
private onTimelineCardClicked = (): void => {
|
private onTimelineCardClicked = (): void => {
|
||||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.Timeline);
|
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.Timeline);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onThreadsPanelClicked = (ev: ButtonEvent): void => {
|
private onThreadsPanelClicked = (ev: ButtonEvent): void => {
|
||||||
|
|