mirror of
https://github.com/element-hq/element-web.git
synced 2025-12-09 01:30:57 +00:00
Compare commits
18 Commits
e2ee-recov
...
hs/update-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b17591f83 | ||
|
|
9e3533dfb7 | ||
|
|
7154f85682 | ||
|
|
a8fda2b1a1 | ||
|
|
6ac073f5b0 | ||
|
|
6caf0e21f3 | ||
|
|
c8cf16a3a2 | ||
|
|
974574159f | ||
|
|
c5eb09d96b | ||
|
|
bef1a5f902 | ||
|
|
7d121c5bc9 | ||
|
|
f7d550c847 | ||
|
|
1cb78d1842 | ||
|
|
7ec3b2e732 | ||
|
|
dc0a3bc7e8 | ||
|
|
eeedd6ddba | ||
|
|
8afa1c1220 | ||
|
|
c138f1bec3 |
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -2,7 +2,6 @@
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] I have read through [review guidelines](../docs/review.md) and [CONTRIBUTING.md](../CONTRIBUTING.md).
|
||||
- [ ] Tests written for new code (and old code if feasible).
|
||||
- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation.
|
||||
- [ ] Linter and other CI checks pass.
|
||||
|
||||
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@@ -88,7 +88,7 @@ jobs:
|
||||
run: mdbook build
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
|
||||
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
|
||||
with:
|
||||
path: ./book
|
||||
|
||||
|
||||
25
CHANGELOG.md
25
CHANGELOG.md
@@ -1,28 +1,3 @@
|
||||
Changes in [1.11.110](https://github.com/element-hq/element-web/releases/tag/v1.11.110) (2025-08-27)
|
||||
====================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Hide recovery key when re-entering it while creating or changing it ([#30499](https://github.com/element-hq/element-web/pull/30499)). Contributed by @andybalaam.
|
||||
* Add `?no_universal_links=true` to OIDC url so EX doesn't try to handle it ([#29439](https://github.com/element-hq/element-web/pull/29439)). Contributed by @t3chguy.
|
||||
* Show a blue lock for unencrypted rooms and hide the grey shield for encrypted rooms ([#30440](https://github.com/element-hq/element-web/pull/30440)). Contributed by @langleyd.
|
||||
* Add support for Module API 1.4 ([#30185](https://github.com/element-hq/element-web/pull/30185)). Contributed by @t3chguy.
|
||||
* MVVM - Introduce some helpers for snapshot management ([#30398](https://github.com/element-hq/element-web/pull/30398)). Contributed by @MidhunSureshR.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* A11y: move focus to right panel when opened ([#30553](https://github.com/element-hq/element-web/pull/30553)). Contributed by @florianduros.
|
||||
* Fix e2e warning icon should be white ([#30539](https://github.com/element-hq/element-web/pull/30539)). Contributed by @florianduros.
|
||||
* Remove NoOneHere disabled reason. ([#30524](https://github.com/element-hq/element-web/pull/30524)). Contributed by @toger5.
|
||||
* Fix downloading files with authenticated media API ([#30520](https://github.com/element-hq/element-web/pull/30520)). Contributed by @t3chguy.
|
||||
* Fix call permissions check confusion around element call ([#30521](https://github.com/element-hq/element-web/pull/30521)). Contributed by @t3chguy.
|
||||
* Fix line wrap around emoji verification ([#30523](https://github.com/element-hq/element-web/pull/30523)). Contributed by @t3chguy.
|
||||
* Don't highlight redacted events ([#30519](https://github.com/element-hq/element-web/pull/30519)). Contributed by @t3chguy.
|
||||
* Fix matrix.to links not being handled in the app ([#30522](https://github.com/element-hq/element-web/pull/30522)). Contributed by @t3chguy.
|
||||
* Fix issue of new room list taking up the full width ([#30459](https://github.com/element-hq/element-web/pull/30459)). Contributed by @langleyd.
|
||||
* Fix widget persistence in React development mode ([#30509](https://github.com/element-hq/element-web/pull/30509)). Contributed by @robintown.
|
||||
* Fix widget initialization in React development mode ([#30463](https://github.com/element-hq/element-web/pull/30463)). Contributed by @robintown.
|
||||
|
||||
|
||||
Changes in [1.11.109](https://github.com/element-hq/element-web/releases/tag/v1.11.109) (2025-08-11)
|
||||
====================================================================================================
|
||||
This release supports the upcoming v12 ("hydra") Matrix room version and is necessary to view and participate in these rooms.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# syntax=docker.io/docker/dockerfile:1.17-labs@sha256:9187104f31e3a002a8a6a3209ea1f937fb7486c093cbbde1e14b0fa0d7e4f1b5
|
||||
|
||||
# Builder
|
||||
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:f7f28d1962d93cc096ea6327378d990284757fec281ce48e42436e7b4b167fa2 AS builder
|
||||
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:9e34ba52e1f3c31ed9bd4d0bcf784f5909db17cda61c220e29c8d7a8ebfb402e AS builder
|
||||
|
||||
# Support custom branch of the js-sdk. This also helps us build images of element-web develop.
|
||||
ARG USE_CUSTOM_SDKS=false
|
||||
@@ -19,7 +19,7 @@ RUN /src/scripts/docker-package.sh
|
||||
RUN cp /src/config.sample.json /src/webapp/config.json
|
||||
|
||||
# App
|
||||
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:0d019e980f83728002de7a6d8819d0d4af7179046d3946b8b37749953fbb28e6
|
||||
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:ea6c4b8b568824ea94cd1fabd47e1c4e7c0c04744f344a3793f7e9c8ac3a3636
|
||||
|
||||
# Need root user to install packages & manipulate the usr directory
|
||||
USER root
|
||||
|
||||
@@ -14,9 +14,10 @@ entrypoint_log() {
|
||||
mkdir -p /tmp/element-web-config
|
||||
cp /app/config*.json /tmp/element-web-config/
|
||||
|
||||
# If the module directory exists AND the module directory has modules in it
|
||||
if [ -d "/modules" ] && [ "$( ls -A '/modules' )" ]; then
|
||||
# If there are modules to be loaded
|
||||
if [ -d "/modules" ]; then
|
||||
cd /modules
|
||||
|
||||
for MODULE in *
|
||||
do
|
||||
# If the module has a package.json, use its main field as the entrypoint
|
||||
|
||||
43
docs/e2ee.md
43
docs/e2ee.md
@@ -38,20 +38,45 @@ When `force_disable` is true:
|
||||
Note: If the server is configured to forcibly enable encryption for some or all rooms,
|
||||
this behaviour will be overridden.
|
||||
|
||||
# Setting up recovery
|
||||
# Secure backup
|
||||
|
||||
By default, Element strongly encourages (but does not require) users to set up
|
||||
recovery so that you can access history on your new devices as well as retain access to your message history and cryptographic identity when you lose all of your devices.
|
||||
Secure Backup so that cross-signing identity key and message keys can be
|
||||
recovered in case of a disaster where you lose access to all active devices.
|
||||
|
||||
## Removal of old settings
|
||||
## Requiring secure backup
|
||||
|
||||
Support for the configuration options `secure_backup_required` and `secure_backup_setup_methods`
|
||||
in the `/.well-known/matrix/client` config has been removed.
|
||||
To require Secure Backup to be configured before Element can be used, set the
|
||||
following on your homeserver's `/.well-known/matrix/client` config:
|
||||
|
||||
Setting up recovery is now always recommended to all users by showing a one-off toast and a
|
||||
permanent red dot on the _Encryption_ tab in the _Settings_ dialog. When creating a new
|
||||
recovery key, the UI only supports auto-generated keys. Using an existing (custom) passphrase
|
||||
still works, but is not exposed in the UI when setting up recovery.
|
||||
```json
|
||||
{
|
||||
"io.element.e2ee": {
|
||||
"secure_backup_required": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Preferring setup methods
|
||||
|
||||
By default, Element offers users a choice of a random key or user-chosen
|
||||
passphrase when setting up Secure Backup. If a homeserver admin would like to
|
||||
only offer one of these, you can signal this via the
|
||||
`/.well-known/matrix/client` config, for example:
|
||||
|
||||
```json
|
||||
{
|
||||
"io.element.e2ee": {
|
||||
"secure_backup_setup_methods": ["passphrase"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The field `secure_backup_setup_methods` is an array listing the methods the
|
||||
client should display. Supported values currently include `key` and
|
||||
`passphrase`. If the `secure_backup_setup_methods` field is not present or
|
||||
exists but does not contain any supported methods, Element will fallback to the
|
||||
default value of: `["key", "passphrase"]`.
|
||||
|
||||
# Compatibility
|
||||
|
||||
|
||||
11
knip.ts
11
knip.ts
@@ -2,6 +2,7 @@ import { KnipConfig } from "knip";
|
||||
|
||||
export default {
|
||||
entry: [
|
||||
"src/vector/index.ts",
|
||||
"src/serviceworker/index.ts",
|
||||
"src/workers/*.worker.ts",
|
||||
"src/utils/exportUtils/exportJS.js",
|
||||
@@ -11,6 +12,8 @@ export default {
|
||||
"res/decoder-ring/**",
|
||||
"res/jitsi_external_api.min.js",
|
||||
"docs/**",
|
||||
// Used by jest
|
||||
"__mocks__/maplibre-gl.js",
|
||||
],
|
||||
project: ["**/*.{js,ts,jsx,tsx}"],
|
||||
ignore: [
|
||||
@@ -39,18 +42,10 @@ export default {
|
||||
"util",
|
||||
// Embedded into webapp
|
||||
"@element-hq/element-call-embedded",
|
||||
|
||||
// Used by matrix-js-sdk, which means we have to include them as a
|
||||
// dependency so that // we can run `tsc` (since we import the typescript
|
||||
// source of js-sdk, rather than the transpiled and annotated JS like you
|
||||
// would with a normal library).
|
||||
"@types/content-type",
|
||||
"@types/sdp-transform",
|
||||
],
|
||||
ignoreBinaries: [
|
||||
// Used in scripts & workflows
|
||||
"jq",
|
||||
"wait-on",
|
||||
],
|
||||
ignoreExportsUsedInFile: true,
|
||||
} satisfies KnipConfig;
|
||||
|
||||
18
package.json
18
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "element-web",
|
||||
"version": "1.11.110",
|
||||
"version": "1.11.109",
|
||||
"description": "Element: the future of secure communication",
|
||||
"author": "New Vector Ltd.",
|
||||
"repository": {
|
||||
@@ -75,8 +75,8 @@
|
||||
"resolutions": {
|
||||
"**/pretty-format/react-is": "19.1.1",
|
||||
"@playwright/test": "1.54.2",
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"@types/react": "19.1.10",
|
||||
"@types/react-dom": "19.1.7",
|
||||
"oidc-client-ts": "3.3.0",
|
||||
"jwt-decode": "4.0.0",
|
||||
"caniuse-lite": "1.0.30001724",
|
||||
@@ -142,7 +142,7 @@
|
||||
"opus-recorder": "^8.0.3",
|
||||
"pako": "^2.0.3",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"posthog-js": "1.261.0",
|
||||
"posthog-js": "1.260.1",
|
||||
"qrcode": "1.5.4",
|
||||
"re-resizable": "6.11.2",
|
||||
"react": "^19.0.0",
|
||||
@@ -158,7 +158,7 @@
|
||||
"sanitize-html": "2.17.0",
|
||||
"tar-js": "^0.3.0",
|
||||
"temporal-polyfill": "^0.3.0",
|
||||
"ua-parser-js": "1.0.40",
|
||||
"ua-parser-js": "^1.0.2",
|
||||
"uuid": "^11.0.0",
|
||||
"what-input": "^5.2.10"
|
||||
},
|
||||
@@ -184,7 +184,7 @@
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@casualbot/jest-sonar-reporter": "2.2.7",
|
||||
"@element-hq/element-call-embedded": "0.15.0",
|
||||
"@element-hq/element-call-embedded": "0.14.1",
|
||||
"@element-hq/element-web-playwright-common": "^1.4.6",
|
||||
"@peculiar/webcrypto": "^1.4.3",
|
||||
"@playwright/test": "^1.50.1",
|
||||
@@ -203,7 +203,6 @@
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/commonmark": "^0.27.4",
|
||||
"@types/content-type": "^1.1.9",
|
||||
"@types/counterpart": "^0.18.1",
|
||||
"@types/css-tree": "^2.3.8",
|
||||
"@types/diff-match-patch": "^1.0.32",
|
||||
@@ -222,12 +221,11 @@
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"@types/pako": "^2.0.0",
|
||||
"@types/qrcode": "^1.3.5",
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react": "19.1.10",
|
||||
"@types/react-beautiful-dnd": "^13.0.0",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"@types/react-dom": "19.1.7",
|
||||
"@types/react-transition-group": "^4.4.0",
|
||||
"@types/sanitize-html": "2.16.0",
|
||||
"@types/sdp-transform": "^2.4.10",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/tar-js": "^0.3.5",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
|
||||
@@ -126,7 +126,7 @@ test.describe("'Turn on key storage' toast", () => {
|
||||
await toast.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Then we see the Encryption settings dialog with an option to turn on key storage
|
||||
await expect(page.getByRole("switch", { name: "Allow key storage" })).toBeVisible();
|
||||
await expect(page.getByRole("checkbox", { name: "Allow key storage" })).toBeVisible();
|
||||
|
||||
// And when we close that
|
||||
await page.getByRole("button", { name: "Close dialog" }).click();
|
||||
@@ -153,7 +153,7 @@ test.describe("'Turn on key storage' toast", () => {
|
||||
await page.getByRole("button", { name: "Go to Settings" }).click();
|
||||
|
||||
// Then we see Encryption settings again
|
||||
await expect(page.getByRole("switch", { name: "Allow key storage" })).toBeVisible();
|
||||
await expect(page.getByRole("checkbox", { name: "Allow key storage" })).toBeVisible();
|
||||
|
||||
// And when we close that, see the toast, click Dismiss, and Yes, Dismiss
|
||||
await page.getByRole("button", { name: "Close dialog" }).click();
|
||||
|
||||
@@ -300,9 +300,9 @@ export async function doTwoWaySasVerification(page: Page, verifier: JSHandle<Ver
|
||||
export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
|
||||
const encryptionTab = await app.settings.openUserSettings("Encryption");
|
||||
|
||||
const keyStorageToggle = encryptionTab.getByRole("switch", { name: "Allow key storage" });
|
||||
const keyStorageToggle = encryptionTab.getByRole("checkbox", { name: "Allow key storage" });
|
||||
if (!(await keyStorageToggle.isChecked())) {
|
||||
await encryptionTab.getByRole("switch", { name: "Allow key storage" }).click();
|
||||
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).click();
|
||||
}
|
||||
|
||||
await encryptionTab.getByRole("button", { name: "Set up recovery" }).click();
|
||||
@@ -323,11 +323,11 @@ export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
|
||||
export async function disableKeyBackup(app: ElementAppPage): Promise<void> {
|
||||
const encryptionTab = await app.settings.openUserSettings("Encryption");
|
||||
|
||||
const keyStorageToggle = encryptionTab.getByRole("switch", { name: "Allow key storage" });
|
||||
const keyStorageToggle = encryptionTab.getByRole("checkbox", { name: "Allow key storage" });
|
||||
if (await keyStorageToggle.isChecked()) {
|
||||
await encryptionTab.getByRole("switch", { name: "Allow key storage" }).click();
|
||||
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).click();
|
||||
await encryptionTab.getByRole("button", { name: "Delete key storage" }).click();
|
||||
await encryptionTab.getByRole("switch", { name: "Allow key storage" }).isVisible();
|
||||
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).isVisible();
|
||||
|
||||
// Wait for the update to account data to stick
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
@@ -193,9 +193,6 @@ test.describe("Room list", () => {
|
||||
|
||||
await roomListView.getByRole("option", { name: "Open room room20" }).click();
|
||||
|
||||
// Make sure the room with the unread is visible before we press the keyboard action to select it
|
||||
await expect(roomListView.getByRole("option", { name: "1 notification" })).toBeVisible();
|
||||
|
||||
await page.keyboard.press("Alt+Shift+ArrowDown");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "1 notification", level: 1 })).toBeVisible();
|
||||
|
||||
@@ -100,51 +100,3 @@ test.describe("permalinks", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("triple-click message selection", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
});
|
||||
|
||||
test("should select entire message line when triple-clicking on message with pills", async ({
|
||||
page,
|
||||
app,
|
||||
user,
|
||||
bot,
|
||||
}) => {
|
||||
await bot.prepareClient();
|
||||
|
||||
const roomId = await app.client.createRoom({ name: "Test Room" });
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await app.viewRoomByName("Test Room");
|
||||
|
||||
// Send a message with user and room pills
|
||||
await app.client.sendMessage(
|
||||
roomId,
|
||||
`Testing triple-click message selection. ` +
|
||||
`User: ${permalinkPrefix}${bot.credentials.userId}, ` +
|
||||
`Room: ${permalinkPrefix}${roomId}, ` +
|
||||
`Message: ${permalinkPrefix}${roomId}/$dummy-event, ` +
|
||||
`and @room mention.`,
|
||||
);
|
||||
|
||||
const timeline = page.locator(".mx_RoomView_timeline");
|
||||
const messageTile = timeline.locator(".mx_EventTile").last();
|
||||
|
||||
// Triple-click on the message body to select its entire content
|
||||
const messageBody = messageTile.locator(".mx_EventTile_body");
|
||||
await messageBody.click({ clickCount: 3 });
|
||||
|
||||
// Get the expected text content of the message, including pills
|
||||
const expectedText = await messageBody.innerText();
|
||||
|
||||
// Get the currently selected text from the page
|
||||
const selectedText = await page.evaluate(() => {
|
||||
const selection = window.getSelection();
|
||||
return selection ? selection.toString().trim() : "";
|
||||
});
|
||||
|
||||
// Verify that the selected text exactly matches the message content
|
||||
expect(selectedText).toBe(expectedText);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,8 +30,9 @@ export class Helpers {
|
||||
/**
|
||||
* Get the release announcement with the given name.
|
||||
* @param name
|
||||
* @private
|
||||
*/
|
||||
public getReleaseAnnouncement(name: string) {
|
||||
private getReleaseAnnouncement(name: string) {
|
||||
return this.page.getByRole("dialog", { name });
|
||||
}
|
||||
|
||||
@@ -54,6 +55,16 @@ export class Helpers {
|
||||
assertReleaseAnnouncementIsNotVisible(name: string) {
|
||||
return expect(this.getReleaseAnnouncement(name)).not.toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the release announcement with the given name as read.
|
||||
* If the release announcement is not visible, this will throw an error.
|
||||
* @param name
|
||||
*/
|
||||
async markReleaseAnnouncementAsRead(name: string) {
|
||||
const dialog = this.getReleaseAnnouncement(name);
|
||||
await dialog.getByRole("button", { name: "Ok" }).click();
|
||||
}
|
||||
}
|
||||
|
||||
export { expect };
|
||||
|
||||
@@ -22,25 +22,25 @@ test.describe("Release announcement", () => {
|
||||
await app.viewRoomById(roomId);
|
||||
await use({ roomId });
|
||||
},
|
||||
labsFlags: ["feature_new_room_list"],
|
||||
});
|
||||
|
||||
test(
|
||||
"should display the new room list release announcement",
|
||||
"should display the pinned messages release announcement",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, room, util }) => {
|
||||
const name = "Chats has a new look!";
|
||||
await app.toggleRoomInfoPanel();
|
||||
|
||||
const name = "All new pinned messages";
|
||||
|
||||
// The release announcement should be displayed
|
||||
await util.assertReleaseAnnouncementIsVisible(name);
|
||||
// Hide the release announcement
|
||||
const dialog = util.getReleaseAnnouncement(name);
|
||||
await dialog.getByRole("button", { name: "Next" }).click();
|
||||
|
||||
await util.markReleaseAnnouncementAsRead(name);
|
||||
await util.assertReleaseAnnouncementIsNotVisible(name);
|
||||
|
||||
await page.reload();
|
||||
await expect(page.getByRole("button", { name: "Room options" })).toBeVisible();
|
||||
await app.toggleRoomInfoPanel();
|
||||
await expect(page.getByRole("menuitem", { name: "Pinned messages" })).toBeVisible();
|
||||
// Check that once the release announcement has been marked as viewed, it does not appear again
|
||||
await util.assertReleaseAnnouncementIsNotVisible(name);
|
||||
},
|
||||
|
||||
@@ -85,7 +85,7 @@ class Helpers {
|
||||
* Return the system theme toggle
|
||||
*/
|
||||
getMatchSystemThemeCheckbox() {
|
||||
return this.getThemePanel().getByRole("switch", { name: "Match system theme" });
|
||||
return this.getThemePanel().getByRole("checkbox", { name: "Match system theme" });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -219,7 +219,7 @@ class Helpers {
|
||||
* Return the compact layout checkbox
|
||||
*/
|
||||
getCompactLayoutCheckbox() {
|
||||
return this.getMessageLayoutPanel().getByRole("switch", { name: "Show compact text and messages" });
|
||||
return this.getMessageLayoutPanel().getByRole("checkbox", { name: "Show compact text and messages" });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -117,7 +117,7 @@ test.describe("Encryption tab", () => {
|
||||
await verifySession(app, recoveryKey.encodedPrivateKey);
|
||||
await util.openEncryptionTab();
|
||||
|
||||
await page.getByRole("switch", { name: "Allow key storage" }).click();
|
||||
await page.getByRole("checkbox", { name: "Allow key storage" }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Are you sure you want to turn off key storage and delete it?" }),
|
||||
@@ -136,7 +136,7 @@ test.describe("Encryption tab", () => {
|
||||
|
||||
await page.getByRole("button", { name: "Delete key storage" }).click();
|
||||
|
||||
await expect(page.getByRole("switch", { name: "Allow key storage" })).not.toBeChecked();
|
||||
await expect(page.getByRole("checkbox", { name: "Allow key storage" })).not.toBeChecked();
|
||||
|
||||
for (const prom of deleteRequestPromises) {
|
||||
const request = await prom;
|
||||
|
||||
@@ -104,10 +104,7 @@ class Helpers {
|
||||
|
||||
const clipboardContent = await this.app.getClipboard();
|
||||
await dialog.getByRole("textbox").fill(clipboardContent);
|
||||
const button = dialog.getByRole("button", { name: confirmButtonLabel });
|
||||
await button.click();
|
||||
// Button should disable immediately after clicking.
|
||||
await expect(button).toBeDisabled();
|
||||
await dialog.getByRole("button", { name: confirmButtonLabel }).click();
|
||||
await expect(this.getEncryptionRecoverySection()).toMatchScreenshot("default-recovery.png");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -908,37 +908,23 @@ test.describe("Timeline", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test(
|
||||
"should be able to hide an image",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, homeserver, room, context }) => {
|
||||
await app.viewRoomById(room.roomId);
|
||||
|
||||
const bot = new Bot(page, homeserver, {});
|
||||
await bot.prepareClient();
|
||||
await app.client.inviteUser(room.roomId, bot.credentials.userId);
|
||||
|
||||
await sendImage(bot, room.roomId, NEW_AVATAR);
|
||||
await app.timeline.scrollToBottom();
|
||||
const imgTile = page.locator(".mx_MImageBody").first();
|
||||
await expect(imgTile).toBeVisible();
|
||||
await imgTile.hover();
|
||||
await page.getByRole("button", { name: "Hide" }).click();
|
||||
|
||||
// Check that the image is now hidden.
|
||||
await expect(page.getByRole("button", { name: "Show image" })).toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
test("should be able to hide a video", async ({ page, app, homeserver, room, context }) => {
|
||||
test("should be able to hide an image", { tag: "@screenshot" }, async ({ page, app, room, context }) => {
|
||||
await app.viewRoomById(room.roomId);
|
||||
await sendImage(app.client, room.roomId, NEW_AVATAR);
|
||||
await app.timeline.scrollToBottom();
|
||||
const imgTile = page.locator(".mx_MImageBody").first();
|
||||
await expect(imgTile).toBeVisible();
|
||||
await imgTile.hover();
|
||||
await page.getByRole("button", { name: "Hide" }).click();
|
||||
|
||||
const bot = new Bot(page, homeserver, {});
|
||||
await bot.prepareClient();
|
||||
await app.client.inviteUser(room.roomId, bot.credentials.userId);
|
||||
// Check that the image is now hidden.
|
||||
await expect(page.getByRole("button", { name: "Show image" })).toBeVisible();
|
||||
});
|
||||
|
||||
const upload = await bot.uploadContent(VIDEO_FILE, { name: "bbb.webm", type: "video/webm" });
|
||||
await bot.sendEvent(room.roomId, null, "m.room.message" as EventType, {
|
||||
test("should be able to hide a video", async ({ page, app, room, context }) => {
|
||||
await app.viewRoomById(room.roomId);
|
||||
const upload = await app.client.uploadContent(VIDEO_FILE, { name: "bbb.webm", type: "video/webm" });
|
||||
await app.client.sendEvent(room.roomId, null, "m.room.message" as EventType, {
|
||||
msgtype: "m.video" as MsgType,
|
||||
body: "bbb.webm",
|
||||
url: upload.content_uri,
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 9.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 85 KiB |
@@ -10,7 +10,7 @@ import {
|
||||
type StartedPostgreSqlContainer,
|
||||
} from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||
|
||||
const TAG = "main@sha256:64b638f2c0ddd7aa0ddcbc39d21cdf3cedab91508b5d7953e2e85c9901ac5b26";
|
||||
const TAG = "main@sha256:430b1f00e74c3f89f078670f676b4333f6bbe5a339962344b3ae84e99e9bcd7f";
|
||||
|
||||
/**
|
||||
* MatrixAuthenticationServiceContainer which freezes the docker digest to
|
||||
|
||||
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||
|
||||
const TAG = "develop@sha256:fa82823795aab3ff29243e016a04e821e80f64e7b7257ed29a323b73525d3c0d";
|
||||
const TAG = "develop@sha256:18e9e77eac01709e9ab4d26cf20c36bf5a1567756bb5a78c00cabf366d65a950";
|
||||
|
||||
/**
|
||||
* SynapseContainer which freezes the docker digest to stabilise tests,
|
||||
|
||||
@@ -11,7 +11,8 @@ Please see LICENSE files in the repository root for full details.
|
||||
line-height: $font-17px;
|
||||
border-radius: $font-16px;
|
||||
vertical-align: text-top;
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
@@ -56,8 +57,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
margin-inline-start: -0.3em; /* Otherwise the gap is too large */
|
||||
margin-inline-end: 0.2em;
|
||||
min-width: $font-16px; /* ensure the avatar is not compressed */
|
||||
user-select: text;
|
||||
vertical-align: -2.5px;
|
||||
}
|
||||
|
||||
.mx_Pill_text {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
padding: var(--cpd-space-1x) var(--cpd-space-3x) var(--cpd-space-1x) var(--cpd-space-1x);
|
||||
font: var(--cpd-font-body-xs-medium);
|
||||
background-color: var(--cpd-color-bg-subtle-secondary);
|
||||
background-color: var(--cpd-color-alpha-gray-200);
|
||||
color: var(--cpd-color-text-secondary);
|
||||
|
||||
border-radius: 99px;
|
||||
|
||||
@@ -32,8 +32,4 @@
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomListHeaderView_ReleaseAnnouncementAnchor {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
7
src/@types/global.d.ts
vendored
7
src/@types/global.d.ts
vendored
@@ -71,13 +71,6 @@ type ElectronChannel =
|
||||
| "serverSupportedVersions";
|
||||
|
||||
declare global {
|
||||
// use `number` as the return type in all cases for globalThis.set{Interval,Timeout},
|
||||
// so we don't accidentally use the methods on NodeJS.Timeout - they only exist in a subset of environments.
|
||||
// The overload for clear{Interval,Timeout} is resolved as expected.
|
||||
// We use `ReturnType<typeof setTimeout>` in the code to be agnostic of if this definition gets loaded.
|
||||
function setInterval(handler: TimerHandler, timeout: number, ...arguments: any[]): number;
|
||||
function setTimeout(handler: TimerHandler, timeout: number, ...arguments: any[]): number;
|
||||
|
||||
interface Window {
|
||||
mxSendRageshake: (text: string, withLogs?: boolean) => void;
|
||||
matrixLogger: typeof logger;
|
||||
|
||||
@@ -112,7 +112,6 @@ export enum LegacyCallHandlerEvent {
|
||||
CallsChanged = "calls_changed",
|
||||
CallChangeRoom = "call_change_room",
|
||||
SilencedCallsChanged = "silenced_calls_changed",
|
||||
ShownSidebarsChanged = "shown_sidebars_changed",
|
||||
CallState = "call_state",
|
||||
ProtocolSupport = "protocol_support",
|
||||
}
|
||||
@@ -121,7 +120,6 @@ type EventEmitterMap = {
|
||||
[LegacyCallHandlerEvent.CallsChanged]: (calls: Map<string, MatrixCall>) => void;
|
||||
[LegacyCallHandlerEvent.CallChangeRoom]: (call: MatrixCall) => void;
|
||||
[LegacyCallHandlerEvent.SilencedCallsChanged]: (calls: Set<string>) => void;
|
||||
[LegacyCallHandlerEvent.ShownSidebarsChanged]: (sidebarsShown: Map<string, boolean>) => void;
|
||||
[LegacyCallHandlerEvent.CallState]: (mappedRoomId: string | null, status: CallState) => void;
|
||||
[LegacyCallHandlerEvent.ProtocolSupport]: () => void;
|
||||
};
|
||||
@@ -146,8 +144,6 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
|
||||
|
||||
private silencedCalls = new Set<string>(); // callIds
|
||||
|
||||
private shownSidebars = new Map<string, boolean>(); // callId (call) -> sidebar show
|
||||
|
||||
private backgroundAudio = new BackgroundAudio();
|
||||
private playingSources: Record<string, AudioBufferSourceNode> = {}; // Record them for stopping
|
||||
|
||||
@@ -244,15 +240,6 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
|
||||
return false;
|
||||
}
|
||||
|
||||
public setCallSidebarShown(callId: string, sidebarShown: boolean): void {
|
||||
this.shownSidebars.set(callId, sidebarShown);
|
||||
this.emit(LegacyCallHandlerEvent.ShownSidebarsChanged, this.shownSidebars);
|
||||
}
|
||||
|
||||
public isCallSidebarShown(callId?: string): boolean {
|
||||
return !!callId && (this.shownSidebars.get(callId) ?? true);
|
||||
}
|
||||
|
||||
private async checkProtocols(maxTries: number): Promise<void> {
|
||||
try {
|
||||
const protocols = await MatrixClientPeg.safeGet().getThirdpartyProtocols();
|
||||
|
||||
@@ -6,8 +6,6 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
type CacheResult = { roomId: string; viaServers: string[] };
|
||||
|
||||
/**
|
||||
* This is meant to be a cache of room alias to room ID so that moving between
|
||||
* rooms happens smoothly (for example using browser back / forward buttons).
|
||||
@@ -18,12 +16,12 @@ type CacheResult = { roomId: string; viaServers: string[] };
|
||||
* A similar thing could also be achieved via `pushState` with a state object,
|
||||
* but keeping it separate like this seems easier in case we do want to extend.
|
||||
*/
|
||||
const cache = new Map<string, CacheResult>();
|
||||
const aliasToIDMap = new Map<string, string>();
|
||||
|
||||
export function storeRoomAliasInCache(alias: string, roomId: string, viaServers: string[]): void {
|
||||
cache.set(alias, { roomId, viaServers });
|
||||
export function storeRoomAliasInCache(alias: string, id: string): void {
|
||||
aliasToIDMap.set(alias, id);
|
||||
}
|
||||
|
||||
export function getCachedRoomIdForAlias(alias: string): CacheResult | undefined {
|
||||
return cache.get(alias);
|
||||
export function getCachedRoomIDForAlias(alias: string): string | undefined {
|
||||
return aliasToIDMap.get(alias);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ import ThemeController from "../../settings/controllers/ThemeController";
|
||||
import { startAnyRegistrationFlow } from "../../Registration";
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils";
|
||||
import { calculateRoomVia, makeRoomPermalink } from "../../utils/permalinks/Permalinks";
|
||||
import { makeRoomPermalink } from "../../utils/permalinks/Permalinks";
|
||||
import ThemeWatcher, { ThemeWatcherEvent } from "../../settings/watchers/ThemeWatcher";
|
||||
import { FontWatcher } from "../../settings/watchers/FontWatcher";
|
||||
import { storeRoomAliasInCache } from "../../RoomAliasCache";
|
||||
@@ -238,8 +238,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
private readonly stores: SdkContextClass;
|
||||
private loadSessionAbortController = new AbortController();
|
||||
|
||||
private sessionLoadStarted = false;
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
this.stores = SdkContextClass.instance;
|
||||
@@ -472,20 +470,15 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
this.fontWatcher.start();
|
||||
|
||||
initSentry(SdkConfig.get("sentry"));
|
||||
window.addEventListener("resize", this.onWindowResized);
|
||||
|
||||
// Once we start loading the MatrixClient, we can't stop, even if MatrixChat gets unmounted (as it does
|
||||
// in React's Strict Mode). So, start loading the session now, but only if this MatrixChat was not previously
|
||||
// mounted.
|
||||
if (!this.sessionLoadStarted) {
|
||||
this.sessionLoadStarted = true;
|
||||
if (!checkSessionLockFree()) {
|
||||
// another instance holds the lock; confirm its theft before proceeding
|
||||
setTimeout(() => this.setState({ view: Views.CONFIRM_LOCK_THEFT }), 0);
|
||||
} else {
|
||||
this.startInitSession();
|
||||
}
|
||||
if (!checkSessionLockFree()) {
|
||||
// another instance holds the lock; confirm its theft before proceeding
|
||||
setTimeout(() => this.setState({ view: Views.CONFIRM_LOCK_THEFT }), 0);
|
||||
} else {
|
||||
this.startInitSession();
|
||||
}
|
||||
|
||||
window.addEventListener("resize", this.onWindowResized);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
|
||||
@@ -1026,7 +1019,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
presentedId = theAlias;
|
||||
// Store display alias of the presented room in cache to speed future
|
||||
// navigation.
|
||||
storeRoomAliasInCache(theAlias, room.roomId, calculateRoomVia(room));
|
||||
storeRoomAliasInCache(theAlias, room.roomId);
|
||||
}
|
||||
|
||||
// Store this as the ID of the last room accessed. This is so that we can
|
||||
|
||||
@@ -245,7 +245,6 @@ class PipContainerInner extends React.Component<IProps, IState> {
|
||||
secondaryCall={this.state.secondaryCall}
|
||||
pipMode={pipMode}
|
||||
onResize={onResize}
|
||||
sidebarShown={false}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
@@ -184,32 +184,28 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex
|
||||
|
||||
// Guard against null/undefined events and modified keys which we don't want to handle here but do
|
||||
// at the settings level shortcuts(E.g. Select next room, etc )
|
||||
// Guard against null/undefined events and modified keys
|
||||
if (!e || isModifiedKeyEvent(e)) {
|
||||
onKeyDown?.(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.code === Key.ARROW_UP && currentIndex !== undefined) {
|
||||
scrollToItem(currentIndex - 1, false);
|
||||
handled = true;
|
||||
} else if (e.code === Key.ARROW_DOWN && currentIndex !== undefined) {
|
||||
scrollToItem(currentIndex + 1, true);
|
||||
handled = true;
|
||||
} else if (e.code === Key.HOME) {
|
||||
scrollToIndex(0);
|
||||
handled = true;
|
||||
} else if (e.code === Key.END) {
|
||||
scrollToIndex(items.length - 1);
|
||||
handled = true;
|
||||
} else if (e.code === Key.PAGE_DOWN && visibleRange && currentIndex !== undefined) {
|
||||
const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex;
|
||||
scrollToItem(Math.min(currentIndex + numberDisplayed, items.length - 1), true, `start`);
|
||||
handled = true;
|
||||
} else if (e.code === Key.PAGE_UP && visibleRange && currentIndex !== undefined) {
|
||||
const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex;
|
||||
scrollToItem(Math.max(currentIndex - numberDisplayed, 0), false, `start`);
|
||||
handled = true;
|
||||
if (e || !isModifiedKeyEvent(e)) {
|
||||
if (e.code === Key.ARROW_UP && currentIndex !== undefined) {
|
||||
scrollToItem(currentIndex - 1, false);
|
||||
handled = true;
|
||||
} else if (e.code === Key.ARROW_DOWN && currentIndex !== undefined) {
|
||||
scrollToItem(currentIndex + 1, true);
|
||||
handled = true;
|
||||
} else if (e.code === Key.HOME) {
|
||||
scrollToIndex(0);
|
||||
handled = true;
|
||||
} else if (e.code === Key.END) {
|
||||
scrollToIndex(items.length - 1);
|
||||
handled = true;
|
||||
} else if (e.code === Key.PAGE_DOWN && visibleRange && currentIndex !== undefined) {
|
||||
const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex;
|
||||
scrollToItem(Math.min(currentIndex + numberDisplayed, items.length - 1), true, `start`);
|
||||
handled = true;
|
||||
} else if (e.code === Key.PAGE_UP && visibleRange && currentIndex !== undefined) {
|
||||
const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex;
|
||||
scrollToItem(Math.max(currentIndex - numberDisplayed, 0), false, `start`);
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
|
||||
@@ -145,9 +145,6 @@ export default class BaseDialog extends React.Component<IProps> {
|
||||
const lockProps: Record<string, any> = {
|
||||
"onKeyDown": this.onKeyDown,
|
||||
"role": "dialog",
|
||||
// Allow the dialog to be keyboard focusable
|
||||
// So the escape key handling works in more cases (say you select the header)
|
||||
"tabIndex": -1,
|
||||
// This should point to a node describing the dialog.
|
||||
// If we were about to completely follow this recommendation we'd need to
|
||||
// make all the components relying on BaseDialog to be aware of it.
|
||||
|
||||
@@ -26,7 +26,6 @@ import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||
import { privateShouldBeEncrypted } from "../../../utils/rooms";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import LabelledCheckbox from "../elements/LabelledCheckbox";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
|
||||
interface IProps {
|
||||
type?: RoomType;
|
||||
@@ -84,7 +83,6 @@ interface IState {
|
||||
|
||||
export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||
private readonly askToJoinEnabled: boolean;
|
||||
private readonly advancedSettingsEnabled: boolean;
|
||||
private readonly supportsRestricted: boolean;
|
||||
private nameField = createRef<Field>();
|
||||
private aliasField = createRef<RoomAliasField>();
|
||||
@@ -93,8 +91,6 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||
super(props);
|
||||
|
||||
this.askToJoinEnabled = SettingsStore.getValue("feature_ask_to_join");
|
||||
this.advancedSettingsEnabled = SettingsStore.getValue(UIFeature.AdvancedSettings);
|
||||
|
||||
this.supportsRestricted = !!this.props.parentSpace;
|
||||
|
||||
let joinRule = JoinRule.Invite;
|
||||
@@ -431,21 +427,19 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||
{visibilitySection}
|
||||
{e2eeSection}
|
||||
{aliasField}
|
||||
{this.advancedSettingsEnabled && (
|
||||
<details onToggle={this.onDetailsToggled} className="mx_CreateRoomDialog_details">
|
||||
<summary className="mx_CreateRoomDialog_details_summary">
|
||||
{this.state.detailsOpen ? _t("action|hide_advanced") : _t("action|show_advanced")}
|
||||
</summary>
|
||||
<LabelledToggleSwitch
|
||||
label={_t("create_room|unfederated", {
|
||||
serverName: MatrixClientPeg.safeGet().getDomain(),
|
||||
})}
|
||||
onChange={this.onNoFederateChange}
|
||||
value={this.state.noFederate}
|
||||
/>
|
||||
<p>{federateLabel}</p>
|
||||
</details>
|
||||
)}
|
||||
<details onToggle={this.onDetailsToggled} className="mx_CreateRoomDialog_details">
|
||||
<summary className="mx_CreateRoomDialog_details_summary">
|
||||
{this.state.detailsOpen ? _t("action|hide_advanced") : _t("action|show_advanced")}
|
||||
</summary>
|
||||
<LabelledToggleSwitch
|
||||
label={_t("create_room|unfederated", {
|
||||
serverName: MatrixClientPeg.safeGet().getDomain(),
|
||||
})}
|
||||
onChange={this.onNoFederateChange}
|
||||
value={this.state.noFederate}
|
||||
/>
|
||||
<p>{federateLabel}</p>
|
||||
</details>
|
||||
</div>
|
||||
</form>
|
||||
<DialogButtons
|
||||
|
||||
@@ -1,53 +1,55 @@
|
||||
/*
|
||||
Copyright 2024, 2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from "react";
|
||||
import React from "react";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import { UserTab } from "./UserTab";
|
||||
|
||||
interface IProps {
|
||||
onFinished(): void;
|
||||
}
|
||||
|
||||
export const IntegrationsDisabledDialog: React.FC<IProps> = ({ onFinished }) => {
|
||||
const onOpenSettingsClick = useCallback(() => {
|
||||
onFinished();
|
||||
dis.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Security,
|
||||
});
|
||||
}, [onFinished]);
|
||||
export default class IntegrationsDisabledDialog extends React.Component<IProps> {
|
||||
private onAcknowledgeClick = (): void => {
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_IntegrationsDisabledDialog"
|
||||
hasCancel={true}
|
||||
onFinished={onFinished}
|
||||
title={_t("integrations|disabled_dialog_title")}
|
||||
>
|
||||
<div className="mx_IntegrationsDisabledDialog_content">
|
||||
<p>
|
||||
{_t("integrations|disabled_dialog_description", {
|
||||
manageIntegrations: _t("integration_manager|manage_title"),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("common|settings")}
|
||||
onPrimaryButtonClick={onOpenSettingsClick}
|
||||
cancelButton={_t("action|ok")}
|
||||
onCancel={onFinished}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
};
|
||||
private onOpenSettingsClick = (): void => {
|
||||
this.props.onFinished();
|
||||
dis.fire(Action.ViewUserSettings);
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_IntegrationsDisabledDialog"
|
||||
hasCancel={true}
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("integrations|disabled_dialog_title")}
|
||||
>
|
||||
<div className="mx_IntegrationsDisabledDialog_content">
|
||||
<p>
|
||||
{_t("integrations|disabled_dialog_description", {
|
||||
manageIntegrations: _t("integration_manager|manage_title"),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<DialogButtons
|
||||
primaryButton={_t("common|settings")}
|
||||
onPrimaryButtonClick={this.onOpenSettingsClick}
|
||||
cancelButton={_t("action|ok")}
|
||||
onCancel={this.onAcknowledgeClick}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ import { getKeyBindingsManager } from "../../../../KeyBindingsManager";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../../MatrixClientPeg";
|
||||
import { PosthogAnalytics } from "../../../../PosthogAnalytics";
|
||||
import { getCachedRoomIdForAlias } from "../../../../RoomAliasCache";
|
||||
import { getCachedRoomIDForAlias } from "../../../../RoomAliasCache";
|
||||
import { showStartChatInviteDialog } from "../../../../RoomInvite";
|
||||
import { SettingLevel } from "../../../../settings/SettingLevel";
|
||||
import SettingsStore from "../../../../settings/SettingsStore";
|
||||
@@ -912,7 +912,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
||||
if (
|
||||
trimmedQuery.startsWith("#") &&
|
||||
trimmedQuery.includes(":") &&
|
||||
(!getCachedRoomIdForAlias(trimmedQuery) || !cli.getRoom(getCachedRoomIdForAlias(trimmedQuery)!.roomId))
|
||||
(!getCachedRoomIDForAlias(trimmedQuery) || !cli.getRoom(getCachedRoomIDForAlias(trimmedQuery)))
|
||||
) {
|
||||
joinRoomSection = (
|
||||
<div className="mx_SpotlightDialog_section mx_SpotlightDialog_otherSearches" role="group">
|
||||
|
||||
@@ -151,7 +151,7 @@ interface Props extends ReplacerOptions {
|
||||
const EventContentBody = memo(
|
||||
({ as, mxEvent, stripReply, content, linkify, highlights, includeDir = true, ref, ...options }: Props) => {
|
||||
const enableBigEmoji = useSettingValue("TextualBody.enableBigEmoji");
|
||||
const [mediaIsVisible] = useMediaVisible(mxEvent);
|
||||
const [mediaIsVisible] = useMediaVisible(mxEvent?.getId(), mxEvent?.getRoomId());
|
||||
|
||||
const replacer = useReplacer(content, mxEvent, options);
|
||||
const linkifyOptions = useMemo(
|
||||
|
||||
@@ -25,7 +25,7 @@ interface IProps {
|
||||
* Quick action button for marking a media event as hidden.
|
||||
*/
|
||||
export const HideActionButton: React.FC<IProps> = ({ mxEvent }) => {
|
||||
const [mediaIsVisible, setVisible] = useMediaVisible(mxEvent);
|
||||
const [mediaIsVisible, setVisible] = useMediaVisible(mxEvent.getId(), mxEvent.getRoomId());
|
||||
|
||||
if (!mediaIsVisible) {
|
||||
return;
|
||||
|
||||
@@ -48,9 +48,4 @@ export interface IBodyProps {
|
||||
// Set to `true` to disable interactions (e.g. video controls) and to remove controls from the tab order.
|
||||
// This may be useful when displaying a preview of the event.
|
||||
inhibitInteraction?: boolean;
|
||||
|
||||
/**
|
||||
* Optional ID for the root element.
|
||||
*/
|
||||
id?: string;
|
||||
}
|
||||
|
||||
@@ -686,7 +686,7 @@ export class MImageBodyInner extends React.Component<IProps, IState> {
|
||||
|
||||
// Wrap MImageBody component so we can use a hook here.
|
||||
const MImageBody: React.FC<IBodyProps> = (props) => {
|
||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent);
|
||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
|
||||
return <MImageBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class MImageReplyBodyInner extends MImageBodyInner {
|
||||
}
|
||||
}
|
||||
const MImageReplyBody: React.FC<IBodyProps> = (props) => {
|
||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent);
|
||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
|
||||
return <MImageReplyBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class MStickerBodyInner extends MImageBodyInner {
|
||||
protected onClick = (ev: React.MouseEvent): void => {
|
||||
ev.preventDefault();
|
||||
if (!this.props.mediaVisible) {
|
||||
this.props.setMediaVisible(true);
|
||||
this.props.setMediaVisible?.(true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -79,7 +79,7 @@ class MStickerBodyInner extends MImageBodyInner {
|
||||
}
|
||||
|
||||
const MStickerBody: React.FC<IBodyProps> = (props) => {
|
||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent);
|
||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
|
||||
return <MStickerBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -342,7 +342,7 @@ class MVideoBodyInner extends React.PureComponent<IProps, IState> {
|
||||
|
||||
// Wrap MVideoBody component so we can use a hook here.
|
||||
const MVideoBody: React.FC<IBodyProps> = (props) => {
|
||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent);
|
||||
const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId(), props.mxEvent.getRoomId());
|
||||
return <MVideoBodyInner mediaVisible={mediaVisible} setMediaVisible={setVisible} {...props} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -51,11 +51,6 @@ interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper
|
||||
getRelationsForEvent?: GetRelationsForEvent;
|
||||
|
||||
isSeeingThroughMessageHiddenForModeration?: boolean;
|
||||
|
||||
/**
|
||||
* Optional ID for the root element.
|
||||
*/
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface IOperableEventTile {
|
||||
@@ -313,7 +308,6 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
|
||||
getRelationsForEvent: this.props.getRelationsForEvent,
|
||||
isSeeingThroughMessageHiddenForModeration: this.props.isSeeingThroughMessageHiddenForModeration,
|
||||
inhibitInteraction: this.props.inhibitInteraction,
|
||||
id: this.props.id,
|
||||
};
|
||||
if (hasCaption) {
|
||||
return <CaptionBody {...bodyProps} WrappedBodyType={BodyType} />;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type HTMLProps, type JSX } from "react";
|
||||
import React, { type JSX } from "react";
|
||||
import PinIcon from "@vector-im/compound-design-tokens/assets/web/icons/pin-solid";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
@@ -13,9 +13,9 @@ import { _t } from "../../../languageHandler";
|
||||
/**
|
||||
* A badge to indicate that a message is pinned.
|
||||
*/
|
||||
export function PinnedMessageBadge(props: Readonly<HTMLProps<HTMLDivElement>>): JSX.Element {
|
||||
export function PinnedMessageBadge(): JSX.Element {
|
||||
return (
|
||||
<div {...props} className="mx_PinnedMessageBadge">
|
||||
<div className="mx_PinnedMessageBadge">
|
||||
<PinIcon width="16px" height="16px" />
|
||||
{_t("room|pinned_message_badge")}
|
||||
</div>
|
||||
|
||||
@@ -384,12 +384,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
||||
|
||||
if (isEmote) {
|
||||
return (
|
||||
<div
|
||||
id={this.props.id}
|
||||
className="mx_MEmoteBody mx_EventTile_content"
|
||||
onClick={this.onBodyLinkClick}
|
||||
dir="auto"
|
||||
>
|
||||
<div className="mx_MEmoteBody mx_EventTile_content" onClick={this.onBodyLinkClick} dir="auto">
|
||||
*
|
||||
<span className="mx_MEmoteBody_sender" onClick={this.onEmoteSenderClick}>
|
||||
{mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()}
|
||||
@@ -402,7 +397,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
||||
}
|
||||
if (isNotice) {
|
||||
return (
|
||||
<div id={this.props.id} className="mx_MNoticeBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
|
||||
<div className="mx_MNoticeBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
|
||||
{body}
|
||||
{widgets}
|
||||
</div>
|
||||
@@ -410,14 +405,14 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
||||
}
|
||||
if (isCaption) {
|
||||
return (
|
||||
<div id={this.props.id} className="mx_MTextBody mx_EventTile_caption" onClick={this.onBodyLinkClick}>
|
||||
<div className="mx_MTextBody mx_EventTile_caption" onClick={this.onBodyLinkClick}>
|
||||
{body}
|
||||
{widgets}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div id={this.props.id} className="mx_MTextBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
|
||||
<div className="mx_MTextBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
|
||||
{body}
|
||||
{widgets}
|
||||
</div>
|
||||
|
||||
@@ -49,6 +49,7 @@ import RoomName from "../elements/RoomName.tsx";
|
||||
import { Flex } from "../../../shared-components/utils/Flex";
|
||||
import { Linkify, topicToHtml } from "../../../HtmlUtils.tsx";
|
||||
import { Box } from "../../../shared-components/utils/Box";
|
||||
import { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement.tsx";
|
||||
import { useRoomSummaryCardViewModel } from "../../viewmodels/right_panel/RoomSummaryCardViewModel.tsx";
|
||||
import { useRoomTopicViewModel } from "../../viewmodels/right_panel/RoomSummaryCardTopicViewModel.tsx";
|
||||
|
||||
@@ -250,15 +251,25 @@ const RoomSummaryCardView: React.FC<IProps> = ({
|
||||
<MenuItem Icon={ThreadsIcon} label={_t("common|threads")} onSelect={vm.onRoomThreadsClick} />
|
||||
{!vm.isVideoRoom && (
|
||||
<>
|
||||
<MenuItem
|
||||
Icon={PinIcon}
|
||||
label={_t("right_panel|pinned_messages_button")}
|
||||
onSelect={vm.onRoomPinsClick}
|
||||
<ReleaseAnnouncement
|
||||
feature="pinningMessageList"
|
||||
header={_t("right_panel|pinned_messages|release_announcement|title")}
|
||||
description={_t("right_panel|pinned_messages|release_announcement|description")}
|
||||
closeLabel={_t("right_panel|pinned_messages|release_announcement|close")}
|
||||
placement="top"
|
||||
>
|
||||
<Text as="span" size="sm">
|
||||
{vm.pinCount}
|
||||
</Text>
|
||||
</MenuItem>
|
||||
<div>
|
||||
<MenuItem
|
||||
Icon={PinIcon}
|
||||
label={_t("right_panel|pinned_messages_button")}
|
||||
onSelect={vm.onRoomPinsClick}
|
||||
>
|
||||
<Text as="span" size="sm">
|
||||
{vm.pinCount}
|
||||
</Text>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</ReleaseAnnouncement>
|
||||
<MenuItem
|
||||
Icon={FilesIcon}
|
||||
label={_t("right_panel|files_button")}
|
||||
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
type UserVerificationStatus,
|
||||
} from "matrix-js-sdk/src/crypto-api";
|
||||
import { Tooltip } from "@vector-im/compound-web";
|
||||
import { uniqueId } from "lodash";
|
||||
|
||||
import ReplyChain from "../elements/ReplyChain";
|
||||
import { _t } from "../../../languageHandler";
|
||||
@@ -919,8 +918,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
public render(): ReactNode {
|
||||
const msgtype = this.props.mxEvent.getContent().msgtype;
|
||||
const eventType = this.props.mxEvent.getType();
|
||||
const id = uniqueId();
|
||||
|
||||
const {
|
||||
hasRenderer,
|
||||
isBubbleMessage,
|
||||
@@ -1145,7 +1142,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
|
||||
let pinnedMessageBadge: JSX.Element | undefined;
|
||||
if (PinningUtils.isPinned(MatrixClientPeg.safeGet(), this.props.mxEvent)) {
|
||||
pinnedMessageBadge = <PinnedMessageBadge aria-describedby={id} tabIndex={0} />;
|
||||
pinnedMessageBadge = <PinnedMessageBadge />;
|
||||
}
|
||||
|
||||
let reactionsRow: JSX.Element | undefined;
|
||||
@@ -1240,7 +1237,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
{avatar}
|
||||
{sender}
|
||||
</div>,
|
||||
<div id={id} className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
|
||||
<div className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
|
||||
{this.renderContextMenu()}
|
||||
{replyChain}
|
||||
{renderTile(TimelineRenderingType.Thread, {
|
||||
@@ -1428,7 +1425,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
||||
{sender}
|
||||
{ircPadlock}
|
||||
{avatar}
|
||||
<div id={id} className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
|
||||
<div className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
|
||||
{this.renderContextMenu()}
|
||||
{groupTimestamp}
|
||||
{groupPadlock}
|
||||
|
||||
@@ -30,7 +30,7 @@ interface IProps {
|
||||
const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const [expanded, toggleExpanded] = useStateToggle();
|
||||
const [mediaVisible] = useMediaVisible(mxEvent);
|
||||
const [mediaVisible] = useMediaVisible(mxEvent.getId(), mxEvent.getRoomId());
|
||||
|
||||
const ts = mxEvent.getTs();
|
||||
const previews = useAsyncMemo<[string, IPreviewUrlResponse][]>(
|
||||
|
||||
@@ -531,15 +531,12 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||
if (!this.props.e2eStatus) {
|
||||
leftIcon = (
|
||||
<div className="mx_MessageComposer_e2eIconWrapper">
|
||||
<Tooltip label={_t("composer|room_unencrypted")}>
|
||||
<LockOffIcon
|
||||
aria-label={_t("composer|room_unencrypted")}
|
||||
width={12}
|
||||
height={12}
|
||||
color="var(--cpd-color-icon-info-primary)"
|
||||
className="mx_E2EIcon mx_MessageComposer_e2eIcon"
|
||||
/>
|
||||
</Tooltip>
|
||||
<LockOffIcon
|
||||
width={12}
|
||||
height={12}
|
||||
color="var(--cpd-color-icon-info-primary)"
|
||||
className="mx_E2EIcon mx_MessageComposer_e2eIcon"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.e2eStatus !== E2EStatus.Normal) {
|
||||
|
||||
@@ -30,8 +30,6 @@ import { privateShouldBeEncrypted } from "../../../utils/rooms";
|
||||
import { LocalRoom } from "../../../models/LocalRoom";
|
||||
import { shouldEncryptRoomWithSingle3rdPartyInvite } from "../../../utils/room/shouldEncryptRoomWithSingle3rdPartyInvite";
|
||||
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx";
|
||||
import { useTopic } from "../../../hooks/room/useTopic";
|
||||
import { topicToHtml, Linkify } from "../../../HtmlUtils";
|
||||
|
||||
function hasExpectedEncryptionSettings(matrixClient: MatrixClient, room: Room): boolean {
|
||||
const isEncrypted: boolean = matrixClient.isRoomEncrypted(room.roomId);
|
||||
@@ -54,7 +52,6 @@ const determineIntroMessage = (room: Room, encryptedSingle3rdPartyInvite: boolea
|
||||
const NewRoomIntro: React.FC = () => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const { room, roomId } = useScopedRoomContext("room", "roomId");
|
||||
const topic = useTopic(room);
|
||||
|
||||
if (!room || !roomId) {
|
||||
throw new Error("Unable to create a NewRoomIntro without room and roomId");
|
||||
@@ -109,6 +106,7 @@ const NewRoomIntro: React.FC = () => {
|
||||
);
|
||||
} else {
|
||||
const inRoom = room && room.getMyMembership() === KnownMembership.Join;
|
||||
const topic = room.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic;
|
||||
const canAddTopic = inRoom && room.currentState.maySendStateEvent(EventType.RoomTopic, cli.getSafeUserId());
|
||||
|
||||
const onTopicClick = (): void => {
|
||||
@@ -128,23 +126,18 @@ const NewRoomIntro: React.FC = () => {
|
||||
let topicText;
|
||||
if (canAddTopic && topic) {
|
||||
topicText = _t(
|
||||
"room|intro|edit_topic",
|
||||
{},
|
||||
"room|intro|topic_edit",
|
||||
{ topic },
|
||||
{
|
||||
a: (sub) => (
|
||||
<AccessibleButton element="a" kind="link_inline" onClick={onTopicClick}>
|
||||
{sub}
|
||||
</AccessibleButton>
|
||||
),
|
||||
topic: () => <Linkify>{topicToHtml(topic?.text, topic?.html)}</Linkify>,
|
||||
},
|
||||
);
|
||||
} else if (topic) {
|
||||
topicText = _t(
|
||||
"room|intro|display_topic",
|
||||
{},
|
||||
{ topic: () => <Linkify>{topicToHtml(topic?.text, topic?.html)}</Linkify> },
|
||||
);
|
||||
topicText = _t("room|intro|topic", { topic });
|
||||
} else if (canAddTopic) {
|
||||
topicText = _t(
|
||||
"room|intro|no_topic",
|
||||
@@ -252,7 +245,7 @@ const NewRoomIntro: React.FC = () => {
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
<p data-testid="topic">{topicText}</p>
|
||||
<p>{topicText}</p>
|
||||
{buttons}
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type JSX, useCallback, useId, useState } from "react";
|
||||
import React, { type JSX, useCallback, useState } from "react";
|
||||
import { EventTimeline, EventType, type MatrixEvent, type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { IconButton, Menu, MenuItem, Separator, Tooltip } from "@vector-im/compound-web";
|
||||
import ViewIcon from "@vector-im/compound-design-tokens/assets/web/icons/visibility-on";
|
||||
@@ -67,7 +67,6 @@ export function PinnedEventTile({ event, room, permalinkCreator }: PinnedEventTi
|
||||
|
||||
const isInThread = Boolean(event.threadRootId);
|
||||
const displayThreadInfo = !event.isThreadRoot && isInThread;
|
||||
const id = useId();
|
||||
|
||||
return (
|
||||
<div className="mx_PinnedEventTile" role="listitem">
|
||||
@@ -86,10 +85,9 @@ export function PinnedEventTile({ event, room, permalinkCreator }: PinnedEventTi
|
||||
{event.sender?.name || sender}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<PinMenu event={event} room={room} permalinkCreator={permalinkCreator} contentId={id} />
|
||||
<PinMenu event={event} room={room} permalinkCreator={permalinkCreator} />
|
||||
</div>
|
||||
<MessageEvent
|
||||
id={id}
|
||||
mxEvent={event}
|
||||
maxImageHeight={150}
|
||||
permalinkCreator={permalinkCreator}
|
||||
@@ -133,17 +131,12 @@ export function PinnedEventTile({ event, room, permalinkCreator }: PinnedEventTi
|
||||
/**
|
||||
* Properties for {@link PinMenu}.
|
||||
*/
|
||||
interface PinMenuProps extends PinnedEventTileProps {
|
||||
/**
|
||||
* HTML ID of the pinned message content.
|
||||
*/
|
||||
contentId: string;
|
||||
}
|
||||
interface PinMenuProps extends PinnedEventTileProps {}
|
||||
|
||||
/**
|
||||
* A popover menu with actions on the pinned event
|
||||
*/
|
||||
function PinMenu({ event, room, permalinkCreator, contentId }: PinMenuProps): JSX.Element {
|
||||
function PinMenu({ event, room, permalinkCreator }: PinMenuProps): JSX.Element {
|
||||
const [open, setOpen] = useState(false);
|
||||
const matrixClient = useMatrixClientContext();
|
||||
|
||||
@@ -224,11 +217,7 @@ function PinMenu({ event, room, permalinkCreator, contentId }: PinMenuProps): JS
|
||||
side="right"
|
||||
align="start"
|
||||
trigger={
|
||||
<IconButton
|
||||
size="24px"
|
||||
aria-label={_t("right_panel|pinned_messages|menu")}
|
||||
aria-describedby={contentId}
|
||||
>
|
||||
<IconButton size="24px" aria-label={_t("right_panel|pinned_messages|menu")}>
|
||||
<TriggerIcon />
|
||||
</IconButton>
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type JSX, useEffect, useId, useRef, useState } from "react";
|
||||
import React, { type JSX, useEffect, useRef, useState } from "react";
|
||||
import PinIcon from "@vector-im/compound-design-tokens/assets/web/icons/pin-solid";
|
||||
import { Button } from "@vector-im/compound-web";
|
||||
import { type MatrixEvent, type Room } from "matrix-js-sdk/src/matrix";
|
||||
@@ -64,13 +64,9 @@ export function PinnedMessageBanner({
|
||||
setCurrentEventIndex(() => eventCount - 1);
|
||||
}, [eventCount]);
|
||||
|
||||
const isLastMessage = currentEventIndex === eventCount - 1;
|
||||
|
||||
const pinnedEvent = pinnedEvents[currentEventIndex];
|
||||
useNotifyTimeline(pinnedEvent, resizeNotifier);
|
||||
|
||||
const id = useId();
|
||||
|
||||
if (!pinnedEvent) return null;
|
||||
|
||||
const shouldUseMessageEvent = pinnedEvent.isRedacted() || pinnedEvent.isDecryptionFailure();
|
||||
@@ -94,24 +90,18 @@ export function PinnedMessageBanner({
|
||||
|
||||
return (
|
||||
<div
|
||||
role="region"
|
||||
className="mx_PinnedMessageBanner"
|
||||
data-single-message={isSinglePinnedEvent}
|
||||
aria-label={_t("room|pinned_message_banner|description")}
|
||||
data-testid="pinned-message-banner"
|
||||
>
|
||||
<button
|
||||
aria-label={
|
||||
isLastMessage
|
||||
? _t("room|pinned_message_banner|go_to_newest_message")
|
||||
: _t("room|pinned_message_banner|go_to_next_message")
|
||||
}
|
||||
aria-describedby={id}
|
||||
aria-label={_t("room|pinned_message_banner|go_to_message")}
|
||||
type="button"
|
||||
className="mx_PinnedMessageBanner_main"
|
||||
onClick={onBannerClick}
|
||||
>
|
||||
<div className="mx_PinnedMessageBanner_content" id={id}>
|
||||
<div className="mx_PinnedMessageBanner_content">
|
||||
<Indicators count={eventCount} currentIndex={currentEventIndex} />
|
||||
<PinIcon width="20px" height="20px" className="mx_PinnedMessageBanner_PinIcon" />
|
||||
{!isSinglePinnedEvent && (
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useRef, useState, type JSX } from "react";
|
||||
import React, { useCallback, useRef, type JSX } from "react";
|
||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
import { type ScrollIntoViewLocation } from "react-virtuoso";
|
||||
import { isEqual } from "lodash";
|
||||
@@ -33,7 +33,6 @@ export function RoomList({ vm: { roomsResult, activeIndex } }: RoomListProps): J
|
||||
const lastSpaceId = useRef<string | undefined>(undefined);
|
||||
const lastFilterKeys = useRef<FilterKey[] | undefined>(undefined);
|
||||
const roomCount = roomsResult.rooms.length;
|
||||
const [isScrolling, setIsScrolling] = useState(false);
|
||||
const getItemComponent = useCallback(
|
||||
(
|
||||
index: number,
|
||||
@@ -58,11 +57,10 @@ export function RoomList({ vm: { roomsResult, activeIndex } }: RoomListProps): J
|
||||
roomIndex={index}
|
||||
roomCount={roomCount}
|
||||
onFocus={onFocus}
|
||||
listIsScrolling={isScrolling}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[activeIndex, roomCount, isScrolling],
|
||||
[activeIndex, roomCount],
|
||||
);
|
||||
|
||||
const getItemKey = useCallback((item: Room): string => {
|
||||
@@ -118,7 +116,6 @@ export function RoomList({ vm: { roomsResult, activeIndex } }: RoomListProps): J
|
||||
getItemKey={getItemKey}
|
||||
isItemFocusable={() => true}
|
||||
onKeyDown={keyDownCallback}
|
||||
isScrolling={setIsScrolling}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
useRoomListHeaderViewModel,
|
||||
} from "../../../viewmodels/roomlist/RoomListHeaderViewModel";
|
||||
import { RoomListOptionsMenu } from "./RoomListOptionsMenu";
|
||||
import { ReleaseAnnouncement } from "../../../structures/ReleaseAnnouncement";
|
||||
|
||||
/**
|
||||
* The header view for the room list
|
||||
@@ -46,38 +45,15 @@ export function RoomListHeaderView(): JSX.Element {
|
||||
{vm.displaySpaceMenu && <SpaceMenu vm={vm} />}
|
||||
</Flex>
|
||||
<Flex align="center" gap="var(--cpd-space-2x)">
|
||||
<ReleaseAnnouncement
|
||||
feature="newRoomList_sort"
|
||||
header={_t("room_list|release_announcement|sort|title")}
|
||||
description={_t("room_list|release_announcement|sort|description")}
|
||||
closeLabel={_t("room_list|release_announcement|next")}
|
||||
placement="bottom"
|
||||
>
|
||||
<div className="mx_RoomListHeaderView_ReleaseAnnouncementAnchor">
|
||||
<RoomListOptionsMenu vm={vm} />
|
||||
</div>
|
||||
</ReleaseAnnouncement>
|
||||
|
||||
<RoomListOptionsMenu vm={vm} />
|
||||
{/* If we don't display the compose menu, it means that the user can only send DM */}
|
||||
<ReleaseAnnouncement
|
||||
feature="newRoomList_intro"
|
||||
header={_t("room_list|release_announcement|intro|title")}
|
||||
description={_t("room_list|release_announcement|intro|description")}
|
||||
closeLabel={_t("room_list|release_announcement|next")}
|
||||
>
|
||||
<div className="mx_RoomListHeaderView_ReleaseAnnouncementAnchor">
|
||||
{vm.displayComposeMenu ? (
|
||||
<ComposeMenu vm={vm} />
|
||||
) : (
|
||||
<IconButton
|
||||
aria-label={_t("action|start_chat")}
|
||||
onClick={(e) => vm.createChatRoom(e.nativeEvent)}
|
||||
>
|
||||
<ComposeIcon color="var(--cpd-color-icon-secondary)" />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
</ReleaseAnnouncement>
|
||||
{vm.displayComposeMenu ? (
|
||||
<ComposeMenu vm={vm} />
|
||||
) : (
|
||||
<IconButton aria-label={_t("action|start_chat")} onClick={(e) => vm.createChatRoom(e.nativeEvent)}>
|
||||
<ComposeIcon color="var(--cpd-color-icon-secondary)" />
|
||||
</IconButton>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -41,10 +41,6 @@ interface RoomListItemViewProps extends React.HTMLAttributes<HTMLButtonElement>
|
||||
* The total number of rooms in the list
|
||||
*/
|
||||
roomCount: number;
|
||||
/**
|
||||
* Whether the list is currently scrolling
|
||||
*/
|
||||
listIsScrolling: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,7 +53,6 @@ export const RoomListItemView = memo(function RoomListItemView({
|
||||
onFocus,
|
||||
roomIndex: index,
|
||||
roomCount: count,
|
||||
listIsScrolling,
|
||||
...props
|
||||
}: RoomListItemViewProps): JSX.Element {
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
@@ -146,11 +141,7 @@ export const RoomListItemView = memo(function RoomListItemView({
|
||||
</Flex>
|
||||
);
|
||||
|
||||
// Rendering multiple context menus can causes crashes in radix upstream,
|
||||
// See https://github.com/radix-ui/primitives/issues/2717.
|
||||
// We also don't need the context menu while scrolling so can improve scroll performance
|
||||
// by not rendering it.
|
||||
if (!vm.showContextMenu || listIsScrolling) return content;
|
||||
if (!vm.showContextMenu) return content;
|
||||
|
||||
return (
|
||||
<RoomListItemContextMenuView
|
||||
|
||||
@@ -11,8 +11,6 @@ import { useRoomListViewModel } from "../../../viewmodels/roomlist/RoomListViewM
|
||||
import { RoomList } from "./RoomList";
|
||||
import { EmptyRoomList } from "./EmptyRoomList";
|
||||
import { RoomListPrimaryFilters } from "./RoomListPrimaryFilters";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { ReleaseAnnouncement } from "../../../structures/ReleaseAnnouncement";
|
||||
|
||||
/**
|
||||
* Host the room list and the (future) room filters
|
||||
@@ -30,17 +28,7 @@ export function RoomListView(): JSX.Element {
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<ReleaseAnnouncement
|
||||
feature="newRoomList_filter"
|
||||
header={_t("room_list|release_announcement|filter|title")}
|
||||
description={_t("room_list|release_announcement|filter|description")}
|
||||
closeLabel={_t("room_list|release_announcement|next")}
|
||||
placement="right"
|
||||
>
|
||||
<div>
|
||||
<RoomListPrimaryFilters vm={vm} />
|
||||
</div>
|
||||
</ReleaseAnnouncement>
|
||||
<RoomListPrimaryFilters vm={vm} />
|
||||
{listBody}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type JSX, type MouseEventHandler, useState } from "react";
|
||||
import React, { type FormEventHandler, type JSX, type MouseEventHandler, useState } from "react";
|
||||
import {
|
||||
Breadcrumb,
|
||||
Button,
|
||||
@@ -310,7 +310,7 @@ interface KeyFormProps {
|
||||
/**
|
||||
* Called when the form is submitted.
|
||||
*/
|
||||
onSubmit: () => Promise<void>;
|
||||
onSubmit: FormEventHandler;
|
||||
/**
|
||||
* The recovery key to confirm.
|
||||
*/
|
||||
@@ -329,7 +329,6 @@ interface KeyFormProps {
|
||||
function KeyForm({ onCancelClick, onSubmit, recoveryKey, submitButtonLabel }: KeyFormProps): JSX.Element {
|
||||
// Undefined by default, as the key is not filled yet
|
||||
const [isKeyValid, setIsKeyValid] = useState<boolean>();
|
||||
const [isKeyChangeInProgress, setIsKeyChangeInProgress] = useState<boolean>(false);
|
||||
const isKeyInvalidAndFilled = isKeyValid === false;
|
||||
|
||||
return (
|
||||
@@ -337,14 +336,7 @@ function KeyForm({ onCancelClick, onSubmit, recoveryKey, submitButtonLabel }: Ke
|
||||
className="mx_KeyForm"
|
||||
onSubmit={(evt) => {
|
||||
evt.preventDefault();
|
||||
if (isKeyChangeInProgress) {
|
||||
// Don't allow repeated attempts.
|
||||
return;
|
||||
}
|
||||
setIsKeyChangeInProgress(true);
|
||||
onSubmit().finally(() => {
|
||||
setIsKeyChangeInProgress(false);
|
||||
});
|
||||
onSubmit(evt);
|
||||
}}
|
||||
onChange={async (evt) => {
|
||||
evt.preventDefault();
|
||||
@@ -368,7 +360,7 @@ function KeyForm({ onCancelClick, onSubmit, recoveryKey, submitButtonLabel }: Ke
|
||||
)}
|
||||
</Field>
|
||||
<EncryptionCardButtons>
|
||||
<Button disabled={!isKeyValid || isKeyChangeInProgress}>{submitButtonLabel}</Button>
|
||||
<Button disabled={!isKeyValid}>{submitButtonLabel}</Button>
|
||||
<Button kind="tertiary" onClick={onCancelClick}>
|
||||
{_t("action|cancel")}
|
||||
</Button>
|
||||
|
||||
@@ -29,7 +29,6 @@ import QuickThemeSwitcher from "./QuickThemeSwitcher";
|
||||
import Modal from "../../../Modal";
|
||||
import DevtoolsDialog from "../dialogs/DevtoolsDialog";
|
||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||
import { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement";
|
||||
|
||||
const QuickSettingsButton: React.FC<{
|
||||
isPanelCollapsed: boolean;
|
||||
@@ -138,24 +137,16 @@ const QuickSettingsButton: React.FC<{
|
||||
|
||||
return (
|
||||
<>
|
||||
<ReleaseAnnouncement
|
||||
feature="newRoomList_settings"
|
||||
header={_t("room_list|release_announcement|settings|title")}
|
||||
description={_t("room_list|release_announcement|settings|description")}
|
||||
closeLabel={_t("room_list|release_announcement|done")}
|
||||
placement="right"
|
||||
<AccessibleButton
|
||||
className={classNames("mx_QuickSettingsButton", { expanded: !isPanelCollapsed })}
|
||||
onClick={openMenu}
|
||||
aria-label={_t("quick_settings|title")}
|
||||
title={isPanelCollapsed ? _t("quick_settings|title") : undefined}
|
||||
ref={handle}
|
||||
aria-expanded={!isPanelCollapsed}
|
||||
>
|
||||
<AccessibleButton
|
||||
className={classNames("mx_QuickSettingsButton", { expanded: !isPanelCollapsed })}
|
||||
onClick={openMenu}
|
||||
aria-label={_t("quick_settings|title")}
|
||||
title={isPanelCollapsed ? _t("quick_settings|title") : undefined}
|
||||
ref={handle}
|
||||
aria-expanded={!isPanelCollapsed}
|
||||
>
|
||||
{!isPanelCollapsed ? _t("common|settings") : null}
|
||||
</AccessibleButton>
|
||||
</ReleaseAnnouncement>
|
||||
{!isPanelCollapsed ? _t("common|settings") : null}
|
||||
</AccessibleButton>
|
||||
|
||||
{contextMenu}
|
||||
</>
|
||||
|
||||
@@ -50,10 +50,6 @@ interface IProps {
|
||||
onMouseDownOnHeader?: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||
|
||||
showApps?: boolean;
|
||||
|
||||
sidebarShown: boolean;
|
||||
|
||||
setSidebarShown?: (sidebarShown: boolean) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
@@ -66,6 +62,7 @@ interface IState {
|
||||
primaryFeed?: CallFeed;
|
||||
secondaryFeed?: CallFeed;
|
||||
sidebarFeeds: Array<CallFeed>;
|
||||
sidebarShown: boolean;
|
||||
}
|
||||
|
||||
function getFullScreenElement(): Element | null {
|
||||
@@ -100,6 +97,7 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
|
||||
primaryFeed: primary,
|
||||
secondaryFeed: secondary,
|
||||
sidebarFeeds: sidebar,
|
||||
sidebarShown: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -271,9 +269,8 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
|
||||
isScreensharing = await this.props.call.setScreensharingEnabled(true);
|
||||
}
|
||||
|
||||
this.props.setSidebarShown?.(true);
|
||||
|
||||
this.setState({
|
||||
sidebarShown: true,
|
||||
screensharing: isScreensharing,
|
||||
});
|
||||
};
|
||||
@@ -323,12 +320,12 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
|
||||
};
|
||||
|
||||
private onToggleSidebar = (): void => {
|
||||
this.props.setSidebarShown?.(!this.props.sidebarShown);
|
||||
this.setState({ sidebarShown: !this.state.sidebarShown });
|
||||
};
|
||||
|
||||
private renderCallControls(): JSX.Element {
|
||||
const { call, pipMode, sidebarShown } = this.props;
|
||||
const { callState, micMuted, vidMuted, screensharing, secondaryFeed, sidebarFeeds } = this.state;
|
||||
const { call, pipMode } = this.props;
|
||||
const { callState, micMuted, vidMuted, screensharing, sidebarShown, secondaryFeed, sidebarFeeds } = this.state;
|
||||
|
||||
// If SDPStreamMetadata isn't supported don't show video mute button in voice calls
|
||||
const vidMuteButtonShown = call.opponentSupportsSDPStreamMetadata() || call.hasLocalUserMediaVideoTrack;
|
||||
@@ -340,8 +337,7 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
|
||||
(call.opponentSupportsSDPStreamMetadata() || call.hasLocalUserMediaVideoTrack) &&
|
||||
call.state === CallState.Connected;
|
||||
// Show the sidebar button only if there is something to hide/show
|
||||
const sidebarButtonShown =
|
||||
!pipMode && ((secondaryFeed && !secondaryFeed.isVideoMuted()) || sidebarFeeds.length > 0);
|
||||
const sidebarButtonShown = (secondaryFeed && !secondaryFeed.isVideoMuted()) || sidebarFeeds.length > 0;
|
||||
// The dial pad & 'more' button actions are only relevant in a connected call
|
||||
const contextMenuButtonShown = callState === CallState.Connected;
|
||||
const dialpadButtonShown = callState === CallState.Connected && call.opponentSupportsDTMF();
|
||||
@@ -376,7 +372,7 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
private renderToast(): JSX.Element | null {
|
||||
const { call, sidebarShown } = this.props;
|
||||
const { call } = this.props;
|
||||
const someoneIsScreensharing = call.getFeeds().some((feed) => {
|
||||
return feed.purpose === SDPStreamMetadataPurpose.Screenshare;
|
||||
});
|
||||
@@ -384,7 +380,7 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
|
||||
if (!someoneIsScreensharing) return null;
|
||||
|
||||
const isScreensharing = call.isScreensharing();
|
||||
const { primaryFeed } = this.state;
|
||||
const { primaryFeed, sidebarShown } = this.state;
|
||||
const sharerName = primaryFeed?.getMember()?.name;
|
||||
if (!sharerName) return null;
|
||||
|
||||
@@ -397,8 +393,8 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
private renderContent(): JSX.Element {
|
||||
const { pipMode, call, onResize, sidebarShown } = this.props;
|
||||
const { isLocalOnHold, isRemoteOnHold, primaryFeed, secondaryFeed, sidebarFeeds } = this.state;
|
||||
const { pipMode, call, onResize } = this.props;
|
||||
const { isLocalOnHold, isRemoteOnHold, sidebarShown, primaryFeed, secondaryFeed, sidebarFeeds } = this.state;
|
||||
|
||||
const callRoomId = LegacyCallHandler.instance.roomIdForCall(call);
|
||||
const callRoom = (callRoomId ? MatrixClientPeg.safeGet().getRoom(callRoomId) : undefined) ?? undefined;
|
||||
@@ -541,8 +537,8 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const { call, secondaryCall, pipMode, showApps, onMouseDownOnHeader, sidebarShown } = this.props;
|
||||
const { sidebarFeeds } = this.state;
|
||||
const { call, secondaryCall, pipMode, showApps, onMouseDownOnHeader } = this.props;
|
||||
const { sidebarShown, sidebarFeeds } = this.state;
|
||||
|
||||
const client = MatrixClientPeg.safeGet();
|
||||
const callRoomId = LegacyCallHandler.instance.roomIdForCall(call);
|
||||
|
||||
@@ -25,7 +25,6 @@ interface IProps {
|
||||
|
||||
interface IState {
|
||||
call: MatrixCall | null;
|
||||
sidebarShown: boolean;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -35,23 +34,19 @@ interface IState {
|
||||
export default class LegacyCallViewForRoom extends React.Component<IProps, IState> {
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
const call = this.getCall();
|
||||
this.state = {
|
||||
call,
|
||||
sidebarShown: !!call && LegacyCallHandler.instance.isCallSidebarShown(call.callId),
|
||||
call: this.getCall(),
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallState, this.updateCall);
|
||||
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCall);
|
||||
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.ShownSidebarsChanged, this.updateCall);
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.updateCall);
|
||||
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCall);
|
||||
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.ShownSidebarsChanged, this.updateCall);
|
||||
}
|
||||
|
||||
private updateCall = (): void => {
|
||||
@@ -59,10 +54,6 @@ export default class LegacyCallViewForRoom extends React.Component<IProps, IStat
|
||||
if (newCall !== this.state.call) {
|
||||
this.setState({ call: newCall });
|
||||
}
|
||||
const newSidebarShown = !!newCall && LegacyCallHandler.instance.isCallSidebarShown(newCall.callId);
|
||||
if (newSidebarShown !== this.state.sidebarShown) {
|
||||
this.setState({ sidebarShown: newSidebarShown });
|
||||
}
|
||||
};
|
||||
|
||||
private getCall(): MatrixCall | null {
|
||||
@@ -84,11 +75,6 @@ export default class LegacyCallViewForRoom extends React.Component<IProps, IStat
|
||||
this.props.resizeNotifier.stopResizing();
|
||||
};
|
||||
|
||||
private setSidebarShown = (sidebarShown: boolean): void => {
|
||||
if (!this.state.call) return;
|
||||
LegacyCallHandler.instance.setCallSidebarShown(this.state.call.callId, sidebarShown);
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
if (!this.state.call) return null;
|
||||
|
||||
@@ -113,13 +99,7 @@ export default class LegacyCallViewForRoom extends React.Component<IProps, IStat
|
||||
className="mx_LegacyCallViewForRoom_ResizeWrapper"
|
||||
handleClasses={{ bottom: "mx_LegacyCallViewForRoom_ResizeHandle" }}
|
||||
>
|
||||
<LegacyCallView
|
||||
call={this.state.call}
|
||||
pipMode={false}
|
||||
showApps={this.props.showApps}
|
||||
sidebarShown={this.state.sidebarShown}
|
||||
setSidebarShown={this.setSidebarShown}
|
||||
/>
|
||||
<LegacyCallView call={this.state.call} pipMode={false} showApps={this.props.showApps} />
|
||||
</Resizable>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { JoinRule, type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { JoinRule } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { SettingLevel } from "../settings/SettingLevel";
|
||||
import { useSettingValue } from "./useSettings";
|
||||
@@ -19,25 +19,14 @@ const PRIVATE_JOIN_RULES: JoinRule[] = [JoinRule.Invite, JoinRule.Knock, JoinRul
|
||||
|
||||
/**
|
||||
* Should the media event be visible in the client, or hidden.
|
||||
*
|
||||
* This function uses the `mediaPreviewConfig` setting to determine the rules for the room
|
||||
* along with the `showMediaEventIds` setting for specific events.
|
||||
*
|
||||
* A function may be provided to alter the visible state.
|
||||
*
|
||||
* @param The event that contains the media. If not provided, the global rule is used.
|
||||
*
|
||||
* @returns Returns a tuple of:
|
||||
* A boolean describing the hidden status.
|
||||
* A function to show or hide the event.
|
||||
* @param eventId The eventId of the media event.
|
||||
* @returns A boolean describing the hidden status, and a function to set the visiblity.
|
||||
*/
|
||||
export function useMediaVisible(mxEvent?: MatrixEvent): [boolean, (visible: boolean) => void] {
|
||||
const eventId = mxEvent?.getId();
|
||||
const mediaPreviewSetting = useSettingValue("mediaPreviewConfig", mxEvent?.getRoomId());
|
||||
export function useMediaVisible(eventId?: string, roomId?: string): [boolean, (visible: boolean) => void] {
|
||||
const mediaPreviewSetting = useSettingValue("mediaPreviewConfig", roomId);
|
||||
const client = useMatrixClientContext();
|
||||
const eventVisibility = useSettingValue("showMediaEventIds");
|
||||
const room = client.getRoom(mxEvent?.getRoomId()) ?? undefined;
|
||||
const joinRule = useRoomState(room, (state) => state.getJoinRule());
|
||||
const joinRule = useRoomState(client.getRoom(roomId) ?? undefined, (state) => state.getJoinRule());
|
||||
const setMediaVisible = useCallback(
|
||||
(visible: boolean) => {
|
||||
SettingsStore.setValue("showMediaEventIds", null, SettingLevel.DEVICE, {
|
||||
@@ -54,9 +43,6 @@ export function useMediaVisible(mxEvent?: MatrixEvent): [boolean, (visible: bool
|
||||
// Always prefer the explicit per-event user preference here.
|
||||
if (explicitEventVisiblity !== undefined) {
|
||||
return [explicitEventVisiblity, setMediaVisible];
|
||||
} else if (mxEvent?.getSender() === client.getUserId()) {
|
||||
// If this event is ours and we've not set an explicit visibility, default to on.
|
||||
return [true, setMediaVisible];
|
||||
} else if (mediaPreviewSetting.media_previews === MediaPreviewValue.Off) {
|
||||
return [false, setMediaVisible];
|
||||
} else if (mediaPreviewSetting.media_previews === MediaPreviewValue.On) {
|
||||
|
||||
@@ -646,12 +646,12 @@
|
||||
"mode_plain": "Skrýt formátování",
|
||||
"mode_rich_text": "Zobrazit formátování",
|
||||
"no_perms_notice": "Nemáte oprávnění zveřejňovat příspěvky v této místnosti",
|
||||
"placeholder": "Odeslat nešifrovanou zprávu…",
|
||||
"placeholder_encrypted": "Odeslat zprávu...",
|
||||
"placeholder_reply": "Odeslat nešifrovanou odpověď…",
|
||||
"placeholder_reply_encrypted": "Odeslat odpověď…",
|
||||
"placeholder_thread": "Odpovědět v nešifrovaném vláknu…",
|
||||
"placeholder_thread_encrypted": "Odpovědět ve vláknu…",
|
||||
"placeholder": "Odeslat zprávu…",
|
||||
"placeholder_encrypted": "Odeslat šifrovanou zprávu…",
|
||||
"placeholder_reply": "Odpovědět…",
|
||||
"placeholder_reply_encrypted": "Odeslat šifrovanou odpověď…",
|
||||
"placeholder_thread": "Odpovědět na vlákno…",
|
||||
"placeholder_thread_encrypted": "Odpovědět na zašifrované vlákno…",
|
||||
"poll_button": "Hlasování",
|
||||
"poll_button_no_perms_description": "Nemáte oprávnění zahajovat hlasování v této místnosti.",
|
||||
"poll_button_no_perms_title": "Vyžaduje oprávnění",
|
||||
@@ -922,8 +922,7 @@
|
||||
},
|
||||
"privacy_warning": "Ujistěte se, že tuto obrazovku nikdo nevidí!",
|
||||
"restoring": "Obnovení klíčů ze zálohy",
|
||||
"security_key_label": "Klíč pro obnovení",
|
||||
"security_key_title": "Zadejte klíč pro obnovení"
|
||||
"security_key_title": "Klíč pro obnovení"
|
||||
},
|
||||
"bootstrap_title": "Příprava klíčů",
|
||||
"confirm_encryption_setup_body": "Kliknutím na tlačítko níže potvrďte nastavení šifrování.",
|
||||
@@ -1368,10 +1367,6 @@
|
||||
"name_email_mxid_share_space": "Pozvěte někoho pomocí jeho jména, e-mailové adresy, uživatelského jména (například <userId/>) nebo <a>sdílejte tento prostor</a>.",
|
||||
"name_mxid_share_room": "Pozvěte někoho pomocí svého jména, uživatelského jména (například <userId />) nebo <a>sdílejte tuto místnost</a>.",
|
||||
"name_mxid_share_space": "Pozvěte někoho pomocí jeho jména, uživatelského jména (například <userId/>) nebo <a>sdílejte tento prostor</a>.",
|
||||
"progress": {
|
||||
"dont_close": "Nezavírejte aplikaci, dokud neskončíte.",
|
||||
"preparing": "Příprava pozvánek..."
|
||||
},
|
||||
"recents_section": "Nedávné konverzace",
|
||||
"room_failed_partial": "Poslali jsme ostatním, ale níže uvedení lidé nemohli být pozváni do <RoomName/>",
|
||||
"room_failed_partial_title": "Některé pozvánky nebylo možné odeslat",
|
||||
@@ -1540,9 +1535,6 @@
|
||||
"render_reaction_images_description": "Někdy se označují jako \"vlastní emoji\".",
|
||||
"report_to_moderators": "Nahlásit moderátorům",
|
||||
"report_to_moderators_description": "V místnostech, které podporují moderování, můžete pomocí tlačítka \"Nahlásit\" nahlásit zneužití moderátorům místnosti.",
|
||||
"share_history_on_invite": "Sdílet šifrovanou historii s novými členy",
|
||||
"share_history_on_invite_description": "Při pozvání uživatele do šifrované místnosti, u které je viditelnost historie nastavena na „sdílená“, sdílet šifrovanou historii s tímto uživatelem a přijmout šifrovanou historii, když jste pozváni do takové místnosti.",
|
||||
"share_history_on_invite_warning": "Tato funkce je EXPERIMENTÁLNÍ a nejsou v ní implementována všechna bezpečnostní opatření. Neaktivujte ji na produkčních účtech.",
|
||||
"sliding_sync": "Režim klouzavé synchronizace",
|
||||
"sliding_sync_description": "V aktivním vývoji, nelze zakázat.",
|
||||
"sliding_sync_disabled_notice": "Pro vypnutí se odhlaste a znovu přihlaste",
|
||||
@@ -1665,7 +1657,6 @@
|
||||
"filter_placeholder": "Najít člena místnosti",
|
||||
"invite_button_no_perms_tooltip": "Nemáte oprávnění zvát uživatele",
|
||||
"invited_label": "Pozván",
|
||||
"list_title": "Seznam členů",
|
||||
"no_matches": "Žádné shody"
|
||||
},
|
||||
"member_list_back_action_label": "Členové místnosti",
|
||||
@@ -1770,7 +1761,6 @@
|
||||
},
|
||||
"power_level": {
|
||||
"admin": "Správce",
|
||||
"creator": "Vlastník",
|
||||
"custom": "Vlastní (%(level)s)",
|
||||
"custom_level": "Vlastní úroveň",
|
||||
"default": "Výchozí",
|
||||
@@ -1924,7 +1914,6 @@
|
||||
"thread_list": {
|
||||
"context_menu_label": "Možnosti vláken"
|
||||
},
|
||||
"title": "Pravý panel",
|
||||
"video_room_chat": {
|
||||
"title": "Chatovat"
|
||||
}
|
||||
@@ -2073,6 +2062,7 @@
|
||||
"button_close_list": "Zavřít seznam",
|
||||
"button_view_all": "Zobrazit vše",
|
||||
"description": "Tato místnost má připnuté zprávy. Kliknutím je zobrazíte.",
|
||||
"go_to_message": "Zobrazit připnutou zprávu na časové ose.",
|
||||
"title": "<bold>%(index)sz%(length)s</bold> Připnuté zprávy"
|
||||
},
|
||||
"read_topic": "Klikněte pro přečtení tématu",
|
||||
@@ -3410,7 +3400,6 @@
|
||||
"unable_to_find": "Pokusili jste se načíst bod na časové ose místnosti, ale nepodařilo se ho najít."
|
||||
},
|
||||
"m.audio": {
|
||||
"audio_player": "Audio přehrávač",
|
||||
"error_downloading_audio": "Chyba při stahování audia",
|
||||
"error_processing_audio": "Došlo k chybě při zpracovávání hlasové zprávy",
|
||||
"error_processing_voice_message": "Chyba při zpracování hlasové zprávy",
|
||||
|
||||
@@ -2110,6 +2110,7 @@
|
||||
"button_close_list": "Cau'r rhestr",
|
||||
"button_view_all": "Gweld popeth",
|
||||
"description": "Mae negeseuon wedi'u pinio yn yr ystafell hon. Cliciwch i'w gweld.",
|
||||
"go_to_message": "Gweld y neges sydd wedi'i phinnio yn y llinell amser.",
|
||||
"title": "<bold>%(index)s o %(length)s</bold> Neges wedi'u pinio"
|
||||
},
|
||||
"read_topic": "Cliciwch i ddarllen y pwnc",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1658,6 +1658,9 @@
|
||||
"not_found_title": "Αυτό το δωμάτιο ή ο χώρος δεν υπάρχει.",
|
||||
"not_found_title_name": "Το %(roomName)s δεν υπάρχει.",
|
||||
"peek_join_prompt": "Κάνετε προεπισκόπηση στο %(roomName)s. Θέλετε να συμμετάσχετε;",
|
||||
"pinned_message_banner": {
|
||||
"go_to_message": "Εμφάνιση καρφιτσωμένου μηνύματος στο χρονολόγιο."
|
||||
},
|
||||
"rejoin_button": "Επανασύνδεση",
|
||||
"status_bar": {
|
||||
"delete_all": "Διαγραφή όλων",
|
||||
|
||||
@@ -654,7 +654,6 @@
|
||||
"poll_button_no_perms_description": "You do not have permission to start polls in this room.",
|
||||
"poll_button_no_perms_title": "Permission Required",
|
||||
"replying_title": "Replying",
|
||||
"room_unencrypted": "Messages in this room are not end-to-end encrypted",
|
||||
"room_upgraded_link": "The conversation continues here.",
|
||||
"room_upgraded_notice": "This room has been replaced and is no longer active.",
|
||||
"send_button_title": "Send message",
|
||||
@@ -1026,7 +1025,7 @@
|
||||
"text": "Supply the ID and fingerprint of one of your own devices to verify it. NOTE this allows the other device to send and receive messages as you. IF SOMEONE TOLD YOU TO PASTE SOMETHING HERE, IT IS LIKELY YOU ARE BEING SCAMMED!",
|
||||
"wrong_fingerprint": "Unable to verify device '%(deviceId)s' - the supplied fingerprint '%(fingerprint)s' does not match the device fingerprint, '%(fprint)s'"
|
||||
},
|
||||
"no_key_or_device": "It looks like you don't have a Recovery Key or any other devices you can verify against. This device will not be able to access old encrypted messages. In order to verify your identity on this device, you'll need to reset your cryptographic identity.",
|
||||
"no_key_or_device": "It looks like you don't have a Recovery Key or any other devices you can verify against. This device will not be able to access old encrypted messages. In order to verify your identity on this device, you'll need to reset your verification keys.",
|
||||
"no_support_qr_emoji": "The device you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.",
|
||||
"other_party_cancelled": "The other party cancelled the verification.",
|
||||
"prompt_encrypted": "Verify all users in a room to ensure it's secure.",
|
||||
@@ -1877,6 +1876,11 @@
|
||||
"other": "You can only pin up to %(count)s widgets"
|
||||
},
|
||||
"menu": "Open menu",
|
||||
"release_announcement": {
|
||||
"close": "Ok",
|
||||
"description": "Find all pinned messages here. Rollover any message and select “Pin” to add it.",
|
||||
"title": "All new pinned messages"
|
||||
},
|
||||
"reply_thread": "Reply to a <link>thread message</link>",
|
||||
"unpin_all": {
|
||||
"button": "Unpin all messages",
|
||||
@@ -1998,9 +2002,7 @@
|
||||
"inaccessible_subtitle_1": "Try again later, or ask a room or space admin to check if you have access.",
|
||||
"inaccessible_subtitle_2": "%(errcode)s was returned while trying to access the room or space. If you think you're seeing this message in error, please <issueLink>submit a bug report</issueLink>.",
|
||||
"intro": {
|
||||
"display_topic": "Topic: <topic/>",
|
||||
"dm_caption": "Only the two of you are in this conversation, unless either of you invites anyone to join.",
|
||||
"edit_topic": "Topic: <topic/> (<a>edit</a>)",
|
||||
"enable_encryption_prompt": "Enable encryption in settings.",
|
||||
"encrypted_3pid_dm_pending_join": "Once everyone has joined, you’ll be able to chat",
|
||||
"no_avatar_label": "Add a photo, so people can easily spot your room.",
|
||||
@@ -2010,6 +2012,8 @@
|
||||
"send_message_start_dm": "Send your first message to invite <displayName/> to chat",
|
||||
"start_of_dm_history": "This is the beginning of your direct message history with <displayName/>.",
|
||||
"start_of_room": "This is the start of <roomName/>.",
|
||||
"topic": "Topic: %(topic)s ",
|
||||
"topic_edit": "Topic: %(topic)s (<a>edit</a>)",
|
||||
"unencrypted_warning": "End-to-end encryption isn't enabled",
|
||||
"user_created": "%(displayName)s created this room.",
|
||||
"you_created": "You created this room."
|
||||
@@ -2064,9 +2068,8 @@
|
||||
"pinned_message_banner": {
|
||||
"button_close_list": "Close list",
|
||||
"button_view_all": "View all",
|
||||
"description": "Pinned messages",
|
||||
"go_to_newest_message": "View the pinned message in the timeline and the newest pinned message here",
|
||||
"go_to_next_message": "View the pinned message in the timeline and the next oldest pinned message here",
|
||||
"description": "This room has pinned messages. Click to view them.",
|
||||
"go_to_message": "View the pinned message in the timeline.",
|
||||
"title": "<bold>%(index)s of %(length)s</bold> Pinned messages"
|
||||
},
|
||||
"read_topic": "Click to read topic",
|
||||
@@ -2127,7 +2130,7 @@
|
||||
"no_chats": "No chats yet",
|
||||
"no_chats_description": "Get started by messaging someone or by creating a room",
|
||||
"no_chats_description_no_room_rights": "Get started by messaging someone",
|
||||
"no_favourites": "You don't have favourite chats yet",
|
||||
"no_favourites": "You don't have favourite chat yet",
|
||||
"no_favourites_description": "You can add a chat to your favourites in the chat settings",
|
||||
"no_invites": "You don't have any unread invites",
|
||||
"no_lowpriority": "You don't have any low priority rooms",
|
||||
@@ -2175,26 +2178,6 @@
|
||||
"one": "Currently removing messages in %(count)s room",
|
||||
"other": "Currently removing messages in %(count)s rooms"
|
||||
},
|
||||
"release_announcement": {
|
||||
"done": "Done",
|
||||
"filter": {
|
||||
"description": "Filter your chats with a single click. Expand to view more filters.",
|
||||
"title": "New quick filters"
|
||||
},
|
||||
"intro": {
|
||||
"description": "The chats list has been updated to be more clear and simple to use.",
|
||||
"title": "Chats has a new look!"
|
||||
},
|
||||
"next": "Next",
|
||||
"settings": {
|
||||
"description": "To show or hide message previews, go to All settings > Preferences > Room list",
|
||||
"title": "Some settings have moved"
|
||||
},
|
||||
"sort": {
|
||||
"description": "Change the ordering of your chats from most recent to A-Z",
|
||||
"title": "Sort your chats"
|
||||
}
|
||||
},
|
||||
"room": {
|
||||
"more_options": "More Options",
|
||||
"open_room": "Open room %(roomName)s"
|
||||
|
||||
@@ -654,7 +654,6 @@
|
||||
"poll_button_no_perms_description": "Sul ei ole õigusi küsitluste korraldamiseks siin jututoas.",
|
||||
"poll_button_no_perms_title": "Vaja on täiendavaid õigusi",
|
||||
"replying_title": "Vastan",
|
||||
"room_unencrypted": "Selle jututoa sõnumid pole läbivalt krüptitud",
|
||||
"room_upgraded_link": "Vestlus jätkub siin.",
|
||||
"room_upgraded_notice": "See jututuba on asendatud teise jututoaga ning ei ole enam kasutusel.",
|
||||
"send_button_title": "Saada sõnum",
|
||||
@@ -1367,10 +1366,6 @@
|
||||
"name_email_mxid_share_space": "Kutsu teist osapoolt tema nime, e-posti aadressi, kasutajanime (nagu <userId/>) alusel või <a>jaga seda kogukonnakeskust</a>.",
|
||||
"name_mxid_share_room": "Kutsu kedagi tema nime, kasutajanime (nagu <userId/>) alusel või <a>jaga seda jututuba</a>.",
|
||||
"name_mxid_share_space": "Kutsu kedagi tema nime, kasutajanime (nagu <userId/>) alusel või <a>jaga seda kogukonnakeskust</a>.",
|
||||
"progress": {
|
||||
"dont_close": "Palun ära sulge vaadet enne kui tegevus pole lõppenud.",
|
||||
"preparing": "Valmistan kutseid ette..."
|
||||
},
|
||||
"recents_section": "Hiljutised vestlused",
|
||||
"room_failed_partial": "Teised kasutajad said kutse, kuid allpool toodud kasutajatele ei õnnestunud saata kutset <RoomName/> jututuppa",
|
||||
"room_failed_partial_title": "Mõnede kutsete saatmine ei õnnestunud",
|
||||
@@ -2070,6 +2065,7 @@
|
||||
"button_close_list": "Sulge loend",
|
||||
"button_view_all": "Vaata kõiki",
|
||||
"description": "Selles jututoas on esiletõstetud sõnumeid. Nende vaatamiseks klõpsi.",
|
||||
"go_to_message": "Vaata esiletõstetud sõnumit ajajoonel.",
|
||||
"title": "<bold>%(index)s of %(length)s</bold> Esiletõstetud sõnumid"
|
||||
},
|
||||
"read_topic": "Teema lugemiseks klõpsi",
|
||||
|
||||
@@ -1749,6 +1749,7 @@
|
||||
"pinned_message_banner": {
|
||||
"button_view_all": "Näytä kaikki",
|
||||
"description": "Tässä huoneessa on kiinnitettyjä viestejä. Napsauta nähdäksesi ne.",
|
||||
"go_to_message": "Näytä kiinnitetty viesti aikajanalla.",
|
||||
"title": "<bold>%(index)s/%(length)s</bold> kiinnitettyä viestiä"
|
||||
},
|
||||
"read_topic": "Lue aihe napsauttamalla",
|
||||
|
||||
@@ -654,7 +654,6 @@
|
||||
"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",
|
||||
@@ -1367,10 +1366,6 @@
|
||||
"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",
|
||||
@@ -2068,9 +2063,8 @@
|
||||
"pinned_message_banner": {
|
||||
"button_close_list": "Fermer la liste",
|
||||
"button_view_all": "Voir tout",
|
||||
"description": "Messages épinglés",
|
||||
"go_to_newest_message": "Afficher le message épinglé dans la discussion et le plus récent ici",
|
||||
"go_to_next_message": "Afficher le message épinglé dans la discussion et le plus ancien suivant ici",
|
||||
"description": "Ce salon contient des messages épinglés. Cliquez pour les consulter.",
|
||||
"go_to_message": "Afficher le message épinglé dans la discussion.",
|
||||
"title": "<bold>%(index)s de %(length)s</bold> messages épinglés"
|
||||
},
|
||||
"read_topic": "Cliquer pour lire le sujet",
|
||||
@@ -3513,7 +3507,7 @@
|
||||
"unknown": "%(senderDisplayName)s a changé l’accès des visiteurs en %(rule)s"
|
||||
},
|
||||
"m.room.history_visibility": {
|
||||
"invited": "%(senderName)s a changé un paramètre : désormais les nouveaux membres pourront voir l'historique de conversation du salon à partir du moment où ils ont été invités.",
|
||||
"invited": "%(senderName)s a rendu l’historique visible à tous les membres du salon, depuis le moment où ils ont été invités.",
|
||||
"joined": "%(senderName)s a rendu l’historique visible à tous les membres du salon, à partir de leur arrivée.",
|
||||
"shared": "%(senderName)s a rendu l’historique visible à tous les membres du salon.",
|
||||
"unknown": "%(senderName)s a rendu l’historique visible à inconnu (%(visibility)s).",
|
||||
|
||||
@@ -652,7 +652,6 @@
|
||||
"poll_button_no_perms_description": "Nincs joga szavazást kezdeményezni ebben a szobában.",
|
||||
"poll_button_no_perms_title": "Jogosultság szükséges",
|
||||
"replying_title": "Válasz",
|
||||
"room_unencrypted": "A szobában lévő üzenetek nincsenek végponttól végpontig titkosítva",
|
||||
"room_upgraded_link": "A beszélgetés itt folytatódik.",
|
||||
"room_upgraded_notice": "Ezt a szobát lecseréltük és nem aktív többé.",
|
||||
"send_button_title": "Üzenet küldése",
|
||||
@@ -1359,10 +1358,6 @@
|
||||
"name_email_mxid_share_space": "Hívjon meg valakit a nevét, e-mail-címét vagy felhasználónevét (például <userId/>) megadva, vagy <a>ossza meg ezt a teret</a>.",
|
||||
"name_mxid_share_room": "Hívjon meg valakit a nevét vagy felhasználónevét (például <userId/>) megadva, vagy <a>ossza meg ezt a szobát</a>.",
|
||||
"name_mxid_share_space": "Hívjon meg valakit a nevét vagy felhasználónevét (például <userId/>) megadva, vagy <a>ossza meg ezt a teret</a>.",
|
||||
"progress": {
|
||||
"dont_close": "Ne zárja be az alkalmazást, amíg be nem fejezte.",
|
||||
"preparing": "Meghívók előkészítése…"
|
||||
},
|
||||
"recents_section": "Legújabb beszélgetések",
|
||||
"room_failed_partial": "Az alábbi embereket nem sikerül meghívni ide: <RoomName/>, de a többi meghívó elküldve",
|
||||
"room_failed_partial_title": "Néhány meghívót nem sikerült elküldeni",
|
||||
@@ -2056,6 +2051,7 @@
|
||||
"button_close_list": "A lista bezárása",
|
||||
"button_view_all": "Összes megtekintése",
|
||||
"description": "Ez a szoba rögzített üzeneteket tartalmaz. Kattintson ide a megtekintésükhöz.",
|
||||
"go_to_message": "Tekintse meg a rögzített üzenetet az idővonalon.",
|
||||
"title": "<bold>%(index)s. / %(length)s </bold> rögzített üzenet"
|
||||
},
|
||||
"read_topic": "Kattintson a téma elolvasásához",
|
||||
|
||||
@@ -917,7 +917,6 @@
|
||||
},
|
||||
"privacy_warning": "Pastikan tidak ada yang bisa melihat layar ini!",
|
||||
"restoring": "Memulihkan kunci-kunci dari cadangan",
|
||||
"security_key_label": "Kunci pemulihan",
|
||||
"security_key_title": "Konfirmasi kunci pemulihan Anda"
|
||||
},
|
||||
"bootstrap_title": "Menyiapkan kunci",
|
||||
@@ -1363,10 +1362,6 @@
|
||||
"name_email_mxid_share_space": "Undang seseorang menggunakan namanya, alamat email, nama pengguna (seperti <userId/>) atau <a>bagikan space ini</a>.",
|
||||
"name_mxid_share_room": "Undang seseorang menggunakan namanya, nama pengguna (seperti <userId/>) atau <a>bagikan ruangan ini</a>.",
|
||||
"name_mxid_share_space": "Undang seseorang menggunakan namanya, nama pengguna (seperti <userId/>) atau <a>bagikan space ini</a>.",
|
||||
"progress": {
|
||||
"dont_close": "Jangan tutup aplikasi sampai selesai.",
|
||||
"preparing": "Menyiapkan undangan..."
|
||||
},
|
||||
"recents_section": "Obrolan Terkini",
|
||||
"room_failed_partial": "Kami telah mengirim yang lainnya, tetapi orang berikut ini tidak dapat diundang ke <RoomName/>",
|
||||
"room_failed_partial_title": "Beberapa undangan tidak dapat dikirim",
|
||||
@@ -1763,7 +1758,6 @@
|
||||
},
|
||||
"power_level": {
|
||||
"admin": "Admin",
|
||||
"creator": "Pemilik",
|
||||
"custom": "Kustom (%(level)s)",
|
||||
"custom_level": "Tingkat kustom",
|
||||
"default": "Bawaan",
|
||||
@@ -1915,7 +1909,6 @@
|
||||
"thread_list": {
|
||||
"context_menu_label": "Opsi utasan"
|
||||
},
|
||||
"title": "Panel kanan",
|
||||
"video_room_chat": {
|
||||
"title": "Obrolan"
|
||||
}
|
||||
@@ -2062,6 +2055,7 @@
|
||||
"button_close_list": "Tutup daftar",
|
||||
"button_view_all": "Lihat semua",
|
||||
"description": "Ruangan ini memiliki pesan yang disematkan. Klik untuk melihatnya.",
|
||||
"go_to_message": "Lihat pesan yang disematkan di lini masa.",
|
||||
"title": "<bold>%(index)s dari %(length)s</bold> Pesan yang disematkan"
|
||||
},
|
||||
"read_topic": "Klik untuk membaca topik",
|
||||
@@ -3395,7 +3389,6 @@
|
||||
"unable_to_find": "Mencoba memuat titik spesifik di lini masa ruangan ini, tetapi tidak dapat menemukannya."
|
||||
},
|
||||
"m.audio": {
|
||||
"audio_player": "Pemutar audio",
|
||||
"error_downloading_audio": "Terjadi kesalahan mengunduh audio",
|
||||
"error_processing_audio": "Terjadi kesalahan mengolah pesan suara",
|
||||
"error_processing_voice_message": "Terjadi kesalahan mengolah pesan suara",
|
||||
|
||||
@@ -654,7 +654,6 @@
|
||||
"poll_button_no_perms_description": "Du har ikke tillatelse til å starte avstemninger i dette rommet.",
|
||||
"poll_button_no_perms_title": "Tillatelse kreves",
|
||||
"replying_title": "Svarer på",
|
||||
"room_unencrypted": "Meldinger i dette rommet er ikke ende-til-ende krypterte",
|
||||
"room_upgraded_link": "Samtalen fortsetter her.",
|
||||
"room_upgraded_notice": "Dette rommet har blitt erstattet og er ikke lenger aktivt.",
|
||||
"send_button_title": "Send melding",
|
||||
@@ -2069,9 +2068,8 @@
|
||||
"pinned_message_banner": {
|
||||
"button_close_list": "Lukk liste",
|
||||
"button_view_all": "Vis alle",
|
||||
"description": "Festede meldinger",
|
||||
"go_to_newest_message": "Se den festede meldingen i tidslinjen og den nyeste festede meldingen her",
|
||||
"go_to_next_message": "Se den festede meldingen i tidslinjen og den nest eldste festede meldingen her",
|
||||
"description": "Dette rommet har festede meldinger. Klikk for å se dem.",
|
||||
"go_to_message": "Vis den festede meldingen i tidslinjen.",
|
||||
"title": "<bold>%(index)s av %(length)s</bold> festede meldinger"
|
||||
},
|
||||
"read_topic": "Klikk for å lese emnet",
|
||||
|
||||
@@ -2073,6 +2073,7 @@
|
||||
"button_close_list": "Zamknij listę",
|
||||
"button_view_all": "Pokaż wszystkie",
|
||||
"description": "Ten pokój ma przypięte wiadomości. Kliknij, aby je wyświetlić.",
|
||||
"go_to_message": "Wyświetl przypiętą wiadomość na osi czasu.",
|
||||
"title": "<bold>%(index)s z %(length)s</bold> przypiętych wiadomości"
|
||||
},
|
||||
"read_topic": "Kliknij, aby przeczytać temat",
|
||||
|
||||
@@ -1999,6 +1999,7 @@
|
||||
"button_close_list": "Fechar lista",
|
||||
"button_view_all": "Ver tudo",
|
||||
"description": "Esta sala tem mensagens afixadas. Clica para as veres.",
|
||||
"go_to_message": "Visualiza a mensagem fixada na linha do tempo.",
|
||||
"title": "<bold>%(index)s de %(length)s</bold> Mensagens fixadas"
|
||||
},
|
||||
"read_topic": "Clica para ler o tópico",
|
||||
|
||||
@@ -1916,7 +1916,6 @@
|
||||
"thread_list": {
|
||||
"context_menu_label": "Opções de tópico"
|
||||
},
|
||||
"title": "Painel direito",
|
||||
"video_room_chat": {
|
||||
"title": "Bate-papo"
|
||||
}
|
||||
@@ -2064,6 +2063,7 @@
|
||||
"button_close_list": "Fechar lista",
|
||||
"button_view_all": "Ver tudo",
|
||||
"description": "Esta sala tem mensagens fixadas. Clique para visualizá-las.",
|
||||
"go_to_message": "Veja a mensagem fixada no histórico.",
|
||||
"title": "<bold>%(index)s de %(length)s </bold> mensagens fixadas"
|
||||
},
|
||||
"read_topic": "Clique para ler o tópico",
|
||||
|
||||
@@ -2064,6 +2064,7 @@
|
||||
"button_close_list": "Закрыть список",
|
||||
"button_view_all": "Посмотреть все",
|
||||
"description": "В этой комнате есть закрепленные сообщения. Нажмите, чтобы просмотреть их.",
|
||||
"go_to_message": "Показать прикрепленное сообщение на временной шкале.",
|
||||
"title": "<bold>%(index)s из %(length)s</bold> Закрепленные сообщения"
|
||||
},
|
||||
"read_topic": "Нажмите, чтобы увидеть тему",
|
||||
|
||||
@@ -661,7 +661,6 @@
|
||||
"poll_button_no_perms_description": "Nemáte povolenie spúšťať ankety v tejto miestnosti.",
|
||||
"poll_button_no_perms_title": "Vyžaduje sa povolenie",
|
||||
"replying_title": "Odpoveď",
|
||||
"room_unencrypted": "Správy v tejto miestnosti nie sú end-to-end šifrované",
|
||||
"room_upgraded_link": "Konverzácia pokračuje tu.",
|
||||
"room_upgraded_notice": "Táto miestnosť bola nahradená a nie je viac aktívna.",
|
||||
"send_button_title": "Odoslať správu",
|
||||
@@ -2097,6 +2096,7 @@
|
||||
"button_close_list": "Zatvoriť zoznam",
|
||||
"button_view_all": "Zobraziť všetko",
|
||||
"description": "Táto miestnosť má pripnuté správy. Kliknutím ich zobrazíte.",
|
||||
"go_to_message": "Zobraziť pripnuté správy na časovej osi.",
|
||||
"title": "<bold>%(index)s z %(length)s</bold> pripnutých správ"
|
||||
},
|
||||
"read_topic": "Kliknutím si prečítate tému",
|
||||
|
||||
@@ -2068,6 +2068,7 @@
|
||||
"button_close_list": "Stäng listan",
|
||||
"button_view_all": "Visa alla",
|
||||
"description": "Det här rummet har fästa meddelanden. Klicka för att se dem.",
|
||||
"go_to_message": "Visa det fästa meddelandet på tidslinjen.",
|
||||
"title": "<bold>%(index)s av %(length)s</bold> fästa meddelanden"
|
||||
},
|
||||
"read_topic": "Klicka för att läsa ämne",
|
||||
|
||||
@@ -1997,6 +1997,7 @@
|
||||
"button_close_list": "Listeyi kapat",
|
||||
"button_view_all": "Tümünü görüntüle",
|
||||
"description": "Bu odada sabitlenmiş mesajlar var. Görüntülemek için tıklayın.",
|
||||
"go_to_message": "Sabitlenmiş mesajı zaman çizelgesinde görüntüle.",
|
||||
"title": "<bold>%(index)s / %(length)s</bold> Sabitlenmiş mesajlar"
|
||||
},
|
||||
"read_topic": "Başlığı okumak için tıklayın",
|
||||
|
||||
@@ -2070,6 +2070,7 @@
|
||||
"button_close_list": "Закрити список",
|
||||
"button_view_all": "Подивитись все",
|
||||
"description": "У цій кімнаті є закріплені повідомлення. Натисніть, щоб переглянути їх.",
|
||||
"go_to_message": "Переглянути закріплене повідомлення у стрічці часу.",
|
||||
"title": "<bold>%(index)s з %(length)s</bold> закріплених повідомлень"
|
||||
},
|
||||
"read_topic": "Натисніть, щоб побачити тему",
|
||||
|
||||
@@ -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,12 +23,14 @@ import { type IWidgetApiRequest, type ClientWidgetApi, type IWidgetData } from "
|
||||
import {
|
||||
type MatrixRTCSession,
|
||||
MatrixRTCSessionEvent,
|
||||
type CallMembership,
|
||||
MatrixRTCSessionManagerEvents,
|
||||
} from "matrix-js-sdk/src/matrixrtc";
|
||||
|
||||
import type EventEmitter from "events";
|
||||
import type { IApp } from "../stores/WidgetStore";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../MediaDeviceHandler";
|
||||
import { timeout } from "../utils/promise";
|
||||
import WidgetUtils from "../utils/WidgetUtils";
|
||||
import { WidgetType } from "../widgets/WidgetType";
|
||||
@@ -191,6 +193,18 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
|
||||
*/
|
||||
public abstract clean(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Contacts the widget to connect to the call or prompt the user to connect to the call.
|
||||
* @param {MediaDeviceInfo | null} audioInput The audio input to use, or
|
||||
* null to start muted.
|
||||
* @param {MediaDeviceInfo | null} audioInput The video input to use, or
|
||||
* null to start muted.
|
||||
*/
|
||||
protected abstract performConnection(
|
||||
audioInput: MediaDeviceInfo | null,
|
||||
videoInput: MediaDeviceInfo | null,
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Contacts the widget to disconnect from the call.
|
||||
*/
|
||||
@@ -198,10 +212,28 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
|
||||
|
||||
/**
|
||||
* Starts the communication between the widget and the call.
|
||||
* The widget associated with the call must be active for this to succeed.
|
||||
* The call then waits for the necessary requirements to actually perform the connection
|
||||
* or connects right away depending on the call type. (Jitsi, Legacy, ElementCall...)
|
||||
* It uses the media devices set in MediaDeviceHandler.
|
||||
* The widget associated with the call must be active
|
||||
* for this to succeed.
|
||||
* Only call this if the call state is: ConnectionState.Disconnected.
|
||||
*/
|
||||
public async start(): Promise<void> {
|
||||
const { [MediaDeviceKindEnum.AudioInput]: audioInputs, [MediaDeviceKindEnum.VideoInput]: videoInputs } =
|
||||
(await MediaDeviceHandler.getDevices())!;
|
||||
|
||||
let audioInput: MediaDeviceInfo | null = null;
|
||||
if (!MediaDeviceHandler.startWithAudioMuted) {
|
||||
const deviceId = MediaDeviceHandler.getAudioInput();
|
||||
audioInput = audioInputs.find((d) => d.deviceId === deviceId) ?? audioInputs[0] ?? null;
|
||||
}
|
||||
let videoInput: MediaDeviceInfo | null = null;
|
||||
if (!MediaDeviceHandler.startWithVideoMuted) {
|
||||
const deviceId = MediaDeviceHandler.getVideoInput();
|
||||
videoInput = videoInputs.find((d) => d.deviceId === deviceId) ?? videoInputs[0] ?? null;
|
||||
}
|
||||
|
||||
const messagingStore = WidgetMessagingStore.instance;
|
||||
this.messaging = messagingStore.getMessagingForUid(this.widgetUid) ?? null;
|
||||
if (!this.messaging) {
|
||||
@@ -222,23 +254,13 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
|
||||
throw new Error(`Failed to bind call widget in room ${this.roomId}: ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
await this.performConnection(audioInput, videoInput);
|
||||
|
||||
protected setConnected(): void {
|
||||
this.room.on(RoomEvent.MyMembership, this.onMyMembership);
|
||||
window.addEventListener("beforeunload", this.beforeUnload);
|
||||
this.connectionState = ConnectionState.Connected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually marks the call as disconnected.
|
||||
*/
|
||||
protected setDisconnected(): void {
|
||||
this.room.off(RoomEvent.MyMembership, this.onMyMembership);
|
||||
window.removeEventListener("beforeunload", this.beforeUnload);
|
||||
this.connectionState = ConnectionState.Disconnected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the user from the call.
|
||||
*/
|
||||
@@ -251,6 +273,15 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
|
||||
this.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually marks the call as disconnected.
|
||||
*/
|
||||
public setDisconnected(): void {
|
||||
this.room.off(RoomEvent.MyMembership, this.onMyMembership);
|
||||
window.removeEventListener("beforeunload", this.beforeUnload);
|
||||
this.connectionState = ConnectionState.Disconnected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops further communication with the widget and tells the UI to close.
|
||||
*/
|
||||
@@ -436,10 +467,66 @@ export class JitsiCall extends Call {
|
||||
});
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
await super.start();
|
||||
this.messaging!.on(`action:${ElementWidgetActions.JoinCall}`, this.onJoin);
|
||||
protected async performConnection(
|
||||
audioInput: MediaDeviceInfo | null,
|
||||
videoInput: MediaDeviceInfo | null,
|
||||
): Promise<void> {
|
||||
// Ensure that the messaging doesn't get stopped while we're waiting for responses
|
||||
const dontStopMessaging = new Promise<void>((resolve, reject) => {
|
||||
const messagingStore = WidgetMessagingStore.instance;
|
||||
|
||||
const listener = (uid: string): void => {
|
||||
if (uid === this.widgetUid) {
|
||||
cleanup();
|
||||
reject(new Error("Messaging stopped"));
|
||||
}
|
||||
};
|
||||
const done = (): void => {
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
const cleanup = (): void => {
|
||||
messagingStore.off(WidgetMessagingStoreEvent.StopMessaging, listener);
|
||||
this.off(CallEvent.ConnectionState, done);
|
||||
};
|
||||
|
||||
messagingStore.on(WidgetMessagingStoreEvent.StopMessaging, listener);
|
||||
this.on(CallEvent.ConnectionState, done);
|
||||
});
|
||||
|
||||
// Empirically, it's possible for Jitsi Meet to crash instantly at startup,
|
||||
// sending a hangup event that races with the rest of this method, so we need
|
||||
// to add the hangup listener now rather than later
|
||||
this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
|
||||
// Actually perform the join
|
||||
const response = waitForEvent(
|
||||
this.messaging!,
|
||||
`action:${ElementWidgetActions.JoinCall}`,
|
||||
(ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
ev.preventDefault();
|
||||
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
return true;
|
||||
},
|
||||
);
|
||||
const request = this.messaging!.transport.send(ElementWidgetActions.JoinCall, {
|
||||
audioInput: audioInput?.label ?? null,
|
||||
videoInput: videoInput?.label ?? null,
|
||||
});
|
||||
try {
|
||||
await Promise.race([Promise.all([request, response]), dontStopMessaging]);
|
||||
} catch (e) {
|
||||
// If it timed out, clean up our advance preparations
|
||||
this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
|
||||
if (this.messaging!.transport.ready) {
|
||||
// The messaging still exists, which means Jitsi might still be going in the background
|
||||
this.messaging!.transport.send(ElementWidgetActions.HangupCall, { force: true });
|
||||
}
|
||||
|
||||
throw new Error(`Failed to join call in room ${this.roomId}: ${e}`);
|
||||
}
|
||||
|
||||
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Dock, this.onDock);
|
||||
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Undock, this.onUndock);
|
||||
}
|
||||
@@ -462,17 +549,18 @@ export class JitsiCall extends Call {
|
||||
}
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.messaging!.off(`action:${ElementWidgetActions.JoinCall}`, this.onJoin);
|
||||
this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
public setDisconnected(): void {
|
||||
// During tests this.messaging can be undefined
|
||||
this.messaging?.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Dock, this.onDock);
|
||||
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Undock, this.onUndock);
|
||||
super.close();
|
||||
|
||||
super.setDisconnected();
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.room.off(RoomStateEvent.Update, this.onRoomState);
|
||||
this.off(CallEvent.ConnectionState, this.onConnectionState);
|
||||
this.on(CallEvent.ConnectionState, this.onConnectionState);
|
||||
if (this.participantsExpirationTimer !== null) {
|
||||
clearTimeout(this.participantsExpirationTimer);
|
||||
this.participantsExpirationTimer = null;
|
||||
@@ -524,21 +612,27 @@ export class JitsiCall extends Call {
|
||||
await this.messaging!.transport.send(ElementWidgetActions.SpotlightLayout, {});
|
||||
};
|
||||
|
||||
private readonly onJoin = (ev: CustomEvent<IWidgetApiRequest>): void => {
|
||||
ev.preventDefault();
|
||||
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
this.setConnected();
|
||||
};
|
||||
|
||||
private readonly onHangup = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
|
||||
// If we're already in the middle of a client-initiated disconnection,
|
||||
// ignore the event
|
||||
if (this.connectionState === ConnectionState.Disconnecting) return;
|
||||
|
||||
ev.preventDefault();
|
||||
|
||||
// In case this hangup is caused by Jitsi Meet crashing at startup,
|
||||
// wait for the connection event in order to avoid racing
|
||||
if (this.connectionState === ConnectionState.Disconnected) {
|
||||
await waitForEvent(this, CallEvent.ConnectionState);
|
||||
}
|
||||
|
||||
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
this.setDisconnected();
|
||||
if (!isVideoRoom(this.room)) this.close();
|
||||
this.close();
|
||||
// In video rooms we immediately want to restart the call after hangup
|
||||
// The lobby will be shown again and it connects to all signals from Jitsi.
|
||||
if (isVideoRoom(this.room)) {
|
||||
this.start();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -766,38 +860,54 @@ export class ElementCall extends Call {
|
||||
ElementCall.createOrGetCallWidget(room.roomId, room.client, skipLobby, isVideoRoom(room));
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
await super.start();
|
||||
this.messaging!.on(`action:${ElementWidgetActions.JoinCall}`, this.onJoin);
|
||||
protected async performConnection(
|
||||
audioInput: MediaDeviceInfo | null,
|
||||
videoInput: MediaDeviceInfo | null,
|
||||
): Promise<void> {
|
||||
this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
this.messaging!.on(`action:${ElementWidgetActions.Close}`, this.onClose);
|
||||
this.messaging!.once(`action:${ElementWidgetActions.Close}`, this.onClose);
|
||||
this.messaging!.on(`action:${ElementWidgetActions.DeviceMute}`, this.onDeviceMute);
|
||||
|
||||
// TODO: Watch for a widget action telling us that the join button was clicked, rather than
|
||||
// relying on the MatrixRTC session state, to set the state to connecting
|
||||
const session = this.client.matrixRTC.getActiveRoomSession(this.room);
|
||||
if (session) {
|
||||
await waitForEvent(
|
||||
session,
|
||||
MatrixRTCSessionEvent.MembershipsChanged,
|
||||
(_, newMemberships: CallMembership[]) =>
|
||||
newMemberships.some((m) => m.sender === this.client.getUserId()),
|
||||
false, // allow user to wait as long as they want (no timeout)
|
||||
);
|
||||
} else {
|
||||
await waitForEvent(
|
||||
this.client.matrixRTC,
|
||||
MatrixRTCSessionManagerEvents.SessionStarted,
|
||||
(roomId: string, session: MatrixRTCSession) =>
|
||||
this.session.callId === session.callId && roomId === this.roomId,
|
||||
false, // allow user to wait as long as they want (no timeout)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected async performDisconnection(): Promise<void> {
|
||||
const response = waitForEvent(
|
||||
this.messaging!,
|
||||
`action:${ElementWidgetActions.HangupCall}`,
|
||||
(ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
ev.preventDefault();
|
||||
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
return true;
|
||||
},
|
||||
);
|
||||
const request = this.messaging!.transport.send(ElementWidgetActions.HangupCall, {});
|
||||
try {
|
||||
await Promise.all([request, response]);
|
||||
await this.messaging!.transport.send(ElementWidgetActions.HangupCall, {});
|
||||
await waitForEvent(
|
||||
this.session,
|
||||
MatrixRTCSessionEvent.MembershipsChanged,
|
||||
(_, newMemberships: CallMembership[]) =>
|
||||
!newMemberships.some((m) => m.sender === this.client.getUserId()),
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to hangup call in room ${this.roomId}: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.messaging!.off(`action:${ElementWidgetActions.JoinCall}`, this.onJoin);
|
||||
public setDisconnected(): void {
|
||||
this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
this.messaging!.off(`action:${ElementWidgetActions.Close}`, this.onClose);
|
||||
this.messaging!.off(`action:${ElementWidgetActions.DeviceMute}`, this.onDeviceMute);
|
||||
super.close();
|
||||
super.setDisconnected();
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
@@ -844,27 +954,22 @@ export class ElementCall extends Call {
|
||||
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
};
|
||||
|
||||
private readonly onJoin = (ev: CustomEvent<IWidgetApiRequest>): void => {
|
||||
ev.preventDefault();
|
||||
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
this.setConnected();
|
||||
};
|
||||
|
||||
private readonly onHangup = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
|
||||
// If we're already in the middle of a client-initiated disconnection,
|
||||
// ignore the event
|
||||
if (this.connectionState === ConnectionState.Disconnecting) return;
|
||||
|
||||
ev.preventDefault();
|
||||
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
this.setDisconnected();
|
||||
// In video rooms we immediately want to reconnect after hangup
|
||||
// This starts the lobby again and connects to all signals from EC.
|
||||
if (isVideoRoom(this.room)) {
|
||||
this.start();
|
||||
}
|
||||
};
|
||||
|
||||
private readonly onClose = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
|
||||
ev.preventDefault();
|
||||
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
this.setDisconnected(); // Just in case the widget forgot to emit a hangup action (maybe it's in an error state)
|
||||
this.close(); // User is done with the call; tell the UI to close it
|
||||
// User is done with the call; tell the UI to close it
|
||||
this.close();
|
||||
};
|
||||
|
||||
public clean(): Promise<void> {
|
||||
|
||||
@@ -9,31 +9,33 @@ import { type NavigationApi as INavigationApi } from "@element-hq/element-web-mo
|
||||
|
||||
import { navigateToPermalink } from "../utils/permalinks/navigator.ts";
|
||||
import { parsePermalink } from "../utils/permalinks/Permalinks.ts";
|
||||
import { getCachedRoomIDForAlias } from "../RoomAliasCache.ts";
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg.ts";
|
||||
import dispatcher from "../dispatcher/dispatcher.ts";
|
||||
import { Action } from "../dispatcher/actions.ts";
|
||||
import type { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload.ts";
|
||||
import SettingsStore from "../settings/SettingsStore.ts";
|
||||
|
||||
export class NavigationApi implements INavigationApi {
|
||||
public async toMatrixToLink(link: string, join = false): Promise<void> {
|
||||
navigateToPermalink(link);
|
||||
|
||||
const parts = parsePermalink(link);
|
||||
if (parts?.roomIdOrAlias) {
|
||||
if (parts.roomIdOrAlias.startsWith("#")) {
|
||||
dispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_alias: parts.roomIdOrAlias,
|
||||
via_servers: parts.viaServers ?? undefined,
|
||||
auto_join: join,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
} else {
|
||||
dispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: parts.roomIdOrAlias,
|
||||
via_servers: parts.viaServers ?? undefined,
|
||||
auto_join: join,
|
||||
metricsTrigger: undefined,
|
||||
if (parts?.roomIdOrAlias && join) {
|
||||
let roomId: string | undefined = parts.roomIdOrAlias;
|
||||
if (roomId.startsWith("#")) {
|
||||
roomId = getCachedRoomIDForAlias(parts.roomIdOrAlias);
|
||||
if (!roomId) {
|
||||
// alias resolution failed
|
||||
const result = await MatrixClientPeg.safeGet().getRoomIdForAlias(parts.roomIdOrAlias);
|
||||
roomId = result.room_id;
|
||||
}
|
||||
}
|
||||
|
||||
if (roomId) {
|
||||
dispatcher.dispatch({
|
||||
action: Action.JoinRoom,
|
||||
canAskToJoin: SettingsStore.getValue("feature_ask_to_join"),
|
||||
roomId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,12 +28,13 @@ import dispatcher from "../dispatcher/dispatcher";
|
||||
import { navigateToPermalink } from "../utils/permalinks/navigator";
|
||||
import { parsePermalink } from "../utils/permalinks/Permalinks";
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import { getCachedRoomIDForAlias } from "../RoomAliasCache";
|
||||
import { Action } from "../dispatcher/actions";
|
||||
import { type OverwriteLoginPayload } from "../dispatcher/payloads/OverwriteLoginPayload";
|
||||
import { type ActionPayload } from "../dispatcher/payloads";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import WidgetStore, { type IApp } from "../stores/WidgetStore";
|
||||
import { type Container, WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
|
||||
import type { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload.ts";
|
||||
|
||||
/**
|
||||
* Glue between the `ModuleApi` interface and the react-sdk. Anticipates one instance
|
||||
@@ -182,22 +183,28 @@ export class ProxiedModuleApi implements ModuleApi {
|
||||
navigateToPermalink(uri);
|
||||
|
||||
const parts = parsePermalink(uri);
|
||||
if (parts?.roomIdOrAlias) {
|
||||
if (parts.roomIdOrAlias.startsWith("#")) {
|
||||
dispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_alias: parts.roomIdOrAlias,
|
||||
via_servers: parts.viaServers ?? undefined,
|
||||
auto_join: andJoin ?? false,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
} else {
|
||||
dispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: parts.roomIdOrAlias,
|
||||
via_servers: parts.viaServers ?? undefined,
|
||||
auto_join: andJoin ?? false,
|
||||
metricsTrigger: undefined,
|
||||
if (parts?.roomIdOrAlias && andJoin) {
|
||||
let roomId: string | undefined = parts.roomIdOrAlias;
|
||||
let servers = parts.viaServers;
|
||||
if (roomId.startsWith("#")) {
|
||||
roomId = getCachedRoomIDForAlias(parts.roomIdOrAlias);
|
||||
if (!roomId) {
|
||||
// alias resolution failed
|
||||
const result = await MatrixClientPeg.safeGet().getRoomIdForAlias(parts.roomIdOrAlias);
|
||||
roomId = result.room_id;
|
||||
if (!servers) servers = result.servers; // use provided servers first, if available
|
||||
}
|
||||
}
|
||||
dispatcher.dispatch({
|
||||
action: Action.ViewRoom,
|
||||
room_id: roomId,
|
||||
via_servers: servers,
|
||||
});
|
||||
|
||||
if (andJoin) {
|
||||
dispatcher.dispatch({
|
||||
action: Action.JoinRoom,
|
||||
canAskToJoin: SettingsStore.getValue("feature_ask_to_join"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type ReactNode } from "react";
|
||||
import { STABLE_MSC4133_EXTENDED_PROFILES, UNSTABLE_MSC4133_EXTENDED_PROFILES } from "matrix-js-sdk/src/matrix";
|
||||
import { UNSTABLE_MSC4133_EXTENDED_PROFILES } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { type MediaPreviewConfig } from "../@types/media_preview.ts";
|
||||
// Import i18n.tsx instead of languageHandler to avoid circular deps
|
||||
@@ -844,7 +844,7 @@ export const SETTINGS: Settings = {
|
||||
controller: new ServerSupportUnstableFeatureController(
|
||||
"userTimezonePublish",
|
||||
defaultWatchManager,
|
||||
[[UNSTABLE_MSC4133_EXTENDED_PROFILES], [STABLE_MSC4133_EXTENDED_PROFILES]],
|
||||
[[UNSTABLE_MSC4133_EXTENDED_PROFILES]],
|
||||
undefined,
|
||||
_td("labs|extended_profiles_msc_support"),
|
||||
),
|
||||
|
||||
@@ -340,7 +340,7 @@ export default class SettingsStore {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the internationalised reason a setting is disabled if one is assigned.
|
||||
* Retrieves the reason a setting is disabled if one is assigned.
|
||||
* If a setting is not disabled, or no reason is given by the `SettingController`,
|
||||
* this will return undefined.
|
||||
* @param {string} settingName The setting to look up.
|
||||
|
||||
@@ -11,7 +11,6 @@ import MatrixClientBackedController from "./MatrixClientBackedController";
|
||||
import { type WatchManager } from "../WatchManager";
|
||||
import SettingsStore from "../SettingsStore";
|
||||
import { type SettingKey } from "../Settings.tsx";
|
||||
import { _t, type TranslationKey } from "../../languageHandler.tsx";
|
||||
|
||||
/**
|
||||
* Disables a given setting if the server unstable feature it requires is not supported
|
||||
@@ -34,7 +33,7 @@ export default class ServerSupportUnstableFeatureController extends MatrixClient
|
||||
private readonly watchers: WatchManager,
|
||||
private readonly unstableFeatureGroups: string[][],
|
||||
private readonly stableVersion?: string,
|
||||
private readonly disabledMessage?: TranslationKey,
|
||||
private readonly disabledMessage?: string,
|
||||
private readonly forcedValue: any = false,
|
||||
) {
|
||||
super();
|
||||
@@ -97,7 +96,7 @@ export default class ServerSupportUnstableFeatureController extends MatrixClient
|
||||
|
||||
public get settingDisabled(): boolean | string {
|
||||
if (this.disabled) {
|
||||
return this.disabledMessage ? _t(this.disabledMessage) : true;
|
||||
return this.disabledMessage ?? true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import { Features } from "../settings/Settings";
|
||||
/**
|
||||
* The features are shown in the array order.
|
||||
*/
|
||||
const FEATURES = ["newRoomList_intro", "newRoomList_sort", "newRoomList_filter", "newRoomList_settings"] as const;
|
||||
const FEATURES = ["pinningMessageList"] as const;
|
||||
/**
|
||||
* All the features that can be shown in the release announcements.
|
||||
*/
|
||||
|
||||
@@ -26,7 +26,7 @@ import { type MatrixDispatcher } from "../dispatcher/dispatcher";
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import Modal from "../Modal";
|
||||
import { _t } from "../languageHandler";
|
||||
import { getCachedRoomIdForAlias, storeRoomAliasInCache } from "../RoomAliasCache";
|
||||
import { getCachedRoomIDForAlias, storeRoomAliasInCache } from "../RoomAliasCache";
|
||||
import { Action } from "../dispatcher/actions";
|
||||
import { retry } from "../utils/promise";
|
||||
import { TimelineRenderingType } from "../contexts/RoomContext";
|
||||
@@ -438,7 +438,6 @@ export class RoomViewStore extends EventEmitter {
|
||||
action: Action.JoinRoom,
|
||||
roomId: payload.room_id,
|
||||
metricsTrigger: payload.metricsTrigger as JoinRoomPayload["metricsTrigger"],
|
||||
canAskToJoin: SettingsStore.getValue("feature_ask_to_join"),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -446,16 +445,10 @@ export class RoomViewStore extends EventEmitter {
|
||||
await setMarkedUnreadState(room, MatrixClientPeg.safeGet(), false);
|
||||
}
|
||||
} else if (payload.room_alias) {
|
||||
let roomId: string;
|
||||
let viaServers: string[] | undefined;
|
||||
|
||||
// Try the room alias to room ID navigation cache first to avoid
|
||||
// blocking room navigation on the homeserver.
|
||||
const cachedResult = getCachedRoomIdForAlias(payload.room_alias);
|
||||
if (cachedResult) {
|
||||
roomId = cachedResult.roomId;
|
||||
viaServers = cachedResult.viaServers;
|
||||
} else {
|
||||
let roomId = getCachedRoomIDForAlias(payload.room_alias);
|
||||
if (!roomId) {
|
||||
// Room alias cache miss, so let's ask the homeserver. Resolve the alias
|
||||
// and then do a second dispatch with the room ID acquired.
|
||||
this.setState({
|
||||
@@ -474,9 +467,8 @@ export class RoomViewStore extends EventEmitter {
|
||||
});
|
||||
try {
|
||||
const result = await MatrixClientPeg.safeGet().getRoomIdForAlias(payload.room_alias);
|
||||
storeRoomAliasInCache(payload.room_alias, result.room_id, result.servers);
|
||||
storeRoomAliasInCache(payload.room_alias, result.room_id);
|
||||
roomId = result.room_id;
|
||||
viaServers = result.servers;
|
||||
} catch (err) {
|
||||
logger.error("RVS failed to get room id for alias: ", err);
|
||||
this.dis?.dispatch<ViewRoomErrorPayload>({
|
||||
@@ -493,7 +485,6 @@ export class RoomViewStore extends EventEmitter {
|
||||
this.dis?.dispatch({
|
||||
...payload,
|
||||
room_id: roomId,
|
||||
via_servers: viaServers,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -518,13 +509,12 @@ export class RoomViewStore extends EventEmitter {
|
||||
joining: true,
|
||||
});
|
||||
|
||||
// take a copy of roomAlias, roomId & viaServers as they may change by the time the join is complete
|
||||
const { roomAlias, roomId = payload.roomId, viaServers = [] } = this.state;
|
||||
// prefer the room alias if we have one as it allows joining over federation even with no viaServers
|
||||
const address = roomAlias || roomId!;
|
||||
// take a copy of roomAlias & roomId as they may change by the time the join is complete
|
||||
const { roomAlias, roomId } = this.state;
|
||||
const address = payload.roomId || roomAlias || roomId!;
|
||||
|
||||
const joinOpts: IJoinRoomOpts = {
|
||||
viaServers,
|
||||
viaServers: this.state.viaServers || [],
|
||||
...(payload.opts ?? {}),
|
||||
};
|
||||
if (SettingsStore.getValue("feature_share_history_on_invite")) {
|
||||
@@ -557,7 +547,7 @@ export class RoomViewStore extends EventEmitter {
|
||||
canAskToJoin: payload.canAskToJoin,
|
||||
});
|
||||
|
||||
if (payload.canAskToJoin && err instanceof MatrixError && err.httpStatus === 403) {
|
||||
if (payload.canAskToJoin) {
|
||||
this.dis?.dispatch({ action: Action.PromptAskToJoin });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ import {
|
||||
UPDATE_SUGGESTED_ROOMS,
|
||||
UPDATE_TOP_LEVEL_SPACES,
|
||||
} from ".";
|
||||
import { getCachedRoomIdForAlias } from "../../RoomAliasCache";
|
||||
import { getCachedRoomIDForAlias } from "../../RoomAliasCache";
|
||||
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
|
||||
import {
|
||||
flattenSpaceHierarchyWithCache,
|
||||
@@ -1249,8 +1249,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<EmptyObject> {
|
||||
let roomId = payload.room_id;
|
||||
|
||||
if (payload.room_alias && !roomId) {
|
||||
const result = getCachedRoomIdForAlias(payload.room_alias);
|
||||
if (result) roomId = result.roomId;
|
||||
roomId = getCachedRoomIDForAlias(payload.room_alias);
|
||||
}
|
||||
|
||||
if (!roomId) return; // we'll get re-fired with the room ID shortly
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -79,6 +79,7 @@ export class MockedCall extends Call {
|
||||
// No action needed for any of the following methods since this is just a mock
|
||||
public async clean(): Promise<void> {}
|
||||
// Public to allow spying
|
||||
public async performConnection(): Promise<void> {}
|
||||
public async performDisconnection(): Promise<void> {}
|
||||
|
||||
public destroy() {
|
||||
|
||||
@@ -585,42 +585,4 @@ describe("LegacyCallHandler without third party protocols", () => {
|
||||
expect(mockAudioBufferSourceNode.start).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("sidebar state", () => {
|
||||
const roomId = "test-room-id";
|
||||
|
||||
it("should default to showing sidebar", () => {
|
||||
const call = new MatrixCall({
|
||||
client: MatrixClientPeg.safeGet(),
|
||||
roomId,
|
||||
});
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
cli.emit(CallEventHandlerEvent.Incoming, call);
|
||||
|
||||
expect(callHandler.isCallSidebarShown(call.callId)).toEqual(true);
|
||||
});
|
||||
|
||||
it("should remember sidebar state per call", () => {
|
||||
const call = new MatrixCall({
|
||||
client: MatrixClientPeg.safeGet(),
|
||||
roomId,
|
||||
});
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
cli.emit(CallEventHandlerEvent.Incoming, call);
|
||||
|
||||
expect(callHandler.isCallSidebarShown(call.callId)).toEqual(true);
|
||||
callHandler.setCallSidebarShown(call.callId, false);
|
||||
expect(callHandler.isCallSidebarShown(call.callId)).toEqual(false);
|
||||
|
||||
call.emit(CallEvent.Hangup, call);
|
||||
|
||||
const call2 = new MatrixCall({
|
||||
client: MatrixClientPeg.safeGet(),
|
||||
roomId,
|
||||
});
|
||||
cli.emit(CallEventHandlerEvent.Incoming, call2);
|
||||
|
||||
expect(callHandler.isCallSidebarShown(call2.callId)).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -664,24 +664,24 @@ describe("TextForEvent", () => {
|
||||
["the legacy key", { topic: "My topic" }, { result: '@a changed the topic to "My topic".' }],
|
||||
[
|
||||
"the legacy key with an empty m.topic key",
|
||||
{ "topic": "My topic", "m.topic": { "m.text": [] } },
|
||||
{ "topic": "My topic", "m.topic": [] },
|
||||
{ result: '@a changed the topic to "My topic".' },
|
||||
],
|
||||
[
|
||||
"the m.topic key",
|
||||
{ "topic": "Ignore this", "m.topic": { "m.text": [{ mimetype: "text/plain", body: "My topic" }] } },
|
||||
{ "topic": "Ignore this", "m.topic": [{ mimetype: "text/plain", body: "My topic" }] },
|
||||
{ result: '@a changed the topic to "My topic".' },
|
||||
],
|
||||
[
|
||||
"the m.topic key and the legacy key undefined",
|
||||
{ "topic": undefined, "m.topic": { "m.text": [{ mimetype: "text/plain", body: "My topic" }] } },
|
||||
{ "topic": undefined, "m.topic": [{ mimetype: "text/plain", body: "My topic" }] },
|
||||
{ result: '@a changed the topic to "My topic".' },
|
||||
],
|
||||
["the legacy key undefined", { topic: undefined }, { result: "@a removed the topic." }],
|
||||
["the legacy key empty string", { topic: "" }, { result: "@a removed the topic." }],
|
||||
[
|
||||
"both the legacy and new keys removed",
|
||||
{ "topic": undefined, "m.topic": { "m.text": [] } },
|
||||
{ "topic": undefined, "m.topic": [] },
|
||||
{ result: "@a removed the topic." },
|
||||
],
|
||||
];
|
||||
|
||||
@@ -7,7 +7,6 @@ exports[`dialogTermsInteractionCallback should render a dialog with the expected
|
||||
class=""
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
|
||||
@@ -12,7 +12,6 @@ exports[`<NewRecoveryMethodDialog /> when key backup is disabled 1`] = `
|
||||
class="mx_KeyBackupFailedDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
@@ -84,7 +83,6 @@ exports[`<NewRecoveryMethodDialog /> when key backup is enabled 1`] = `
|
||||
class="mx_KeyBackupFailedDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
|
||||
@@ -23,7 +23,7 @@ describe("ReleaseAnnouncement", () => {
|
||||
function renderReleaseAnnouncement() {
|
||||
return render(
|
||||
<ReleaseAnnouncement
|
||||
feature="newRoomList_intro"
|
||||
feature="pinningMessageList"
|
||||
header="header"
|
||||
description="description"
|
||||
closeLabel="close"
|
||||
|
||||
@@ -388,7 +388,6 @@ exports[`<MatrixChat /> with an existing session onAction() room actions leave_r
|
||||
class="mx_QuestionDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
@@ -445,7 +444,6 @@ exports[`<MatrixChat /> with an existing session onAction() room actions leave_r
|
||||
class="mx_QuestionDialog mx_Dialog_fixedWidth"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="mx_Dialog_header"
|
||||
|
||||
@@ -59,7 +59,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="«rh5»"
|
||||
aria-labelledby="«rg4»"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -75,7 +75,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Voice call"
|
||||
aria-labelledby="«rha»"
|
||||
aria-labelledby="«rg9»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -101,7 +101,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
|
||||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
aria-labelledby="«rhf»"
|
||||
aria-labelledby="«rge»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -128,7 +128,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
|
||||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
aria-labelledby="«rhk»"
|
||||
aria-labelledby="«rgj»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -158,7 +158,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
|
||||
>
|
||||
<div
|
||||
aria-label="2 members"
|
||||
aria-labelledby="«rhp»"
|
||||
aria-labelledby="«rgo»"
|
||||
class="mx_AccessibleButton mx_FacePile"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@@ -278,7 +278,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="«ri3»"
|
||||
aria-labelledby="«rh2»"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -294,7 +294,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Voice call"
|
||||
aria-labelledby="«ri8»"
|
||||
aria-labelledby="«rh7»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -320,7 +320,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
|
||||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
aria-labelledby="«rid»"
|
||||
aria-labelledby="«rhc»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -347,7 +347,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
|
||||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
aria-labelledby="«rii»"
|
||||
aria-labelledby="«rhh»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -377,7 +377,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
|
||||
>
|
||||
<div
|
||||
aria-label="2 members"
|
||||
aria-labelledby="«rin»"
|
||||
aria-labelledby="«rhm»"
|
||||
class="mx_AccessibleButton mx_FacePile"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@@ -583,7 +583,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="«rcd»"
|
||||
aria-labelledby="«rbo»"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -599,7 +599,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Voice call"
|
||||
aria-labelledby="«rci»"
|
||||
aria-labelledby="«rbt»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -625,7 +625,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
|
||||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
aria-labelledby="«rcn»"
|
||||
aria-labelledby="«rc2»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -652,7 +652,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
|
||||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
aria-labelledby="«rcs»"
|
||||
aria-labelledby="«rc7»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -682,7 +682,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
|
||||
>
|
||||
<div
|
||||
aria-label="2 members"
|
||||
aria-labelledby="«rd1»"
|
||||
aria-labelledby="«rcc»"
|
||||
class="mx_AccessibleButton mx_FacePile"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@@ -800,8 +800,6 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
|
||||
class="mx_MessageComposer_e2eIconWrapper"
|
||||
>
|
||||
<svg
|
||||
aria-label="Messages in this room are not end-to-end encrypted"
|
||||
aria-labelledby="«rda»"
|
||||
class="mx_E2EIcon mx_MessageComposer_e2eIcon"
|
||||
color="var(--cpd-color-icon-info-primary)"
|
||||
fill="currentColor"
|
||||
@@ -984,7 +982,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="«rep»"
|
||||
aria-labelledby="«rdu»"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -1000,7 +998,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Voice call"
|
||||
aria-labelledby="«reu»"
|
||||
aria-labelledby="«re3»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -1026,7 +1024,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
||||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
aria-labelledby="«rf3»"
|
||||
aria-labelledby="«re8»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -1053,7 +1051,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
||||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
aria-labelledby="«rf8»"
|
||||
aria-labelledby="«red»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -1083,7 +1081,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
||||
>
|
||||
<div
|
||||
aria-label="2 members"
|
||||
aria-labelledby="«rfd»"
|
||||
aria-labelledby="«rei»"
|
||||
class="mx_AccessibleButton mx_FacePile"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@@ -1196,8 +1194,6 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
||||
class="mx_MessageComposer_e2eIconWrapper"
|
||||
>
|
||||
<svg
|
||||
aria-label="Messages in this room are not end-to-end encrypted"
|
||||
aria-labelledby="«rfm»"
|
||||
class="mx_E2EIcon mx_MessageComposer_e2eIcon"
|
||||
color="var(--cpd-color-icon-info-primary)"
|
||||
fill="currentColor"
|
||||
@@ -1466,7 +1462,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="«r2j»"
|
||||
aria-labelledby="«r2c»"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -1482,7 +1478,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Voice call"
|
||||
aria-labelledby="«r2o»"
|
||||
aria-labelledby="«r2h»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -1508,7 +1504,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
aria-labelledby="«r2t»"
|
||||
aria-labelledby="«r2m»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -1535,7 +1531,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
aria-labelledby="«r32»"
|
||||
aria-labelledby="«r2r»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -1565,7 +1561,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
>
|
||||
<div
|
||||
aria-label="0 members"
|
||||
aria-labelledby="«r37»"
|
||||
aria-labelledby="«r30»"
|
||||
class="mx_AccessibleButton mx_FacePile"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@@ -1678,7 +1674,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="«r2j»"
|
||||
aria-labelledby="«r2c»"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -1694,7 +1690,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Voice call"
|
||||
aria-labelledby="«r2o»"
|
||||
aria-labelledby="«r2h»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -1720,7 +1716,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
aria-labelledby="«r2t»"
|
||||
aria-labelledby="«r2m»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -1747,7 +1743,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
aria-labelledby="«r32»"
|
||||
aria-labelledby="«r2r»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -1777,7 +1773,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
>
|
||||
<div
|
||||
aria-label="0 members"
|
||||
aria-labelledby="«r37»"
|
||||
aria-labelledby="«r30»"
|
||||
class="mx_AccessibleButton mx_FacePile"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@@ -1845,7 +1841,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="«r3m»"
|
||||
aria-labelledby="«r3e»"
|
||||
class="mx_E2EIcon mx_E2EIcon_verified mx_MessageComposer_e2eIcon"
|
||||
data-testid="e2e-icon"
|
||||
>
|
||||
@@ -2056,7 +2052,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
||||
</button>
|
||||
<button
|
||||
aria-label="Chat"
|
||||
aria-labelledby="«r7l»"
|
||||
aria-labelledby="«r7c»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -2083,7 +2079,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
||||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
aria-labelledby="«r7q»"
|
||||
aria-labelledby="«r7h»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -2110,7 +2106,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
||||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
aria-labelledby="«r7v»"
|
||||
aria-labelledby="«r7m»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -2140,7 +2136,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
||||
>
|
||||
<div
|
||||
aria-label="0 members"
|
||||
aria-labelledby="«r84»"
|
||||
aria-labelledby="«r7r»"
|
||||
class="mx_AccessibleButton mx_FacePile"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@@ -2216,7 +2212,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
aria-labelledby="«r8d»"
|
||||
aria-labelledby="«r84»"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="secondary"
|
||||
data-testid="base-card-close-button"
|
||||
@@ -2275,8 +2271,6 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
||||
class="mx_MessageComposer_e2eIconWrapper"
|
||||
>
|
||||
<svg
|
||||
aria-label="Messages in this room are not end-to-end encrypted"
|
||||
aria-labelledby="«r8m»"
|
||||
class="mx_E2EIcon mx_MessageComposer_e2eIcon"
|
||||
color="var(--cpd-color-icon-info-primary)"
|
||||
fill="currentColor"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user