mirror of
https://github.com/element-hq/element-web.git
synced 2025-09-17 11:04:05 +02:00
Compare commits
38 Commits
hs/user-pr
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87d40ab0e0 | ||
|
|
8e9a43d70c | ||
|
|
9a11a80483 | ||
|
|
8d07e797c5 | ||
|
|
2e8e6e92cc | ||
|
|
c7c0e91fdc | ||
|
|
fc06cf1276 | ||
|
|
4f702b70aa | ||
|
|
9f15532d12 | ||
|
|
71cf19f4b2 | ||
|
|
1925132a3c | ||
|
|
8fa3d7e4b7 | ||
|
|
1b4a979b6c | ||
|
|
d287ac07a3 | ||
|
|
8903927e0c | ||
|
|
4d48d1b2f2 | ||
|
|
f75d41054f | ||
|
|
701019052c | ||
|
|
cf692e751b | ||
|
|
1a005ad5d2 | ||
|
|
42f7bc1d0d | ||
|
|
b7f89db43c | ||
|
|
98a04e1812 | ||
|
|
42d726a4ff | ||
|
|
b6f5843028 | ||
|
|
81d054bb99 | ||
|
|
a1f56ebbf2 | ||
|
|
a003ebcb35 | ||
|
|
87b4918d34 | ||
|
|
c6f47cfd8e | ||
|
|
a112dfe1db | ||
|
|
4b4cb896eb | ||
|
|
6a1c0502aa | ||
|
|
ea5e525133 | ||
|
|
14d16364db | ||
|
|
aab1fae299 | ||
|
|
f5d6f8f639 | ||
|
|
cc20136170 |
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -2,6 +2,7 @@
|
||||
|
||||
## 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.
|
||||
|
||||
54
CHANGELOG.md
54
CHANGELOG.md
@@ -1,3 +1,57 @@
|
||||
Changes in [1.11.112](https://github.com/element-hq/element-web/releases/tag/v1.11.112) (2025-09-16)
|
||||
====================================================================================================
|
||||
Fix [CVE-2025-59161](https://www.cve.org/CVERecord?id=CVE-2025-59161) / [GHSA-m6c8-98f4-75rr](https://github.com/element-hq/element-web/security/advisories/GHSA-m6c8-98f4-75rr)
|
||||
|
||||
|
||||
Changes in [1.11.111](https://github.com/element-hq/element-web/releases/tag/v1.11.111) (2025-09-10)
|
||||
====================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Do not hide media from your own user by default ([#29797](https://github.com/element-hq/element-web/pull/29797)). Contributed by @Half-Shot.
|
||||
* Remember whether sidebar is shown for calls when switching rooms ([#30262](https://github.com/element-hq/element-web/pull/30262)). Contributed by @bojidar-bg.
|
||||
* Open the proper integration settings on integrations disabled error ([#30538](https://github.com/element-hq/element-web/pull/30538)). Contributed by @Half-Shot.
|
||||
* Show a "progress" dialog while invites are being sent ([#30561](https://github.com/element-hq/element-web/pull/30561)). Contributed by @richvdh.
|
||||
* Move the room list to the new ListView(backed by react-virtuoso) ([#30515](https://github.com/element-hq/element-web/pull/30515)). Contributed by @langleyd.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [Backport staging] Ensure container starts if it is mounted with an empty /modules directory. ([#30705](https://github.com/element-hq/element-web/pull/30705)). Contributed by @RiotRobot.
|
||||
* Fix room joining over federation not specifying vias or using aliases ([#30641](https://github.com/element-hq/element-web/pull/30641)). Contributed by @t3chguy.
|
||||
* Fix stable-suffixed MSC4133 support ([#30649](https://github.com/element-hq/element-web/pull/30649)). Contributed by @dbkr.
|
||||
* Fix i18n of message when a setting is disabled ([#30646](https://github.com/element-hq/element-web/pull/30646)). Contributed by @dbkr.
|
||||
* ListView should not handle the arrow keys if there is a modifier applied ([#30633](https://github.com/element-hq/element-web/pull/30633)). Contributed by @langleyd.
|
||||
* Make BaseDialog's div keyboard focusable and fix test. ([#30631](https://github.com/element-hq/element-web/pull/30631)). Contributed by @langleyd.
|
||||
* Fix: Allow triple-click text selection to flow around pills ([#30349](https://github.com/element-hq/element-web/pull/30349)). Contributed by @AlirezaMrtz.
|
||||
* Watch for a 'join' action to know when the call is connected ([#29492](https://github.com/element-hq/element-web/pull/29492)). Contributed by @robintown.
|
||||
* Fix: add missing tooltip and aria-label to lock icon next to composer ([#30623](https://github.com/element-hq/element-web/pull/30623)). Contributed by @florianduros.
|
||||
* Don't render context menu when scrolling ([#30613](https://github.com/element-hq/element-web/pull/30613)). Contributed by @langleyd.
|
||||
|
||||
|
||||
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.
|
||||
|
||||
@@ -14,10 +14,9 @@ entrypoint_log() {
|
||||
mkdir -p /tmp/element-web-config
|
||||
cp /app/config*.json /tmp/element-web-config/
|
||||
|
||||
# If there are modules to be loaded
|
||||
if [ -d "/modules" ]; then
|
||||
# If the module directory exists AND the module directory has modules in it
|
||||
if [ -d "/modules" ] && [ "$( ls -A '/modules' )" ]; then
|
||||
cd /modules
|
||||
|
||||
for MODULE in *
|
||||
do
|
||||
# If the module has a package.json, use its main field as the entrypoint
|
||||
|
||||
7
knip.ts
7
knip.ts
@@ -42,6 +42,13 @@ 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "element-web",
|
||||
"version": "1.11.109",
|
||||
"version": "1.11.112",
|
||||
"description": "Element: the future of secure communication",
|
||||
"author": "New Vector Ltd.",
|
||||
"repository": {
|
||||
@@ -134,7 +134,7 @@
|
||||
"maplibre-gl": "^5.0.0",
|
||||
"matrix-encrypt-attachment": "^1.0.3",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||
"matrix-js-sdk": "38.2.0",
|
||||
"matrix-widget-api": "^1.10.0",
|
||||
"memoize-one": "^6.0.0",
|
||||
"mime": "^4.0.4",
|
||||
@@ -203,6 +203,7 @@
|
||||
"@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",
|
||||
@@ -226,6 +227,7 @@
|
||||
"@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",
|
||||
|
||||
@@ -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("checkbox", { name: "Allow key storage" })).toBeVisible();
|
||||
await expect(page.getByRole("switch", { 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("checkbox", { name: "Allow key storage" })).toBeVisible();
|
||||
await expect(page.getByRole("switch", { 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();
|
||||
|
||||
@@ -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("checkbox", { name: "Allow key storage" });
|
||||
const keyStorageToggle = encryptionTab.getByRole("switch", { name: "Allow key storage" });
|
||||
if (!(await keyStorageToggle.isChecked())) {
|
||||
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).click();
|
||||
await encryptionTab.getByRole("switch", { 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("checkbox", { name: "Allow key storage" });
|
||||
const keyStorageToggle = encryptionTab.getByRole("switch", { name: "Allow key storage" });
|
||||
if (await keyStorageToggle.isChecked()) {
|
||||
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).click();
|
||||
await encryptionTab.getByRole("switch", { name: "Allow key storage" }).click();
|
||||
await encryptionTab.getByRole("button", { name: "Delete key storage" }).click();
|
||||
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).isVisible();
|
||||
await encryptionTab.getByRole("switch", { name: "Allow key storage" }).isVisible();
|
||||
|
||||
// Wait for the update to account data to stick
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
@@ -193,6 +193,9 @@ 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();
|
||||
|
||||
@@ -100,3 +100,51 @@ 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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -85,7 +85,7 @@ class Helpers {
|
||||
* Return the system theme toggle
|
||||
*/
|
||||
getMatchSystemThemeCheckbox() {
|
||||
return this.getThemePanel().getByRole("checkbox", { name: "Match system theme" });
|
||||
return this.getThemePanel().getByRole("switch", { name: "Match system theme" });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -219,7 +219,7 @@ class Helpers {
|
||||
* Return the compact layout checkbox
|
||||
*/
|
||||
getCompactLayoutCheckbox() {
|
||||
return this.getMessageLayoutPanel().getByRole("checkbox", { name: "Show compact text and messages" });
|
||||
return this.getMessageLayoutPanel().getByRole("switch", { name: "Show compact text and messages" });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -117,7 +117,7 @@ test.describe("Encryption tab", () => {
|
||||
await verifySession(app, recoveryKey.encodedPrivateKey);
|
||||
await util.openEncryptionTab();
|
||||
|
||||
await page.getByRole("checkbox", { name: "Allow key storage" }).click();
|
||||
await page.getByRole("switch", { 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("checkbox", { name: "Allow key storage" })).not.toBeChecked();
|
||||
await expect(page.getByRole("switch", { name: "Allow key storage" })).not.toBeChecked();
|
||||
|
||||
for (const prom of deleteRequestPromises) {
|
||||
const request = await prom;
|
||||
|
||||
@@ -908,23 +908,37 @@ test.describe("Timeline", () => {
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
test(
|
||||
"should be able to hide an image",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, homeserver, room, context }) => {
|
||||
await app.viewRoomById(room.roomId);
|
||||
|
||||
// Check that the image is now hidden.
|
||||
await expect(page.getByRole("button", { name: "Show image" })).toBeVisible();
|
||||
});
|
||||
const bot = new Bot(page, homeserver, {});
|
||||
await bot.prepareClient();
|
||||
await app.client.inviteUser(room.roomId, bot.credentials.userId);
|
||||
|
||||
test("should be able to hide a video", async ({ page, app, room, context }) => {
|
||||
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 }) => {
|
||||
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, {
|
||||
|
||||
const bot = new Bot(page, homeserver, {});
|
||||
await bot.prepareClient();
|
||||
await app.client.inviteUser(room.roomId, bot.credentials.userId);
|
||||
|
||||
const upload = await bot.uploadContent(VIDEO_FILE, { name: "bbb.webm", type: "video/webm" });
|
||||
await bot.sendEvent(room.roomId, null, "m.room.message" as EventType, {
|
||||
msgtype: "m.video" as MsgType,
|
||||
body: "bbb.webm",
|
||||
url: upload.content_uri,
|
||||
|
||||
@@ -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:18e9e77eac01709e9ab4d26cf20c36bf5a1567756bb5a78c00cabf366d65a950";
|
||||
const TAG = "develop@sha256:8ce4c1a466e1e32bcffde390250a6785527d235436ae42afa9bf94d2a9288746";
|
||||
|
||||
/**
|
||||
* SynapseContainer which freezes the docker digest to stabilise tests,
|
||||
|
||||
@@ -11,8 +11,7 @@ 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-flex;
|
||||
align-items: center;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
@@ -57,6 +56,8 @@ 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 {
|
||||
|
||||
10
src/@types/global.d.ts
vendored
10
src/@types/global.d.ts
vendored
@@ -68,9 +68,17 @@ type ElectronChannel =
|
||||
| "openDesktopCapturerSourcePicker"
|
||||
| "userAccessToken"
|
||||
| "homeserverUrl"
|
||||
| "serverSupportedVersions";
|
||||
| "serverSupportedVersions"
|
||||
| "showToast";
|
||||
|
||||
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;
|
||||
|
||||
@@ -112,6 +112,7 @@ export enum LegacyCallHandlerEvent {
|
||||
CallsChanged = "calls_changed",
|
||||
CallChangeRoom = "call_change_room",
|
||||
SilencedCallsChanged = "silenced_calls_changed",
|
||||
ShownSidebarsChanged = "shown_sidebars_changed",
|
||||
CallState = "call_state",
|
||||
ProtocolSupport = "protocol_support",
|
||||
}
|
||||
@@ -120,6 +121,7 @@ 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;
|
||||
};
|
||||
@@ -144,6 +146,8 @@ 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
|
||||
|
||||
@@ -240,6 +244,15 @@ 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();
|
||||
|
||||
@@ -6,6 +6,8 @@ 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).
|
||||
@@ -16,12 +18,12 @@ Please see LICENSE files in the repository root for full details.
|
||||
* 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 aliasToIDMap = new Map<string, string>();
|
||||
const cache = new Map<string, CacheResult>();
|
||||
|
||||
export function storeRoomAliasInCache(alias: string, id: string): void {
|
||||
aliasToIDMap.set(alias, id);
|
||||
export function storeRoomAliasInCache(alias: string, roomId: string, viaServers: string[]): void {
|
||||
cache.set(alias, { roomId, viaServers });
|
||||
}
|
||||
|
||||
export function getCachedRoomIDForAlias(alias: string): string | undefined {
|
||||
return aliasToIDMap.get(alias);
|
||||
export function getCachedRoomIdForAlias(alias: string): CacheResult | undefined {
|
||||
return cache.get(alias);
|
||||
}
|
||||
|
||||
@@ -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 { makeRoomPermalink } from "../../utils/permalinks/Permalinks";
|
||||
import { calculateRoomVia, makeRoomPermalink } from "../../utils/permalinks/Permalinks";
|
||||
import ThemeWatcher, { ThemeWatcherEvent } from "../../settings/watchers/ThemeWatcher";
|
||||
import { FontWatcher } from "../../settings/watchers/FontWatcher";
|
||||
import { storeRoomAliasInCache } from "../../RoomAliasCache";
|
||||
@@ -238,6 +238,8 @@ 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;
|
||||
@@ -470,15 +472,20 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
this.fontWatcher.start();
|
||||
|
||||
initSentry(SdkConfig.get("sentry"));
|
||||
|
||||
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);
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
|
||||
@@ -1019,7 +1026,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);
|
||||
storeRoomAliasInCache(theAlias, room.roomId, calculateRoomVia(room));
|
||||
}
|
||||
|
||||
// Store this as the ID of the last room accessed. This is so that we can
|
||||
|
||||
@@ -245,6 +245,7 @@ class PipContainerInner extends React.Component<IProps, IState> {
|
||||
secondaryCall={this.state.secondaryCall}
|
||||
pipMode={pipMode}
|
||||
onResize={onResize}
|
||||
sidebarShown={false}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
@@ -184,28 +184,32 @@ 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 )
|
||||
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;
|
||||
}
|
||||
// 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 (handled) {
|
||||
|
||||
@@ -145,6 +145,9 @@ 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.
|
||||
|
||||
@@ -1,55 +1,53 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024, 2025 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 from "react";
|
||||
import React, { useCallback } 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 default class IntegrationsDisabledDialog extends React.Component<IProps> {
|
||||
private onAcknowledgeClick = (): void => {
|
||||
this.props.onFinished();
|
||||
};
|
||||
export const IntegrationsDisabledDialog: React.FC<IProps> = ({ onFinished }) => {
|
||||
const onOpenSettingsClick = useCallback(() => {
|
||||
onFinished();
|
||||
dis.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Security,
|
||||
});
|
||||
}, [onFinished]);
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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)))
|
||||
(!getCachedRoomIdForAlias(trimmedQuery) || !cli.getRoom(getCachedRoomIdForAlias(trimmedQuery)!.roomId))
|
||||
) {
|
||||
joinRoomSection = (
|
||||
<div className="mx_SpotlightDialog_section mx_SpotlightDialog_otherSearches" role="group">
|
||||
|
||||
@@ -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?.getId(), mxEvent?.getRoomId());
|
||||
const [mediaIsVisible] = useMediaVisible(mxEvent);
|
||||
|
||||
const replacer = useReplacer(content, mxEvent, options);
|
||||
const linkifyOptions = useMemo(
|
||||
|
||||
@@ -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.getId(), mxEvent.getRoomId());
|
||||
const [mediaIsVisible, setVisible] = useMediaVisible(mxEvent);
|
||||
|
||||
if (!mediaIsVisible) {
|
||||
return;
|
||||
|
||||
@@ -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.getId(), props.mxEvent.getRoomId());
|
||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent);
|
||||
return <MImageBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class MImageReplyBodyInner extends MImageBodyInner {
|
||||
}
|
||||
}
|
||||
const MImageReplyBody: React.FC<IBodyProps> = (props) => {
|
||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
|
||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent);
|
||||
return <MImageReplyBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -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.getId(), props.mxEvent.getRoomId());
|
||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent);
|
||||
return <MStickerBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -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.getId(), props.mxEvent.getRoomId());
|
||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent);
|
||||
return <MVideoBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -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.getId(), mxEvent.getRoomId());
|
||||
const [mediaVisible] = useMediaVisible(mxEvent);
|
||||
|
||||
const ts = mxEvent.getTs();
|
||||
const previews = useAsyncMemo<[string, IPreviewUrlResponse][]>(
|
||||
|
||||
@@ -531,12 +531,15 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||
if (!this.props.e2eStatus) {
|
||||
leftIcon = (
|
||||
<div className="mx_MessageComposer_e2eIconWrapper">
|
||||
<LockOffIcon
|
||||
width={12}
|
||||
height={12}
|
||||
color="var(--cpd-color-icon-info-primary)"
|
||||
className="mx_E2EIcon mx_MessageComposer_e2eIcon"
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.e2eStatus !== E2EStatus.Normal) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useRef, type JSX } from "react";
|
||||
import React, { useCallback, useRef, useState, type JSX } from "react";
|
||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { type ScrollIntoViewLocation } from "react-virtuoso";
|
||||
import { isEqual } from "lodash";
|
||||
@@ -33,6 +33,7 @@ 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,
|
||||
@@ -57,10 +58,11 @@ export function RoomList({ vm: { roomsResult, activeIndex } }: RoomListProps): J
|
||||
roomIndex={index}
|
||||
roomCount={roomCount}
|
||||
onFocus={onFocus}
|
||||
listIsScrolling={isScrolling}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[activeIndex, roomCount],
|
||||
[activeIndex, roomCount, isScrolling],
|
||||
);
|
||||
|
||||
const getItemKey = useCallback((item: Room): string => {
|
||||
@@ -116,6 +118,7 @@ export function RoomList({ vm: { roomsResult, activeIndex } }: RoomListProps): J
|
||||
getItemKey={getItemKey}
|
||||
isItemFocusable={() => true}
|
||||
onKeyDown={keyDownCallback}
|
||||
isScrolling={setIsScrolling}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,6 +41,10 @@ interface RoomListItemViewProps extends React.HTMLAttributes<HTMLButtonElement>
|
||||
* The total number of rooms in the list
|
||||
*/
|
||||
roomCount: number;
|
||||
/**
|
||||
* Whether the list is currently scrolling
|
||||
*/
|
||||
listIsScrolling: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,6 +57,7 @@ export const RoomListItemView = memo(function RoomListItemView({
|
||||
onFocus,
|
||||
roomIndex: index,
|
||||
roomCount: count,
|
||||
listIsScrolling,
|
||||
...props
|
||||
}: RoomListItemViewProps): JSX.Element {
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
@@ -141,7 +146,11 @@ export const RoomListItemView = memo(function RoomListItemView({
|
||||
</Flex>
|
||||
);
|
||||
|
||||
if (!vm.showContextMenu) return content;
|
||||
// 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;
|
||||
|
||||
return (
|
||||
<RoomListItemContextMenuView
|
||||
|
||||
@@ -50,6 +50,10 @@ interface IProps {
|
||||
onMouseDownOnHeader?: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||
|
||||
showApps?: boolean;
|
||||
|
||||
sidebarShown: boolean;
|
||||
|
||||
setSidebarShown?: (sidebarShown: boolean) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
@@ -62,7 +66,6 @@ interface IState {
|
||||
primaryFeed?: CallFeed;
|
||||
secondaryFeed?: CallFeed;
|
||||
sidebarFeeds: Array<CallFeed>;
|
||||
sidebarShown: boolean;
|
||||
}
|
||||
|
||||
function getFullScreenElement(): Element | null {
|
||||
@@ -97,7 +100,6 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
|
||||
primaryFeed: primary,
|
||||
secondaryFeed: secondary,
|
||||
sidebarFeeds: sidebar,
|
||||
sidebarShown: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -269,8 +271,9 @@ 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,
|
||||
});
|
||||
};
|
||||
@@ -320,12 +323,12 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
|
||||
};
|
||||
|
||||
private onToggleSidebar = (): void => {
|
||||
this.setState({ sidebarShown: !this.state.sidebarShown });
|
||||
this.props.setSidebarShown?.(!this.props.sidebarShown);
|
||||
};
|
||||
|
||||
private renderCallControls(): JSX.Element {
|
||||
const { call, pipMode } = this.props;
|
||||
const { callState, micMuted, vidMuted, screensharing, sidebarShown, secondaryFeed, sidebarFeeds } = this.state;
|
||||
const { call, pipMode, sidebarShown } = this.props;
|
||||
const { callState, micMuted, vidMuted, screensharing, secondaryFeed, sidebarFeeds } = this.state;
|
||||
|
||||
// If SDPStreamMetadata isn't supported don't show video mute button in voice calls
|
||||
const vidMuteButtonShown = call.opponentSupportsSDPStreamMetadata() || call.hasLocalUserMediaVideoTrack;
|
||||
@@ -337,7 +340,8 @@ 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 = (secondaryFeed && !secondaryFeed.isVideoMuted()) || sidebarFeeds.length > 0;
|
||||
const sidebarButtonShown =
|
||||
!pipMode && ((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();
|
||||
@@ -372,7 +376,7 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
private renderToast(): JSX.Element | null {
|
||||
const { call } = this.props;
|
||||
const { call, sidebarShown } = this.props;
|
||||
const someoneIsScreensharing = call.getFeeds().some((feed) => {
|
||||
return feed.purpose === SDPStreamMetadataPurpose.Screenshare;
|
||||
});
|
||||
@@ -380,7 +384,7 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
|
||||
if (!someoneIsScreensharing) return null;
|
||||
|
||||
const isScreensharing = call.isScreensharing();
|
||||
const { primaryFeed, sidebarShown } = this.state;
|
||||
const { primaryFeed } = this.state;
|
||||
const sharerName = primaryFeed?.getMember()?.name;
|
||||
if (!sharerName) return null;
|
||||
|
||||
@@ -393,8 +397,8 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
private renderContent(): JSX.Element {
|
||||
const { pipMode, call, onResize } = this.props;
|
||||
const { isLocalOnHold, isRemoteOnHold, sidebarShown, primaryFeed, secondaryFeed, sidebarFeeds } = this.state;
|
||||
const { pipMode, call, onResize, sidebarShown } = this.props;
|
||||
const { isLocalOnHold, isRemoteOnHold, primaryFeed, secondaryFeed, sidebarFeeds } = this.state;
|
||||
|
||||
const callRoomId = LegacyCallHandler.instance.roomIdForCall(call);
|
||||
const callRoom = (callRoomId ? MatrixClientPeg.safeGet().getRoom(callRoomId) : undefined) ?? undefined;
|
||||
@@ -537,8 +541,8 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const { call, secondaryCall, pipMode, showApps, onMouseDownOnHeader } = this.props;
|
||||
const { sidebarShown, sidebarFeeds } = this.state;
|
||||
const { call, secondaryCall, pipMode, showApps, onMouseDownOnHeader, sidebarShown } = this.props;
|
||||
const { sidebarFeeds } = this.state;
|
||||
|
||||
const client = MatrixClientPeg.safeGet();
|
||||
const callRoomId = LegacyCallHandler.instance.roomIdForCall(call);
|
||||
|
||||
@@ -25,6 +25,7 @@ interface IProps {
|
||||
|
||||
interface IState {
|
||||
call: MatrixCall | null;
|
||||
sidebarShown: boolean;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -34,19 +35,23 @@ interface IState {
|
||||
export default class LegacyCallViewForRoom extends React.Component<IProps, IState> {
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
const call = this.getCall();
|
||||
this.state = {
|
||||
call: this.getCall(),
|
||||
call,
|
||||
sidebarShown: !!call && LegacyCallHandler.instance.isCallSidebarShown(call.callId),
|
||||
};
|
||||
}
|
||||
|
||||
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 => {
|
||||
@@ -54,6 +59,10 @@ 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 {
|
||||
@@ -75,6 +84,11 @@ 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;
|
||||
|
||||
@@ -99,7 +113,13 @@ 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} />
|
||||
<LegacyCallView
|
||||
call={this.state.call}
|
||||
pipMode={false}
|
||||
showApps={this.props.showApps}
|
||||
sidebarShown={this.state.sidebarShown}
|
||||
setSidebarShown={this.setSidebarShown}
|
||||
/>
|
||||
</Resizable>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { JoinRule } from "matrix-js-sdk/src/matrix";
|
||||
import { JoinRule, type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { SettingLevel } from "../settings/SettingLevel";
|
||||
import { useSettingValue } from "./useSettings";
|
||||
@@ -19,14 +19,25 @@ const PRIVATE_JOIN_RULES: JoinRule[] = [JoinRule.Invite, JoinRule.Knock, JoinRul
|
||||
|
||||
/**
|
||||
* Should the media event be visible in the client, or hidden.
|
||||
* @param eventId The eventId of the media event.
|
||||
* @returns A boolean describing the hidden status, and a function to set the visiblity.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
export function useMediaVisible(eventId?: string, roomId?: string): [boolean, (visible: boolean) => void] {
|
||||
const mediaPreviewSetting = useSettingValue("mediaPreviewConfig", roomId);
|
||||
export function useMediaVisible(mxEvent?: MatrixEvent): [boolean, (visible: boolean) => void] {
|
||||
const eventId = mxEvent?.getId();
|
||||
const mediaPreviewSetting = useSettingValue("mediaPreviewConfig", mxEvent?.getRoomId());
|
||||
const client = useMatrixClientContext();
|
||||
const eventVisibility = useSettingValue("showMediaEventIds");
|
||||
const joinRule = useRoomState(client.getRoom(roomId) ?? undefined, (state) => state.getJoinRule());
|
||||
const room = client.getRoom(mxEvent?.getRoomId()) ?? undefined;
|
||||
const joinRule = useRoomState(room, (state) => state.getJoinRule());
|
||||
const setMediaVisible = useCallback(
|
||||
(visible: boolean) => {
|
||||
SettingsStore.setValue("showMediaEventIds", null, SettingLevel.DEVICE, {
|
||||
@@ -43,6 +54,9 @@ export function useMediaVisible(eventId?: string, roomId?: string): [boolean, (v
|
||||
// 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) {
|
||||
|
||||
@@ -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 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…",
|
||||
"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…",
|
||||
"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,7 +922,8 @@
|
||||
},
|
||||
"privacy_warning": "Ujistěte se, že tuto obrazovku nikdo nevidí!",
|
||||
"restoring": "Obnovení klíčů ze zálohy",
|
||||
"security_key_title": "Klíč pro obnovení"
|
||||
"security_key_label": "Klíč pro obnovení",
|
||||
"security_key_title": "Zadejte klíč pro obnovení"
|
||||
},
|
||||
"bootstrap_title": "Příprava klíčů",
|
||||
"confirm_encryption_setup_body": "Kliknutím na tlačítko níže potvrďte nastavení šifrování.",
|
||||
@@ -1367,6 +1368,10 @@
|
||||
"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",
|
||||
@@ -1535,6 +1540,9 @@
|
||||
"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",
|
||||
@@ -1657,6 +1665,7 @@
|
||||
"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",
|
||||
@@ -1761,6 +1770,7 @@
|
||||
},
|
||||
"power_level": {
|
||||
"admin": "Správce",
|
||||
"creator": "Vlastník",
|
||||
"custom": "Vlastní (%(level)s)",
|
||||
"custom_level": "Vlastní úroveň",
|
||||
"default": "Výchozí",
|
||||
@@ -1914,6 +1924,7 @@
|
||||
"thread_list": {
|
||||
"context_menu_label": "Možnosti vláken"
|
||||
},
|
||||
"title": "Pravý panel",
|
||||
"video_room_chat": {
|
||||
"title": "Chatovat"
|
||||
}
|
||||
@@ -3400,6 +3411,7 @@
|
||||
"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",
|
||||
|
||||
@@ -654,6 +654,7 @@
|
||||
"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",
|
||||
|
||||
@@ -654,6 +654,7 @@
|
||||
"poll_button_no_perms_description": "Vous n’avez 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 n’est plus actif.",
|
||||
"send_button_title": "Envoyer le message",
|
||||
@@ -1366,6 +1367,10 @@
|
||||
"name_email_mxid_share_space": "Invitez quelqu’un grâce à son nom, adresse e-mail, nom d’utilisateur (tel que <userId/>) ou <a>partagez cet espace</a>.",
|
||||
"name_mxid_share_room": "Invitez quelqu’un à partir de son nom, pseudo (comme <userId/>) ou <a>partagez ce salon</a>.",
|
||||
"name_mxid_share_space": "Invitez quelqu’un grâce à son nom, nom d’utilisateur (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 n’ont pas pu être invitées à rejoindre <RoomName/>",
|
||||
"room_failed_partial_title": "Certaines invitations n’ont pas pu être envoyées",
|
||||
|
||||
@@ -654,6 +654,7 @@
|
||||
"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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -23,14 +23,12 @@ 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";
|
||||
@@ -193,18 +191,6 @@ 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.
|
||||
*/
|
||||
@@ -212,28 +198,10 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
|
||||
|
||||
/**
|
||||
* Starts the communication between the widget and the call.
|
||||
* 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.
|
||||
* 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) {
|
||||
@@ -254,13 +222,23 @@ 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.
|
||||
*/
|
||||
@@ -273,15 +251,6 @@ 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.
|
||||
*/
|
||||
@@ -467,66 +436,10 @@ export class JitsiCall extends Call {
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
public async start(): Promise<void> {
|
||||
await super.start();
|
||||
this.messaging!.on(`action:${ElementWidgetActions.JoinCall}`, this.onJoin);
|
||||
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);
|
||||
}
|
||||
@@ -549,18 +462,17 @@ export class JitsiCall extends Call {
|
||||
}
|
||||
}
|
||||
|
||||
public setDisconnected(): void {
|
||||
// During tests this.messaging can be undefined
|
||||
this.messaging?.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
public close(): void {
|
||||
this.messaging!.off(`action:${ElementWidgetActions.JoinCall}`, this.onJoin);
|
||||
this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Dock, this.onDock);
|
||||
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Undock, this.onUndock);
|
||||
|
||||
super.setDisconnected();
|
||||
super.close();
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.room.off(RoomStateEvent.Update, this.onRoomState);
|
||||
this.on(CallEvent.ConnectionState, this.onConnectionState);
|
||||
this.off(CallEvent.ConnectionState, this.onConnectionState);
|
||||
if (this.participantsExpirationTimer !== null) {
|
||||
clearTimeout(this.participantsExpirationTimer);
|
||||
this.participantsExpirationTimer = null;
|
||||
@@ -612,27 +524,21 @@ 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();
|
||||
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();
|
||||
}
|
||||
if (!isVideoRoom(this.room)) this.close();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -860,54 +766,38 @@ export class ElementCall extends Call {
|
||||
ElementCall.createOrGetCallWidget(room.roomId, room.client, skipLobby, isVideoRoom(room));
|
||||
}
|
||||
|
||||
protected async performConnection(
|
||||
audioInput: MediaDeviceInfo | null,
|
||||
videoInput: MediaDeviceInfo | null,
|
||||
): Promise<void> {
|
||||
public async start(): Promise<void> {
|
||||
await super.start();
|
||||
this.messaging!.on(`action:${ElementWidgetActions.JoinCall}`, this.onJoin);
|
||||
this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
this.messaging!.once(`action:${ElementWidgetActions.Close}`, this.onClose);
|
||||
this.messaging!.on(`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 this.messaging!.transport.send(ElementWidgetActions.HangupCall, {});
|
||||
await waitForEvent(
|
||||
this.session,
|
||||
MatrixRTCSessionEvent.MembershipsChanged,
|
||||
(_, newMemberships: CallMembership[]) =>
|
||||
!newMemberships.some((m) => m.sender === this.client.getUserId()),
|
||||
);
|
||||
await Promise.all([request, response]);
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to hangup call in room ${this.roomId}: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
public setDisconnected(): void {
|
||||
public close(): void {
|
||||
this.messaging!.off(`action:${ElementWidgetActions.JoinCall}`, this.onJoin);
|
||||
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.setDisconnected();
|
||||
super.close();
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
@@ -954,22 +844,27 @@ 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
|
||||
// User is done with the call; tell the UI to close it
|
||||
this.close();
|
||||
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
|
||||
};
|
||||
|
||||
public clean(): Promise<void> {
|
||||
|
||||
@@ -9,33 +9,31 @@ 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 SettingsStore from "../settings/SettingsStore.ts";
|
||||
import type { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload.ts";
|
||||
|
||||
export class NavigationApi implements INavigationApi {
|
||||
public async toMatrixToLink(link: string, join = false): Promise<void> {
|
||||
navigateToPermalink(link);
|
||||
|
||||
const parts = parsePermalink(link);
|
||||
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,
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,13 +28,12 @@ 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
|
||||
@@ -183,28 +182,22 @@ export class ProxiedModuleApi implements ModuleApi {
|
||||
navigateToPermalink(uri);
|
||||
|
||||
const parts = parsePermalink(uri);
|
||||
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"),
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type ReactNode } from "react";
|
||||
import { UNSTABLE_MSC4133_EXTENDED_PROFILES } from "matrix-js-sdk/src/matrix";
|
||||
import { STABLE_MSC4133_EXTENDED_PROFILES, 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]],
|
||||
[[UNSTABLE_MSC4133_EXTENDED_PROFILES], [STABLE_MSC4133_EXTENDED_PROFILES]],
|
||||
undefined,
|
||||
_td("labs|extended_profiles_msc_support"),
|
||||
),
|
||||
|
||||
@@ -340,7 +340,7 @@ export default class SettingsStore {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the reason a setting is disabled if one is assigned.
|
||||
* Retrieves the internationalised 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.
|
||||
|
||||
@@ -11,6 +11,7 @@ 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
|
||||
@@ -33,7 +34,7 @@ export default class ServerSupportUnstableFeatureController extends MatrixClient
|
||||
private readonly watchers: WatchManager,
|
||||
private readonly unstableFeatureGroups: string[][],
|
||||
private readonly stableVersion?: string,
|
||||
private readonly disabledMessage?: string,
|
||||
private readonly disabledMessage?: TranslationKey,
|
||||
private readonly forcedValue: any = false,
|
||||
) {
|
||||
super();
|
||||
@@ -96,7 +97,7 @@ export default class ServerSupportUnstableFeatureController extends MatrixClient
|
||||
|
||||
public get settingDisabled(): boolean | string {
|
||||
if (this.disabled) {
|
||||
return this.disabledMessage ?? true;
|
||||
return this.disabledMessage ? _t(this.disabledMessage) : true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
|
||||
|
||||
// If the room is upgraded, use that room instead. We'll also splice out
|
||||
// any children of the room.
|
||||
const history = this.matrixClient?.getRoomUpgradeHistory(room.roomId, false, msc3946ProcessDynamicPredecessor);
|
||||
const history = this.matrixClient?.getRoomUpgradeHistory(room.roomId, true, msc3946ProcessDynamicPredecessor);
|
||||
if (history && history.length > 1) {
|
||||
room = history[history.length - 1]; // Last room is most recent in history
|
||||
|
||||
|
||||
@@ -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,6 +438,7 @@ 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"),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -445,10 +446,16 @@ 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.
|
||||
let roomId = getCachedRoomIDForAlias(payload.room_alias);
|
||||
if (!roomId) {
|
||||
const cachedResult = getCachedRoomIdForAlias(payload.room_alias);
|
||||
if (cachedResult) {
|
||||
roomId = cachedResult.roomId;
|
||||
viaServers = cachedResult.viaServers;
|
||||
} else {
|
||||
// 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({
|
||||
@@ -467,8 +474,9 @@ export class RoomViewStore extends EventEmitter {
|
||||
});
|
||||
try {
|
||||
const result = await MatrixClientPeg.safeGet().getRoomIdForAlias(payload.room_alias);
|
||||
storeRoomAliasInCache(payload.room_alias, result.room_id);
|
||||
storeRoomAliasInCache(payload.room_alias, result.room_id, result.servers);
|
||||
roomId = result.room_id;
|
||||
viaServers = result.servers;
|
||||
} catch (err) {
|
||||
logger.error("RVS failed to get room id for alias: ", err);
|
||||
this.dis?.dispatch<ViewRoomErrorPayload>({
|
||||
@@ -485,6 +493,7 @@ export class RoomViewStore extends EventEmitter {
|
||||
this.dis?.dispatch({
|
||||
...payload,
|
||||
room_id: roomId,
|
||||
via_servers: viaServers,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -509,12 +518,13 @@ export class RoomViewStore extends EventEmitter {
|
||||
joining: true,
|
||||
});
|
||||
|
||||
// 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!;
|
||||
// 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!;
|
||||
|
||||
const joinOpts: IJoinRoomOpts = {
|
||||
viaServers: this.state.viaServers || [],
|
||||
viaServers,
|
||||
...(payload.opts ?? {}),
|
||||
};
|
||||
if (SettingsStore.getValue("feature_share_history_on_invite")) {
|
||||
@@ -547,7 +557,7 @@ export class RoomViewStore extends EventEmitter {
|
||||
canAskToJoin: payload.canAskToJoin,
|
||||
});
|
||||
|
||||
if (payload.canAskToJoin) {
|
||||
if (payload.canAskToJoin && err instanceof MatrixError && err.httpStatus === 403) {
|
||||
this.dis?.dispatch({ action: Action.PromptAskToJoin });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { EventType, KnownMembership } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import type { EmptyObject, Room, RoomState } from "matrix-js-sdk/src/matrix";
|
||||
import type { EmptyObject, Room } from "matrix-js-sdk/src/matrix";
|
||||
import type { MatrixDispatcher } from "../../dispatcher/dispatcher";
|
||||
import type { ActionPayload } from "../../dispatcher/payloads";
|
||||
import type { FilterKey } from "./skip-list/filters";
|
||||
@@ -250,12 +250,15 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
|
||||
// If we're joining an upgraded room, we'll want to make sure we don't proliferate
|
||||
// the dead room in the list.
|
||||
if (oldMembership !== EffectiveMembership.Join && newMembership === EffectiveMembership.Join) {
|
||||
const roomState: RoomState = payload.room.currentState;
|
||||
const predecessor = roomState.findPredecessor(this.msc3946ProcessDynamicPredecessor);
|
||||
if (predecessor) {
|
||||
const prevRoom = this.matrixClient?.getRoom(predecessor.roomId);
|
||||
if (prevRoom) this.roomSkipList.removeRoom(prevRoom);
|
||||
else logger.warn(`Unable to find predecessor room with id ${predecessor.roomId}`);
|
||||
const room: Room = payload.room;
|
||||
const roomUpgradeHistory = room.client.getRoomUpgradeHistory(
|
||||
room.roomId,
|
||||
true,
|
||||
this.msc3946ProcessDynamicPredecessor,
|
||||
);
|
||||
const predecessors = roomUpgradeHistory.slice(0, roomUpgradeHistory.indexOf(room));
|
||||
for (const predecessor of predecessors) {
|
||||
this.roomSkipList.removeRoom(predecessor);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,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 { type MatrixClient, type Room, type RoomState, EventType, type EmptyObject } from "matrix-js-sdk/src/matrix";
|
||||
import { type MatrixClient, type Room, EventType, type EmptyObject } from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
@@ -308,24 +308,22 @@ export class RoomListStoreClass extends AsyncStoreWithClient<EmptyObject> implem
|
||||
const oldMembership = getEffectiveMembership(membershipPayload.oldMembership);
|
||||
const newMembership = getEffectiveMembershipTag(membershipPayload.room, membershipPayload.membership);
|
||||
if (oldMembership !== EffectiveMembership.Join && newMembership === EffectiveMembership.Join) {
|
||||
// If we're joining an upgraded room, we'll want to make sure we don't proliferate
|
||||
// the dead room in the list.
|
||||
const roomState: RoomState = membershipPayload.room.currentState;
|
||||
const predecessor = roomState.findPredecessor(this.msc3946ProcessDynamicPredecessor);
|
||||
if (predecessor) {
|
||||
const prevRoom = this.matrixClient?.getRoom(predecessor.roomId);
|
||||
if (prevRoom) {
|
||||
const isSticky = this.algorithm.stickyRoom === prevRoom;
|
||||
if (isSticky) {
|
||||
this.algorithm.setStickyRoom(null);
|
||||
}
|
||||
|
||||
// Note: we hit the algorithm instead of our handleRoomUpdate() function to
|
||||
// avoid redundant updates.
|
||||
this.algorithm.handleRoomUpdate(prevRoom, RoomUpdateCause.RoomRemoved);
|
||||
} else {
|
||||
logger.warn(`Unable to find predecessor room with id ${predecessor.roomId}`);
|
||||
// If we're joining an upgraded room, we'll want to make sure we don't proliferate the dead room in the list.
|
||||
const room: Room = membershipPayload.room;
|
||||
const roomUpgradeHistory = room.client.getRoomUpgradeHistory(
|
||||
room.roomId,
|
||||
true,
|
||||
this.msc3946ProcessDynamicPredecessor,
|
||||
);
|
||||
const predecessors = roomUpgradeHistory.slice(0, roomUpgradeHistory.indexOf(room));
|
||||
for (const predecessor of predecessors) {
|
||||
const isSticky = this.algorithm.stickyRoom === predecessor;
|
||||
if (isSticky) {
|
||||
this.algorithm.setStickyRoom(null);
|
||||
}
|
||||
// Note: we hit the algorithm instead of our handleRoomUpdate() function to
|
||||
// avoid redundant updates.
|
||||
this.algorithm.handleRoomUpdate(predecessor, RoomUpdateCause.RoomRemoved);
|
||||
}
|
||||
|
||||
await this.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom);
|
||||
|
||||
@@ -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,7 +1249,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient<EmptyObject> {
|
||||
let roomId = payload.room_id;
|
||||
|
||||
if (payload.room_alias && !roomId) {
|
||||
roomId = getCachedRoomIDForAlias(payload.room_alias);
|
||||
const result = getCachedRoomIdForAlias(payload.room_alias);
|
||||
if (result) roomId = result.roomId;
|
||||
}
|
||||
|
||||
if (!roomId) return; // we'll get re-fired with the room ID shortly
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -40,7 +40,7 @@ export async function leaveRoomBehaviour(
|
||||
let leavingAllVersions = true;
|
||||
const history = matrixClient.getRoomUpgradeHistory(
|
||||
roomId,
|
||||
false,
|
||||
true,
|
||||
SettingsStore.getValue("feature_dynamic_room_predecessors"),
|
||||
);
|
||||
if (history && history.length > 0) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import React from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { uniqueId } from "lodash";
|
||||
|
||||
import BasePlatform, { UpdateCheckStatus, type UpdateStatus } from "../../BasePlatform";
|
||||
import type BaseEventIndexManager from "../../indexing/BaseEventIndexManager";
|
||||
@@ -43,6 +44,7 @@ import { SeshatIndexManager } from "./SeshatIndexManager";
|
||||
import { IPCManager } from "./IPCManager";
|
||||
import { _t } from "../../languageHandler";
|
||||
import { BadgeOverlayRenderer } from "../../favicon";
|
||||
import GenericToast from "../../components/views/toasts/GenericToast.tsx";
|
||||
|
||||
interface SquirrelUpdate {
|
||||
releaseNotes: string;
|
||||
@@ -95,6 +97,7 @@ export default class ElectronPlatform extends BasePlatform {
|
||||
private badgeOverlayRenderer?: BadgeOverlayRenderer;
|
||||
private config!: IConfigOptions;
|
||||
private supportedSettings?: Record<string, boolean>;
|
||||
private clientStartedPromiseWithResolvers = Promise.withResolvers<void>();
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
@@ -182,6 +185,27 @@ export default class ElectronPlatform extends BasePlatform {
|
||||
await this.ipc.call("callDisplayMediaCallback", source ?? { id: "", name: "", thumbnailURL: "" });
|
||||
});
|
||||
|
||||
this.electron.on("showToast", async (ev, { title, description, priority = 40 }) => {
|
||||
await this.clientStartedPromiseWithResolvers.promise;
|
||||
|
||||
const key = uniqueId("electron_showToast_");
|
||||
const onPrimaryClick = (): void => {
|
||||
ToastStore.sharedInstance().dismissToast(key);
|
||||
};
|
||||
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key,
|
||||
title,
|
||||
props: {
|
||||
description,
|
||||
primaryLabel: _t("action|dismiss"),
|
||||
onPrimaryClick,
|
||||
},
|
||||
component: GenericToast,
|
||||
priority,
|
||||
});
|
||||
});
|
||||
|
||||
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||
|
||||
this.initialised = this.initialise();
|
||||
@@ -193,6 +217,10 @@ export default class ElectronPlatform extends BasePlatform {
|
||||
if (["call_state"].includes(payload.action)) {
|
||||
this.electron.send("app_onAction", payload);
|
||||
}
|
||||
|
||||
if (payload.action === "client_started") {
|
||||
this.clientStartedPromiseWithResolvers.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
private async initialise(): Promise<void> {
|
||||
|
||||
@@ -79,7 +79,6 @@ 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() {
|
||||
|
||||
@@ -585,4 +585,42 @@ 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ exports[`dialogTermsInteractionCallback should render a dialog with the expected
|
||||
class=""
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
|
||||
@@ -12,6 +12,7 @@ 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"
|
||||
@@ -83,6 +84,7 @@ 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"
|
||||
|
||||
@@ -388,6 +388,7 @@ 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"
|
||||
@@ -444,6 +445,7 @@ 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"
|
||||
|
||||
@@ -1,225 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RoomView for a local room in state CREATING should match the snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_RoomView mx_RoomView--local"
|
||||
>
|
||||
<header
|
||||
class="flex mx_RoomHeader light-panel"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
aria-label="Open room settings"
|
||||
aria-live="off"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar _avatar-imageless_1qbcf_52"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="button"
|
||||
style="--cpd-avatar-size: 40px;"
|
||||
tabindex="-1"
|
||||
>
|
||||
u
|
||||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
class="mx_RoomHeader_infoWrapper"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomHeader_info box-flex"
|
||||
style="--mx-box-flex: 1;"
|
||||
>
|
||||
<div
|
||||
aria-level="1"
|
||||
class="_typography_6v6n8_153 _font-body-lg-semibold_6v6n8_74 mx_RoomHeader_heading"
|
||||
dir="auto"
|
||||
role="heading"
|
||||
>
|
||||
<span
|
||||
class="mx_RoomHeader_truncated mx_lineClamp"
|
||||
>
|
||||
@user:example.com
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Video call"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="«rg4»"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Voice call"
|
||||
aria-labelledby="«rg9»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m20.958 16.374.039 3.527q0 .427-.33.756-.33.33-.756.33a16 16 0 0 1-6.57-1.105 16.2 16.2 0 0 1-5.563-3.663 16.1 16.1 0 0 1-3.653-5.573 16.3 16.3 0 0 1-1.115-6.56q0-.427.33-.757T4.095 3l3.528.039a1.07 1.07 0 0 1 1.085.93l.543 3.954q.039.271-.039.504a1.1 1.1 0 0 1-.271.426l-1.64 1.64q.505 1.008 1.154 1.909c.433.6 1.444 1.696 1.444 1.696s1.095 1.01 1.696 1.444q.9.65 1.909 1.153l1.64-1.64q.193-.193.426-.27t.504-.04l3.954.543q.406.059.668.359t.262.727"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
aria-labelledby="«rge»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
class=""
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M4 3h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6l-2.293 2.293c-.63.63-1.707.184-1.707-.707V5a2 2 0 0 1 2-2m3 7h10q.424 0 .712-.287A.97.97 0 0 0 18 9a.97.97 0 0 0-.288-.713A.97.97 0 0 0 17 8H7a.97.97 0 0 0-.713.287A.97.97 0 0 0 6 9q0 .424.287.713Q6.576 10 7 10m0 4h6q.424 0 .713-.287A.97.97 0 0 0 14 13a.97.97 0 0 0-.287-.713A.97.97 0 0 0 13 12H7a.97.97 0 0 0-.713.287A.97.97 0 0 0 6 13q0 .424.287.713Q6.576 14 7 14"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
aria-labelledby="«rgj»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
class=""
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16v-4a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 11a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 12v4q0 .424.287.712.288.288.713.288m0-8q.424 0 .713-.287A.97.97 0 0 0 13 8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8q0 .424.287.713Q11.576 9 12 9m0 13a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
class="_typography_6v6n8_153 _font-body-sm-medium_6v6n8_41"
|
||||
>
|
||||
<div
|
||||
aria-label="2 members"
|
||||
aria-labelledby="«rgo»"
|
||||
class="mx_AccessibleButton mx_FacePile"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_stacked-avatars_1qbcf_102"
|
||||
>
|
||||
<span
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar _avatar-imageless_1qbcf_52"
|
||||
data-color="2"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 20px;"
|
||||
>
|
||||
u
|
||||
</span>
|
||||
<span
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar _avatar-imageless_1qbcf_52"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 20px;"
|
||||
>
|
||||
u
|
||||
</span>
|
||||
</div>
|
||||
2
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
class="mx_RoomView_body"
|
||||
>
|
||||
<div
|
||||
class="mx_LargeLoader"
|
||||
>
|
||||
<div
|
||||
class="mx_Spinner"
|
||||
>
|
||||
<div
|
||||
aria-label="Loading…"
|
||||
class="mx_Spinner_icon"
|
||||
data-testid="spinner"
|
||||
role="progressbar"
|
||||
style="width: 45px; height: 45px;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_LargeLoader_text"
|
||||
>
|
||||
We're creating a room with @user:example.com
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RoomView for a local room in state ERROR should match the snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_RoomView mx_RoomView--local"
|
||||
@@ -410,6 +191,225 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
class="mx_RoomView_body"
|
||||
>
|
||||
<div
|
||||
class="mx_LargeLoader"
|
||||
>
|
||||
<div
|
||||
class="mx_Spinner"
|
||||
>
|
||||
<div
|
||||
aria-label="Loading…"
|
||||
class="mx_Spinner_icon"
|
||||
data-testid="spinner"
|
||||
role="progressbar"
|
||||
style="width: 45px; height: 45px;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_LargeLoader_text"
|
||||
>
|
||||
We're creating a room with @user:example.com
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RoomView for a local room in state ERROR should match the snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_RoomView mx_RoomView--local"
|
||||
>
|
||||
<header
|
||||
class="flex mx_RoomHeader light-panel"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
aria-label="Open room settings"
|
||||
aria-live="off"
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar _avatar-imageless_1qbcf_52"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="button"
|
||||
style="--cpd-avatar-size: 40px;"
|
||||
tabindex="-1"
|
||||
>
|
||||
u
|
||||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
class="mx_RoomHeader_infoWrapper"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_RoomHeader_info box-flex"
|
||||
style="--mx-box-flex: 1;"
|
||||
>
|
||||
<div
|
||||
aria-level="1"
|
||||
class="_typography_6v6n8_153 _font-body-lg-semibold_6v6n8_74 mx_RoomHeader_heading"
|
||||
dir="auto"
|
||||
role="heading"
|
||||
>
|
||||
<span
|
||||
class="mx_RoomHeader_truncated mx_lineClamp"
|
||||
>
|
||||
@user:example.com
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Video call"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="«ri0»"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Voice call"
|
||||
aria-labelledby="«ri5»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m20.958 16.374.039 3.527q0 .427-.33.756-.33.33-.756.33a16 16 0 0 1-6.57-1.105 16.2 16.2 0 0 1-5.563-3.663 16.1 16.1 0 0 1-3.653-5.573 16.3 16.3 0 0 1-1.115-6.56q0-.427.33-.757T4.095 3l3.528.039a1.07 1.07 0 0 1 1.085.93l.543 3.954q.039.271-.039.504a1.1 1.1 0 0 1-.271.426l-1.64 1.64q.505 1.008 1.154 1.909c.433.6 1.444 1.696 1.444 1.696s1.095 1.01 1.696 1.444q.9.65 1.909 1.153l1.64-1.64q.193-.193.426-.27t.504-.04l3.954.543q.406.059.668.359t.262.727"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
aria-labelledby="«ria»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
class=""
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M4 3h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6l-2.293 2.293c-.63.63-1.707.184-1.707-.707V5a2 2 0 0 1 2-2m3 7h10q.424 0 .712-.287A.97.97 0 0 0 18 9a.97.97 0 0 0-.288-.713A.97.97 0 0 0 17 8H7a.97.97 0 0 0-.713.287A.97.97 0 0 0 6 9q0 .424.287.713Q6.576 10 7 10m0 4h6q.424 0 .713-.287A.97.97 0 0 0 14 13a.97.97 0 0 0-.287-.713A.97.97 0 0 0 13 12H7a.97.97 0 0 0-.713.287A.97.97 0 0 0 6 13q0 .424.287.713Q6.576 14 7 14"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
aria-labelledby="«rif»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_zr2a0_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
class=""
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 17q.424 0 .713-.288A.97.97 0 0 0 13 16v-4a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 11a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 12v4q0 .424.287.712.288.288.713.288m0-8q.424 0 .713-.287A.97.97 0 0 0 13 8a.97.97 0 0 0-.287-.713A.97.97 0 0 0 12 7a.97.97 0 0 0-.713.287A.97.97 0 0 0 11 8q0 .424.287.713Q11.576 9 12 9m0 13a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
class="_typography_6v6n8_153 _font-body-sm-medium_6v6n8_41"
|
||||
>
|
||||
<div
|
||||
aria-label="2 members"
|
||||
aria-labelledby="«rik»"
|
||||
class="mx_AccessibleButton mx_FacePile"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_stacked-avatars_1qbcf_102"
|
||||
>
|
||||
<span
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar _avatar-imageless_1qbcf_52"
|
||||
data-color="2"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 20px;"
|
||||
>
|
||||
u
|
||||
</span>
|
||||
<span
|
||||
class="_avatar_1qbcf_8 mx_BaseAvatar _avatar-imageless_1qbcf_52"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 20px;"
|
||||
>
|
||||
u
|
||||
</span>
|
||||
</div>
|
||||
2
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main
|
||||
aria-label="Room content"
|
||||
class="mx_RoomView_body"
|
||||
@@ -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="«rbo»"
|
||||
aria-labelledby="«rca»"
|
||||
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="«rbt»"
|
||||
aria-labelledby="«rcf»"
|
||||
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="«rc2»"
|
||||
aria-labelledby="«rck»"
|
||||
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="«rc7»"
|
||||
aria-labelledby="«rcp»"
|
||||
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="«rcc»"
|
||||
aria-labelledby="«rcu»"
|
||||
class="mx_AccessibleButton mx_FacePile"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@@ -800,6 +800,8 @@ 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="«rd7»"
|
||||
class="mx_E2EIcon mx_MessageComposer_e2eIcon"
|
||||
color="var(--cpd-color-icon-info-primary)"
|
||||
fill="currentColor"
|
||||
@@ -982,7 +984,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="«rdu»"
|
||||
aria-labelledby="«rem»"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -998,7 +1000,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="«re3»"
|
||||
aria-labelledby="«rer»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -1024,7 +1026,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
||||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
aria-labelledby="«re8»"
|
||||
aria-labelledby="«rf0»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -1051,7 +1053,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
||||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
aria-labelledby="«red»"
|
||||
aria-labelledby="«rf5»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -1081,7 +1083,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
||||
>
|
||||
<div
|
||||
aria-label="2 members"
|
||||
aria-labelledby="«rei»"
|
||||
aria-labelledby="«rfa»"
|
||||
class="mx_AccessibleButton mx_FacePile"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@@ -1194,6 +1196,8 @@ 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="«rfj»"
|
||||
class="mx_E2EIcon mx_MessageComposer_e2eIcon"
|
||||
color="var(--cpd-color-icon-info-primary)"
|
||||
fill="currentColor"
|
||||
@@ -1462,7 +1466,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="«r2c»"
|
||||
aria-labelledby="«r2i»"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -1478,7 +1482,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Voice call"
|
||||
aria-labelledby="«r2h»"
|
||||
aria-labelledby="«r2n»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -1504,7 +1508,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
aria-labelledby="«r2m»"
|
||||
aria-labelledby="«r2s»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -1531,7 +1535,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
aria-labelledby="«r2r»"
|
||||
aria-labelledby="«r31»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -1561,7 +1565,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
>
|
||||
<div
|
||||
aria-label="0 members"
|
||||
aria-labelledby="«r30»"
|
||||
aria-labelledby="«r36»"
|
||||
class="mx_AccessibleButton mx_FacePile"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@@ -1674,7 +1678,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="«r2c»"
|
||||
aria-labelledby="«r2i»"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -1690,7 +1694,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Voice call"
|
||||
aria-labelledby="«r2h»"
|
||||
aria-labelledby="«r2n»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -1716,7 +1720,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
aria-labelledby="«r2m»"
|
||||
aria-labelledby="«r2s»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -1743,7 +1747,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
aria-labelledby="«r2r»"
|
||||
aria-labelledby="«r31»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -1773,7 +1777,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
>
|
||||
<div
|
||||
aria-label="0 members"
|
||||
aria-labelledby="«r30»"
|
||||
aria-labelledby="«r36»"
|
||||
class="mx_AccessibleButton mx_FacePile"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@@ -1841,7 +1845,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="«r3e»"
|
||||
aria-labelledby="«r3k»"
|
||||
class="mx_E2EIcon mx_E2EIcon_verified mx_MessageComposer_e2eIcon"
|
||||
data-testid="e2e-icon"
|
||||
>
|
||||
@@ -2052,7 +2056,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
||||
</button>
|
||||
<button
|
||||
aria-label="Chat"
|
||||
aria-labelledby="«r7c»"
|
||||
aria-labelledby="«r7i»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -2079,7 +2083,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
||||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
aria-labelledby="«r7h»"
|
||||
aria-labelledby="«r7n»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -2106,7 +2110,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
||||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
aria-labelledby="«r7m»"
|
||||
aria-labelledby="«r7s»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -2136,7 +2140,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
||||
>
|
||||
<div
|
||||
aria-label="0 members"
|
||||
aria-labelledby="«r7r»"
|
||||
aria-labelledby="«r81»"
|
||||
class="mx_AccessibleButton mx_FacePile"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@@ -2212,7 +2216,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
aria-labelledby="«r84»"
|
||||
aria-labelledby="«r8a»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="secondary"
|
||||
data-testid="base-card-close-button"
|
||||
@@ -2271,6 +2275,8 @@ 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="«r8j»"
|
||||
class="mx_E2EIcon mx_MessageComposer_e2eIcon"
|
||||
color="var(--cpd-color-icon-info-primary)"
|
||||
fill="currentColor"
|
||||
|
||||
@@ -14,7 +14,12 @@ import BaseDialog from "../../../../../src/components/views/dialogs/BaseDialog.t
|
||||
describe("BaseDialog", () => {
|
||||
it("calls onFinished when Escape is pressed", async () => {
|
||||
const onFinished = jest.fn();
|
||||
render(<BaseDialog onFinished={onFinished} />);
|
||||
const { container } = render(<BaseDialog onFinished={onFinished} />);
|
||||
// Autolock's autofocus in the empty dialog is focusing on the close button and bringing up the tooltip
|
||||
// So we either need to call escape twice(one for the tooltip and one for the dialog) or focus
|
||||
// on the dialog first.
|
||||
const dialog = container.querySelector('[role="dialog"]') as HTMLElement;
|
||||
dialog?.focus();
|
||||
await userEvent.keyboard("{Escape}");
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* 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 from "react";
|
||||
import { fireEvent, render } from "jest-matrix-react";
|
||||
|
||||
import { IntegrationsDisabledDialog } from "../../../../../src/components/views/dialogs/IntegrationsDisabledDialog.tsx";
|
||||
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher.ts";
|
||||
import { Action } from "../../../../../src/dispatcher/actions.ts";
|
||||
import { UserTab } from "../../../../../src/components/views/dialogs/UserTab.ts";
|
||||
|
||||
describe("<IntegrationsDisabledDialog />", () => {
|
||||
const onFinished = jest.fn();
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
function renderComponent() {
|
||||
return render(<IntegrationsDisabledDialog onFinished={onFinished} />);
|
||||
}
|
||||
|
||||
it("should render as expected", () => {
|
||||
const { asFragment } = renderComponent();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
it("should do nothing on clicking OK", () => {
|
||||
const { getByText } = renderComponent();
|
||||
fireEvent.click(getByText("OK"));
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
it("should open the correct user settings tab on clicking Settings", () => {
|
||||
jest.spyOn(defaultDispatcher, "dispatch").mockImplementation(() => {});
|
||||
const { getByText } = renderComponent();
|
||||
fireEvent.click(getByText("Settings"));
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Security,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,7 @@ exports[`<ChangelogDialog /> should fetch github proxy url for each repo with ol
|
||||
class="mx_QuestionDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
|
||||
@@ -13,6 +13,7 @@ exports[`ConfirmRejectInviteDialog can reject with options selected 1`] = `
|
||||
class="mx_DeclineAndBlockInviteDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
|
||||
@@ -13,6 +13,7 @@ exports[`ConfirmUserActionDialog renders 1`] = `
|
||||
class="mx_ConfirmUserActionDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
|
||||
@@ -12,6 +12,7 @@ exports[`DevtoolsDialog renders the devtools dialog 1`] = `
|
||||
class="mx_QuestionDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
@@ -33,6 +34,7 @@ exports[`DevtoolsDialog renders the devtools dialog 1`] = `
|
||||
>
|
||||
Room ID: !id
|
||||
<div
|
||||
aria-describedby="«r2»"
|
||||
aria-label="Copy"
|
||||
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||
role="button"
|
||||
|
||||
@@ -7,6 +7,7 @@ exports[`<ExportDialog /> renders export dialog 1`] = `
|
||||
class="mx_ExportDialog false mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
|
||||
@@ -13,6 +13,7 @@ exports[`FeedbackDialog should respect feedback config 1`] = `
|
||||
class="mx_QuestionDialog mx_FeedbackDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<IntegrationsDisabledDialog /> should render as expected 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-labelledby="mx_BaseDialog_title"
|
||||
class="mx_IntegrationsDisabledDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
>
|
||||
<h1
|
||||
class="mx_Heading_h3 mx_Dialog_title"
|
||||
id="mx_BaseDialog_title"
|
||||
>
|
||||
Integrations are disabled
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="mx_IntegrationsDisabledDialog_content"
|
||||
>
|
||||
<p>
|
||||
Enable 'Manage integrations' in Settings to do this.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="mx_Dialog_buttons"
|
||||
>
|
||||
<span
|
||||
class="mx_Dialog_buttons_row"
|
||||
>
|
||||
<button
|
||||
data-testid="dialog-cancel-button"
|
||||
type="button"
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
<button
|
||||
class="mx_Dialog_primary"
|
||||
data-testid="dialog-primary-button"
|
||||
type="button"
|
||||
>
|
||||
Settings
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -13,6 +13,7 @@ exports[`LogoutDialog Prompts user to go to settings if there is a backup on the
|
||||
class="mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
@@ -73,6 +74,7 @@ exports[`LogoutDialog Prompts user to go to settings if there is a backup on the
|
||||
</details>
|
||||
</div>
|
||||
<div
|
||||
aria-describedby="«rq»"
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
@@ -100,6 +102,7 @@ exports[`LogoutDialog Prompts user to go to settings if there is no backup on th
|
||||
class="mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
@@ -160,6 +163,7 @@ exports[`LogoutDialog Prompts user to go to settings if there is no backup on th
|
||||
</details>
|
||||
</div>
|
||||
<div
|
||||
aria-describedby="«r10»"
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
@@ -187,6 +191,7 @@ exports[`LogoutDialog shows a regular dialog when crypto is disabled 1`] = `
|
||||
class="mx_QuestionDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
|
||||
@@ -12,6 +12,7 @@ exports[`<ManageRestrictedJoinRuleDialog /> should list spaces which are not par
|
||||
class="mx_ManageRestrictedJoinRuleDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
@@ -189,6 +190,7 @@ exports[`<ManageRestrictedJoinRuleDialog /> should render empty state 1`] = `
|
||||
class="mx_ManageRestrictedJoinRuleDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
|
||||
@@ -13,6 +13,7 @@ exports[`ManualDeviceKeyVerificationDialog should render correctly 1`] = `
|
||||
class="mx_QuestionDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
|
||||
@@ -12,6 +12,7 @@ exports[`<MessageEditHistory /> should match the snapshot 1`] = `
|
||||
class="mx_MessageEditHistoryDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
@@ -102,6 +103,7 @@ exports[`<MessageEditHistory /> should match the snapshot 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-describedby="«r2»"
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
@@ -128,6 +130,7 @@ exports[`<MessageEditHistory /> should support events with 1`] = `
|
||||
class="mx_MessageEditHistoryDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
@@ -315,6 +318,7 @@ exports[`<MessageEditHistory /> should support events with 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-describedby="«r8»"
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
|
||||
@@ -13,6 +13,7 @@ exports[`ReportRoomDialog displays admin message 1`] = `
|
||||
class="mx_ReportRoomDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
|
||||
@@ -13,6 +13,7 @@ exports[`<ServerPickerDialog /> should render dialog 1`] = `
|
||||
class="mx_ServerPickerDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
|
||||
@@ -13,6 +13,7 @@ exports[`ShareDialog should not render the QR code if disabled 1`] = `
|
||||
class="mx_ShareDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
@@ -143,6 +144,7 @@ exports[`ShareDialog should not render the socials if disabled 1`] = `
|
||||
class="mx_ShareDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
@@ -229,6 +231,7 @@ exports[`ShareDialog should render a share dialog for a matrix event 1`] = `
|
||||
class="mx_ShareDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
@@ -402,6 +405,7 @@ exports[`ShareDialog should render a share dialog for a room 1`] = `
|
||||
class="mx_ShareDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
@@ -574,6 +578,7 @@ exports[`ShareDialog should render a share dialog for a room member 1`] = `
|
||||
class="mx_ShareDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
@@ -719,6 +724,7 @@ exports[`ShareDialog should render a share dialog for an URL 1`] = `
|
||||
class="mx_ShareDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
|
||||
@@ -12,6 +12,7 @@ exports[`<UnpinAllDialog /> should render 1`] = `
|
||||
class="mx_UnpinAllDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
|
||||
@@ -12,6 +12,7 @@ exports[`<UntrustedDeviceDialog /> should display the dialog for the device of a
|
||||
class="mx_UntrustedDeviceDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
@@ -91,6 +92,7 @@ exports[`<UntrustedDeviceDialog /> should display the dialog for the device of t
|
||||
class="mx_UntrustedDeviceDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
|
||||
@@ -12,6 +12,7 @@ exports[`CreateSecretStorageDialog handles the happy path 1`] = `
|
||||
class="mx_CreateSecretStorageDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
@@ -140,6 +141,7 @@ exports[`CreateSecretStorageDialog handles the happy path 2`] = `
|
||||
class="mx_CreateSecretStorageDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
@@ -231,6 +233,7 @@ exports[`CreateSecretStorageDialog when there is an error fetching the backup ve
|
||||
class="mx_CreateSecretStorageDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
|
||||
@@ -12,6 +12,7 @@ exports[`ExportE2eKeysDialog renders 1`] = `
|
||||
class="mx_exportE2eKeysDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
|
||||
@@ -12,6 +12,7 @@ exports[`ImportE2eKeysDialog renders 1`] = `
|
||||
class="mx_importE2eKeysDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
|
||||
@@ -12,6 +12,7 @@ exports[`<RestoreKeyBackupDialog /> should display an error when recovery key is
|
||||
class="mx_RestoreKeyBackupDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
@@ -112,6 +113,7 @@ exports[`<RestoreKeyBackupDialog /> should not raise an error when recovery is v
|
||||
class="mx_RestoreKeyBackupDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
@@ -211,6 +213,7 @@ exports[`<RestoreKeyBackupDialog /> should render 1`] = `
|
||||
class="mx_RestoreKeyBackupDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
@@ -283,6 +286,7 @@ exports[`<RestoreKeyBackupDialog /> should render 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-describedby="«r2»"
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
@@ -309,6 +313,7 @@ exports[`<RestoreKeyBackupDialog /> should restore key backup when Recovery key
|
||||
class="mx_RestoreKeyBackupDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
@@ -374,6 +379,7 @@ exports[`<RestoreKeyBackupDialog /> should restore key backup when passphrase is
|
||||
class="mx_RestoreKeyBackupDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
@@ -439,6 +445,7 @@ exports[`<RestoreKeyBackupDialog /> should restore key backup when the key is ca
|
||||
class="mx_RestoreKeyBackupDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
@@ -478,6 +485,7 @@ exports[`<RestoreKeyBackupDialog /> should restore key backup when the key is ca
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-describedby="«rk»"
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
@@ -504,6 +512,7 @@ exports[`<RestoreKeyBackupDialog /> should restore key backup when the key is in
|
||||
class="mx_RestoreKeyBackupDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
@@ -543,6 +552,7 @@ exports[`<RestoreKeyBackupDialog /> should restore key backup when the key is in
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-describedby="«rq»"
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_Dialog_cancelButton"
|
||||
role="button"
|
||||
|
||||
@@ -340,8 +340,8 @@ exports[`AppTile for a pinned widget should render permission request 1`] = `
|
||||
<span>
|
||||
Using this widget may share data
|
||||
<div
|
||||
aria-describedby="«r2j»"
|
||||
aria-labelledby="«r2i»"
|
||||
aria-describedby="«r2n»"
|
||||
aria-labelledby="«r2m»"
|
||||
class="mx_TextWithTooltip_target mx_TextWithTooltip_target--helpIcon"
|
||||
>
|
||||
<svg
|
||||
|
||||
@@ -21,6 +21,7 @@ exports[`<ImageView /> renders correctly 1`] = `
|
||||
class="mx_ImageView_toolbar"
|
||||
>
|
||||
<div
|
||||
aria-describedby="«r2»"
|
||||
aria-label="Zoom out"
|
||||
class="mx_AccessibleButton mx_ImageView_button mx_ImageView_button_zoomOut"
|
||||
role="button"
|
||||
|
||||
@@ -32,6 +32,7 @@ exports[`<LocationViewDialog /> renders map correctly 1`] = `
|
||||
class="mx_ZoomButtons"
|
||||
>
|
||||
<div
|
||||
aria-describedby="«r6»"
|
||||
aria-label="Zoom in"
|
||||
class="mx_AccessibleButton mx_ZoomButtons_button"
|
||||
data-testid="map-zoom-in-button"
|
||||
|
||||
@@ -151,7 +151,7 @@ describe("CallEvent", () => {
|
||||
}),
|
||||
);
|
||||
defaultDispatcher.unregister(dispatcherRef);
|
||||
await act(() => call.start());
|
||||
act(() => call.setConnectionState(ConnectionState.Connected));
|
||||
|
||||
// Test that the leave button works
|
||||
fireEvent.click(screen.getByRole("button", { name: "Leave" }));
|
||||
|
||||
@@ -48,6 +48,7 @@ describe("HideActionButton", () => {
|
||||
beforeEach(() => {
|
||||
cli = getMockClientWithEventEmitter({
|
||||
getRoom: jest.fn(),
|
||||
getUserId: jest.fn(),
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
|
||||
@@ -35,10 +35,11 @@ jest.mock("matrix-encrypt-attachment", () => ({
|
||||
}));
|
||||
|
||||
describe("<MImageBody/>", () => {
|
||||
const userId = "@user:server";
|
||||
const ourUserId = "@user:server";
|
||||
const senderUserId = "@other_use:server";
|
||||
const deviceId = "DEADB33F";
|
||||
const cli = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
...mockClientMethodsUser(ourUserId),
|
||||
...mockClientMethodsServer(),
|
||||
...mockClientMethodsDevice(deviceId),
|
||||
...mockClientMethodsCrypto(),
|
||||
@@ -62,7 +63,7 @@ describe("<MImageBody/>", () => {
|
||||
const encryptedMediaEvent = new MatrixEvent({
|
||||
event_id: "$foo:bar",
|
||||
room_id: "!room:server",
|
||||
sender: userId,
|
||||
sender: senderUserId,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
body: "alt for a test image",
|
||||
@@ -201,7 +202,7 @@ describe("<MImageBody/>", () => {
|
||||
|
||||
const event = new MatrixEvent({
|
||||
room_id: "!room:server",
|
||||
sender: userId,
|
||||
sender: senderUserId,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
body: "alt for a test image",
|
||||
@@ -254,7 +255,7 @@ describe("<MImageBody/>", () => {
|
||||
|
||||
const event = new MatrixEvent({
|
||||
room_id: "!room:server",
|
||||
sender: userId,
|
||||
sender: senderUserId,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
body: "alt for a test image",
|
||||
@@ -281,7 +282,7 @@ describe("<MImageBody/>", () => {
|
||||
it("should show banner on hover", async () => {
|
||||
const event = new MatrixEvent({
|
||||
room_id: "!room:server",
|
||||
sender: userId,
|
||||
sender: senderUserId,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
body: "alt for a test image",
|
||||
|
||||
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import React from "react";
|
||||
import { EventType, getHttpUriForMxc, type IContent, type MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { fireEvent, render, screen, type RenderResult } from "jest-matrix-react";
|
||||
import { fireEvent, render, screen } from "jest-matrix-react";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import { type MockedObject } from "jest-mock";
|
||||
|
||||
@@ -34,7 +34,8 @@ jest.mock("matrix-encrypt-attachment", () => ({
|
||||
}));
|
||||
|
||||
describe("MVideoBody", () => {
|
||||
const userId = "@user:server";
|
||||
const ourUserId = "@user:server";
|
||||
const senderUserId = "@other_use:server";
|
||||
const deviceId = "DEADB33F";
|
||||
|
||||
const thumbUrl = "https://server/_matrix/media/v3/download/server/encrypted-poster";
|
||||
@@ -42,7 +43,7 @@ describe("MVideoBody", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cli = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
...mockClientMethodsUser(ourUserId),
|
||||
...mockClientMethodsServer(),
|
||||
...mockClientMethodsDevice(deviceId),
|
||||
...mockClientMethodsCrypto(),
|
||||
@@ -67,7 +68,7 @@ describe("MVideoBody", () => {
|
||||
|
||||
const encryptedMediaEvent = new MatrixEvent({
|
||||
room_id: "!room:server",
|
||||
sender: userId,
|
||||
sender: senderUserId,
|
||||
type: EventType.RoomMessage,
|
||||
event_id: "$foo:bar",
|
||||
content: {
|
||||
@@ -86,10 +87,47 @@ describe("MVideoBody", () => {
|
||||
},
|
||||
});
|
||||
|
||||
it("does not crash when given a portrait image", () => {
|
||||
it("does not crash when given portrait dimensions", () => {
|
||||
// Check for an unreliable crash caused by a fractional-sized
|
||||
// image dimension being used for a CanvasImageData.
|
||||
const { asFragment } = makeMVideoBody(720, 1280);
|
||||
const content: IContent = {
|
||||
info: {
|
||||
"w": 720,
|
||||
"h": 1280,
|
||||
"mimetype": "video/mp4",
|
||||
"size": 2495675,
|
||||
"thumbnail_file": {
|
||||
url: "",
|
||||
key: { alg: "", key_ops: [], kty: "", k: "", ext: true },
|
||||
iv: "",
|
||||
hashes: {},
|
||||
v: "",
|
||||
},
|
||||
"thumbnail_info": { mimetype: "" },
|
||||
"xyz.amorgan.blurhash": "TrGl6bofof~paxWC?bj[oL%2fPj]",
|
||||
},
|
||||
url: "http://example.com",
|
||||
};
|
||||
|
||||
const event = new MatrixEvent({
|
||||
content,
|
||||
});
|
||||
|
||||
const defaultProps: IBodyProps = {
|
||||
mxEvent: event,
|
||||
highlights: [],
|
||||
highlightLink: "",
|
||||
onMessageAllowed: jest.fn(),
|
||||
permalinkCreator: {} as RoomPermalinkCreator,
|
||||
mediaEventHelper: { media: { isEncrypted: false } } as MediaEventHelper,
|
||||
};
|
||||
|
||||
const { asFragment } = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<MVideoBody {...defaultProps} />
|
||||
</MatrixClientContext.Provider>,
|
||||
withClientContextRenderOptions(cli),
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
// If we get here, we did not crash.
|
||||
});
|
||||
@@ -153,50 +191,39 @@ describe("MVideoBody", () => {
|
||||
|
||||
expect(fetchMock).toHaveFetched(thumbUrl);
|
||||
});
|
||||
|
||||
it("should download video if we were the sender", async () => {
|
||||
fetchMock.getOnce(thumbUrl, { status: 200 });
|
||||
const ourEncryptedMediaEvent = new MatrixEvent({
|
||||
room_id: "!room:server",
|
||||
sender: ourUserId,
|
||||
type: EventType.RoomMessage,
|
||||
event_id: "$foo:bar",
|
||||
content: {
|
||||
body: "alt for a test video",
|
||||
info: {
|
||||
duration: 420,
|
||||
w: 40,
|
||||
h: 50,
|
||||
thumbnail_file: {
|
||||
url: "mxc://server/encrypted-poster",
|
||||
},
|
||||
},
|
||||
file: {
|
||||
url: "mxc://server/encrypted-image",
|
||||
},
|
||||
},
|
||||
});
|
||||
const { asFragment } = render(
|
||||
<MVideoBody
|
||||
mxEvent={ourEncryptedMediaEvent}
|
||||
mediaEventHelper={new MediaEventHelper(ourEncryptedMediaEvent)}
|
||||
/>,
|
||||
withClientContextRenderOptions(cli),
|
||||
);
|
||||
|
||||
expect(fetchMock).toHaveFetched(thumbUrl);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function makeMVideoBody(w: number, h: number): RenderResult {
|
||||
const content: IContent = {
|
||||
info: {
|
||||
"w": w,
|
||||
"h": h,
|
||||
"mimetype": "video/mp4",
|
||||
"size": 2495675,
|
||||
"thumbnail_file": {
|
||||
url: "",
|
||||
key: { alg: "", key_ops: [], kty: "", k: "", ext: true },
|
||||
iv: "",
|
||||
hashes: {},
|
||||
v: "",
|
||||
},
|
||||
"thumbnail_info": { mimetype: "" },
|
||||
"xyz.amorgan.blurhash": "TrGl6bofof~paxWC?bj[oL%2fPj]",
|
||||
},
|
||||
url: "http://example.com",
|
||||
};
|
||||
|
||||
const event = new MatrixEvent({
|
||||
content,
|
||||
});
|
||||
|
||||
const defaultProps: IBodyProps = {
|
||||
mxEvent: event,
|
||||
highlights: [],
|
||||
highlightLink: "",
|
||||
onMessageAllowed: jest.fn(),
|
||||
permalinkCreator: {} as RoomPermalinkCreator,
|
||||
mediaEventHelper: { media: { isEncrypted: false } } as MediaEventHelper,
|
||||
};
|
||||
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
mxcUrlToHttp: jest.fn(),
|
||||
getRoom: jest.fn(),
|
||||
});
|
||||
|
||||
return render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<MVideoBody {...defaultProps} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`MVideoBody does not crash when given a portrait image 1`] = `
|
||||
exports[`MVideoBody does not crash when given portrait dimensions 1`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
class="mx_MVideoBody"
|
||||
@@ -48,3 +48,28 @@ exports[`MVideoBody should show poster for encrypted media before downloading it
|
||||
</span>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`MVideoBody with video previews/thumbnails disabled should download video if we were the sender 1`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
class="mx_MVideoBody"
|
||||
>
|
||||
<div
|
||||
class="mx_MVideoBody_container"
|
||||
style="max-width: 40px; max-height: 50px;"
|
||||
>
|
||||
<video
|
||||
class="mx_MVideoBody"
|
||||
controls=""
|
||||
controlslist="nodownload"
|
||||
poster="https://server/_matrix/media/v3/download/server/encrypted-poster"
|
||||
preload="none"
|
||||
title="alt for a test video"
|
||||
/>
|
||||
<div
|
||||
style="width: 40px; height: 50px;"
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
@@ -215,6 +215,7 @@ exports[`<RoomSummaryCard /> has button to edit topic 1`] = `
|
||||
aria-hidden="true"
|
||||
class="_input_19o42_24"
|
||||
id="«rv»"
|
||||
role="switch"
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
@@ -939,6 +940,7 @@ exports[`<RoomSummaryCard /> renders the room summary 1`] = `
|
||||
aria-hidden="true"
|
||||
class="_input_19o42_24"
|
||||
id="«r5»"
|
||||
role="switch"
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
@@ -1701,6 +1703,7 @@ exports[`<RoomSummaryCard /> renders the room topic in the summary 1`] = `
|
||||
aria-hidden="true"
|
||||
class="_input_19o42_24"
|
||||
id="«ri»"
|
||||
role="switch"
|
||||
type="checkbox"
|
||||
/>
|
||||
<div
|
||||
|
||||
@@ -52,7 +52,6 @@ import * as ShieldUtils from "../../../../../../src/utils/ShieldUtils";
|
||||
import { Container, WidgetLayoutStore } from "../../../../../../src/stores/widgets/WidgetLayoutStore";
|
||||
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
|
||||
import { _t } from "../../../../../../src/languageHandler";
|
||||
import * as UseCall from "../../../../../../src/hooks/useCall";
|
||||
import { SdkContextClass } from "../../../../../../src/contexts/SDKContext";
|
||||
import WidgetStore, { type IApp } from "../../../../../../src/stores/WidgetStore";
|
||||
import { UIFeature } from "../../../../../../src/settings/UIFeature";
|
||||
@@ -96,6 +95,10 @@ describe("RoomHeader", () => {
|
||||
|
||||
setCardSpy = jest.spyOn(RightPanelStore.instance, "setCard");
|
||||
jest.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(ShieldUtils.E2EStatus.Normal);
|
||||
|
||||
// Mock CallStore.instance.getCall to return null by default
|
||||
// Individual tests can override this when they need a specific Call object
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(null);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -555,7 +558,8 @@ describe("RoomHeader", () => {
|
||||
|
||||
it("join button is shown if there is an ongoing call", async () => {
|
||||
mockRoomMembers(room, 3);
|
||||
jest.spyOn(UseCall, "useParticipantCount").mockReturnValue(3);
|
||||
// Mock CallStore to return a call with 3 participants
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(createMockCall(ROOM_ID, 3));
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
const joinButton = getByLabelText(document.body, "Join");
|
||||
expect(joinButton).not.toHaveAttribute("aria-disabled", "true");
|
||||
@@ -563,7 +567,8 @@ describe("RoomHeader", () => {
|
||||
|
||||
it("join button is disabled if there is an other ongoing call", async () => {
|
||||
mockRoomMembers(room, 3);
|
||||
jest.spyOn(UseCall, "useParticipantCount").mockReturnValue(3);
|
||||
// Mock CallStore to return a call with 3 participants
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(createMockCall(ROOM_ID, 3));
|
||||
jest.spyOn(CallStore.prototype, "connectedCalls", "get").mockReturnValue(
|
||||
new Set([{ roomId: "some_other_room" } as Call]),
|
||||
);
|
||||
@@ -583,7 +588,8 @@ describe("RoomHeader", () => {
|
||||
|
||||
it("close lobby button is shown if there is an ongoing call but we are viewing the lobby", async () => {
|
||||
mockRoomMembers(room, 3);
|
||||
jest.spyOn(UseCall, "useParticipantCount").mockReturnValue(3);
|
||||
// Mock CallStore to return a call with 3 participants
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(createMockCall(ROOM_ID, 3));
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "isViewingCall").mockReturnValue(true);
|
||||
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
@@ -789,6 +795,34 @@ describe("RoomHeader", () => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a mock Call object with stable participants to prevent React dependency errors
|
||||
*/
|
||||
function createMockCall(roomId: string = "!1:example.org", participantCount: number = 0): Call {
|
||||
const participants = new Map();
|
||||
|
||||
// Create mock participants with devices
|
||||
for (let i = 0; i < participantCount; i++) {
|
||||
const mockMember = {
|
||||
userId: `@user-${i}:example.org`,
|
||||
name: `Member ${i}`,
|
||||
} as RoomMember;
|
||||
|
||||
const deviceSet = new Set([`device-${i}`]);
|
||||
participants.set(mockMember, deviceSet);
|
||||
}
|
||||
|
||||
return {
|
||||
roomId,
|
||||
participants,
|
||||
widget: { id: "test-widget" },
|
||||
connectionState: "disconnected",
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
} as unknown as Call;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param count the number of users to create
|
||||
|
||||
@@ -37,6 +37,7 @@ describe("<RoomListItemView />", () => {
|
||||
onFocus: jest.fn(),
|
||||
roomIndex: 0,
|
||||
roomCount: 1,
|
||||
listIsScrolling: false,
|
||||
};
|
||||
|
||||
return render(<RoomListItemView {...defaultProps} {...props} />, withClientContextRenderOptions(matrixClient));
|
||||
@@ -128,6 +129,7 @@ describe("<RoomListItemView />", () => {
|
||||
onFocus={jest.fn()}
|
||||
roomIndex={0}
|
||||
roomCount={1}
|
||||
listIsScrolling={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -191,4 +193,26 @@ describe("<RoomListItemView />", () => {
|
||||
await user.keyboard("{Escape}");
|
||||
expect(screen.queryByRole("menu")).toBeNull();
|
||||
});
|
||||
|
||||
test("should not render context menu when list is scrolling", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
mocked(useRoomListItemViewModel).mockReturnValue({
|
||||
...defaultValue,
|
||||
showContextMenu: true,
|
||||
});
|
||||
|
||||
renderRoomListItem({
|
||||
listIsScrolling: true,
|
||||
});
|
||||
|
||||
const button = screen.getByRole("option", { name: `Open room ${room.name}` });
|
||||
await user.pointer([{ target: button }, { keys: "[MouseRight]", target: button }]);
|
||||
|
||||
// Context menu should not appear when scrolling
|
||||
expect(screen.queryByRole("menu")).toBeNull();
|
||||
|
||||
// But the room item itself should still be rendered
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,6 +46,7 @@ import { UIComponent } from "../../../../../src/settings/UIFeature";
|
||||
import { MessagePreviewStore } from "../../../../../src/stores/room-list/MessagePreviewStore";
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
import { ConnectionState } from "../../../../../src/models/Call";
|
||||
|
||||
jest.mock("../../../../../src/customisations/helpers/UIComponents", () => ({
|
||||
shouldShowComponent: jest.fn(),
|
||||
@@ -215,7 +216,7 @@ describe("RoomTile", () => {
|
||||
it("tracks connection state", async () => {
|
||||
renderRoomTile();
|
||||
screen.getByText("Video");
|
||||
await act(() => call.start());
|
||||
act(() => call.setConnectionState(ConnectionState.Connected));
|
||||
screen.getByText("Joined");
|
||||
await act(() => call.disconnect());
|
||||
screen.getByText("Video");
|
||||
|
||||
@@ -185,7 +185,7 @@ exports[`<PinnedEventTile /> should render the menu with all the options 1`] = `
|
||||
aria-label="Open menu"
|
||||
aria-labelledby="radix-«r10»"
|
||||
aria-orientation="vertical"
|
||||
class="_menu_19sse_8"
|
||||
class="_menu_1glhz_8"
|
||||
data-align="start"
|
||||
data-orientation="vertical"
|
||||
data-radix-menu-content=""
|
||||
@@ -376,7 +376,7 @@ exports[`<PinnedEventTile /> should render the menu without unpin and delete 1`]
|
||||
aria-label="Open menu"
|
||||
aria-labelledby="radix-«rl»"
|
||||
aria-orientation="vertical"
|
||||
class="_menu_19sse_8"
|
||||
class="_menu_1glhz_8"
|
||||
data-align="start"
|
||||
data-orientation="vertical"
|
||||
data-radix-menu-content=""
|
||||
|
||||
@@ -70,12 +70,12 @@ describe("<LayoutSwitcher />", () => {
|
||||
await SettingsStore.setValue("useCompactLayout", null, SettingLevel.DEVICE, true);
|
||||
await renderLayoutSwitcher();
|
||||
|
||||
expect(screen.getByRole("checkbox", { name: "Show compact text and messages" })).toBeChecked();
|
||||
expect(screen.getByRole("switch", { name: "Show compact text and messages" })).toBeChecked();
|
||||
});
|
||||
|
||||
it("should change the setting when toggled", async () => {
|
||||
await renderLayoutSwitcher();
|
||||
act(() => screen.getByRole("checkbox", { name: "Show compact text and messages" }).click());
|
||||
act(() => screen.getByRole("switch", { name: "Show compact text and messages" }).click());
|
||||
|
||||
await waitFor(() => expect(SettingsStore.getValue("useCompactLayout")).toBe(true));
|
||||
});
|
||||
@@ -83,7 +83,7 @@ describe("<LayoutSwitcher />", () => {
|
||||
it("should be disabled when the modern layout is not enabled", async () => {
|
||||
await SettingsStore.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
|
||||
await renderLayoutSwitcher();
|
||||
expect(screen.getByRole("checkbox", { name: "Show compact text and messages" })).toBeDisabled();
|
||||
expect(screen.getByRole("switch", { name: "Show compact text and messages" })).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -56,24 +56,24 @@ describe("<ThemeChoicePanel />", () => {
|
||||
describe("system theme", () => {
|
||||
it("should disable Match system theme", async () => {
|
||||
render(<ThemeChoicePanel />);
|
||||
expect(screen.getByRole("checkbox", { name: "Match system theme" })).not.toBeChecked();
|
||||
expect(screen.getByRole("switch", { name: "Match system theme" })).not.toBeChecked();
|
||||
});
|
||||
|
||||
it("should enable Match system theme", async () => {
|
||||
await enableSystemTheme(true);
|
||||
|
||||
render(<ThemeChoicePanel />);
|
||||
expect(screen.getByRole("checkbox", { name: "Match system theme" })).toBeChecked();
|
||||
expect(screen.getByRole("switch", { name: "Match system theme" })).toBeChecked();
|
||||
});
|
||||
|
||||
it("should change the system theme when clicked", async () => {
|
||||
jest.spyOn(SettingsStore, "setValue");
|
||||
|
||||
render(<ThemeChoicePanel />);
|
||||
act(() => screen.getByRole("checkbox", { name: "Match system theme" }).click());
|
||||
act(() => screen.getByRole("switch", { name: "Match system theme" }).click());
|
||||
|
||||
// The system theme should be enabled
|
||||
expect(screen.getByRole("checkbox", { name: "Match system theme" })).toBeChecked();
|
||||
expect(screen.getByRole("switch", { name: "Match system theme" })).toBeChecked();
|
||||
expect(SettingsStore.setValue).toHaveBeenCalledWith("use_system_theme", null, "device", true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -418,6 +418,7 @@ exports[`<LayoutSwitcher /> should render 1`] = `
|
||||
class="_input_19o42_24"
|
||||
id="radix-«rr»"
|
||||
name="compactLayout"
|
||||
role="switch"
|
||||
title=""
|
||||
type="checkbox"
|
||||
/>
|
||||
|
||||
@@ -34,6 +34,7 @@ exports[`<ThemeChoicePanel /> custom theme should display custom theme 1`] = `
|
||||
class="_input_19o42_24"
|
||||
id="radix-«r28»"
|
||||
name="systemTheme"
|
||||
role="switch"
|
||||
title=""
|
||||
type="checkbox"
|
||||
/>
|
||||
@@ -312,6 +313,7 @@ exports[`<ThemeChoicePanel /> custom theme should render the custom theme sectio
|
||||
class="_input_19o42_24"
|
||||
id="radix-«r10»"
|
||||
name="systemTheme"
|
||||
role="switch"
|
||||
title=""
|
||||
type="checkbox"
|
||||
/>
|
||||
@@ -590,6 +592,7 @@ exports[`<ThemeChoicePanel /> renders the theme choice UI 1`] = `
|
||||
class="_input_19o42_24"
|
||||
id="radix-«r0»"
|
||||
name="systemTheme"
|
||||
role="switch"
|
||||
title=""
|
||||
type="checkbox"
|
||||
/>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user