Compare commits

..

18 Commits

Author SHA1 Message Date
Half-Shot
8b17591f83 Revert mistaken s/checkbox/switch/ 2025-08-26 09:15:09 +01:00
Half-Shot
9e3533dfb7 Fix wrong snaps from using wrong compound version 2025-08-26 09:13:13 +01:00
Half-Shot
7154f85682 update widget screenshot 2025-08-26 09:12:57 +01:00
Half-Shot
a8fda2b1a1 Clear up playwright tests 2025-08-26 08:54:18 +01:00
Half-Shot
6ac073f5b0 Update jest tests 2025-08-26 08:33:44 +01:00
Half-Shot
6caf0e21f3 Merge remote-tracking branch 'origin/develop' into hs/update-toggles-to-use-consistent-style-playwright-only 2025-08-26 08:15:49 +01:00
Will Hunt
c8cf16a3a2 Merge branch 'develop' into hs/update-toggles-to-use-consistent-style-playwright-only 2025-07-22 11:20:42 +01:00
Half-Shot
974574159f update snapshot 2025-07-22 11:04:27 +01:00
Half-Shot
c5eb09d96b fixup tests 2025-07-17 13:48:34 +01:00
Half-Shot
bef1a5f902 Fix permissions dialog 2025-07-17 13:47:34 +01:00
Half-Shot
7d121c5bc9 remove extra test 2025-07-17 13:47:25 +01:00
Half-Shot
f7d550c847 Fixup headers 2025-07-17 13:28:31 +01:00
Half-Shot
1cb78d1842 Disable region test 2025-07-17 13:17:20 +01:00
Half-Shot
7ec3b2e732 Fix accessibility for devtools 2025-07-17 13:06:23 +01:00
Will Hunt
dc0a3bc7e8 Merge branch 'develop' into hs/update-toggles-to-use-consistent-style-playwright-only 2025-07-16 08:17:53 +01:00
Will Hunt
eeedd6ddba Update screenshots 2025-07-15 15:10:08 +01:00
Will Hunt
8afa1c1220 import pages/ remove duplicate create-room 2025-07-15 14:11:12 +01:00
Will Hunt
c138f1bec3 Add playwright tests 2025-07-15 13:35:36 +01:00
173 changed files with 3272 additions and 4338 deletions

View File

@@ -2,7 +2,6 @@
## Checklist
- [ ] I have read through [review guidelines](../docs/review.md) and [CONTRIBUTING.md](../CONTRIBUTING.md).
- [ ] Tests written for new code (and old code if feasible).
- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation.
- [ ] Linter and other CI checks pass.

View File

@@ -88,7 +88,7 @@ jobs:
run: mdbook build
- name: Upload artifact
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
with:
path: ./book

View File

@@ -1,28 +1,3 @@
Changes in [1.11.110](https://github.com/element-hq/element-web/releases/tag/v1.11.110) (2025-08-27)
====================================================================================================
## ✨ Features
* Hide recovery key when re-entering it while creating or changing it ([#30499](https://github.com/element-hq/element-web/pull/30499)). Contributed by @andybalaam.
* Add `?no_universal_links=true` to OIDC url so EX doesn't try to handle it ([#29439](https://github.com/element-hq/element-web/pull/29439)). Contributed by @t3chguy.
* Show a blue lock for unencrypted rooms and hide the grey shield for encrypted rooms ([#30440](https://github.com/element-hq/element-web/pull/30440)). Contributed by @langleyd.
* Add support for Module API 1.4 ([#30185](https://github.com/element-hq/element-web/pull/30185)). Contributed by @t3chguy.
* MVVM - Introduce some helpers for snapshot management ([#30398](https://github.com/element-hq/element-web/pull/30398)). Contributed by @MidhunSureshR.
## 🐛 Bug Fixes
* A11y: move focus to right panel when opened ([#30553](https://github.com/element-hq/element-web/pull/30553)). Contributed by @florianduros.
* Fix e2e warning icon should be white ([#30539](https://github.com/element-hq/element-web/pull/30539)). Contributed by @florianduros.
* Remove NoOneHere disabled reason. ([#30524](https://github.com/element-hq/element-web/pull/30524)). Contributed by @toger5.
* Fix downloading files with authenticated media API ([#30520](https://github.com/element-hq/element-web/pull/30520)). Contributed by @t3chguy.
* Fix call permissions check confusion around element call ([#30521](https://github.com/element-hq/element-web/pull/30521)). Contributed by @t3chguy.
* Fix line wrap around emoji verification ([#30523](https://github.com/element-hq/element-web/pull/30523)). Contributed by @t3chguy.
* Don't highlight redacted events ([#30519](https://github.com/element-hq/element-web/pull/30519)). Contributed by @t3chguy.
* Fix matrix.to links not being handled in the app ([#30522](https://github.com/element-hq/element-web/pull/30522)). Contributed by @t3chguy.
* Fix issue of new room list taking up the full width ([#30459](https://github.com/element-hq/element-web/pull/30459)). Contributed by @langleyd.
* Fix widget persistence in React development mode ([#30509](https://github.com/element-hq/element-web/pull/30509)). Contributed by @robintown.
* Fix widget initialization in React development mode ([#30463](https://github.com/element-hq/element-web/pull/30463)). Contributed by @robintown.
Changes in [1.11.109](https://github.com/element-hq/element-web/releases/tag/v1.11.109) (2025-08-11)
====================================================================================================
This release supports the upcoming v12 ("hydra") Matrix room version and is necessary to view and participate in these rooms.

View File

@@ -1,7 +1,7 @@
# syntax=docker.io/docker/dockerfile:1.17-labs@sha256:9187104f31e3a002a8a6a3209ea1f937fb7486c093cbbde1e14b0fa0d7e4f1b5
# Builder
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:f7f28d1962d93cc096ea6327378d990284757fec281ce48e42436e7b4b167fa2 AS builder
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:9e34ba52e1f3c31ed9bd4d0bcf784f5909db17cda61c220e29c8d7a8ebfb402e AS builder
# Support custom branch of the js-sdk. This also helps us build images of element-web develop.
ARG USE_CUSTOM_SDKS=false
@@ -19,7 +19,7 @@ RUN /src/scripts/docker-package.sh
RUN cp /src/config.sample.json /src/webapp/config.json
# App
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:0d019e980f83728002de7a6d8819d0d4af7179046d3946b8b37749953fbb28e6
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:ea6c4b8b568824ea94cd1fabd47e1c4e7c0c04744f344a3793f7e9c8ac3a3636
# Need root user to install packages & manipulate the usr directory
USER root

View File

@@ -14,9 +14,10 @@ entrypoint_log() {
mkdir -p /tmp/element-web-config
cp /app/config*.json /tmp/element-web-config/
# If the module directory exists AND the module directory has modules in it
if [ -d "/modules" ] && [ "$( ls -A '/modules' )" ]; then
# If there are modules to be loaded
if [ -d "/modules" ]; then
cd /modules
for MODULE in *
do
# If the module has a package.json, use its main field as the entrypoint

View File

@@ -38,20 +38,45 @@ When `force_disable` is true:
Note: If the server is configured to forcibly enable encryption for some or all rooms,
this behaviour will be overridden.
# Setting up recovery
# Secure backup
By default, Element strongly encourages (but does not require) users to set up
recovery so that you can access history on your new devices as well as retain access to your message history and cryptographic identity when you lose all of your devices.
Secure Backup so that cross-signing identity key and message keys can be
recovered in case of a disaster where you lose access to all active devices.
## Removal of old settings
## Requiring secure backup
Support for the configuration options `secure_backup_required` and `secure_backup_setup_methods`
in the `/.well-known/matrix/client` config has been removed.
To require Secure Backup to be configured before Element can be used, set the
following on your homeserver's `/.well-known/matrix/client` config:
Setting up recovery is now always recommended to all users by showing a one-off toast and a
permanent red dot on the _Encryption_ tab in the _Settings_ dialog. When creating a new
recovery key, the UI only supports auto-generated keys. Using an existing (custom) passphrase
still works, but is not exposed in the UI when setting up recovery.
```json
{
"io.element.e2ee": {
"secure_backup_required": true
}
}
```
## Preferring setup methods
By default, Element offers users a choice of a random key or user-chosen
passphrase when setting up Secure Backup. If a homeserver admin would like to
only offer one of these, you can signal this via the
`/.well-known/matrix/client` config, for example:
```json
{
"io.element.e2ee": {
"secure_backup_setup_methods": ["passphrase"]
}
}
```
The field `secure_backup_setup_methods` is an array listing the methods the
client should display. Supported values currently include `key` and
`passphrase`. If the `secure_backup_setup_methods` field is not present or
exists but does not contain any supported methods, Element will fallback to the
default value of: `["key", "passphrase"]`.
# Compatibility

11
knip.ts
View File

@@ -2,6 +2,7 @@ import { KnipConfig } from "knip";
export default {
entry: [
"src/vector/index.ts",
"src/serviceworker/index.ts",
"src/workers/*.worker.ts",
"src/utils/exportUtils/exportJS.js",
@@ -11,6 +12,8 @@ export default {
"res/decoder-ring/**",
"res/jitsi_external_api.min.js",
"docs/**",
// Used by jest
"__mocks__/maplibre-gl.js",
],
project: ["**/*.{js,ts,jsx,tsx}"],
ignore: [
@@ -39,18 +42,10 @@ export default {
"util",
// Embedded into webapp
"@element-hq/element-call-embedded",
// Used by matrix-js-sdk, which means we have to include them as a
// dependency so that // we can run `tsc` (since we import the typescript
// source of js-sdk, rather than the transpiled and annotated JS like you
// would with a normal library).
"@types/content-type",
"@types/sdp-transform",
],
ignoreBinaries: [
// Used in scripts & workflows
"jq",
"wait-on",
],
ignoreExportsUsedInFile: true,
} satisfies KnipConfig;

View File

@@ -1,6 +1,6 @@
{
"name": "element-web",
"version": "1.11.110",
"version": "1.11.109",
"description": "Element: the future of secure communication",
"author": "New Vector Ltd.",
"repository": {
@@ -75,8 +75,8 @@
"resolutions": {
"**/pretty-format/react-is": "19.1.1",
"@playwright/test": "1.54.2",
"@types/react": "19.1.12",
"@types/react-dom": "19.1.9",
"@types/react": "19.1.10",
"@types/react-dom": "19.1.7",
"oidc-client-ts": "3.3.0",
"jwt-decode": "4.0.0",
"caniuse-lite": "1.0.30001724",
@@ -142,7 +142,7 @@
"opus-recorder": "^8.0.3",
"pako": "^2.0.3",
"png-chunks-extract": "^1.0.0",
"posthog-js": "1.261.0",
"posthog-js": "1.260.1",
"qrcode": "1.5.4",
"re-resizable": "6.11.2",
"react": "^19.0.0",
@@ -158,7 +158,7 @@
"sanitize-html": "2.17.0",
"tar-js": "^0.3.0",
"temporal-polyfill": "^0.3.0",
"ua-parser-js": "1.0.40",
"ua-parser-js": "^1.0.2",
"uuid": "^11.0.0",
"what-input": "^5.2.10"
},
@@ -184,7 +184,7 @@
"@babel/preset-typescript": "^7.12.7",
"@babel/runtime": "^7.12.5",
"@casualbot/jest-sonar-reporter": "2.2.7",
"@element-hq/element-call-embedded": "0.15.0",
"@element-hq/element-call-embedded": "0.14.1",
"@element-hq/element-web-playwright-common": "^1.4.6",
"@peculiar/webcrypto": "^1.4.3",
"@playwright/test": "^1.50.1",
@@ -203,7 +203,6 @@
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2",
"@types/commonmark": "^0.27.4",
"@types/content-type": "^1.1.9",
"@types/counterpart": "^0.18.1",
"@types/css-tree": "^2.3.8",
"@types/diff-match-patch": "^1.0.32",
@@ -222,12 +221,11 @@
"@types/node-fetch": "^2.6.2",
"@types/pako": "^2.0.0",
"@types/qrcode": "^1.3.5",
"@types/react": "19.1.12",
"@types/react": "19.1.10",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/react-dom": "19.1.9",
"@types/react-dom": "19.1.7",
"@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "2.16.0",
"@types/sdp-transform": "^2.4.10",
"@types/semver": "^7.5.8",
"@types/tar-js": "^0.3.5",
"@types/ua-parser-js": "^0.7.36",

View File

@@ -126,7 +126,7 @@ test.describe("'Turn on key storage' toast", () => {
await toast.getByRole("button", { name: "Continue" }).click();
// Then we see the Encryption settings dialog with an option to turn on key storage
await expect(page.getByRole("switch", { name: "Allow key storage" })).toBeVisible();
await expect(page.getByRole("checkbox", { name: "Allow key storage" })).toBeVisible();
// And when we close that
await page.getByRole("button", { name: "Close dialog" }).click();
@@ -153,7 +153,7 @@ test.describe("'Turn on key storage' toast", () => {
await page.getByRole("button", { name: "Go to Settings" }).click();
// Then we see Encryption settings again
await expect(page.getByRole("switch", { name: "Allow key storage" })).toBeVisible();
await expect(page.getByRole("checkbox", { name: "Allow key storage" })).toBeVisible();
// And when we close that, see the toast, click Dismiss, and Yes, Dismiss
await page.getByRole("button", { name: "Close dialog" }).click();

View File

@@ -300,9 +300,9 @@ export async function doTwoWaySasVerification(page: Page, verifier: JSHandle<Ver
export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
const encryptionTab = await app.settings.openUserSettings("Encryption");
const keyStorageToggle = encryptionTab.getByRole("switch", { name: "Allow key storage" });
const keyStorageToggle = encryptionTab.getByRole("checkbox", { name: "Allow key storage" });
if (!(await keyStorageToggle.isChecked())) {
await encryptionTab.getByRole("switch", { name: "Allow key storage" }).click();
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).click();
}
await encryptionTab.getByRole("button", { name: "Set up recovery" }).click();
@@ -323,11 +323,11 @@ export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
export async function disableKeyBackup(app: ElementAppPage): Promise<void> {
const encryptionTab = await app.settings.openUserSettings("Encryption");
const keyStorageToggle = encryptionTab.getByRole("switch", { name: "Allow key storage" });
const keyStorageToggle = encryptionTab.getByRole("checkbox", { name: "Allow key storage" });
if (await keyStorageToggle.isChecked()) {
await encryptionTab.getByRole("switch", { name: "Allow key storage" }).click();
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).click();
await encryptionTab.getByRole("button", { name: "Delete key storage" }).click();
await encryptionTab.getByRole("switch", { name: "Allow key storage" }).isVisible();
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).isVisible();
// Wait for the update to account data to stick
await new Promise((resolve) => setTimeout(resolve, 2000));

View File

@@ -193,9 +193,6 @@ test.describe("Room list", () => {
await roomListView.getByRole("option", { name: "Open room room20" }).click();
// Make sure the room with the unread is visible before we press the keyboard action to select it
await expect(roomListView.getByRole("option", { name: "1 notification" })).toBeVisible();
await page.keyboard.press("Alt+Shift+ArrowDown");
await expect(page.getByRole("heading", { name: "1 notification", level: 1 })).toBeVisible();

View File

@@ -100,51 +100,3 @@ test.describe("permalinks", () => {
});
});
});
test.describe("triple-click message selection", () => {
test.use({
displayName: "Alice",
});
test("should select entire message line when triple-clicking on message with pills", async ({
page,
app,
user,
bot,
}) => {
await bot.prepareClient();
const roomId = await app.client.createRoom({ name: "Test Room" });
await app.client.inviteUser(roomId, bot.credentials.userId);
await app.viewRoomByName("Test Room");
// Send a message with user and room pills
await app.client.sendMessage(
roomId,
`Testing triple-click message selection. ` +
`User: ${permalinkPrefix}${bot.credentials.userId}, ` +
`Room: ${permalinkPrefix}${roomId}, ` +
`Message: ${permalinkPrefix}${roomId}/$dummy-event, ` +
`and @room mention.`,
);
const timeline = page.locator(".mx_RoomView_timeline");
const messageTile = timeline.locator(".mx_EventTile").last();
// Triple-click on the message body to select its entire content
const messageBody = messageTile.locator(".mx_EventTile_body");
await messageBody.click({ clickCount: 3 });
// Get the expected text content of the message, including pills
const expectedText = await messageBody.innerText();
// Get the currently selected text from the page
const selectedText = await page.evaluate(() => {
const selection = window.getSelection();
return selection ? selection.toString().trim() : "";
});
// Verify that the selected text exactly matches the message content
expect(selectedText).toBe(expectedText);
});
});

View File

@@ -30,8 +30,9 @@ export class Helpers {
/**
* Get the release announcement with the given name.
* @param name
* @private
*/
public getReleaseAnnouncement(name: string) {
private getReleaseAnnouncement(name: string) {
return this.page.getByRole("dialog", { name });
}
@@ -54,6 +55,16 @@ export class Helpers {
assertReleaseAnnouncementIsNotVisible(name: string) {
return expect(this.getReleaseAnnouncement(name)).not.toBeVisible();
}
/**
* Mark the release announcement with the given name as read.
* If the release announcement is not visible, this will throw an error.
* @param name
*/
async markReleaseAnnouncementAsRead(name: string) {
const dialog = this.getReleaseAnnouncement(name);
await dialog.getByRole("button", { name: "Ok" }).click();
}
}
export { expect };

View File

@@ -22,25 +22,25 @@ test.describe("Release announcement", () => {
await app.viewRoomById(roomId);
await use({ roomId });
},
labsFlags: ["feature_new_room_list"],
});
test(
"should display the new room list release announcement",
"should display the pinned messages release announcement",
{ tag: "@screenshot" },
async ({ page, app, room, util }) => {
const name = "Chats has a new look!";
await app.toggleRoomInfoPanel();
const name = "All new pinned messages";
// The release announcement should be displayed
await util.assertReleaseAnnouncementIsVisible(name);
// Hide the release announcement
const dialog = util.getReleaseAnnouncement(name);
await dialog.getByRole("button", { name: "Next" }).click();
await util.markReleaseAnnouncementAsRead(name);
await util.assertReleaseAnnouncementIsNotVisible(name);
await page.reload();
await expect(page.getByRole("button", { name: "Room options" })).toBeVisible();
await app.toggleRoomInfoPanel();
await expect(page.getByRole("menuitem", { name: "Pinned messages" })).toBeVisible();
// Check that once the release announcement has been marked as viewed, it does not appear again
await util.assertReleaseAnnouncementIsNotVisible(name);
},

View File

@@ -85,7 +85,7 @@ class Helpers {
* Return the system theme toggle
*/
getMatchSystemThemeCheckbox() {
return this.getThemePanel().getByRole("switch", { name: "Match system theme" });
return this.getThemePanel().getByRole("checkbox", { name: "Match system theme" });
}
/**
@@ -219,7 +219,7 @@ class Helpers {
* Return the compact layout checkbox
*/
getCompactLayoutCheckbox() {
return this.getMessageLayoutPanel().getByRole("switch", { name: "Show compact text and messages" });
return this.getMessageLayoutPanel().getByRole("checkbox", { name: "Show compact text and messages" });
}
/**

View File

@@ -117,7 +117,7 @@ test.describe("Encryption tab", () => {
await verifySession(app, recoveryKey.encodedPrivateKey);
await util.openEncryptionTab();
await page.getByRole("switch", { name: "Allow key storage" }).click();
await page.getByRole("checkbox", { name: "Allow key storage" }).click();
await expect(
page.getByRole("heading", { name: "Are you sure you want to turn off key storage and delete it?" }),
@@ -136,7 +136,7 @@ test.describe("Encryption tab", () => {
await page.getByRole("button", { name: "Delete key storage" }).click();
await expect(page.getByRole("switch", { name: "Allow key storage" })).not.toBeChecked();
await expect(page.getByRole("checkbox", { name: "Allow key storage" })).not.toBeChecked();
for (const prom of deleteRequestPromises) {
const request = await prom;

View File

@@ -104,10 +104,7 @@ class Helpers {
const clipboardContent = await this.app.getClipboard();
await dialog.getByRole("textbox").fill(clipboardContent);
const button = dialog.getByRole("button", { name: confirmButtonLabel });
await button.click();
// Button should disable immediately after clicking.
await expect(button).toBeDisabled();
await dialog.getByRole("button", { name: confirmButtonLabel }).click();
await expect(this.getEncryptionRecoverySection()).toMatchScreenshot("default-recovery.png");
}
}

View File

@@ -908,37 +908,23 @@ test.describe("Timeline", () => {
});
});
test(
"should be able to hide an image",
{ tag: "@screenshot" },
async ({ page, app, homeserver, room, context }) => {
await app.viewRoomById(room.roomId);
const bot = new Bot(page, homeserver, {});
await bot.prepareClient();
await app.client.inviteUser(room.roomId, bot.credentials.userId);
await sendImage(bot, room.roomId, NEW_AVATAR);
await app.timeline.scrollToBottom();
const imgTile = page.locator(".mx_MImageBody").first();
await expect(imgTile).toBeVisible();
await imgTile.hover();
await page.getByRole("button", { name: "Hide" }).click();
// Check that the image is now hidden.
await expect(page.getByRole("button", { name: "Show image" })).toBeVisible();
},
);
test("should be able to hide a video", async ({ page, app, homeserver, room, context }) => {
test("should be able to hide an image", { tag: "@screenshot" }, async ({ page, app, room, context }) => {
await app.viewRoomById(room.roomId);
await sendImage(app.client, room.roomId, NEW_AVATAR);
await app.timeline.scrollToBottom();
const imgTile = page.locator(".mx_MImageBody").first();
await expect(imgTile).toBeVisible();
await imgTile.hover();
await page.getByRole("button", { name: "Hide" }).click();
const bot = new Bot(page, homeserver, {});
await bot.prepareClient();
await app.client.inviteUser(room.roomId, bot.credentials.userId);
// Check that the image is now hidden.
await expect(page.getByRole("button", { name: "Show image" })).toBeVisible();
});
const upload = await bot.uploadContent(VIDEO_FILE, { name: "bbb.webm", type: "video/webm" });
await bot.sendEvent(room.roomId, null, "m.room.message" as EventType, {
test("should be able to hide a video", async ({ page, app, room, context }) => {
await app.viewRoomById(room.roomId);
const upload = await app.client.uploadContent(VIDEO_FILE, { name: "bbb.webm", type: "video/webm" });
await app.client.sendEvent(room.roomId, null, "m.room.message" as EventType, {
msgtype: "m.video" as MsgType,
body: "bbb.webm",
url: upload.content_uri,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -10,7 +10,7 @@ import {
type StartedPostgreSqlContainer,
} from "@element-hq/element-web-playwright-common/lib/testcontainers";
const TAG = "main@sha256:64b638f2c0ddd7aa0ddcbc39d21cdf3cedab91508b5d7953e2e85c9901ac5b26";
const TAG = "main@sha256:430b1f00e74c3f89f078670f676b4333f6bbe5a339962344b3ae84e99e9bcd7f";
/**
* MatrixAuthenticationServiceContainer which freezes the docker digest to

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
const TAG = "develop@sha256:fa82823795aab3ff29243e016a04e821e80f64e7b7257ed29a323b73525d3c0d";
const TAG = "develop@sha256:18e9e77eac01709e9ab4d26cf20c36bf5a1567756bb5a78c00cabf366d65a950";
/**
* SynapseContainer which freezes the docker digest to stabilise tests,

View File

@@ -11,7 +11,8 @@ Please see LICENSE files in the repository root for full details.
line-height: $font-17px;
border-radius: $font-16px;
vertical-align: text-top;
display: inline-block;
display: inline-flex;
align-items: center;
box-sizing: border-box;
max-width: 100%;
overflow: hidden;
@@ -56,8 +57,6 @@ Please see LICENSE files in the repository root for full details.
margin-inline-start: -0.3em; /* Otherwise the gap is too large */
margin-inline-end: 0.2em;
min-width: $font-16px; /* ensure the avatar is not compressed */
user-select: text;
vertical-align: -2.5px;
}
.mx_Pill_text {

View File

@@ -13,7 +13,7 @@
padding: var(--cpd-space-1x) var(--cpd-space-3x) var(--cpd-space-1x) var(--cpd-space-1x);
font: var(--cpd-font-body-xs-medium);
background-color: var(--cpd-color-bg-subtle-secondary);
background-color: var(--cpd-color-alpha-gray-200);
color: var(--cpd-color-text-secondary);
border-radius: 99px;

View File

@@ -32,8 +32,4 @@
transform: rotate(180deg);
}
}
.mx_RoomListHeaderView_ReleaseAnnouncementAnchor {
display: inline-flex;
}
}

View File

@@ -71,13 +71,6 @@ type ElectronChannel =
| "serverSupportedVersions";
declare global {
// use `number` as the return type in all cases for globalThis.set{Interval,Timeout},
// so we don't accidentally use the methods on NodeJS.Timeout - they only exist in a subset of environments.
// The overload for clear{Interval,Timeout} is resolved as expected.
// We use `ReturnType<typeof setTimeout>` in the code to be agnostic of if this definition gets loaded.
function setInterval(handler: TimerHandler, timeout: number, ...arguments: any[]): number;
function setTimeout(handler: TimerHandler, timeout: number, ...arguments: any[]): number;
interface Window {
mxSendRageshake: (text: string, withLogs?: boolean) => void;
matrixLogger: typeof logger;

View File

@@ -112,7 +112,6 @@ export enum LegacyCallHandlerEvent {
CallsChanged = "calls_changed",
CallChangeRoom = "call_change_room",
SilencedCallsChanged = "silenced_calls_changed",
ShownSidebarsChanged = "shown_sidebars_changed",
CallState = "call_state",
ProtocolSupport = "protocol_support",
}
@@ -121,7 +120,6 @@ type EventEmitterMap = {
[LegacyCallHandlerEvent.CallsChanged]: (calls: Map<string, MatrixCall>) => void;
[LegacyCallHandlerEvent.CallChangeRoom]: (call: MatrixCall) => void;
[LegacyCallHandlerEvent.SilencedCallsChanged]: (calls: Set<string>) => void;
[LegacyCallHandlerEvent.ShownSidebarsChanged]: (sidebarsShown: Map<string, boolean>) => void;
[LegacyCallHandlerEvent.CallState]: (mappedRoomId: string | null, status: CallState) => void;
[LegacyCallHandlerEvent.ProtocolSupport]: () => void;
};
@@ -146,8 +144,6 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
private silencedCalls = new Set<string>(); // callIds
private shownSidebars = new Map<string, boolean>(); // callId (call) -> sidebar show
private backgroundAudio = new BackgroundAudio();
private playingSources: Record<string, AudioBufferSourceNode> = {}; // Record them for stopping
@@ -244,15 +240,6 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
return false;
}
public setCallSidebarShown(callId: string, sidebarShown: boolean): void {
this.shownSidebars.set(callId, sidebarShown);
this.emit(LegacyCallHandlerEvent.ShownSidebarsChanged, this.shownSidebars);
}
public isCallSidebarShown(callId?: string): boolean {
return !!callId && (this.shownSidebars.get(callId) ?? true);
}
private async checkProtocols(maxTries: number): Promise<void> {
try {
const protocols = await MatrixClientPeg.safeGet().getThirdpartyProtocols();

View File

@@ -6,8 +6,6 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
type CacheResult = { roomId: string; viaServers: string[] };
/**
* This is meant to be a cache of room alias to room ID so that moving between
* rooms happens smoothly (for example using browser back / forward buttons).
@@ -18,12 +16,12 @@ type CacheResult = { roomId: string; viaServers: string[] };
* A similar thing could also be achieved via `pushState` with a state object,
* but keeping it separate like this seems easier in case we do want to extend.
*/
const cache = new Map<string, CacheResult>();
const aliasToIDMap = new Map<string, string>();
export function storeRoomAliasInCache(alias: string, roomId: string, viaServers: string[]): void {
cache.set(alias, { roomId, viaServers });
export function storeRoomAliasInCache(alias: string, id: string): void {
aliasToIDMap.set(alias, id);
}
export function getCachedRoomIdForAlias(alias: string): CacheResult | undefined {
return cache.get(alias);
export function getCachedRoomIDForAlias(alias: string): string | undefined {
return aliasToIDMap.get(alias);
}

View File

@@ -51,7 +51,7 @@ import ThemeController from "../../settings/controllers/ThemeController";
import { startAnyRegistrationFlow } from "../../Registration";
import ResizeNotifier from "../../utils/ResizeNotifier";
import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils";
import { calculateRoomVia, makeRoomPermalink } from "../../utils/permalinks/Permalinks";
import { makeRoomPermalink } from "../../utils/permalinks/Permalinks";
import ThemeWatcher, { ThemeWatcherEvent } from "../../settings/watchers/ThemeWatcher";
import { FontWatcher } from "../../settings/watchers/FontWatcher";
import { storeRoomAliasInCache } from "../../RoomAliasCache";
@@ -238,8 +238,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private readonly stores: SdkContextClass;
private loadSessionAbortController = new AbortController();
private sessionLoadStarted = false;
public constructor(props: IProps) {
super(props);
this.stores = SdkContextClass.instance;
@@ -472,20 +470,15 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.fontWatcher.start();
initSentry(SdkConfig.get("sentry"));
window.addEventListener("resize", this.onWindowResized);
// Once we start loading the MatrixClient, we can't stop, even if MatrixChat gets unmounted (as it does
// in React's Strict Mode). So, start loading the session now, but only if this MatrixChat was not previously
// mounted.
if (!this.sessionLoadStarted) {
this.sessionLoadStarted = true;
if (!checkSessionLockFree()) {
// another instance holds the lock; confirm its theft before proceeding
setTimeout(() => this.setState({ view: Views.CONFIRM_LOCK_THEFT }), 0);
} else {
this.startInitSession();
}
if (!checkSessionLockFree()) {
// another instance holds the lock; confirm its theft before proceeding
setTimeout(() => this.setState({ view: Views.CONFIRM_LOCK_THEFT }), 0);
} else {
this.startInitSession();
}
window.addEventListener("resize", this.onWindowResized);
}
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
@@ -1026,7 +1019,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
presentedId = theAlias;
// Store display alias of the presented room in cache to speed future
// navigation.
storeRoomAliasInCache(theAlias, room.roomId, calculateRoomVia(room));
storeRoomAliasInCache(theAlias, room.roomId);
}
// Store this as the ID of the last room accessed. This is so that we can

View File

@@ -245,7 +245,6 @@ class PipContainerInner extends React.Component<IProps, IState> {
secondaryCall={this.state.secondaryCall}
pipMode={pipMode}
onResize={onResize}
sidebarShown={false}
/>
));
}

View File

@@ -184,32 +184,28 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex
// Guard against null/undefined events and modified keys which we don't want to handle here but do
// at the settings level shortcuts(E.g. Select next room, etc )
// Guard against null/undefined events and modified keys
if (!e || isModifiedKeyEvent(e)) {
onKeyDown?.(e);
return;
}
if (e.code === Key.ARROW_UP && currentIndex !== undefined) {
scrollToItem(currentIndex - 1, false);
handled = true;
} else if (e.code === Key.ARROW_DOWN && currentIndex !== undefined) {
scrollToItem(currentIndex + 1, true);
handled = true;
} else if (e.code === Key.HOME) {
scrollToIndex(0);
handled = true;
} else if (e.code === Key.END) {
scrollToIndex(items.length - 1);
handled = true;
} else if (e.code === Key.PAGE_DOWN && visibleRange && currentIndex !== undefined) {
const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex;
scrollToItem(Math.min(currentIndex + numberDisplayed, items.length - 1), true, `start`);
handled = true;
} else if (e.code === Key.PAGE_UP && visibleRange && currentIndex !== undefined) {
const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex;
scrollToItem(Math.max(currentIndex - numberDisplayed, 0), false, `start`);
handled = true;
if (e || !isModifiedKeyEvent(e)) {
if (e.code === Key.ARROW_UP && currentIndex !== undefined) {
scrollToItem(currentIndex - 1, false);
handled = true;
} else if (e.code === Key.ARROW_DOWN && currentIndex !== undefined) {
scrollToItem(currentIndex + 1, true);
handled = true;
} else if (e.code === Key.HOME) {
scrollToIndex(0);
handled = true;
} else if (e.code === Key.END) {
scrollToIndex(items.length - 1);
handled = true;
} else if (e.code === Key.PAGE_DOWN && visibleRange && currentIndex !== undefined) {
const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex;
scrollToItem(Math.min(currentIndex + numberDisplayed, items.length - 1), true, `start`);
handled = true;
} else if (e.code === Key.PAGE_UP && visibleRange && currentIndex !== undefined) {
const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex;
scrollToItem(Math.max(currentIndex - numberDisplayed, 0), false, `start`);
handled = true;
}
}
if (handled) {

View File

@@ -145,9 +145,6 @@ export default class BaseDialog extends React.Component<IProps> {
const lockProps: Record<string, any> = {
"onKeyDown": this.onKeyDown,
"role": "dialog",
// Allow the dialog to be keyboard focusable
// So the escape key handling works in more cases (say you select the header)
"tabIndex": -1,
// This should point to a node describing the dialog.
// If we were about to completely follow this recommendation we'd need to
// make all the components relying on BaseDialog to be aware of it.

View File

@@ -26,7 +26,6 @@ import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { privateShouldBeEncrypted } from "../../../utils/rooms";
import SettingsStore from "../../../settings/SettingsStore";
import LabelledCheckbox from "../elements/LabelledCheckbox";
import { UIFeature } from "../../../settings/UIFeature";
interface IProps {
type?: RoomType;
@@ -84,7 +83,6 @@ interface IState {
export default class CreateRoomDialog extends React.Component<IProps, IState> {
private readonly askToJoinEnabled: boolean;
private readonly advancedSettingsEnabled: boolean;
private readonly supportsRestricted: boolean;
private nameField = createRef<Field>();
private aliasField = createRef<RoomAliasField>();
@@ -93,8 +91,6 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
super(props);
this.askToJoinEnabled = SettingsStore.getValue("feature_ask_to_join");
this.advancedSettingsEnabled = SettingsStore.getValue(UIFeature.AdvancedSettings);
this.supportsRestricted = !!this.props.parentSpace;
let joinRule = JoinRule.Invite;
@@ -431,21 +427,19 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
{visibilitySection}
{e2eeSection}
{aliasField}
{this.advancedSettingsEnabled && (
<details onToggle={this.onDetailsToggled} className="mx_CreateRoomDialog_details">
<summary className="mx_CreateRoomDialog_details_summary">
{this.state.detailsOpen ? _t("action|hide_advanced") : _t("action|show_advanced")}
</summary>
<LabelledToggleSwitch
label={_t("create_room|unfederated", {
serverName: MatrixClientPeg.safeGet().getDomain(),
})}
onChange={this.onNoFederateChange}
value={this.state.noFederate}
/>
<p>{federateLabel}</p>
</details>
)}
<details onToggle={this.onDetailsToggled} className="mx_CreateRoomDialog_details">
<summary className="mx_CreateRoomDialog_details_summary">
{this.state.detailsOpen ? _t("action|hide_advanced") : _t("action|show_advanced")}
</summary>
<LabelledToggleSwitch
label={_t("create_room|unfederated", {
serverName: MatrixClientPeg.safeGet().getDomain(),
})}
onChange={this.onNoFederateChange}
value={this.state.noFederate}
/>
<p>{federateLabel}</p>
</details>
</div>
</form>
<DialogButtons

View File

@@ -1,53 +1,55 @@
/*
Copyright 2024, 2025 New Vector Ltd.
Copyright 2024 New Vector Ltd.
Copyright 2019 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React, { useCallback } from "react";
import React from "react";
import { _t } from "../../../languageHandler";
import dis from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
import { UserTab } from "./UserTab";
interface IProps {
onFinished(): void;
}
export const IntegrationsDisabledDialog: React.FC<IProps> = ({ onFinished }) => {
const onOpenSettingsClick = useCallback(() => {
onFinished();
dis.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Security,
});
}, [onFinished]);
export default class IntegrationsDisabledDialog extends React.Component<IProps> {
private onAcknowledgeClick = (): void => {
this.props.onFinished();
};
return (
<BaseDialog
className="mx_IntegrationsDisabledDialog"
hasCancel={true}
onFinished={onFinished}
title={_t("integrations|disabled_dialog_title")}
>
<div className="mx_IntegrationsDisabledDialog_content">
<p>
{_t("integrations|disabled_dialog_description", {
manageIntegrations: _t("integration_manager|manage_title"),
})}
</p>
</div>
<DialogButtons
primaryButton={_t("common|settings")}
onPrimaryButtonClick={onOpenSettingsClick}
cancelButton={_t("action|ok")}
onCancel={onFinished}
/>
</BaseDialog>
);
};
private onOpenSettingsClick = (): void => {
this.props.onFinished();
dis.fire(Action.ViewUserSettings);
};
public render(): React.ReactNode {
return (
<BaseDialog
className="mx_IntegrationsDisabledDialog"
hasCancel={true}
onFinished={this.props.onFinished}
title={_t("integrations|disabled_dialog_title")}
>
<div className="mx_IntegrationsDisabledDialog_content">
<p>
{_t("integrations|disabled_dialog_description", {
manageIntegrations: _t("integration_manager|manage_title"),
})}
</p>
</div>
<DialogButtons
primaryButton={_t("common|settings")}
onPrimaryButtonClick={this.onOpenSettingsClick}
cancelButton={_t("action|ok")}
onCancel={this.onAcknowledgeClick}
/>
</BaseDialog>
);
}
}

View File

@@ -53,7 +53,7 @@ import { getKeyBindingsManager } from "../../../../KeyBindingsManager";
import { _t } from "../../../../languageHandler";
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
import { PosthogAnalytics } from "../../../../PosthogAnalytics";
import { getCachedRoomIdForAlias } from "../../../../RoomAliasCache";
import { getCachedRoomIDForAlias } from "../../../../RoomAliasCache";
import { showStartChatInviteDialog } from "../../../../RoomInvite";
import { SettingLevel } from "../../../../settings/SettingLevel";
import SettingsStore from "../../../../settings/SettingsStore";
@@ -912,7 +912,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
if (
trimmedQuery.startsWith("#") &&
trimmedQuery.includes(":") &&
(!getCachedRoomIdForAlias(trimmedQuery) || !cli.getRoom(getCachedRoomIdForAlias(trimmedQuery)!.roomId))
(!getCachedRoomIDForAlias(trimmedQuery) || !cli.getRoom(getCachedRoomIDForAlias(trimmedQuery)))
) {
joinRoomSection = (
<div className="mx_SpotlightDialog_section mx_SpotlightDialog_otherSearches" role="group">

View File

@@ -151,7 +151,7 @@ interface Props extends ReplacerOptions {
const EventContentBody = memo(
({ as, mxEvent, stripReply, content, linkify, highlights, includeDir = true, ref, ...options }: Props) => {
const enableBigEmoji = useSettingValue("TextualBody.enableBigEmoji");
const [mediaIsVisible] = useMediaVisible(mxEvent);
const [mediaIsVisible] = useMediaVisible(mxEvent?.getId(), mxEvent?.getRoomId());
const replacer = useReplacer(content, mxEvent, options);
const linkifyOptions = useMemo(

View File

@@ -25,7 +25,7 @@ interface IProps {
* Quick action button for marking a media event as hidden.
*/
export const HideActionButton: React.FC<IProps> = ({ mxEvent }) => {
const [mediaIsVisible, setVisible] = useMediaVisible(mxEvent);
const [mediaIsVisible, setVisible] = useMediaVisible(mxEvent.getId(), mxEvent.getRoomId());
if (!mediaIsVisible) {
return;

View File

@@ -48,9 +48,4 @@ export interface IBodyProps {
// Set to `true` to disable interactions (e.g. video controls) and to remove controls from the tab order.
// This may be useful when displaying a preview of the event.
inhibitInteraction?: boolean;
/**
* Optional ID for the root element.
*/
id?: string;
}

View File

@@ -686,7 +686,7 @@ export class MImageBodyInner extends React.Component<IProps, IState> {
// Wrap MImageBody component so we can use a hook here.
const MImageBody: React.FC<IBodyProps> = (props) => {
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent);
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
return <MImageBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
};

View File

@@ -38,7 +38,7 @@ class MImageReplyBodyInner extends MImageBodyInner {
}
}
const MImageReplyBody: React.FC<IBodyProps> = (props) => {
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent);
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
return <MImageReplyBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
};

View File

@@ -20,7 +20,7 @@ class MStickerBodyInner extends MImageBodyInner {
protected onClick = (ev: React.MouseEvent): void => {
ev.preventDefault();
if (!this.props.mediaVisible) {
this.props.setMediaVisible(true);
this.props.setMediaVisible?.(true);
}
};
@@ -79,7 +79,7 @@ class MStickerBodyInner extends MImageBodyInner {
}
const MStickerBody: React.FC<IBodyProps> = (props) => {
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent);
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
return <MStickerBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
};

View File

@@ -342,7 +342,7 @@ class MVideoBodyInner extends React.PureComponent<IProps, IState> {
// Wrap MVideoBody component so we can use a hook here.
const MVideoBody: React.FC<IBodyProps> = (props) => {
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent);
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
return <MVideoBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
};

View File

@@ -51,11 +51,6 @@ interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper
getRelationsForEvent?: GetRelationsForEvent;
isSeeingThroughMessageHiddenForModeration?: boolean;
/**
* Optional ID for the root element.
*/
id?: string;
}
export interface IOperableEventTile {
@@ -313,7 +308,6 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
getRelationsForEvent: this.props.getRelationsForEvent,
isSeeingThroughMessageHiddenForModeration: this.props.isSeeingThroughMessageHiddenForModeration,
inhibitInteraction: this.props.inhibitInteraction,
id: this.props.id,
};
if (hasCaption) {
return <CaptionBody {...bodyProps} WrappedBodyType={BodyType} />;

View File

@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
import React, { type HTMLProps, type JSX } from "react";
import React, { type JSX } from "react";
import PinIcon from "@vector-im/compound-design-tokens/assets/web/icons/pin-solid";
import { _t } from "../../../languageHandler";
@@ -13,9 +13,9 @@ import { _t } from "../../../languageHandler";
/**
* A badge to indicate that a message is pinned.
*/
export function PinnedMessageBadge(props: Readonly<HTMLProps<HTMLDivElement>>): JSX.Element {
export function PinnedMessageBadge(): JSX.Element {
return (
<div {...props} className="mx_PinnedMessageBadge">
<div className="mx_PinnedMessageBadge">
<PinIcon width="16px" height="16px" />
{_t("room|pinned_message_badge")}
</div>

View File

@@ -384,12 +384,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
if (isEmote) {
return (
<div
id={this.props.id}
className="mx_MEmoteBody mx_EventTile_content"
onClick={this.onBodyLinkClick}
dir="auto"
>
<div className="mx_MEmoteBody mx_EventTile_content" onClick={this.onBodyLinkClick} dir="auto">
*&nbsp;
<span className="mx_MEmoteBody_sender" onClick={this.onEmoteSenderClick}>
{mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()}
@@ -402,7 +397,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
}
if (isNotice) {
return (
<div id={this.props.id} className="mx_MNoticeBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
<div className="mx_MNoticeBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
{body}
{widgets}
</div>
@@ -410,14 +405,14 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
}
if (isCaption) {
return (
<div id={this.props.id} className="mx_MTextBody mx_EventTile_caption" onClick={this.onBodyLinkClick}>
<div className="mx_MTextBody mx_EventTile_caption" onClick={this.onBodyLinkClick}>
{body}
{widgets}
</div>
);
}
return (
<div id={this.props.id} className="mx_MTextBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
<div className="mx_MTextBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
{body}
{widgets}
</div>

View File

@@ -49,6 +49,7 @@ import RoomName from "../elements/RoomName.tsx";
import { Flex } from "../../../shared-components/utils/Flex";
import { Linkify, topicToHtml } from "../../../HtmlUtils.tsx";
import { Box } from "../../../shared-components/utils/Box";
import { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement.tsx";
import { useRoomSummaryCardViewModel } from "../../viewmodels/right_panel/RoomSummaryCardViewModel.tsx";
import { useRoomTopicViewModel } from "../../viewmodels/right_panel/RoomSummaryCardTopicViewModel.tsx";
@@ -250,15 +251,25 @@ const RoomSummaryCardView: React.FC<IProps> = ({
<MenuItem Icon={ThreadsIcon} label={_t("common|threads")} onSelect={vm.onRoomThreadsClick} />
{!vm.isVideoRoom && (
<>
<MenuItem
Icon={PinIcon}
label={_t("right_panel|pinned_messages_button")}
onSelect={vm.onRoomPinsClick}
<ReleaseAnnouncement
feature="pinningMessageList"
header={_t("right_panel|pinned_messages|release_announcement|title")}
description={_t("right_panel|pinned_messages|release_announcement|description")}
closeLabel={_t("right_panel|pinned_messages|release_announcement|close")}
placement="top"
>
<Text as="span" size="sm">
{vm.pinCount}
</Text>
</MenuItem>
<div>
<MenuItem
Icon={PinIcon}
label={_t("right_panel|pinned_messages_button")}
onSelect={vm.onRoomPinsClick}
>
<Text as="span" size="sm">
{vm.pinCount}
</Text>
</MenuItem>
</div>
</ReleaseAnnouncement>
<MenuItem
Icon={FilesIcon}
label={_t("right_panel|files_button")}

View File

@@ -34,7 +34,6 @@ import {
type UserVerificationStatus,
} from "matrix-js-sdk/src/crypto-api";
import { Tooltip } from "@vector-im/compound-web";
import { uniqueId } from "lodash";
import ReplyChain from "../elements/ReplyChain";
import { _t } from "../../../languageHandler";
@@ -919,8 +918,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
public render(): ReactNode {
const msgtype = this.props.mxEvent.getContent().msgtype;
const eventType = this.props.mxEvent.getType();
const id = uniqueId();
const {
hasRenderer,
isBubbleMessage,
@@ -1145,7 +1142,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
let pinnedMessageBadge: JSX.Element | undefined;
if (PinningUtils.isPinned(MatrixClientPeg.safeGet(), this.props.mxEvent)) {
pinnedMessageBadge = <PinnedMessageBadge aria-describedby={id} tabIndex={0} />;
pinnedMessageBadge = <PinnedMessageBadge />;
}
let reactionsRow: JSX.Element | undefined;
@@ -1240,7 +1237,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
{avatar}
{sender}
</div>,
<div id={id} className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
<div className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
{this.renderContextMenu()}
{replyChain}
{renderTile(TimelineRenderingType.Thread, {
@@ -1428,7 +1425,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
{sender}
{ircPadlock}
{avatar}
<div id={id} className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
<div className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
{this.renderContextMenu()}
{groupTimestamp}
{groupPadlock}

View File

@@ -30,7 +30,7 @@ interface IProps {
const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick }) => {
const cli = useContext(MatrixClientContext);
const [expanded, toggleExpanded] = useStateToggle();
const [mediaVisible] = useMediaVisible(mxEvent);
const [mediaVisible] = useMediaVisible(mxEvent.getId(), mxEvent.getRoomId());
const ts = mxEvent.getTs();
const previews = useAsyncMemo<[string, IPreviewUrlResponse][]>(

View File

@@ -531,15 +531,12 @@ export class MessageComposer extends React.Component<IProps, IState> {
if (!this.props.e2eStatus) {
leftIcon = (
<div className="mx_MessageComposer_e2eIconWrapper">
<Tooltip label={_t("composer|room_unencrypted")}>
<LockOffIcon
aria-label={_t("composer|room_unencrypted")}
width={12}
height={12}
color="var(--cpd-color-icon-info-primary)"
className="mx_E2EIcon mx_MessageComposer_e2eIcon"
/>
</Tooltip>
<LockOffIcon
width={12}
height={12}
color="var(--cpd-color-icon-info-primary)"
className="mx_E2EIcon mx_MessageComposer_e2eIcon"
/>
</div>
);
} else if (this.props.e2eStatus !== E2EStatus.Normal) {

View File

@@ -30,8 +30,6 @@ import { privateShouldBeEncrypted } from "../../../utils/rooms";
import { LocalRoom } from "../../../models/LocalRoom";
import { shouldEncryptRoomWithSingle3rdPartyInvite } from "../../../utils/room/shouldEncryptRoomWithSingle3rdPartyInvite";
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx";
import { useTopic } from "../../../hooks/room/useTopic";
import { topicToHtml, Linkify } from "../../../HtmlUtils";
function hasExpectedEncryptionSettings(matrixClient: MatrixClient, room: Room): boolean {
const isEncrypted: boolean = matrixClient.isRoomEncrypted(room.roomId);
@@ -54,7 +52,6 @@ const determineIntroMessage = (room: Room, encryptedSingle3rdPartyInvite: boolea
const NewRoomIntro: React.FC = () => {
const cli = useContext(MatrixClientContext);
const { room, roomId } = useScopedRoomContext("room", "roomId");
const topic = useTopic(room);
if (!room || !roomId) {
throw new Error("Unable to create a NewRoomIntro without room and roomId");
@@ -109,6 +106,7 @@ const NewRoomIntro: React.FC = () => {
);
} else {
const inRoom = room && room.getMyMembership() === KnownMembership.Join;
const topic = room.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic;
const canAddTopic = inRoom && room.currentState.maySendStateEvent(EventType.RoomTopic, cli.getSafeUserId());
const onTopicClick = (): void => {
@@ -128,23 +126,18 @@ const NewRoomIntro: React.FC = () => {
let topicText;
if (canAddTopic && topic) {
topicText = _t(
"room|intro|edit_topic",
{},
"room|intro|topic_edit",
{ topic },
{
a: (sub) => (
<AccessibleButton element="a" kind="link_inline" onClick={onTopicClick}>
{sub}
</AccessibleButton>
),
topic: () => <Linkify>{topicToHtml(topic?.text, topic?.html)}</Linkify>,
},
);
} else if (topic) {
topicText = _t(
"room|intro|display_topic",
{},
{ topic: () => <Linkify>{topicToHtml(topic?.text, topic?.html)}</Linkify> },
);
topicText = _t("room|intro|topic", { topic });
} else if (canAddTopic) {
topicText = _t(
"room|intro|no_topic",
@@ -252,7 +245,7 @@ const NewRoomIntro: React.FC = () => {
},
)}
</p>
<p data-testid="topic">{topicText}</p>
<p>{topicText}</p>
{buttons}
</React.Fragment>
);

View File

@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import React, { type JSX, useCallback, useId, useState } from "react";
import React, { type JSX, useCallback, useState } from "react";
import { EventTimeline, EventType, type MatrixEvent, type Room } from "matrix-js-sdk/src/matrix";
import { IconButton, Menu, MenuItem, Separator, Tooltip } from "@vector-im/compound-web";
import ViewIcon from "@vector-im/compound-design-tokens/assets/web/icons/visibility-on";
@@ -67,7 +67,6 @@ export function PinnedEventTile({ event, room, permalinkCreator }: PinnedEventTi
const isInThread = Boolean(event.threadRootId);
const displayThreadInfo = !event.isThreadRoot && isInThread;
const id = useId();
return (
<div className="mx_PinnedEventTile" role="listitem">
@@ -86,10 +85,9 @@ export function PinnedEventTile({ event, room, permalinkCreator }: PinnedEventTi
{event.sender?.name || sender}
</span>
</Tooltip>
<PinMenu event={event} room={room} permalinkCreator={permalinkCreator} contentId={id} />
<PinMenu event={event} room={room} permalinkCreator={permalinkCreator} />
</div>
<MessageEvent
id={id}
mxEvent={event}
maxImageHeight={150}
permalinkCreator={permalinkCreator}
@@ -133,17 +131,12 @@ export function PinnedEventTile({ event, room, permalinkCreator }: PinnedEventTi
/**
* Properties for {@link PinMenu}.
*/
interface PinMenuProps extends PinnedEventTileProps {
/**
* HTML ID of the pinned message content.
*/
contentId: string;
}
interface PinMenuProps extends PinnedEventTileProps {}
/**
* A popover menu with actions on the pinned event
*/
function PinMenu({ event, room, permalinkCreator, contentId }: PinMenuProps): JSX.Element {
function PinMenu({ event, room, permalinkCreator }: PinMenuProps): JSX.Element {
const [open, setOpen] = useState(false);
const matrixClient = useMatrixClientContext();
@@ -224,11 +217,7 @@ function PinMenu({ event, room, permalinkCreator, contentId }: PinMenuProps): JS
side="right"
align="start"
trigger={
<IconButton
size="24px"
aria-label={_t("right_panel|pinned_messages|menu")}
aria-describedby={contentId}
>
<IconButton size="24px" aria-label={_t("right_panel|pinned_messages|menu")}>
<TriggerIcon />
</IconButton>
}

View File

@@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details.
*/
import React, { type JSX, useEffect, useId, useRef, useState } from "react";
import React, { type JSX, useEffect, useRef, useState } from "react";
import PinIcon from "@vector-im/compound-design-tokens/assets/web/icons/pin-solid";
import { Button } from "@vector-im/compound-web";
import { type MatrixEvent, type Room } from "matrix-js-sdk/src/matrix";
@@ -64,13 +64,9 @@ export function PinnedMessageBanner({
setCurrentEventIndex(() => eventCount - 1);
}, [eventCount]);
const isLastMessage = currentEventIndex === eventCount - 1;
const pinnedEvent = pinnedEvents[currentEventIndex];
useNotifyTimeline(pinnedEvent, resizeNotifier);
const id = useId();
if (!pinnedEvent) return null;
const shouldUseMessageEvent = pinnedEvent.isRedacted() || pinnedEvent.isDecryptionFailure();
@@ -94,24 +90,18 @@ export function PinnedMessageBanner({
return (
<div
role="region"
className="mx_PinnedMessageBanner"
data-single-message={isSinglePinnedEvent}
aria-label={_t("room|pinned_message_banner|description")}
data-testid="pinned-message-banner"
>
<button
aria-label={
isLastMessage
? _t("room|pinned_message_banner|go_to_newest_message")
: _t("room|pinned_message_banner|go_to_next_message")
}
aria-describedby={id}
aria-label={_t("room|pinned_message_banner|go_to_message")}
type="button"
className="mx_PinnedMessageBanner_main"
onClick={onBannerClick}
>
<div className="mx_PinnedMessageBanner_content" id={id}>
<div className="mx_PinnedMessageBanner_content">
<Indicators count={eventCount} currentIndex={currentEventIndex} />
<PinIcon width="20px" height="20px" className="mx_PinnedMessageBanner_PinIcon" />
{!isSinglePinnedEvent && (

View File

@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
import React, { useCallback, useRef, useState, type JSX } from "react";
import React, { useCallback, useRef, type JSX } from "react";
import { type Room } from "matrix-js-sdk/src/matrix";
import { type ScrollIntoViewLocation } from "react-virtuoso";
import { isEqual } from "lodash";
@@ -33,7 +33,6 @@ export function RoomList({ vm: { roomsResult, activeIndex } }: RoomListProps): J
const lastSpaceId = useRef<string | undefined>(undefined);
const lastFilterKeys = useRef<FilterKey[] | undefined>(undefined);
const roomCount = roomsResult.rooms.length;
const [isScrolling, setIsScrolling] = useState(false);
const getItemComponent = useCallback(
(
index: number,
@@ -58,11 +57,10 @@ export function RoomList({ vm: { roomsResult, activeIndex } }: RoomListProps): J
roomIndex={index}
roomCount={roomCount}
onFocus={onFocus}
listIsScrolling={isScrolling}
/>
);
},
[activeIndex, roomCount, isScrolling],
[activeIndex, roomCount],
);
const getItemKey = useCallback((item: Room): string => {
@@ -118,7 +116,6 @@ export function RoomList({ vm: { roomsResult, activeIndex } }: RoomListProps): J
getItemKey={getItemKey}
isItemFocusable={() => true}
onKeyDown={keyDownCallback}
isScrolling={setIsScrolling}
/>
);
}

View File

@@ -23,7 +23,6 @@ import {
useRoomListHeaderViewModel,
} from "../../../viewmodels/roomlist/RoomListHeaderViewModel";
import { RoomListOptionsMenu } from "./RoomListOptionsMenu";
import { ReleaseAnnouncement } from "../../../structures/ReleaseAnnouncement";
/**
* The header view for the room list
@@ -46,38 +45,15 @@ export function RoomListHeaderView(): JSX.Element {
{vm.displaySpaceMenu && <SpaceMenu vm={vm} />}
</Flex>
<Flex align="center" gap="var(--cpd-space-2x)">
<ReleaseAnnouncement
feature="newRoomList_sort"
header={_t("room_list|release_announcement|sort|title")}
description={_t("room_list|release_announcement|sort|description")}
closeLabel={_t("room_list|release_announcement|next")}
placement="bottom"
>
<div className="mx_RoomListHeaderView_ReleaseAnnouncementAnchor">
<RoomListOptionsMenu vm={vm} />
</div>
</ReleaseAnnouncement>
<RoomListOptionsMenu vm={vm} />
{/* If we don't display the compose menu, it means that the user can only send DM */}
<ReleaseAnnouncement
feature="newRoomList_intro"
header={_t("room_list|release_announcement|intro|title")}
description={_t("room_list|release_announcement|intro|description")}
closeLabel={_t("room_list|release_announcement|next")}
>
<div className="mx_RoomListHeaderView_ReleaseAnnouncementAnchor">
{vm.displayComposeMenu ? (
<ComposeMenu vm={vm} />
) : (
<IconButton
aria-label={_t("action|start_chat")}
onClick={(e) => vm.createChatRoom(e.nativeEvent)}
>
<ComposeIcon color="var(--cpd-color-icon-secondary)" />
</IconButton>
)}
</div>
</ReleaseAnnouncement>
{vm.displayComposeMenu ? (
<ComposeMenu vm={vm} />
) : (
<IconButton aria-label={_t("action|start_chat")} onClick={(e) => vm.createChatRoom(e.nativeEvent)}>
<ComposeIcon color="var(--cpd-color-icon-secondary)" />
</IconButton>
)}
</Flex>
</Flex>
);

View File

@@ -41,10 +41,6 @@ interface RoomListItemViewProps extends React.HTMLAttributes<HTMLButtonElement>
* The total number of rooms in the list
*/
roomCount: number;
/**
* Whether the list is currently scrolling
*/
listIsScrolling: boolean;
}
/**
@@ -57,7 +53,6 @@ export const RoomListItemView = memo(function RoomListItemView({
onFocus,
roomIndex: index,
roomCount: count,
listIsScrolling,
...props
}: RoomListItemViewProps): JSX.Element {
const ref = useRef<HTMLButtonElement>(null);
@@ -146,11 +141,7 @@ export const RoomListItemView = memo(function RoomListItemView({
</Flex>
);
// Rendering multiple context menus can causes crashes in radix upstream,
// See https://github.com/radix-ui/primitives/issues/2717.
// We also don't need the context menu while scrolling so can improve scroll performance
// by not rendering it.
if (!vm.showContextMenu || listIsScrolling) return content;
if (!vm.showContextMenu) return content;
return (
<RoomListItemContextMenuView

View File

@@ -11,8 +11,6 @@ import { useRoomListViewModel } from "../../../viewmodels/roomlist/RoomListViewM
import { RoomList } from "./RoomList";
import { EmptyRoomList } from "./EmptyRoomList";
import { RoomListPrimaryFilters } from "./RoomListPrimaryFilters";
import { _t } from "../../../../languageHandler";
import { ReleaseAnnouncement } from "../../../structures/ReleaseAnnouncement";
/**
* Host the room list and the (future) room filters
@@ -30,17 +28,7 @@ export function RoomListView(): JSX.Element {
}
return (
<>
<ReleaseAnnouncement
feature="newRoomList_filter"
header={_t("room_list|release_announcement|filter|title")}
description={_t("room_list|release_announcement|filter|description")}
closeLabel={_t("room_list|release_announcement|next")}
placement="right"
>
<div>
<RoomListPrimaryFilters vm={vm} />
</div>
</ReleaseAnnouncement>
<RoomListPrimaryFilters vm={vm} />
{listBody}
</>
);

View File

@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
import React, { type JSX, type MouseEventHandler, useState } from "react";
import React, { type FormEventHandler, type JSX, type MouseEventHandler, useState } from "react";
import {
Breadcrumb,
Button,
@@ -310,7 +310,7 @@ interface KeyFormProps {
/**
* Called when the form is submitted.
*/
onSubmit: () => Promise<void>;
onSubmit: FormEventHandler;
/**
* The recovery key to confirm.
*/
@@ -329,7 +329,6 @@ interface KeyFormProps {
function KeyForm({ onCancelClick, onSubmit, recoveryKey, submitButtonLabel }: KeyFormProps): JSX.Element {
// Undefined by default, as the key is not filled yet
const [isKeyValid, setIsKeyValid] = useState<boolean>();
const [isKeyChangeInProgress, setIsKeyChangeInProgress] = useState<boolean>(false);
const isKeyInvalidAndFilled = isKeyValid === false;
return (
@@ -337,14 +336,7 @@ function KeyForm({ onCancelClick, onSubmit, recoveryKey, submitButtonLabel }: Ke
className="mx_KeyForm"
onSubmit={(evt) => {
evt.preventDefault();
if (isKeyChangeInProgress) {
// Don't allow repeated attempts.
return;
}
setIsKeyChangeInProgress(true);
onSubmit().finally(() => {
setIsKeyChangeInProgress(false);
});
onSubmit(evt);
}}
onChange={async (evt) => {
evt.preventDefault();
@@ -368,7 +360,7 @@ function KeyForm({ onCancelClick, onSubmit, recoveryKey, submitButtonLabel }: Ke
)}
</Field>
<EncryptionCardButtons>
<Button disabled={!isKeyValid || isKeyChangeInProgress}>{submitButtonLabel}</Button>
<Button disabled={!isKeyValid}>{submitButtonLabel}</Button>
<Button kind="tertiary" onClick={onCancelClick}>
{_t("action|cancel")}
</Button>

View File

@@ -29,7 +29,6 @@ import QuickThemeSwitcher from "./QuickThemeSwitcher";
import Modal from "../../../Modal";
import DevtoolsDialog from "../dialogs/DevtoolsDialog";
import { SdkContextClass } from "../../../contexts/SDKContext";
import { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement";
const QuickSettingsButton: React.FC<{
isPanelCollapsed: boolean;
@@ -138,24 +137,16 @@ const QuickSettingsButton: React.FC<{
return (
<>
<ReleaseAnnouncement
feature="newRoomList_settings"
header={_t("room_list|release_announcement|settings|title")}
description={_t("room_list|release_announcement|settings|description")}
closeLabel={_t("room_list|release_announcement|done")}
placement="right"
<AccessibleButton
className={classNames("mx_QuickSettingsButton", { expanded: !isPanelCollapsed })}
onClick={openMenu}
aria-label={_t("quick_settings|title")}
title={isPanelCollapsed ? _t("quick_settings|title") : undefined}
ref={handle}
aria-expanded={!isPanelCollapsed}
>
<AccessibleButton
className={classNames("mx_QuickSettingsButton", { expanded: !isPanelCollapsed })}
onClick={openMenu}
aria-label={_t("quick_settings|title")}
title={isPanelCollapsed ? _t("quick_settings|title") : undefined}
ref={handle}
aria-expanded={!isPanelCollapsed}
>
{!isPanelCollapsed ? _t("common|settings") : null}
</AccessibleButton>
</ReleaseAnnouncement>
{!isPanelCollapsed ? _t("common|settings") : null}
</AccessibleButton>
{contextMenu}
</>

View File

@@ -50,10 +50,6 @@ interface IProps {
onMouseDownOnHeader?: (event: React.MouseEvent<Element, MouseEvent>) => void;
showApps?: boolean;
sidebarShown: boolean;
setSidebarShown?: (sidebarShown: boolean) => void;
}
interface IState {
@@ -66,6 +62,7 @@ interface IState {
primaryFeed?: CallFeed;
secondaryFeed?: CallFeed;
sidebarFeeds: Array<CallFeed>;
sidebarShown: boolean;
}
function getFullScreenElement(): Element | null {
@@ -100,6 +97,7 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
primaryFeed: primary,
secondaryFeed: secondary,
sidebarFeeds: sidebar,
sidebarShown: true,
};
}
@@ -271,9 +269,8 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
isScreensharing = await this.props.call.setScreensharingEnabled(true);
}
this.props.setSidebarShown?.(true);
this.setState({
sidebarShown: true,
screensharing: isScreensharing,
});
};
@@ -323,12 +320,12 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
};
private onToggleSidebar = (): void => {
this.props.setSidebarShown?.(!this.props.sidebarShown);
this.setState({ sidebarShown: !this.state.sidebarShown });
};
private renderCallControls(): JSX.Element {
const { call, pipMode, sidebarShown } = this.props;
const { callState, micMuted, vidMuted, screensharing, secondaryFeed, sidebarFeeds } = this.state;
const { call, pipMode } = this.props;
const { callState, micMuted, vidMuted, screensharing, sidebarShown, secondaryFeed, sidebarFeeds } = this.state;
// If SDPStreamMetadata isn't supported don't show video mute button in voice calls
const vidMuteButtonShown = call.opponentSupportsSDPStreamMetadata() || call.hasLocalUserMediaVideoTrack;
@@ -340,8 +337,7 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
(call.opponentSupportsSDPStreamMetadata() || call.hasLocalUserMediaVideoTrack) &&
call.state === CallState.Connected;
// Show the sidebar button only if there is something to hide/show
const sidebarButtonShown =
!pipMode && ((secondaryFeed && !secondaryFeed.isVideoMuted()) || sidebarFeeds.length > 0);
const sidebarButtonShown = (secondaryFeed && !secondaryFeed.isVideoMuted()) || sidebarFeeds.length > 0;
// The dial pad & 'more' button actions are only relevant in a connected call
const contextMenuButtonShown = callState === CallState.Connected;
const dialpadButtonShown = callState === CallState.Connected && call.opponentSupportsDTMF();
@@ -376,7 +372,7 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
}
private renderToast(): JSX.Element | null {
const { call, sidebarShown } = this.props;
const { call } = this.props;
const someoneIsScreensharing = call.getFeeds().some((feed) => {
return feed.purpose === SDPStreamMetadataPurpose.Screenshare;
});
@@ -384,7 +380,7 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
if (!someoneIsScreensharing) return null;
const isScreensharing = call.isScreensharing();
const { primaryFeed } = this.state;
const { primaryFeed, sidebarShown } = this.state;
const sharerName = primaryFeed?.getMember()?.name;
if (!sharerName) return null;
@@ -397,8 +393,8 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
}
private renderContent(): JSX.Element {
const { pipMode, call, onResize, sidebarShown } = this.props;
const { isLocalOnHold, isRemoteOnHold, primaryFeed, secondaryFeed, sidebarFeeds } = this.state;
const { pipMode, call, onResize } = this.props;
const { isLocalOnHold, isRemoteOnHold, sidebarShown, primaryFeed, secondaryFeed, sidebarFeeds } = this.state;
const callRoomId = LegacyCallHandler.instance.roomIdForCall(call);
const callRoom = (callRoomId ? MatrixClientPeg.safeGet().getRoom(callRoomId) : undefined) ?? undefined;
@@ -541,8 +537,8 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
}
public render(): React.ReactNode {
const { call, secondaryCall, pipMode, showApps, onMouseDownOnHeader, sidebarShown } = this.props;
const { sidebarFeeds } = this.state;
const { call, secondaryCall, pipMode, showApps, onMouseDownOnHeader } = this.props;
const { sidebarShown, sidebarFeeds } = this.state;
const client = MatrixClientPeg.safeGet();
const callRoomId = LegacyCallHandler.instance.roomIdForCall(call);

View File

@@ -25,7 +25,6 @@ interface IProps {
interface IState {
call: MatrixCall | null;
sidebarShown: boolean;
}
/*
@@ -35,23 +34,19 @@ interface IState {
export default class LegacyCallViewForRoom extends React.Component<IProps, IState> {
public constructor(props: IProps) {
super(props);
const call = this.getCall();
this.state = {
call,
sidebarShown: !!call && LegacyCallHandler.instance.isCallSidebarShown(call.callId),
call: this.getCall(),
};
}
public componentDidMount(): void {
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallState, this.updateCall);
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCall);
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.ShownSidebarsChanged, this.updateCall);
}
public componentWillUnmount(): void {
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.updateCall);
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCall);
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.ShownSidebarsChanged, this.updateCall);
}
private updateCall = (): void => {
@@ -59,10 +54,6 @@ export default class LegacyCallViewForRoom extends React.Component<IProps, IStat
if (newCall !== this.state.call) {
this.setState({ call: newCall });
}
const newSidebarShown = !!newCall && LegacyCallHandler.instance.isCallSidebarShown(newCall.callId);
if (newSidebarShown !== this.state.sidebarShown) {
this.setState({ sidebarShown: newSidebarShown });
}
};
private getCall(): MatrixCall | null {
@@ -84,11 +75,6 @@ export default class LegacyCallViewForRoom extends React.Component<IProps, IStat
this.props.resizeNotifier.stopResizing();
};
private setSidebarShown = (sidebarShown: boolean): void => {
if (!this.state.call) return;
LegacyCallHandler.instance.setCallSidebarShown(this.state.call.callId, sidebarShown);
};
public render(): React.ReactNode {
if (!this.state.call) return null;
@@ -113,13 +99,7 @@ export default class LegacyCallViewForRoom extends React.Component<IProps, IStat
className="mx_LegacyCallViewForRoom_ResizeWrapper"
handleClasses={{ bottom: "mx_LegacyCallViewForRoom_ResizeHandle" }}
>
<LegacyCallView
call={this.state.call}
pipMode={false}
showApps={this.props.showApps}
sidebarShown={this.state.sidebarShown}
setSidebarShown={this.setSidebarShown}
/>
<LegacyCallView call={this.state.call} pipMode={false} showApps={this.props.showApps} />
</Resizable>
</div>
);

View File

@@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details.
*/
import { useCallback } from "react";
import { JoinRule, type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { JoinRule } from "matrix-js-sdk/src/matrix";
import { SettingLevel } from "../settings/SettingLevel";
import { useSettingValue } from "./useSettings";
@@ -19,25 +19,14 @@ const PRIVATE_JOIN_RULES: JoinRule[] = [JoinRule.Invite, JoinRule.Knock, JoinRul
/**
* Should the media event be visible in the client, or hidden.
*
* This function uses the `mediaPreviewConfig` setting to determine the rules for the room
* along with the `showMediaEventIds` setting for specific events.
*
* A function may be provided to alter the visible state.
*
* @param The event that contains the media. If not provided, the global rule is used.
*
* @returns Returns a tuple of:
* A boolean describing the hidden status.
* A function to show or hide the event.
* @param eventId The eventId of the media event.
* @returns A boolean describing the hidden status, and a function to set the visiblity.
*/
export function useMediaVisible(mxEvent?: MatrixEvent): [boolean, (visible: boolean) => void] {
const eventId = mxEvent?.getId();
const mediaPreviewSetting = useSettingValue("mediaPreviewConfig", mxEvent?.getRoomId());
export function useMediaVisible(eventId?: string, roomId?: string): [boolean, (visible: boolean) => void] {
const mediaPreviewSetting = useSettingValue("mediaPreviewConfig", roomId);
const client = useMatrixClientContext();
const eventVisibility = useSettingValue("showMediaEventIds");
const room = client.getRoom(mxEvent?.getRoomId()) ?? undefined;
const joinRule = useRoomState(room, (state) => state.getJoinRule());
const joinRule = useRoomState(client.getRoom(roomId) ?? undefined, (state) => state.getJoinRule());
const setMediaVisible = useCallback(
(visible: boolean) => {
SettingsStore.setValue("showMediaEventIds", null, SettingLevel.DEVICE, {
@@ -54,9 +43,6 @@ export function useMediaVisible(mxEvent?: MatrixEvent): [boolean, (visible: bool
// Always prefer the explicit per-event user preference here.
if (explicitEventVisiblity !== undefined) {
return [explicitEventVisiblity, setMediaVisible];
} else if (mxEvent?.getSender() === client.getUserId()) {
// If this event is ours and we've not set an explicit visibility, default to on.
return [true, setMediaVisible];
} else if (mediaPreviewSetting.media_previews === MediaPreviewValue.Off) {
return [false, setMediaVisible];
} else if (mediaPreviewSetting.media_previews === MediaPreviewValue.On) {

View File

@@ -646,12 +646,12 @@
"mode_plain": "Skrýt formátování",
"mode_rich_text": "Zobrazit formátování",
"no_perms_notice": "Nemáte oprávnění zveřejňovat příspěvky v této místnosti",
"placeholder": "Odeslat nešifrovanou zprávu…",
"placeholder_encrypted": "Odeslat zprávu...",
"placeholder_reply": "Odeslat nešifrovanou odpověď…",
"placeholder_reply_encrypted": "Odeslat odpověď…",
"placeholder_thread": "Odpovědět v nešifrovaném vláknu…",
"placeholder_thread_encrypted": "Odpovědět ve vláknu…",
"placeholder": "Odeslat zprávu…",
"placeholder_encrypted": "Odeslat šifrovanou zprávu",
"placeholder_reply": "Odpovědět…",
"placeholder_reply_encrypted": "Odeslat šifrovanou odpověď…",
"placeholder_thread": "Odpovědět na vlákno…",
"placeholder_thread_encrypted": "Odpovědět na zašifrované vlákno…",
"poll_button": "Hlasování",
"poll_button_no_perms_description": "Nemáte oprávnění zahajovat hlasování v této místnosti.",
"poll_button_no_perms_title": "Vyžaduje oprávnění",
@@ -922,8 +922,7 @@
},
"privacy_warning": "Ujistěte se, že tuto obrazovku nikdo nevidí!",
"restoring": "Obnovení klíčů ze zálohy",
"security_key_label": "Klíč pro obnovení",
"security_key_title": "Zadejte klíč pro obnovení"
"security_key_title": "Klíč pro obnovení"
},
"bootstrap_title": "Příprava klíčů",
"confirm_encryption_setup_body": "Kliknutím na tlačítko níže potvrďte nastavení šifrování.",
@@ -1368,10 +1367,6 @@
"name_email_mxid_share_space": "Pozvěte někoho pomocí jeho jména, e-mailové adresy, uživatelského jména (například <userId/>) nebo <a>sdílejte tento prostor</a>.",
"name_mxid_share_room": "Pozvěte někoho pomocí svého jména, uživatelského jména (například <userId />) nebo <a>sdílejte tuto místnost</a>.",
"name_mxid_share_space": "Pozvěte někoho pomocí jeho jména, uživatelského jména (například <userId/>) nebo <a>sdílejte tento prostor</a>.",
"progress": {
"dont_close": "Nezavírejte aplikaci, dokud neskončíte.",
"preparing": "Příprava pozvánek..."
},
"recents_section": "Nedávné konverzace",
"room_failed_partial": "Poslali jsme ostatním, ale níže uvedení lidé nemohli být pozváni do <RoomName/>",
"room_failed_partial_title": "Některé pozvánky nebylo možné odeslat",
@@ -1540,9 +1535,6 @@
"render_reaction_images_description": "Někdy se označují jako \"vlastní emoji\".",
"report_to_moderators": "Nahlásit moderátorům",
"report_to_moderators_description": "V místnostech, které podporují moderování, můžete pomocí tlačítka \"Nahlásit\" nahlásit zneužití moderátorům místnosti.",
"share_history_on_invite": "Sdílet šifrovanou historii s novými členy",
"share_history_on_invite_description": "Při pozvání uživatele do šifrované místnosti, u které je viditelnost historie nastavena na „sdílená“, sdílet šifrovanou historii s tímto uživatelem a přijmout šifrovanou historii, když jste pozváni do takové místnosti.",
"share_history_on_invite_warning": "Tato funkce je EXPERIMENTÁLNÍ a nejsou v ní implementována všechna bezpečnostní opatření. Neaktivujte ji na produkčních účtech.",
"sliding_sync": "Režim klouzavé synchronizace",
"sliding_sync_description": "V aktivním vývoji, nelze zakázat.",
"sliding_sync_disabled_notice": "Pro vypnutí se odhlaste a znovu přihlaste",
@@ -1665,7 +1657,6 @@
"filter_placeholder": "Najít člena místnosti",
"invite_button_no_perms_tooltip": "Nemáte oprávnění zvát uživatele",
"invited_label": "Pozván",
"list_title": "Seznam členů",
"no_matches": "Žádné shody"
},
"member_list_back_action_label": "Členové místnosti",
@@ -1770,7 +1761,6 @@
},
"power_level": {
"admin": "Správce",
"creator": "Vlastník",
"custom": "Vlastní (%(level)s)",
"custom_level": "Vlastní úroveň",
"default": "Výchozí",
@@ -1924,7 +1914,6 @@
"thread_list": {
"context_menu_label": "Možnosti vláken"
},
"title": "Pravý panel",
"video_room_chat": {
"title": "Chatovat"
}
@@ -2073,6 +2062,7 @@
"button_close_list": "Zavřít seznam",
"button_view_all": "Zobrazit vše",
"description": "Tato místnost má připnuté zprávy. Kliknutím je zobrazíte.",
"go_to_message": "Zobrazit připnutou zprávu na časové ose.",
"title": "<bold>%(index)sz%(length)s</bold> Připnuté zprávy"
},
"read_topic": "Klikněte pro přečtení tématu",
@@ -3410,7 +3400,6 @@
"unable_to_find": "Pokusili jste se načíst bod na časové ose místnosti, ale nepodařilo se ho najít."
},
"m.audio": {
"audio_player": "Audio přehrávač",
"error_downloading_audio": "Chyba při stahování audia",
"error_processing_audio": "Došlo k chybě při zpracovávání hlasové zprávy",
"error_processing_voice_message": "Chyba při zpracování hlasové zprávy",

View File

@@ -2110,6 +2110,7 @@
"button_close_list": "Cau'r rhestr",
"button_view_all": "Gweld popeth",
"description": "Mae negeseuon wedi'u pinio yn yr ystafell hon. Cliciwch i'w gweld.",
"go_to_message": "Gweld y neges sydd wedi'i phinnio yn y llinell amser.",
"title": "<bold>%(index)s o %(length)s</bold> Neges wedi'u pinio"
},
"read_topic": "Cliciwch i ddarllen y pwnc",

File diff suppressed because it is too large Load Diff

View File

@@ -1658,6 +1658,9 @@
"not_found_title": "Αυτό το δωμάτιο ή ο χώρος δεν υπάρχει.",
"not_found_title_name": "Το %(roomName)s δεν υπάρχει.",
"peek_join_prompt": "Κάνετε προεπισκόπηση στο %(roomName)s. Θέλετε να συμμετάσχετε;",
"pinned_message_banner": {
"go_to_message": "Εμφάνιση καρφιτσωμένου μηνύματος στο χρονολόγιο."
},
"rejoin_button": "Επανασύνδεση",
"status_bar": {
"delete_all": "Διαγραφή όλων",

View File

@@ -654,7 +654,6 @@
"poll_button_no_perms_description": "You do not have permission to start polls in this room.",
"poll_button_no_perms_title": "Permission Required",
"replying_title": "Replying",
"room_unencrypted": "Messages in this room are not end-to-end encrypted",
"room_upgraded_link": "The conversation continues here.",
"room_upgraded_notice": "This room has been replaced and is no longer active.",
"send_button_title": "Send message",
@@ -1026,7 +1025,7 @@
"text": "Supply the ID and fingerprint of one of your own devices to verify it. NOTE this allows the other device to send and receive messages as you. IF SOMEONE TOLD YOU TO PASTE SOMETHING HERE, IT IS LIKELY YOU ARE BEING SCAMMED!",
"wrong_fingerprint": "Unable to verify device '%(deviceId)s' - the supplied fingerprint '%(fingerprint)s' does not match the device fingerprint, '%(fprint)s'"
},
"no_key_or_device": "It looks like you don't have a Recovery Key or any other devices you can verify against. This device will not be able to access old encrypted messages. In order to verify your identity on this device, you'll need to reset your cryptographic identity.",
"no_key_or_device": "It looks like you don't have a Recovery Key or any other devices you can verify against. This device will not be able to access old encrypted messages. In order to verify your identity on this device, you'll need to reset your verification keys.",
"no_support_qr_emoji": "The device you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.",
"other_party_cancelled": "The other party cancelled the verification.",
"prompt_encrypted": "Verify all users in a room to ensure it's secure.",
@@ -1877,6 +1876,11 @@
"other": "You can only pin up to %(count)s widgets"
},
"menu": "Open menu",
"release_announcement": {
"close": "Ok",
"description": "Find all pinned messages here. Rollover any message and select “Pin” to add it.",
"title": "All new pinned messages"
},
"reply_thread": "Reply to a <link>thread message</link>",
"unpin_all": {
"button": "Unpin all messages",
@@ -1998,9 +2002,7 @@
"inaccessible_subtitle_1": "Try again later, or ask a room or space admin to check if you have access.",
"inaccessible_subtitle_2": "%(errcode)s was returned while trying to access the room or space. If you think you're seeing this message in error, please <issueLink>submit a bug report</issueLink>.",
"intro": {
"display_topic": "Topic: <topic/>",
"dm_caption": "Only the two of you are in this conversation, unless either of you invites anyone to join.",
"edit_topic": "Topic: <topic/> (<a>edit</a>)",
"enable_encryption_prompt": "Enable encryption in settings.",
"encrypted_3pid_dm_pending_join": "Once everyone has joined, youll be able to chat",
"no_avatar_label": "Add a photo, so people can easily spot your room.",
@@ -2010,6 +2012,8 @@
"send_message_start_dm": "Send your first message to invite <displayName/> to chat",
"start_of_dm_history": "This is the beginning of your direct message history with <displayName/>.",
"start_of_room": "This is the start of <roomName/>.",
"topic": "Topic: %(topic)s ",
"topic_edit": "Topic: %(topic)s (<a>edit</a>)",
"unencrypted_warning": "End-to-end encryption isn't enabled",
"user_created": "%(displayName)s created this room.",
"you_created": "You created this room."
@@ -2064,9 +2068,8 @@
"pinned_message_banner": {
"button_close_list": "Close list",
"button_view_all": "View all",
"description": "Pinned messages",
"go_to_newest_message": "View the pinned message in the timeline and the newest pinned message here",
"go_to_next_message": "View the pinned message in the timeline and the next oldest pinned message here",
"description": "This room has pinned messages. Click to view them.",
"go_to_message": "View the pinned message in the timeline.",
"title": "<bold>%(index)s of %(length)s</bold> Pinned messages"
},
"read_topic": "Click to read topic",
@@ -2127,7 +2130,7 @@
"no_chats": "No chats yet",
"no_chats_description": "Get started by messaging someone or by creating a room",
"no_chats_description_no_room_rights": "Get started by messaging someone",
"no_favourites": "You don't have favourite chats yet",
"no_favourites": "You don't have favourite chat yet",
"no_favourites_description": "You can add a chat to your favourites in the chat settings",
"no_invites": "You don't have any unread invites",
"no_lowpriority": "You don't have any low priority rooms",
@@ -2175,26 +2178,6 @@
"one": "Currently removing messages in %(count)s room",
"other": "Currently removing messages in %(count)s rooms"
},
"release_announcement": {
"done": "Done",
"filter": {
"description": "Filter your chats with a single click. Expand to view more filters.",
"title": "New quick filters"
},
"intro": {
"description": "The chats list has been updated to be more clear and simple to use.",
"title": "Chats has a new look!"
},
"next": "Next",
"settings": {
"description": "To show or hide message previews, go to All settings > Preferences > Room list",
"title": "Some settings have moved"
},
"sort": {
"description": "Change the ordering of your chats from most recent to A-Z",
"title": "Sort your chats"
}
},
"room": {
"more_options": "More Options",
"open_room": "Open room %(roomName)s"

View File

@@ -654,7 +654,6 @@
"poll_button_no_perms_description": "Sul ei ole õigusi küsitluste korraldamiseks siin jututoas.",
"poll_button_no_perms_title": "Vaja on täiendavaid õigusi",
"replying_title": "Vastan",
"room_unencrypted": "Selle jututoa sõnumid pole läbivalt krüptitud",
"room_upgraded_link": "Vestlus jätkub siin.",
"room_upgraded_notice": "See jututuba on asendatud teise jututoaga ning ei ole enam kasutusel.",
"send_button_title": "Saada sõnum",
@@ -1367,10 +1366,6 @@
"name_email_mxid_share_space": "Kutsu teist osapoolt tema nime, e-posti aadressi, kasutajanime (nagu <userId/>) alusel või <a>jaga seda kogukonnakeskust</a>.",
"name_mxid_share_room": "Kutsu kedagi tema nime, kasutajanime (nagu <userId/>) alusel või <a>jaga seda jututuba</a>.",
"name_mxid_share_space": "Kutsu kedagi tema nime, kasutajanime (nagu <userId/>) alusel või <a>jaga seda kogukonnakeskust</a>.",
"progress": {
"dont_close": "Palun ära sulge vaadet enne kui tegevus pole lõppenud.",
"preparing": "Valmistan kutseid ette..."
},
"recents_section": "Hiljutised vestlused",
"room_failed_partial": "Teised kasutajad said kutse, kuid allpool toodud kasutajatele ei õnnestunud saata kutset <RoomName/> jututuppa",
"room_failed_partial_title": "Mõnede kutsete saatmine ei õnnestunud",
@@ -2070,6 +2065,7 @@
"button_close_list": "Sulge loend",
"button_view_all": "Vaata kõiki",
"description": "Selles jututoas on esiletõstetud sõnumeid. Nende vaatamiseks klõpsi.",
"go_to_message": "Vaata esiletõstetud sõnumit ajajoonel.",
"title": "<bold>%(index)s of %(length)s</bold> Esiletõstetud sõnumid"
},
"read_topic": "Teema lugemiseks klõpsi",

View File

@@ -1749,6 +1749,7 @@
"pinned_message_banner": {
"button_view_all": "Näytä kaikki",
"description": "Tässä huoneessa on kiinnitettyjä viestejä. Napsauta nähdäksesi ne.",
"go_to_message": "Näytä kiinnitetty viesti aikajanalla.",
"title": "<bold>%(index)s/%(length)s</bold> kiinnitettyä viestiä"
},
"read_topic": "Lue aihe napsauttamalla",

View File

@@ -654,7 +654,6 @@
"poll_button_no_perms_description": "Vous navez pas la permission de démarrer un sondage dans ce salon.",
"poll_button_no_perms_title": "Autorisation requise",
"replying_title": "Répond",
"room_unencrypted": "Les messages dans ce salon ne sont pas chiffrés de bout en bout",
"room_upgraded_link": "La discussion continue ici.",
"room_upgraded_notice": "Ce salon a été remplacé et nest plus actif.",
"send_button_title": "Envoyer le message",
@@ -1367,10 +1366,6 @@
"name_email_mxid_share_space": "Invitez quelquun grâce à son nom, adresse e-mail, nom dutilisateur (tel que <userId/>) ou <a>partagez cet espace</a>.",
"name_mxid_share_room": "Invitez quelquun à partir de son nom, pseudo (comme <userId/>) ou <a>partagez ce salon</a>.",
"name_mxid_share_space": "Invitez quelquun grâce à son nom, nom dutilisateur (tel que <userId/>) ou <a>partagez cet espace</a>.",
"progress": {
"dont_close": "Ne fermez pas l\"application tant que l'opération est en cours",
"preparing": "Préparation des invitations..."
},
"recents_section": "Conversations récentes",
"room_failed_partial": "Nous avons envoyé les invitations, mais les personnes ci-dessous nont pas pu être invitées à rejoindre <RoomName/>",
"room_failed_partial_title": "Certaines invitations nont pas pu être envoyées",
@@ -2068,9 +2063,8 @@
"pinned_message_banner": {
"button_close_list": "Fermer la liste",
"button_view_all": "Voir tout",
"description": "Messages épinglés",
"go_to_newest_message": "Afficher le message épinglé dans la discussion et le plus récent ici",
"go_to_next_message": "Afficher le message épinglé dans la discussion et le plus ancien suivant ici",
"description": "Ce salon contient des messages épinglés. Cliquez pour les consulter.",
"go_to_message": "Afficher le message épinglé dans la discussion.",
"title": "<bold>%(index)s de %(length)s</bold> messages épinglés"
},
"read_topic": "Cliquer pour lire le sujet",
@@ -3513,7 +3507,7 @@
"unknown": "%(senderDisplayName)s a changé laccès des visiteurs en %(rule)s"
},
"m.room.history_visibility": {
"invited": "%(senderName)s a changé un paramètre : désormais les nouveaux membres pourront voir l'historique de conversation du salon à partir du moment où ils ont été invités.",
"invited": "%(senderName)s a rendu lhistorique visible à tous les membres du salon, depuis le moment où ils ont été invités.",
"joined": "%(senderName)s a rendu lhistorique visible à tous les membres du salon, à partir de leur arrivée.",
"shared": "%(senderName)s a rendu lhistorique visible à tous les membres du salon.",
"unknown": "%(senderName)s a rendu lhistorique visible à inconnu (%(visibility)s).",

View File

@@ -652,7 +652,6 @@
"poll_button_no_perms_description": "Nincs joga szavazást kezdeményezni ebben a szobában.",
"poll_button_no_perms_title": "Jogosultság szükséges",
"replying_title": "Válasz",
"room_unencrypted": "A szobában lévő üzenetek nincsenek végponttól végpontig titkosítva",
"room_upgraded_link": "A beszélgetés itt folytatódik.",
"room_upgraded_notice": "Ezt a szobát lecseréltük és nem aktív többé.",
"send_button_title": "Üzenet küldése",
@@ -1359,10 +1358,6 @@
"name_email_mxid_share_space": "Hívjon meg valakit a nevét, e-mail-címét vagy felhasználónevét (például <userId/>) megadva, vagy <a>ossza meg ezt a teret</a>.",
"name_mxid_share_room": "Hívjon meg valakit a nevét vagy felhasználónevét (például <userId/>) megadva, vagy <a>ossza meg ezt a szobát</a>.",
"name_mxid_share_space": "Hívjon meg valakit a nevét vagy felhasználónevét (például <userId/>) megadva, vagy <a>ossza meg ezt a teret</a>.",
"progress": {
"dont_close": "Ne zárja be az alkalmazást, amíg be nem fejezte.",
"preparing": "Meghívók előkészítése…"
},
"recents_section": "Legújabb beszélgetések",
"room_failed_partial": "Az alábbi embereket nem sikerül meghívni ide: <RoomName/>, de a többi meghívó elküldve",
"room_failed_partial_title": "Néhány meghívót nem sikerült elküldeni",
@@ -2056,6 +2051,7 @@
"button_close_list": "A lista bezárása",
"button_view_all": "Összes megtekintése",
"description": "Ez a szoba rögzített üzeneteket tartalmaz. Kattintson ide a megtekintésükhöz.",
"go_to_message": "Tekintse meg a rögzített üzenetet az idővonalon.",
"title": "<bold>%(index)s. / %(length)s </bold> rögzített üzenet"
},
"read_topic": "Kattintson a téma elolvasásához",

View File

@@ -917,7 +917,6 @@
},
"privacy_warning": "Pastikan tidak ada yang bisa melihat layar ini!",
"restoring": "Memulihkan kunci-kunci dari cadangan",
"security_key_label": "Kunci pemulihan",
"security_key_title": "Konfirmasi kunci pemulihan Anda"
},
"bootstrap_title": "Menyiapkan kunci",
@@ -1363,10 +1362,6 @@
"name_email_mxid_share_space": "Undang seseorang menggunakan namanya, alamat email, nama pengguna (seperti <userId/>) atau <a>bagikan space ini</a>.",
"name_mxid_share_room": "Undang seseorang menggunakan namanya, nama pengguna (seperti <userId/>) atau <a>bagikan ruangan ini</a>.",
"name_mxid_share_space": "Undang seseorang menggunakan namanya, nama pengguna (seperti <userId/>) atau <a>bagikan space ini</a>.",
"progress": {
"dont_close": "Jangan tutup aplikasi sampai selesai.",
"preparing": "Menyiapkan undangan..."
},
"recents_section": "Obrolan Terkini",
"room_failed_partial": "Kami telah mengirim yang lainnya, tetapi orang berikut ini tidak dapat diundang ke <RoomName/>",
"room_failed_partial_title": "Beberapa undangan tidak dapat dikirim",
@@ -1763,7 +1758,6 @@
},
"power_level": {
"admin": "Admin",
"creator": "Pemilik",
"custom": "Kustom (%(level)s)",
"custom_level": "Tingkat kustom",
"default": "Bawaan",
@@ -1915,7 +1909,6 @@
"thread_list": {
"context_menu_label": "Opsi utasan"
},
"title": "Panel kanan",
"video_room_chat": {
"title": "Obrolan"
}
@@ -2062,6 +2055,7 @@
"button_close_list": "Tutup daftar",
"button_view_all": "Lihat semua",
"description": "Ruangan ini memiliki pesan yang disematkan. Klik untuk melihatnya.",
"go_to_message": "Lihat pesan yang disematkan di lini masa.",
"title": "<bold>%(index)s dari %(length)s</bold> Pesan yang disematkan"
},
"read_topic": "Klik untuk membaca topik",
@@ -3395,7 +3389,6 @@
"unable_to_find": "Mencoba memuat titik spesifik di lini masa ruangan ini, tetapi tidak dapat menemukannya."
},
"m.audio": {
"audio_player": "Pemutar audio",
"error_downloading_audio": "Terjadi kesalahan mengunduh audio",
"error_processing_audio": "Terjadi kesalahan mengolah pesan suara",
"error_processing_voice_message": "Terjadi kesalahan mengolah pesan suara",

View File

@@ -654,7 +654,6 @@
"poll_button_no_perms_description": "Du har ikke tillatelse til å starte avstemninger i dette rommet.",
"poll_button_no_perms_title": "Tillatelse kreves",
"replying_title": "Svarer på",
"room_unencrypted": "Meldinger i dette rommet er ikke ende-til-ende krypterte",
"room_upgraded_link": "Samtalen fortsetter her.",
"room_upgraded_notice": "Dette rommet har blitt erstattet og er ikke lenger aktivt.",
"send_button_title": "Send melding",
@@ -2069,9 +2068,8 @@
"pinned_message_banner": {
"button_close_list": "Lukk liste",
"button_view_all": "Vis alle",
"description": "Festede meldinger",
"go_to_newest_message": "Se den festede meldingen i tidslinjen og den nyeste festede meldingen her",
"go_to_next_message": "Se den festede meldingen i tidslinjen og den nest eldste festede meldingen her",
"description": "Dette rommet har festede meldinger. Klikk for å se dem.",
"go_to_message": "Vis den festede meldingen i tidslinjen.",
"title": "<bold>%(index)s av %(length)s</bold> festede meldinger"
},
"read_topic": "Klikk for å lese emnet",

View File

@@ -2073,6 +2073,7 @@
"button_close_list": "Zamknij listę",
"button_view_all": "Pokaż wszystkie",
"description": "Ten pokój ma przypięte wiadomości. Kliknij, aby je wyświetlić.",
"go_to_message": "Wyświetl przypiętą wiadomość na osi czasu.",
"title": "<bold>%(index)s z %(length)s</bold> przypiętych wiadomości"
},
"read_topic": "Kliknij, aby przeczytać temat",

View File

@@ -1999,6 +1999,7 @@
"button_close_list": "Fechar lista",
"button_view_all": "Ver tudo",
"description": "Esta sala tem mensagens afixadas. Clica para as veres.",
"go_to_message": "Visualiza a mensagem fixada na linha do tempo.",
"title": "<bold>%(index)s de %(length)s</bold> Mensagens fixadas"
},
"read_topic": "Clica para ler o tópico",

View File

@@ -1916,7 +1916,6 @@
"thread_list": {
"context_menu_label": "Opções de tópico"
},
"title": "Painel direito",
"video_room_chat": {
"title": "Bate-papo"
}
@@ -2064,6 +2063,7 @@
"button_close_list": "Fechar lista",
"button_view_all": "Ver tudo",
"description": "Esta sala tem mensagens fixadas. Clique para visualizá-las.",
"go_to_message": "Veja a mensagem fixada no histórico.",
"title": "<bold>%(index)s de %(length)s </bold> mensagens fixadas"
},
"read_topic": "Clique para ler o tópico",

View File

@@ -2064,6 +2064,7 @@
"button_close_list": "Закрыть список",
"button_view_all": "Посмотреть все",
"description": "В этой комнате есть закрепленные сообщения. Нажмите, чтобы просмотреть их.",
"go_to_message": "Показать прикрепленное сообщение на временной шкале.",
"title": "<bold>%(index)s из %(length)s</bold> Закрепленные сообщения"
},
"read_topic": "Нажмите, чтобы увидеть тему",

View File

@@ -661,7 +661,6 @@
"poll_button_no_perms_description": "Nemáte povolenie spúšťať ankety v tejto miestnosti.",
"poll_button_no_perms_title": "Vyžaduje sa povolenie",
"replying_title": "Odpoveď",
"room_unencrypted": "Správy v tejto miestnosti nie sú end-to-end šifrované",
"room_upgraded_link": "Konverzácia pokračuje tu.",
"room_upgraded_notice": "Táto miestnosť bola nahradená a nie je viac aktívna.",
"send_button_title": "Odoslať správu",
@@ -2097,6 +2096,7 @@
"button_close_list": "Zatvoriť zoznam",
"button_view_all": "Zobraziť všetko",
"description": "Táto miestnosť má pripnuté správy. Kliknutím ich zobrazíte.",
"go_to_message": "Zobraziť pripnuté správy na časovej osi.",
"title": "<bold>%(index)s z %(length)s</bold> pripnutých správ"
},
"read_topic": "Kliknutím si prečítate tému",

View File

@@ -2068,6 +2068,7 @@
"button_close_list": "Stäng listan",
"button_view_all": "Visa alla",
"description": "Det här rummet har fästa meddelanden. Klicka för att se dem.",
"go_to_message": "Visa det fästa meddelandet på tidslinjen.",
"title": "<bold>%(index)s av %(length)s</bold> fästa meddelanden"
},
"read_topic": "Klicka för att läsa ämne",

View File

@@ -1997,6 +1997,7 @@
"button_close_list": "Listeyi kapat",
"button_view_all": "Tümünü görüntüle",
"description": "Bu odada sabitlenmiş mesajlar var. Görüntülemek için tıklayın.",
"go_to_message": "Sabitlenmiş mesajı zaman çizelgesinde görüntüle.",
"title": "<bold>%(index)s / %(length)s</bold> Sabitlenmiş mesajlar"
},
"read_topic": "Başlığı okumak için tıklayın",

View File

@@ -2070,6 +2070,7 @@
"button_close_list": "Закрити список",
"button_view_all": "Подивитись все",
"description": "У цій кімнаті є закріплені повідомлення. Натисніть, щоб переглянути їх.",
"go_to_message": "Переглянути закріплене повідомлення у стрічці часу.",
"title": "<bold>%(index)s з %(length)s</bold> закріплених повідомлень"
},
"read_topic": "Натисніть, щоб побачити тему",

View File

@@ -13,7 +13,7 @@ import SdkConfig from "../SdkConfig";
import Modal from "../Modal";
import { IntegrationManagerInstance, Kind } from "./IntegrationManagerInstance";
import IntegrationsImpossibleDialog from "../components/views/dialogs/IntegrationsImpossibleDialog";
import { IntegrationsDisabledDialog } from "../components/views/dialogs/IntegrationsDisabledDialog";
import IntegrationsDisabledDialog from "../components/views/dialogs/IntegrationsDisabledDialog";
import WidgetUtils from "../utils/WidgetUtils";
import { MatrixClientPeg } from "../MatrixClientPeg";

View File

@@ -23,12 +23,14 @@ import { type IWidgetApiRequest, type ClientWidgetApi, type IWidgetData } from "
import {
type MatrixRTCSession,
MatrixRTCSessionEvent,
type CallMembership,
MatrixRTCSessionManagerEvents,
} from "matrix-js-sdk/src/matrixrtc";
import type EventEmitter from "events";
import type { IApp } from "../stores/WidgetStore";
import SettingsStore from "../settings/SettingsStore";
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../MediaDeviceHandler";
import { timeout } from "../utils/promise";
import WidgetUtils from "../utils/WidgetUtils";
import { WidgetType } from "../widgets/WidgetType";
@@ -191,6 +193,18 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
*/
public abstract clean(): Promise<void>;
/**
* Contacts the widget to connect to the call or prompt the user to connect to the call.
* @param {MediaDeviceInfo | null} audioInput The audio input to use, or
* null to start muted.
* @param {MediaDeviceInfo | null} audioInput The video input to use, or
* null to start muted.
*/
protected abstract performConnection(
audioInput: MediaDeviceInfo | null,
videoInput: MediaDeviceInfo | null,
): Promise<void>;
/**
* Contacts the widget to disconnect from the call.
*/
@@ -198,10 +212,28 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
/**
* Starts the communication between the widget and the call.
* The widget associated with the call must be active for this to succeed.
* The call then waits for the necessary requirements to actually perform the connection
* or connects right away depending on the call type. (Jitsi, Legacy, ElementCall...)
* It uses the media devices set in MediaDeviceHandler.
* The widget associated with the call must be active
* for this to succeed.
* Only call this if the call state is: ConnectionState.Disconnected.
*/
public async start(): Promise<void> {
const { [MediaDeviceKindEnum.AudioInput]: audioInputs, [MediaDeviceKindEnum.VideoInput]: videoInputs } =
(await MediaDeviceHandler.getDevices())!;
let audioInput: MediaDeviceInfo | null = null;
if (!MediaDeviceHandler.startWithAudioMuted) {
const deviceId = MediaDeviceHandler.getAudioInput();
audioInput = audioInputs.find((d) => d.deviceId === deviceId) ?? audioInputs[0] ?? null;
}
let videoInput: MediaDeviceInfo | null = null;
if (!MediaDeviceHandler.startWithVideoMuted) {
const deviceId = MediaDeviceHandler.getVideoInput();
videoInput = videoInputs.find((d) => d.deviceId === deviceId) ?? videoInputs[0] ?? null;
}
const messagingStore = WidgetMessagingStore.instance;
this.messaging = messagingStore.getMessagingForUid(this.widgetUid) ?? null;
if (!this.messaging) {
@@ -222,23 +254,13 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
throw new Error(`Failed to bind call widget in room ${this.roomId}: ${e}`);
}
}
}
await this.performConnection(audioInput, videoInput);
protected setConnected(): void {
this.room.on(RoomEvent.MyMembership, this.onMyMembership);
window.addEventListener("beforeunload", this.beforeUnload);
this.connectionState = ConnectionState.Connected;
}
/**
* Manually marks the call as disconnected.
*/
protected setDisconnected(): void {
this.room.off(RoomEvent.MyMembership, this.onMyMembership);
window.removeEventListener("beforeunload", this.beforeUnload);
this.connectionState = ConnectionState.Disconnected;
}
/**
* Disconnects the user from the call.
*/
@@ -251,6 +273,15 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
this.close();
}
/**
* Manually marks the call as disconnected.
*/
public setDisconnected(): void {
this.room.off(RoomEvent.MyMembership, this.onMyMembership);
window.removeEventListener("beforeunload", this.beforeUnload);
this.connectionState = ConnectionState.Disconnected;
}
/**
* Stops further communication with the widget and tells the UI to close.
*/
@@ -436,10 +467,66 @@ export class JitsiCall extends Call {
});
}
public async start(): Promise<void> {
await super.start();
this.messaging!.on(`action:${ElementWidgetActions.JoinCall}`, this.onJoin);
protected async performConnection(
audioInput: MediaDeviceInfo | null,
videoInput: MediaDeviceInfo | null,
): Promise<void> {
// Ensure that the messaging doesn't get stopped while we're waiting for responses
const dontStopMessaging = new Promise<void>((resolve, reject) => {
const messagingStore = WidgetMessagingStore.instance;
const listener = (uid: string): void => {
if (uid === this.widgetUid) {
cleanup();
reject(new Error("Messaging stopped"));
}
};
const done = (): void => {
cleanup();
resolve();
};
const cleanup = (): void => {
messagingStore.off(WidgetMessagingStoreEvent.StopMessaging, listener);
this.off(CallEvent.ConnectionState, done);
};
messagingStore.on(WidgetMessagingStoreEvent.StopMessaging, listener);
this.on(CallEvent.ConnectionState, done);
});
// Empirically, it's possible for Jitsi Meet to crash instantly at startup,
// sending a hangup event that races with the rest of this method, so we need
// to add the hangup listener now rather than later
this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
// Actually perform the join
const response = waitForEvent(
this.messaging!,
`action:${ElementWidgetActions.JoinCall}`,
(ev: CustomEvent<IWidgetApiRequest>) => {
ev.preventDefault();
this.messaging!.transport.reply(ev.detail, {}); // ack
return true;
},
);
const request = this.messaging!.transport.send(ElementWidgetActions.JoinCall, {
audioInput: audioInput?.label ?? null,
videoInput: videoInput?.label ?? null,
});
try {
await Promise.race([Promise.all([request, response]), dontStopMessaging]);
} catch (e) {
// If it timed out, clean up our advance preparations
this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
if (this.messaging!.transport.ready) {
// The messaging still exists, which means Jitsi might still be going in the background
this.messaging!.transport.send(ElementWidgetActions.HangupCall, { force: true });
}
throw new Error(`Failed to join call in room ${this.roomId}: ${e}`);
}
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Dock, this.onDock);
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Undock, this.onUndock);
}
@@ -462,17 +549,18 @@ export class JitsiCall extends Call {
}
}
public close(): void {
this.messaging!.off(`action:${ElementWidgetActions.JoinCall}`, this.onJoin);
this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
public setDisconnected(): void {
// During tests this.messaging can be undefined
this.messaging?.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Dock, this.onDock);
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Undock, this.onUndock);
super.close();
super.setDisconnected();
}
public destroy(): void {
this.room.off(RoomStateEvent.Update, this.onRoomState);
this.off(CallEvent.ConnectionState, this.onConnectionState);
this.on(CallEvent.ConnectionState, this.onConnectionState);
if (this.participantsExpirationTimer !== null) {
clearTimeout(this.participantsExpirationTimer);
this.participantsExpirationTimer = null;
@@ -524,21 +612,27 @@ export class JitsiCall extends Call {
await this.messaging!.transport.send(ElementWidgetActions.SpotlightLayout, {});
};
private readonly onJoin = (ev: CustomEvent<IWidgetApiRequest>): void => {
ev.preventDefault();
this.messaging!.transport.reply(ev.detail, {}); // ack
this.setConnected();
};
private readonly onHangup = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
// If we're already in the middle of a client-initiated disconnection,
// ignore the event
if (this.connectionState === ConnectionState.Disconnecting) return;
ev.preventDefault();
// In case this hangup is caused by Jitsi Meet crashing at startup,
// wait for the connection event in order to avoid racing
if (this.connectionState === ConnectionState.Disconnected) {
await waitForEvent(this, CallEvent.ConnectionState);
}
this.messaging!.transport.reply(ev.detail, {}); // ack
this.setDisconnected();
if (!isVideoRoom(this.room)) this.close();
this.close();
// In video rooms we immediately want to restart the call after hangup
// The lobby will be shown again and it connects to all signals from Jitsi.
if (isVideoRoom(this.room)) {
this.start();
}
};
}
@@ -766,38 +860,54 @@ export class ElementCall extends Call {
ElementCall.createOrGetCallWidget(room.roomId, room.client, skipLobby, isVideoRoom(room));
}
public async start(): Promise<void> {
await super.start();
this.messaging!.on(`action:${ElementWidgetActions.JoinCall}`, this.onJoin);
protected async performConnection(
audioInput: MediaDeviceInfo | null,
videoInput: MediaDeviceInfo | null,
): Promise<void> {
this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
this.messaging!.on(`action:${ElementWidgetActions.Close}`, this.onClose);
this.messaging!.once(`action:${ElementWidgetActions.Close}`, this.onClose);
this.messaging!.on(`action:${ElementWidgetActions.DeviceMute}`, this.onDeviceMute);
// TODO: Watch for a widget action telling us that the join button was clicked, rather than
// relying on the MatrixRTC session state, to set the state to connecting
const session = this.client.matrixRTC.getActiveRoomSession(this.room);
if (session) {
await waitForEvent(
session,
MatrixRTCSessionEvent.MembershipsChanged,
(_, newMemberships: CallMembership[]) =>
newMemberships.some((m) => m.sender === this.client.getUserId()),
false, // allow user to wait as long as they want (no timeout)
);
} else {
await waitForEvent(
this.client.matrixRTC,
MatrixRTCSessionManagerEvents.SessionStarted,
(roomId: string, session: MatrixRTCSession) =>
this.session.callId === session.callId && roomId === this.roomId,
false, // allow user to wait as long as they want (no timeout)
);
}
}
protected async performDisconnection(): Promise<void> {
const response = waitForEvent(
this.messaging!,
`action:${ElementWidgetActions.HangupCall}`,
(ev: CustomEvent<IWidgetApiRequest>) => {
ev.preventDefault();
this.messaging!.transport.reply(ev.detail, {}); // ack
return true;
},
);
const request = this.messaging!.transport.send(ElementWidgetActions.HangupCall, {});
try {
await Promise.all([request, response]);
await this.messaging!.transport.send(ElementWidgetActions.HangupCall, {});
await waitForEvent(
this.session,
MatrixRTCSessionEvent.MembershipsChanged,
(_, newMemberships: CallMembership[]) =>
!newMemberships.some((m) => m.sender === this.client.getUserId()),
);
} catch (e) {
throw new Error(`Failed to hangup call in room ${this.roomId}: ${e}`);
}
}
public close(): void {
this.messaging!.off(`action:${ElementWidgetActions.JoinCall}`, this.onJoin);
public setDisconnected(): void {
this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
this.messaging!.off(`action:${ElementWidgetActions.Close}`, this.onClose);
this.messaging!.off(`action:${ElementWidgetActions.DeviceMute}`, this.onDeviceMute);
super.close();
super.setDisconnected();
}
public destroy(): void {
@@ -844,27 +954,22 @@ export class ElementCall extends Call {
this.messaging!.transport.reply(ev.detail, {}); // ack
};
private readonly onJoin = (ev: CustomEvent<IWidgetApiRequest>): void => {
ev.preventDefault();
this.messaging!.transport.reply(ev.detail, {}); // ack
this.setConnected();
};
private readonly onHangup = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
// If we're already in the middle of a client-initiated disconnection,
// ignore the event
if (this.connectionState === ConnectionState.Disconnecting) return;
ev.preventDefault();
this.messaging!.transport.reply(ev.detail, {}); // ack
this.setDisconnected();
// In video rooms we immediately want to reconnect after hangup
// This starts the lobby again and connects to all signals from EC.
if (isVideoRoom(this.room)) {
this.start();
}
};
private readonly onClose = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
ev.preventDefault();
this.messaging!.transport.reply(ev.detail, {}); // ack
this.setDisconnected(); // Just in case the widget forgot to emit a hangup action (maybe it's in an error state)
this.close(); // User is done with the call; tell the UI to close it
// User is done with the call; tell the UI to close it
this.close();
};
public clean(): Promise<void> {

View File

@@ -9,31 +9,33 @@ import { type NavigationApi as INavigationApi } from "@element-hq/element-web-mo
import { navigateToPermalink } from "../utils/permalinks/navigator.ts";
import { parsePermalink } from "../utils/permalinks/Permalinks.ts";
import { getCachedRoomIDForAlias } from "../RoomAliasCache.ts";
import { MatrixClientPeg } from "../MatrixClientPeg.ts";
import dispatcher from "../dispatcher/dispatcher.ts";
import { Action } from "../dispatcher/actions.ts";
import type { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload.ts";
import SettingsStore from "../settings/SettingsStore.ts";
export class NavigationApi implements INavigationApi {
public async toMatrixToLink(link: string, join = false): Promise<void> {
navigateToPermalink(link);
const parts = parsePermalink(link);
if (parts?.roomIdOrAlias) {
if (parts.roomIdOrAlias.startsWith("#")) {
dispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_alias: parts.roomIdOrAlias,
via_servers: parts.viaServers ?? undefined,
auto_join: join,
metricsTrigger: undefined,
});
} else {
dispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: parts.roomIdOrAlias,
via_servers: parts.viaServers ?? undefined,
auto_join: join,
metricsTrigger: undefined,
if (parts?.roomIdOrAlias && join) {
let roomId: string | undefined = parts.roomIdOrAlias;
if (roomId.startsWith("#")) {
roomId = getCachedRoomIDForAlias(parts.roomIdOrAlias);
if (!roomId) {
// alias resolution failed
const result = await MatrixClientPeg.safeGet().getRoomIdForAlias(parts.roomIdOrAlias);
roomId = result.room_id;
}
}
if (roomId) {
dispatcher.dispatch({
action: Action.JoinRoom,
canAskToJoin: SettingsStore.getValue("feature_ask_to_join"),
roomId,
});
}
}

View File

@@ -28,12 +28,13 @@ import dispatcher from "../dispatcher/dispatcher";
import { navigateToPermalink } from "../utils/permalinks/navigator";
import { parsePermalink } from "../utils/permalinks/Permalinks";
import { MatrixClientPeg } from "../MatrixClientPeg";
import { getCachedRoomIDForAlias } from "../RoomAliasCache";
import { Action } from "../dispatcher/actions";
import { type OverwriteLoginPayload } from "../dispatcher/payloads/OverwriteLoginPayload";
import { type ActionPayload } from "../dispatcher/payloads";
import SettingsStore from "../settings/SettingsStore";
import WidgetStore, { type IApp } from "../stores/WidgetStore";
import { type Container, WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
import type { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload.ts";
/**
* Glue between the `ModuleApi` interface and the react-sdk. Anticipates one instance
@@ -182,22 +183,28 @@ export class ProxiedModuleApi implements ModuleApi {
navigateToPermalink(uri);
const parts = parsePermalink(uri);
if (parts?.roomIdOrAlias) {
if (parts.roomIdOrAlias.startsWith("#")) {
dispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_alias: parts.roomIdOrAlias,
via_servers: parts.viaServers ?? undefined,
auto_join: andJoin ?? false,
metricsTrigger: undefined,
});
} else {
dispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: parts.roomIdOrAlias,
via_servers: parts.viaServers ?? undefined,
auto_join: andJoin ?? false,
metricsTrigger: undefined,
if (parts?.roomIdOrAlias && andJoin) {
let roomId: string | undefined = parts.roomIdOrAlias;
let servers = parts.viaServers;
if (roomId.startsWith("#")) {
roomId = getCachedRoomIDForAlias(parts.roomIdOrAlias);
if (!roomId) {
// alias resolution failed
const result = await MatrixClientPeg.safeGet().getRoomIdForAlias(parts.roomIdOrAlias);
roomId = result.room_id;
if (!servers) servers = result.servers; // use provided servers first, if available
}
}
dispatcher.dispatch({
action: Action.ViewRoom,
room_id: roomId,
via_servers: servers,
});
if (andJoin) {
dispatcher.dispatch({
action: Action.JoinRoom,
canAskToJoin: SettingsStore.getValue("feature_ask_to_join"),
});
}
}

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React, { type ReactNode } from "react";
import { STABLE_MSC4133_EXTENDED_PROFILES, UNSTABLE_MSC4133_EXTENDED_PROFILES } from "matrix-js-sdk/src/matrix";
import { UNSTABLE_MSC4133_EXTENDED_PROFILES } from "matrix-js-sdk/src/matrix";
import { type MediaPreviewConfig } from "../@types/media_preview.ts";
// Import i18n.tsx instead of languageHandler to avoid circular deps
@@ -844,7 +844,7 @@ export const SETTINGS: Settings = {
controller: new ServerSupportUnstableFeatureController(
"userTimezonePublish",
defaultWatchManager,
[[UNSTABLE_MSC4133_EXTENDED_PROFILES], [STABLE_MSC4133_EXTENDED_PROFILES]],
[[UNSTABLE_MSC4133_EXTENDED_PROFILES]],
undefined,
_td("labs|extended_profiles_msc_support"),
),

View File

@@ -340,7 +340,7 @@ export default class SettingsStore {
}
/**
* Retrieves the internationalised reason a setting is disabled if one is assigned.
* Retrieves the reason a setting is disabled if one is assigned.
* If a setting is not disabled, or no reason is given by the `SettingController`,
* this will return undefined.
* @param {string} settingName The setting to look up.

View File

@@ -11,7 +11,6 @@ import MatrixClientBackedController from "./MatrixClientBackedController";
import { type WatchManager } from "../WatchManager";
import SettingsStore from "../SettingsStore";
import { type SettingKey } from "../Settings.tsx";
import { _t, type TranslationKey } from "../../languageHandler.tsx";
/**
* Disables a given setting if the server unstable feature it requires is not supported
@@ -34,7 +33,7 @@ export default class ServerSupportUnstableFeatureController extends MatrixClient
private readonly watchers: WatchManager,
private readonly unstableFeatureGroups: string[][],
private readonly stableVersion?: string,
private readonly disabledMessage?: TranslationKey,
private readonly disabledMessage?: string,
private readonly forcedValue: any = false,
) {
super();
@@ -97,7 +96,7 @@ export default class ServerSupportUnstableFeatureController extends MatrixClient
public get settingDisabled(): boolean | string {
if (this.disabled) {
return this.disabledMessage ? _t(this.disabledMessage) : true;
return this.disabledMessage ?? true;
}
return false;
}

View File

@@ -17,7 +17,7 @@ import { Features } from "../settings/Settings";
/**
* The features are shown in the array order.
*/
const FEATURES = ["newRoomList_intro", "newRoomList_sort", "newRoomList_filter", "newRoomList_settings"] as const;
const FEATURES = ["pinningMessageList"] as const;
/**
* All the features that can be shown in the release announcements.
*/

View File

@@ -26,7 +26,7 @@ import { type MatrixDispatcher } from "../dispatcher/dispatcher";
import { MatrixClientPeg } from "../MatrixClientPeg";
import Modal from "../Modal";
import { _t } from "../languageHandler";
import { getCachedRoomIdForAlias, storeRoomAliasInCache } from "../RoomAliasCache";
import { getCachedRoomIDForAlias, storeRoomAliasInCache } from "../RoomAliasCache";
import { Action } from "../dispatcher/actions";
import { retry } from "../utils/promise";
import { TimelineRenderingType } from "../contexts/RoomContext";
@@ -438,7 +438,6 @@ export class RoomViewStore extends EventEmitter {
action: Action.JoinRoom,
roomId: payload.room_id,
metricsTrigger: payload.metricsTrigger as JoinRoomPayload["metricsTrigger"],
canAskToJoin: SettingsStore.getValue("feature_ask_to_join"),
});
}
@@ -446,16 +445,10 @@ export class RoomViewStore extends EventEmitter {
await setMarkedUnreadState(room, MatrixClientPeg.safeGet(), false);
}
} else if (payload.room_alias) {
let roomId: string;
let viaServers: string[] | undefined;
// Try the room alias to room ID navigation cache first to avoid
// blocking room navigation on the homeserver.
const cachedResult = getCachedRoomIdForAlias(payload.room_alias);
if (cachedResult) {
roomId = cachedResult.roomId;
viaServers = cachedResult.viaServers;
} else {
let roomId = getCachedRoomIDForAlias(payload.room_alias);
if (!roomId) {
// Room alias cache miss, so let's ask the homeserver. Resolve the alias
// and then do a second dispatch with the room ID acquired.
this.setState({
@@ -474,9 +467,8 @@ export class RoomViewStore extends EventEmitter {
});
try {
const result = await MatrixClientPeg.safeGet().getRoomIdForAlias(payload.room_alias);
storeRoomAliasInCache(payload.room_alias, result.room_id, result.servers);
storeRoomAliasInCache(payload.room_alias, result.room_id);
roomId = result.room_id;
viaServers = result.servers;
} catch (err) {
logger.error("RVS failed to get room id for alias: ", err);
this.dis?.dispatch<ViewRoomErrorPayload>({
@@ -493,7 +485,6 @@ export class RoomViewStore extends EventEmitter {
this.dis?.dispatch({
...payload,
room_id: roomId,
via_servers: viaServers,
});
}
}
@@ -518,13 +509,12 @@ export class RoomViewStore extends EventEmitter {
joining: true,
});
// take a copy of roomAlias, roomId & viaServers as they may change by the time the join is complete
const { roomAlias, roomId = payload.roomId, viaServers = [] } = this.state;
// prefer the room alias if we have one as it allows joining over federation even with no viaServers
const address = roomAlias || roomId!;
// take a copy of roomAlias & roomId as they may change by the time the join is complete
const { roomAlias, roomId } = this.state;
const address = payload.roomId || roomAlias || roomId!;
const joinOpts: IJoinRoomOpts = {
viaServers,
viaServers: this.state.viaServers || [],
...(payload.opts ?? {}),
};
if (SettingsStore.getValue("feature_share_history_on_invite")) {
@@ -557,7 +547,7 @@ export class RoomViewStore extends EventEmitter {
canAskToJoin: payload.canAskToJoin,
});
if (payload.canAskToJoin && err instanceof MatrixError && err.httpStatus === 403) {
if (payload.canAskToJoin) {
this.dis?.dispatch({ action: Action.PromptAskToJoin });
}
}

View File

@@ -49,7 +49,7 @@ import {
UPDATE_SUGGESTED_ROOMS,
UPDATE_TOP_LEVEL_SPACES,
} from ".";
import { getCachedRoomIdForAlias } from "../../RoomAliasCache";
import { getCachedRoomIDForAlias } from "../../RoomAliasCache";
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
import {
flattenSpaceHierarchyWithCache,
@@ -1249,8 +1249,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<EmptyObject> {
let roomId = payload.room_id;
if (payload.room_alias && !roomId) {
const result = getCachedRoomIdForAlias(payload.room_alias);
if (result) roomId = result.roomId;
roomId = getCachedRoomIDForAlias(payload.room_alias);
}
if (!roomId) return; // we'll get re-fired with the room ID shortly

View File

@@ -117,7 +117,7 @@ export class MediaEventHelper implements IDestroyable {
/**
* Determine if the media event in question supports being hidden in the timeline.
* @param event Any matrix event.
* @returns `true` if the media can be hidden, otherwise `false`.
* @returns `true` if the media can be hidden, otherwise false.
*/
public static canHide(event: MatrixEvent): boolean {
if (!event) return false;

View File

@@ -79,6 +79,7 @@ export class MockedCall extends Call {
// No action needed for any of the following methods since this is just a mock
public async clean(): Promise<void> {}
// Public to allow spying
public async performConnection(): Promise<void> {}
public async performDisconnection(): Promise<void> {}
public destroy() {

View File

@@ -585,42 +585,4 @@ describe("LegacyCallHandler without third party protocols", () => {
expect(mockAudioBufferSourceNode.start).not.toHaveBeenCalled();
});
});
describe("sidebar state", () => {
const roomId = "test-room-id";
it("should default to showing sidebar", () => {
const call = new MatrixCall({
client: MatrixClientPeg.safeGet(),
roomId,
});
const cli = MatrixClientPeg.safeGet();
cli.emit(CallEventHandlerEvent.Incoming, call);
expect(callHandler.isCallSidebarShown(call.callId)).toEqual(true);
});
it("should remember sidebar state per call", () => {
const call = new MatrixCall({
client: MatrixClientPeg.safeGet(),
roomId,
});
const cli = MatrixClientPeg.safeGet();
cli.emit(CallEventHandlerEvent.Incoming, call);
expect(callHandler.isCallSidebarShown(call.callId)).toEqual(true);
callHandler.setCallSidebarShown(call.callId, false);
expect(callHandler.isCallSidebarShown(call.callId)).toEqual(false);
call.emit(CallEvent.Hangup, call);
const call2 = new MatrixCall({
client: MatrixClientPeg.safeGet(),
roomId,
});
cli.emit(CallEventHandlerEvent.Incoming, call2);
expect(callHandler.isCallSidebarShown(call2.callId)).toEqual(true);
});
});
});

View File

@@ -664,24 +664,24 @@ describe("TextForEvent", () => {
["the legacy key", { topic: "My topic" }, { result: '@a changed the topic to "My topic".' }],
[
"the legacy key with an empty m.topic key",
{ "topic": "My topic", "m.topic": { "m.text": [] } },
{ "topic": "My topic", "m.topic": [] },
{ result: '@a changed the topic to "My topic".' },
],
[
"the m.topic key",
{ "topic": "Ignore this", "m.topic": { "m.text": [{ mimetype: "text/plain", body: "My topic" }] } },
{ "topic": "Ignore this", "m.topic": [{ mimetype: "text/plain", body: "My topic" }] },
{ result: '@a changed the topic to "My topic".' },
],
[
"the m.topic key and the legacy key undefined",
{ "topic": undefined, "m.topic": { "m.text": [{ mimetype: "text/plain", body: "My topic" }] } },
{ "topic": undefined, "m.topic": [{ mimetype: "text/plain", body: "My topic" }] },
{ result: '@a changed the topic to "My topic".' },
],
["the legacy key undefined", { topic: undefined }, { result: "@a removed the topic." }],
["the legacy key empty string", { topic: "" }, { result: "@a removed the topic." }],
[
"both the legacy and new keys removed",
{ "topic": undefined, "m.topic": { "m.text": [] } },
{ "topic": undefined, "m.topic": [] },
{ result: "@a removed the topic." },
],
];

View File

@@ -7,7 +7,6 @@ exports[`dialogTermsInteractionCallback should render a dialog with the expected
class=""
data-focus-lock-disabled="false"
role="dialog"
tabindex="-1"
>
<div
class="mx_Dialog_header"

View File

@@ -12,7 +12,6 @@ exports[`<NewRecoveryMethodDialog /> when key backup is disabled 1`] = `
class="mx_KeyBackupFailedDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
tabindex="-1"
>
<div
class="mx_Dialog_header"
@@ -84,7 +83,6 @@ exports[`<NewRecoveryMethodDialog /> when key backup is enabled 1`] = `
class="mx_KeyBackupFailedDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
tabindex="-1"
>
<div
class="mx_Dialog_header"

View File

@@ -23,7 +23,7 @@ describe("ReleaseAnnouncement", () => {
function renderReleaseAnnouncement() {
return render(
<ReleaseAnnouncement
feature="newRoomList_intro"
feature="pinningMessageList"
header="header"
description="description"
closeLabel="close"

View File

@@ -388,7 +388,6 @@ exports[`<MatrixChat /> with an existing session onAction() room actions leave_r
class="mx_QuestionDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
tabindex="-1"
>
<div
class="mx_Dialog_header"
@@ -445,7 +444,6 @@ exports[`<MatrixChat /> with an existing session onAction() room actions leave_r
class="mx_QuestionDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
tabindex="-1"
>
<div
class="mx_Dialog_header"

View File

@@ -59,7 +59,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
style="--cpd-icon-button-size: 100%;"
>
<svg
aria-labelledby="«rh5»"
aria-labelledby="«rg4»"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@@ -75,7 +75,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
<button
aria-disabled="false"
aria-label="Voice call"
aria-labelledby="«rha»"
aria-labelledby="«rg9»"
class="_icon-button_1pz9o_8"
data-kind="primary"
role="button"
@@ -101,7 +101,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
</button>
<button
aria-label="Threads"
aria-labelledby="«rhf»"
aria-labelledby="«rge»"
class="_icon-button_1pz9o_8"
data-kind="primary"
role="button"
@@ -128,7 +128,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
</button>
<button
aria-label="Room info"
aria-labelledby="«rhk»"
aria-labelledby="«rgj»"
class="_icon-button_1pz9o_8"
data-kind="primary"
role="button"
@@ -158,7 +158,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
>
<div
aria-label="2 members"
aria-labelledby="«rhp»"
aria-labelledby="«rgo»"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
@@ -278,7 +278,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
style="--cpd-icon-button-size: 100%;"
>
<svg
aria-labelledby="«ri3»"
aria-labelledby="«rh2»"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@@ -294,7 +294,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
<button
aria-disabled="false"
aria-label="Voice call"
aria-labelledby="«ri8»"
aria-labelledby="«rh7»"
class="_icon-button_1pz9o_8"
data-kind="primary"
role="button"
@@ -320,7 +320,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
</button>
<button
aria-label="Threads"
aria-labelledby="«rid»"
aria-labelledby="«rhc»"
class="_icon-button_1pz9o_8"
data-kind="primary"
role="button"
@@ -347,7 +347,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
</button>
<button
aria-label="Room info"
aria-labelledby="«rii»"
aria-labelledby="«rhh»"
class="_icon-button_1pz9o_8"
data-kind="primary"
role="button"
@@ -377,7 +377,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
>
<div
aria-label="2 members"
aria-labelledby="«rin»"
aria-labelledby="«rhm»"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
@@ -583,7 +583,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
style="--cpd-icon-button-size: 100%;"
>
<svg
aria-labelledby="«rcd»"
aria-labelledby="«rbo»"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@@ -599,7 +599,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
<button
aria-disabled="false"
aria-label="Voice call"
aria-labelledby="«rci»"
aria-labelledby="«rbt»"
class="_icon-button_1pz9o_8"
data-kind="primary"
role="button"
@@ -625,7 +625,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
</button>
<button
aria-label="Threads"
aria-labelledby="«rcn»"
aria-labelledby="«rc2»"
class="_icon-button_1pz9o_8"
data-kind="primary"
role="button"
@@ -652,7 +652,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
</button>
<button
aria-label="Room info"
aria-labelledby="«rcs»"
aria-labelledby="«rc7»"
class="_icon-button_1pz9o_8"
data-kind="primary"
role="button"
@@ -682,7 +682,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
>
<div
aria-label="2 members"
aria-labelledby="«rd1»"
aria-labelledby="«rcc»"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
@@ -800,8 +800,6 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
class="mx_MessageComposer_e2eIconWrapper"
>
<svg
aria-label="Messages in this room are not end-to-end encrypted"
aria-labelledby="«rda»"
class="mx_E2EIcon mx_MessageComposer_e2eIcon"
color="var(--cpd-color-icon-info-primary)"
fill="currentColor"
@@ -984,7 +982,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
style="--cpd-icon-button-size: 100%;"
>
<svg
aria-labelledby="«rep»"
aria-labelledby="«rdu»"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@@ -1000,7 +998,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
<button
aria-disabled="false"
aria-label="Voice call"
aria-labelledby="«reu»"
aria-labelledby="«re3»"
class="_icon-button_1pz9o_8"
data-kind="primary"
role="button"
@@ -1026,7 +1024,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
</button>
<button
aria-label="Threads"
aria-labelledby="«rf3»"
aria-labelledby="«re8»"
class="_icon-button_1pz9o_8"
data-kind="primary"
role="button"
@@ -1053,7 +1051,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
</button>
<button
aria-label="Room info"
aria-labelledby="«rf8»"
aria-labelledby="«red»"
class="_icon-button_1pz9o_8"
data-kind="primary"
role="button"
@@ -1083,7 +1081,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
>
<div
aria-label="2 members"
aria-labelledby="«rfd»"
aria-labelledby="«rei»"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
@@ -1196,8 +1194,6 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
class="mx_MessageComposer_e2eIconWrapper"
>
<svg
aria-label="Messages in this room are not end-to-end encrypted"
aria-labelledby="«rfm»"
class="mx_E2EIcon mx_MessageComposer_e2eIcon"
color="var(--cpd-color-icon-info-primary)"
fill="currentColor"
@@ -1466,7 +1462,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
style="--cpd-icon-button-size: 100%;"
>
<svg
aria-labelledby="«r2j»"
aria-labelledby="«r2c»"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@@ -1482,7 +1478,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
<button
aria-disabled="false"
aria-label="Voice call"
aria-labelledby="«r2o»"
aria-labelledby="«r2h»"
class="_icon-button_1pz9o_8"
data-kind="primary"
role="button"
@@ -1508,7 +1504,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
</button>
<button
aria-label="Threads"
aria-labelledby="«r2t»"
aria-labelledby="«r2m»"
class="_icon-button_1pz9o_8"
data-kind="primary"
role="button"
@@ -1535,7 +1531,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
</button>
<button
aria-label="Room info"
aria-labelledby="«r32»"
aria-labelledby="«r2r»"
class="_icon-button_1pz9o_8"
data-kind="primary"
role="button"
@@ -1565,7 +1561,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
>
<div
aria-label="0 members"
aria-labelledby="«r37»"
aria-labelledby="«r30»"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
@@ -1678,7 +1674,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
style="--cpd-icon-button-size: 100%;"
>
<svg
aria-labelledby="«r2j»"
aria-labelledby="«r2c»"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@@ -1694,7 +1690,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
<button
aria-disabled="false"
aria-label="Voice call"
aria-labelledby="«r2o»"
aria-labelledby="«r2h»"
class="_icon-button_1pz9o_8"
data-kind="primary"
role="button"
@@ -1720,7 +1716,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
</button>
<button
aria-label="Threads"
aria-labelledby="«r2t»"
aria-labelledby="«r2m»"
class="_icon-button_1pz9o_8"
data-kind="primary"
role="button"
@@ -1747,7 +1743,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
</button>
<button
aria-label="Room info"
aria-labelledby="«r32»"
aria-labelledby="«r2r»"
class="_icon-button_1pz9o_8"
data-kind="primary"
role="button"
@@ -1777,7 +1773,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
>
<div
aria-label="0 members"
aria-labelledby="«r37»"
aria-labelledby="«r30»"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
@@ -1845,7 +1841,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
tabindex="0"
>
<div
aria-labelledby="«r3m»"
aria-labelledby="«r3e»"
class="mx_E2EIcon mx_E2EIcon_verified mx_MessageComposer_e2eIcon"
data-testid="e2e-icon"
>
@@ -2056,7 +2052,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
</button>
<button
aria-label="Chat"
aria-labelledby="«r7l»"
aria-labelledby="«r7c»"
class="_icon-button_1pz9o_8"
data-kind="primary"
role="button"
@@ -2083,7 +2079,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
</button>
<button
aria-label="Threads"
aria-labelledby="«r7q»"
aria-labelledby="«r7h»"
class="_icon-button_1pz9o_8"
data-kind="primary"
role="button"
@@ -2110,7 +2106,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
</button>
<button
aria-label="Room info"
aria-labelledby="«r7v»"
aria-labelledby="«r7m»"
class="_icon-button_1pz9o_8"
data-kind="primary"
role="button"
@@ -2140,7 +2136,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
>
<div
aria-label="0 members"
aria-labelledby="«r84»"
aria-labelledby="«r7r»"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
@@ -2216,7 +2212,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
</p>
</div>
<button
aria-labelledby="«r8d»"
aria-labelledby="«r84»"
class="_icon-button_1pz9o_8"
data-kind="secondary"
data-testid="base-card-close-button"
@@ -2275,8 +2271,6 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
class="mx_MessageComposer_e2eIconWrapper"
>
<svg
aria-label="Messages in this room are not end-to-end encrypted"
aria-labelledby="«r8m»"
class="mx_E2EIcon mx_MessageComposer_e2eIcon"
color="var(--cpd-color-icon-info-primary)"
fill="currentColor"

Some files were not shown because too many files have changed in this diff Show More