Merge branch 'element-hq:master' into master

This commit is contained in:
2025-08-22 15:02:59 +02:00
committed by GitHub
172 changed files with 2817 additions and 1456 deletions
+13
View File
@@ -139,3 +139,16 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: vectorim/element-web
- name: Repository Dispatch
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3
if: github.event_name != 'pull_request'
with:
repository: element-hq/element-web-pro
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
event-type: image-built
# Stable way to determine the :version
client-payload: |-
{
"base-ref": "${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}"
}
+3
View File
@@ -31,6 +31,9 @@ const config: StorybookConfig = {
},
// Needed for counterpart to work
plugins: [nodePolyfills({ include: ["process", "util"] })],
server: {
allowedHosts: ["localhost", ".docker.internal"],
},
});
},
};
+18 -2
View File
@@ -5,7 +5,8 @@ import "../res/css/shared.pcss";
import "./preview.css";
import React, { useLayoutEffect } from "react";
import { FORCE_RE_RENDER } from "storybook/internal/core-events";
import { setLanguage } from "../src/shared-components/i18n";
import { setLanguage } from "../src/shared-components/utils/i18n";
import { TooltipProvider } from "@vector-im/compound-web";
export const globalTypes = {
theme: {
@@ -82,9 +83,24 @@ export const withLanguageProvider: Decorator = (Story, context) => {
);
};
const withTooltipProvider: Decorator = (Story) => {
return (
<TooltipProvider>
<Story />
</TooltipProvider>
);
};
const preview: Preview = {
tags: ["autodocs"],
decorators: [withThemeProvider, withLanguageProvider],
decorators: [withThemeProvider, withLanguageProvider, withTooltipProvider],
parameters: {
options: {
storySort: {
method: "alphabetical",
},
},
},
};
export default preview;
+33
View File
@@ -1,3 +1,36 @@
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.
## ✨ Features
* [Backport staging] Allow /upgraderoom command without developer mode enabled ([#30529](https://github.com/element-hq/element-web/pull/30529)). Contributed by @RiotRobot.
* [Backport staging] Support for creator/owner power level ([#30526](https://github.com/element-hq/element-web/pull/30526)). Contributed by @RiotRobot.
* New room list: change icon and label of menu item for to start a DM ([#30470](https://github.com/element-hq/element-web/pull/30470)). Contributed by @florianduros.
* Implement the member list with virtuoso ([#29869](https://github.com/element-hq/element-web/pull/29869)). Contributed by @langleyd.
* Add labs option for history sharing on invite ([#30313](https://github.com/element-hq/element-web/pull/30313)). Contributed by @richvdh.
* Bump wysiwyg to 2.39.0 adding support for pasting rich text content in the Rich Text Edtior ([#30421](https://github.com/element-hq/element-web/pull/30421)). Contributed by @langleyd.
* Support `EventShieldReason.MISMATCHED_SENDER` ([#30403](https://github.com/element-hq/element-web/pull/30403)). Contributed by @richvdh.
* Change unencrypted and public pills to blue ([#30399](https://github.com/element-hq/element-web/pull/30399)). Contributed by @florianduros.
* Change color of public room icon ([#30390](https://github.com/element-hq/element-web/pull/30390)). Contributed by @florianduros.
* Script for updating storybook screenshots ([#30340](https://github.com/element-hq/element-web/pull/30340)). Contributed by @dbkr.
* Add toggle to hide empty state in devtools ([#30352](https://github.com/element-hq/element-web/pull/30352)). Contributed by @toger5.
## 🐛 Bug Fixes
* [Backport staging] Use userId to filter users in non-federated rooms when showing the InviteDialog ([#30537](https://github.com/element-hq/element-web/pull/30537)). Contributed by @RiotRobot.
* [Backport staging] Catch error when encountering invalid m.room.pinned\_events event ([#30536](https://github.com/element-hq/element-web/pull/30536)). Contributed by @RiotRobot.
* Update for compatibility with v12 rooms ([#30452](https://github.com/element-hq/element-web/pull/30452)). Contributed by @dbkr.
* New room list: fix tooltip on presence ([#30474](https://github.com/element-hq/element-web/pull/30474)). Contributed by @florianduros.
* New room list: add tooltip for presence and room status ([#30472](https://github.com/element-hq/element-web/pull/30472)). Contributed by @florianduros.
* Fix: Clicking on an item in the member list causes it to scroll to the top rather than show the profile view ([#30455](https://github.com/element-hq/element-web/pull/30455)). Contributed by @langleyd.
* Put the 'decrypting' tooltip back ([#30446](https://github.com/element-hq/element-web/pull/30446)). Contributed by @dbkr.
* Use server name explicitly for via. ([#30362](https://github.com/element-hq/element-web/pull/30362)). Contributed by @Half-Shot.
* fix: replace hardcoded string in poll history dialog ([#30402](https://github.com/element-hq/element-web/pull/30402)). Contributed by @florianduros.
* fix: replace hardcoded string on qr code back button ([#30401](https://github.com/element-hq/element-web/pull/30401)). Contributed by @florianduros.
* Fix color of icon button with outline ([#30361](https://github.com/element-hq/element-web/pull/30361)). Contributed by @florianduros.
Changes in [1.11.108](https://github.com/element-hq/element-web/releases/tag/v1.11.108) (2025-07-30)
====================================================================================================
## 🐛 Bug Fixes
+2 -2
View File
@@ -1,7 +1,7 @@
# syntax=docker.io/docker/dockerfile:1.17-labs@sha256:9187104f31e3a002a8a6a3209ea1f937fb7486c093cbbde1e14b0fa0d7e4f1b5
# Builder
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:125996cb2451482467fc2aa4d7653075894b08e9b7711bcd761044ca270a083e AS builder
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:a80324457a2c8d09c83ff9edf2bdf71f378d3288de920e68a358bd3c484b8c4a 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:ef0100e39ffe377a42ad99e1f644b78097a84f1ac60a90eac3b888196b2eeb00
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:86df552d36eb24c45d3f5becf6423bd056a3fd235d7085fe3d5ea28ba89a8232
# Need root user to install packages & manipulate the usr directory
USER root
+2
View File
@@ -22,6 +22,8 @@ const config: Config = {
setupFiles: ["jest-canvas-mock", "web-streams-polyfill/polyfill"],
setupFilesAfterEnv: ["<rootDir>/test/setupTests.ts"],
moduleNameMapper: {
// Support CSS module
"\\.(module.css)$": "identity-obj-proxy",
"\\.(css|scss|pcss)$": "<rootDir>/__mocks__/cssMock.js",
"\\.(gif|png|ttf|woff2)$": "<rootDir>/__mocks__/imageMock.js",
"\\.svg$": "<rootDir>/__mocks__/svg.js",
+15 -12
View File
@@ -1,6 +1,6 @@
{
"name": "element-web",
"version": "1.11.108",
"version": "1.11.109",
"description": "Element: the future of secure communication",
"author": "New Vector Ltd.",
"repository": {
@@ -69,11 +69,12 @@
"storybook": "storybook dev -p 6007",
"build-storybook": "storybook build",
"test:storybook": "test-storybook --url http://localhost:6007/",
"test:storybook:ci": "concurrently -k -s first -n \"SB,TEST\" \"yarn storybook\" \"wait-on tcp:6007 && yarn test-storybook --url http://localhost:6007/ --ci --maxWorkers=2\""
"test:storybook:ci": "concurrently -k -s first -n \"SB,TEST\" \"yarn storybook --no-open\" \"wait-on tcp:6007 && yarn test-storybook --url http://localhost:6007/ --ci --maxWorkers=2\"",
"test:storybook:update": "playwright-screenshots --entrypoint yarn --with-node-modules && playwright-screenshots --entrypoint /work/node_modules/.bin/test-storybook --with-node-modules --url http://host.docker.internal:6007/ --updateSnapshot"
},
"resolutions": {
"**/pretty-format/react-is": "19.1.0",
"@playwright/test": "1.53.2",
"@playwright/test": "1.54.1",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"oidc-client-ts": "3.3.0",
@@ -98,7 +99,7 @@
"@types/react-virtualized": "^9.21.30",
"@vector-im/compound-design-tokens": "^5.0.0",
"@vector-im/compound-web": "^8.1.2",
"@vector-im/matrix-wysiwyg": "2.38.4",
"@vector-im/matrix-wysiwyg": "2.39.0",
"@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4",
"@zxcvbn-ts/language-en": "^3.0.2",
@@ -116,7 +117,7 @@
"emojibase-regex": "15.3.2",
"escape-html": "^1.0.3",
"file-saver": "^2.0.5",
"filesize": "10.1.6",
"filesize": "11.0.2",
"github-markdown-css": "^5.5.1",
"glob-to-regexp": "^0.4.1",
"highlight.js": "^11.3.1",
@@ -127,14 +128,14 @@
"jsrsasign": "^11.0.0",
"jszip": "^3.7.0",
"katex": "^0.16.0",
"linkify-react": "4.3.1",
"linkify-string": "4.3.1",
"linkifyjs": "4.3.1",
"linkify-react": "4.3.2",
"linkify-string": "4.3.2",
"linkifyjs": "4.3.2",
"lodash": "^4.17.21",
"maplibre-gl": "^5.0.0",
"matrix-encrypt-attachment": "^1.0.3",
"matrix-events-sdk": "0.0.1",
"matrix-js-sdk": "37.12.0",
"matrix-js-sdk": "37.13.0",
"matrix-widget-api": "^1.10.0",
"memoize-one": "^6.0.0",
"mime": "^4.0.4",
@@ -142,7 +143,7 @@
"opus-recorder": "^8.0.3",
"pako": "^2.0.3",
"png-chunks-extract": "^1.0.0",
"posthog-js": "1.256.2",
"posthog-js": "1.257.0",
"qrcode": "1.5.4",
"re-resizable": "6.11.2",
"react": "^19.0.0",
@@ -153,6 +154,7 @@
"react-string-replace": "^1.1.1",
"react-transition-group": "^4.4.1",
"react-virtualized": "^9.22.5",
"react-virtuoso": "^4.12.6",
"rfc4648": "^1.4.0",
"sanitize-filename": "^1.6.3",
"sanitize-html": "2.17.0",
@@ -185,12 +187,12 @@
"@babel/runtime": "^7.12.5",
"@casualbot/jest-sonar-reporter": "2.2.7",
"@element-hq/element-call-embedded": "0.13.1",
"@element-hq/element-web-playwright-common": "^1.4.2",
"@element-hq/element-web-playwright-common": "^1.4.4",
"@peculiar/webcrypto": "^1.4.3",
"@playwright/test": "^1.50.1",
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
"@rrweb/types": "^2.0.0-alpha.18",
"@sentry/webpack-plugin": "^3.0.0",
"@sentry/webpack-plugin": "^4.0.0",
"@storybook/addon-designs": "^10.0.1",
"@storybook/addon-docs": "^9.0.12",
"@storybook/icons": "^1.4.0",
@@ -264,6 +266,7 @@
"file-loader": "^6.0.0",
"html-webpack-plugin": "^5.5.3",
"husky": "^9.0.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.6.2",
"jest-canvas-mock": "^2.5.2",
"jest-environment-jsdom": "^29.7.0",
@@ -124,6 +124,10 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
const toasts = new Toasts(page);
await toasts.rejectToast("Notifications");
await toasts.assertNoToasts();
// There may still be a `/sendToDevice/m.secret.request` in flight, which will later throw an error and cause
// a *subsequent* test to fail. Tell playwright to ignore any errors resulting from in-flight routes.
await page.unrouteAll({ behavior: "ignoreErrors" });
});
test("Verify device with QR code during login", async ({ page, app, credentials, homeserver }) => {
@@ -30,6 +30,10 @@ test.describe("Lazy Loading", () => {
});
test.beforeEach(async ({ page, homeserver, user, bot, app }) => {
// The charlies were running off the bottom of the screen.
// We no longer overscan the member list so the result is they are not in the dom.
// Increase the viewport size to ensure they are.
await page.setViewportSize({ width: 1000, height: 1000 });
for (let i = 1; i <= 10; i++) {
const displayName = `Charly #${i}`;
const bot = new Bot(page, homeserver, { displayName, startClient: false, autoAcceptInvites: false });
@@ -35,8 +35,8 @@ test.describe("Header section of the room list", () => {
await expect(page.getByRole("menu")).toMatchScreenshot("room-list-header-compose-menu.png");
// New message should open the direct messages dialog
await page.getByRole("menuitem", { name: "New message" }).click();
// Start chat should open the direct messages dialog
await page.getByRole("menuitem", { name: "Start chat" }).click();
await expect(page.getByRole("heading", { name: "Direct Messages" })).toBeVisible();
await app.closeDialog();
+60 -10
View File
@@ -11,6 +11,32 @@ import { Bot } from "../../pages/bot";
const ROOM_NAME = "Test room";
const NAME = "Alice";
async function setupRoomWithMembers(
app: any,
page: any,
homeserver: any,
roomName: string,
memberNames: string[],
): Promise<string> {
const visibility = await page.evaluate(() => (window as any).matrixcs.Visibility.Public);
const id = await app.client.createRoom({ name: roomName, visibility });
const bots: Bot[] = [];
for (let i = 0; i < memberNames.length; i++) {
const displayName = memberNames[i];
const bot = new Bot(page, homeserver, { displayName, startClient: false, autoAcceptInvites: false });
if (displayName === "Susan") {
await bot.prepareClient();
await app.client.inviteUser(id, bot.credentials?.userId);
} else {
await bot.joinRoom(id);
}
bots.push(bot);
}
return id;
}
test.use({
synapseConfig: {
presence: {
@@ -25,17 +51,8 @@ test.use({
test.describe("Memberlist", () => {
test.beforeEach(async ({ app, user, page, homeserver }, testInfo) => {
testInfo.setTimeout(testInfo.timeout + 30_000);
const id = await app.client.createRoom({ name: ROOM_NAME });
const newBots: Bot[] = [];
const names = ["Bob", "Bob", "Susan"];
for (let i = 0; i < 3; i++) {
const displayName = names[i];
const autoAcceptInvites = displayName !== "Susan";
const bot = new Bot(page, homeserver, { displayName, startClient: true, autoAcceptInvites });
await bot.prepareClient();
await app.client.inviteUser(id, bot.credentials?.userId);
newBots.push(bot);
}
await setupRoomWithMembers(app, page, homeserver, ROOM_NAME, names);
});
test("Renders correctly", { tag: "@screenshot" }, async ({ page, app }) => {
@@ -45,4 +62,37 @@ test.describe("Memberlist", () => {
await expect(memberlist.getByText("Invited")).toHaveCount(1);
await expect(page.locator(".mx_MemberListView")).toMatchScreenshot("with-four-members.png");
});
test("should handle scroll and click to view member profile", async ({ page, app, homeserver }) => {
// Create a room with many members to enable scrolling
const memberNames = Array.from({ length: 15 }, (_, i) => `Member${i.toString()}`);
await setupRoomWithMembers(app, page, homeserver, "Large Room", memberNames);
// Navigate to the room and open member list
await app.viewRoomByName("Large Room");
const memberlist = await app.toggleMemberlistPanel();
// Get the scrollable container
const memberListContainer = memberlist.locator(".mx_AutoHideScrollbar");
// Scroll down to the bottom of the member list
await app.scrollListToBottom(memberListContainer);
// Wait for the target member to be visible after scrolling
const targetName = "Member14";
const targetMember = memberlist.locator(".mx_MemberTileView_name").filter({ hasText: targetName });
await targetMember.waitFor({ state: "visible" });
// Verify Alice is not visible at this point
await expect(memberlist.locator(".mx_MemberTileView_name").filter({ hasText: "Alice" })).toHaveCount(0);
// Click on a member near the bottom of the list
await expect(targetMember).toBeVisible();
await targetMember.click();
// Verify that the user info screen is shown and hasn't scrolled back to top
await expect(page.locator(".mx_UserInfo")).toBeVisible();
await expect(page.locator(".mx_UserInfo_profile").getByText(targetName)).toBeVisible();
});
});
@@ -35,7 +35,7 @@ test.describe("Share dialog", () => {
const rightPanel = await app.toggleRoomInfoPanel();
await rightPanel.getByRole("menuitem", { name: "People" }).click();
await rightPanel.getByRole("button", { name: `${user.userId} (power 100)` }).click();
await rightPanel.getByRole("option", { name: user.displayName }).click();
await rightPanel.getByRole("button", { name: "Share profile" }).click();
const dialog = page.getByRole("dialog", { name: "Share User" });
+18 -11
View File
@@ -23,7 +23,7 @@ async function openSpaceContextMenu(page: Page, app: ElementAppPage, spaceName:
return page.locator(".mx_SpacePanel_contextMenu");
}
function spaceCreateOptions(spaceName: string, roomIds: string[] = []): ICreateRoomOpts {
function spaceCreateOptions(serverName: string, spaceName: string, roomIds: string[] = []): ICreateRoomOpts {
return {
creation_content: {
type: "m.space",
@@ -35,17 +35,21 @@ function spaceCreateOptions(spaceName: string, roomIds: string[] = []): ICreateR
name: spaceName,
},
},
...roomIds.map((r) => spaceChildInitialState(r)),
...roomIds.map((r) => spaceChildInitialState(serverName, r)),
],
};
}
function spaceChildInitialState(roomId: string, order?: string): ICreateRoomOpts["initial_state"]["0"] {
function spaceChildInitialState(
serverName: string,
roomId: string,
order?: string,
): ICreateRoomOpts["initial_state"]["0"] {
return {
type: "m.space.child",
state_key: roomId,
content: {
via: [roomId.split(":")[1]],
via: [serverName],
order,
},
};
@@ -240,7 +244,7 @@ test.describe("Spaces", () => {
});
await expect(await app.getSpacePanelButton("My Space")).toBeVisible();
const roomId = await bot.createRoom(spaceCreateOptions("Space Space"));
const roomId = await bot.createRoom(spaceCreateOptions(user.homeServer, "Space Space"));
await bot.inviteUser(roomId, user.userId);
// Assert that `Space Space` is above `My Space` due to it being an invite
@@ -260,7 +264,10 @@ test.describe("Spaces", () => {
const spaceName = "Spacey Mc. Space Space";
await app.client.createSpace({
name: spaceName,
initial_state: [spaceChildInitialState(roomId1), spaceChildInitialState(roomId2)],
initial_state: [
spaceChildInitialState(user.homeServer, roomId1),
spaceChildInitialState(user.homeServer, roomId2),
],
});
await app.viewSpaceHomeByName(spaceName);
@@ -287,7 +294,7 @@ test.describe("Spaces", () => {
});
await app.client.createSpace({
name: "Root Space",
initial_state: [spaceChildInitialState(childSpaceId)],
initial_state: [spaceChildInitialState(user.homeServer, childSpaceId)],
});
// Find collapsed Space panel
@@ -323,7 +330,7 @@ test.describe("Spaces", () => {
name: "Test Room",
topic: "This is a topic https://github.com/matrix-org/matrix-react-sdk/pull/10060 with a link",
});
const spaceId = await bot.createRoom(spaceCreateOptions("Test Space", [roomId]));
const spaceId = await bot.createRoom(spaceCreateOptions(user.homeServer, "Test Space", [roomId]));
await bot.inviteUser(spaceId, user.userId);
await expect(await app.getSpacePanelButton("Test Space")).toBeVisible();
@@ -361,9 +368,9 @@ test.describe("Spaces", () => {
await app.client.createSpace({
name: "Root Space",
initial_state: [
spaceChildInitialState(childSpaceId1, "a"),
spaceChildInitialState(childSpaceId2, "b"),
spaceChildInitialState(childSpaceId3, "c"),
spaceChildInitialState(user.homeServer, childSpaceId1, "a"),
spaceChildInitialState(user.homeServer, childSpaceId2, "b"),
spaceChildInitialState(user.homeServer, childSpaceId3, "c"),
],
});
await app.viewSpaceByName("Root Space");
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

+1 -1
View File
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
const TAG = "develop@sha256:6e8c604472c7cf13c30f06791c4b3368cf433956b3973e97cd978ec680f4b62f";
const TAG = "develop@sha256:2f6fff14ff23f356705abdbf2ed62c3dd6ca2103cef4ae813714ddc199bbd76a";
/**
* SynapseContainer which freezes the docker digest to stabilise tests,
-2
View File
@@ -53,8 +53,6 @@
@import "./components/views/settings/shared/_SettingsSubsectionHeading.pcss";
@import "./components/views/spaces/_QuickThemeSwitcher.pcss";
@import "./components/views/typography/_Caption.pcss";
@import "./components/views/utils/_Box.pcss";
@import "./components/views/utils/_Flex.pcss";
@import "./compound/_Icon.pcss";
@import "./compound/_SuccessDialog.pcss";
@import "./structures/_AutoHideScrollbar.pcss";
+1 -1
View File
@@ -50,7 +50,7 @@ Please see LICENSE files in the repository root for full details.
color: var(--cpd-color-text-secondary);
}
.mx_Flex {
.mx_ErrorView_flexContainer {
margin: 0 auto;
max-width: max-content;
flex-wrap: wrap;
@@ -32,7 +32,7 @@ Please see LICENSE files in the repository root for full details.
padding: 0 12px;
color: var(--cpd-color-text-secondary);
.mx_Box {
.mx_RoomSummaryCard_topic_box {
width: 100%;
}
-5
View File
@@ -108,11 +108,6 @@ Please see LICENSE files in the repository root for full details.
margin: 0;
font-size: $font-20px;
line-height: $font-25px;
/* E2E icon wrapper */
.mx_Flex > span {
display: inline-block;
}
}
.mx_UserInfo_profile_name {
@@ -12,10 +12,6 @@
border-bottom: var(--cpd-border-width-1) solid var(--cpd-color-bg-subtle-primary);
padding: 0 var(--cpd-space-3x);
svg {
fill: var(--cpd-color-icon-secondary);
}
.mx_RoomListSearch_search {
/* The search button should take all the remaining space */
flex: 1;
@@ -23,6 +19,10 @@
color: var(--cpd-color-text-secondary);
min-width: 0;
svg {
fill: var(--cpd-color-icon-secondary);
}
span {
flex: 1;
@@ -42,10 +42,4 @@
}
}
}
.mx_RoomListSearch_button:hover {
svg {
fill: var(--cpd-color-icon-primary);
}
}
}
+4
View File
@@ -621,6 +621,9 @@ export async function restoreSessionFromStorage(opts?: { ignoreGuest?: boolean }
await getStoredSessionVars();
if (hasAccessToken && !accessToken) {
logger.warn(
"restoreSessionFromStorage: storage indicates we should have an access token, but we do not. Displaying StorageEvictedDialog",
);
await abortLogin();
}
@@ -823,6 +826,7 @@ async function doSetLoggedIn(
// crypto store, we'll be generally confused when handling encrypted data.
// Show a modal recommending a full reset of storage.
if (results.dataInLocalStorage && results.cryptoInited && !results.dataInCryptoStore) {
logger.warn("doSetLoggedIn: StorageManager consistency check failed; displaying StorageEvictedDialog.");
await abortLogin();
}
+1 -1
View File
@@ -148,7 +148,7 @@ export const Commands = [
command: "upgraderoom",
args: "<new_version>",
description: _td("slash_command|upgraderoom"),
isEnabled: (cli) => !isCurrentLocalRoom(cli) && SettingsStore.getValue("developerMode"),
isEnabled: (cli) => !isCurrentLocalRoom(cli),
runFn: function (cli, roomId, threadId, args) {
if (args) {
const room = cli.getRoom(roomId);
+5 -2
View File
@@ -572,8 +572,11 @@ function textForPinnedEvent(event: MatrixEvent, client: MatrixClient, allowJSX:
const senderName = getSenderName(event);
const roomId = event.getRoomId()!;
const pinned = event.getContent<{ pinned: string[] }>().pinned ?? [];
const previouslyPinned: string[] = event.getPrevContent().pinned ?? [];
const content = event.getContent<{ pinned: string[] }>();
const prevContent = event.getPrevContent();
const pinned = Array.isArray(content.pinned) ? content.pinned : [];
const previouslyPinned: string[] = Array.isArray(prevContent.pinned) ? prevContent.pinned : [];
const newlyPinned = pinned.filter((item) => previouslyPinned.indexOf(item) < 0);
const newlyUnpinned = previouslyPinned.filter((item) => pinned.indexOf(item) < 0);
+6 -1
View File
@@ -39,7 +39,12 @@ export function eventTriggersUnreadCount(client: MatrixClient, ev: MatrixEvent):
}
if (ev.isRedacted()) return false;
return haveRendererForEvent(ev, client, false /* hidden messages should never trigger unread counts anyways */);
try {
return haveRendererForEvent(ev, client, false /* hidden messages should never trigger unread counts anyways */);
} catch (e) {
console.warn("Error determining if event should trigger unread count", e);
return false; // If we can't determine if the event should trigger an unread count, assume it does not.
}
}
export function doesRoomHaveUnreadMessages(room: Room, includeThreads: boolean): boolean {
+1 -1
View File
@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
*/
// Import i18n.tsx instead of languageHandler to avoid circular deps
import { _td, type TranslationKey } from "../shared-components/i18n";
import { _td, type TranslationKey } from "../shared-components/utils/i18n";
import { IS_MAC, IS_ELECTRON, Key } from "../Keyboard";
import { type IBaseSetting } from "../settings/Settings";
import { type KeyCombo } from "../KeyBindingsManager";
@@ -10,7 +10,7 @@ import { Text, Heading, Button, Separator } from "@vector-im/compound-web";
import PopOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/pop-out";
import SdkConfig from "../../SdkConfig";
import { Flex } from "../../components/utils/Flex";
import { Flex } from "../../shared-components/utils/Flex";
import { _t } from "../../languageHandler";
import { Icon as AppleIcon } from "../../../res/themes/element/img/compound/apple.svg";
import { Icon as MicrosoftIcon } from "../../../res/themes/element/img/compound/microsoft.svg";
@@ -58,7 +58,7 @@ const MobileAppLinks: React.FC<{
googlePlayUrl?: string;
fdroidUrl?: string;
}> = ({ appleAppStoreUrl, googlePlayUrl, fdroidUrl }) => (
<Flex gap="var(--cpd-space-6x)">
<Flex gap="var(--cpd-space-6x)" className="mx_ErrorView_flexContainer">
{appleAppStoreUrl && (
<a href={appleAppStoreUrl} target="_blank" rel="noreferrer noopener">
<img height="64" src="themes/element/img/download/apple.svg" alt="Apple App Store" />
@@ -84,7 +84,7 @@ const DesktopAppLinks: React.FC<{
linuxUrl?: string;
}> = ({ macOsUrl, win64Url, win64ArmUrl, linuxUrl }) => {
return (
<Flex gap="var(--cpd-space-4x)">
<Flex gap="var(--cpd-space-4x)" className="mx_ErrorView_flexContainer">
{macOsUrl && (
<Button as="a" href={macOsUrl} kind="secondary" Icon={AppleIcon}>
{_t("incompatible_browser|macos")}
@@ -193,7 +193,7 @@ export const UnsupportedBrowserView: React.FC<{
)}
</Text>
<Flex gap="var(--cpd-space-4x)" className="mx_ErrorView_buttons">
<Flex gap="var(--cpd-space-4x)" className="mx_ErrorView_flexContainer mx_ErrorView_buttons">
<Button Icon={PopOutIcon} kind="secondary" size="sm">
{_t("incompatible_browser|learn_more")}
</Button>
+16 -22
View File
@@ -141,6 +141,7 @@ import { type OpenForwardDialogPayload } from "../../dispatcher/payloads/OpenFor
import { ShareFormat, type SharePayload } from "../../dispatcher/payloads/SharePayload";
import Markdown from "../../Markdown";
import { sanitizeHtmlParams } from "../../Linkify";
import { isOnlyAdmin } from "../../utils/membership";
// legacy export
export { default as Views } from "../../Views";
@@ -1255,29 +1256,22 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const client = MatrixClientPeg.get();
if (client && roomToLeave) {
const plEvent = roomToLeave.currentState.getStateEvents(EventType.RoomPowerLevels, "");
const plContent = plEvent ? plEvent.getContent() : {};
const userLevels = plContent.users || {};
const currentUserLevel = userLevels[client.getUserId()!];
const userLevelValues = Object.values(userLevels);
if (userLevelValues.every((x) => typeof x === "number")) {
// If the user is the only user with highest power level
if (isOnlyAdmin(roomToLeave)) {
const userLevelValues = roomToLeave.getJoinedMembers().map((m) => m.powerLevel);
const maxUserLevel = Math.max(...(userLevelValues as number[]));
// If the user is the only user with highest power level
if (
maxUserLevel === currentUserLevel &&
userLevelValues.lastIndexOf(maxUserLevel) == userLevelValues.indexOf(maxUserLevel)
) {
const warning =
maxUserLevel >= 100
? _t("leave_room_dialog|room_leave_admin_warning")
: _t("leave_room_dialog|room_leave_mod_warning");
warnings.push(
<strong className="warning" key="last_admin_warning">
{" " /* Whitespace, otherwise the sentences get smashed together */}
{warning}
</strong>,
);
}
const warning =
maxUserLevel >= 100
? _t("leave_room_dialog|room_leave_admin_warning")
: _t("leave_room_dialog|room_leave_mod_warning");
warnings.push(
<strong className="warning" key="last_admin_warning">
{" " /* Whitespace, otherwise the sentences get smashed together */}
{warning}
</strong>,
);
}
}
+291
View File
@@ -0,0 +1,291 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React, { useRef, type JSX, useCallback, useEffect, useState } from "react";
import { type VirtuosoHandle, type ListRange, Virtuoso, type VirtuosoProps } from "react-virtuoso";
/**
* Context object passed to each list item containing the currently focused key
* and any additional context data from the parent component.
*/
export type ListContext<Context> = {
/** The key of item that should have tabIndex == 0 */
tabIndexKey?: string;
/** Whether an item in the list is currently focused */
focused: boolean;
/** Additional context data passed from the parent component */
context: Context;
};
export interface IListViewProps<Item, Context>
extends Omit<VirtuosoProps<Item, ListContext<Context>>, "data" | "itemContent" | "context"> {
/**
* The array of items to display in the virtualized list.
* Each item will be passed to getItemComponent for rendering.
*/
items: Item[];
/**
* Callback function called when an item is selected (via Enter/Space key).
* @param item - The selected item from the items array
*/
onSelectItem: (item: Item) => void;
/**
* Function that renders each list item as a JSX element.
* @param index - The index of the item in the list
* @param item - The data item to render
* @param context - The context object containing the focused key and any additional data
* @returns JSX element representing the rendered item
*/
getItemComponent: (
index: number,
item: Item,
context: ListContext<Context>,
onFocus: (e: React.FocusEvent) => void,
) => JSX.Element;
/**
* Optional additional context data to pass to each rendered item.
* This will be available in the ListContext passed to getItemComponent.
*/
context?: Context;
/**
* Function to determine if an item can receive focus during keyboard navigation.
* @param item - The item to check for focusability
* @returns true if the item can be focused, false otherwise
*/
isItemFocusable: (item: Item) => boolean;
/**
* Function to get the key to use for focusing an item.
* @param item - The item to get the key for
* @return The key to use for focusing the item
*/
getItemKey: (item: Item) => string;
}
/**
* A generic virtualized list component built on top of react-virtuoso.
* Provides keyboard navigation and virtualized rendering for performance with large lists.
*
* @template Item - The type of data items in the list
* @template Context - The type of additional context data passed to items
*/
export function ListView<Item, Context = any>(props: IListViewProps<Item, Context>): React.ReactElement {
// Extract our custom props to avoid conflicts with Virtuoso props
const { items, onSelectItem, getItemComponent, isItemFocusable, getItemKey, context, ...virtuosoProps } = props;
/** Reference to the Virtuoso component for programmatic scrolling */
const virtuosoHandleRef = useRef<VirtuosoHandle>(null);
/** Reference to the DOM element containing the virtualized list */
const virtuosoDomRef = useRef<HTMLElement | Window>(null);
/** Key of the item that should have tabIndex == 0 */
const [tabIndexKey, setTabIndexKey] = useState<string | undefined>(
props.items[0] ? getItemKey(props.items[0]) : undefined,
);
/** Range of currently visible items in the viewport */
const [visibleRange, setVisibleRange] = useState<ListRange | undefined>(undefined);
/** Map from item keys to their indices in the items array */
const [keyToIndexMap, setKeyToIndexMap] = useState<Map<string, number>>(new Map());
/** Whether the list is currently scrolling to an item */
const isScrollingToItem = useRef<boolean>(false);
/** Whether the list is currently focused */
const [isFocused, setIsFocused] = useState<boolean>(false);
// Update the key-to-index mapping whenever items change
useEffect(() => {
const newKeyToIndexMap = new Map<string, number>();
items.forEach((item, index) => {
const key = getItemKey(item);
newKeyToIndexMap.set(key, index);
});
setKeyToIndexMap(newKeyToIndexMap);
}, [items, getItemKey]);
// Ensure the tabIndexKey is set if there is none already or if the existing key is no longer displayed
useEffect(() => {
if (items.length && (!tabIndexKey || keyToIndexMap.get(tabIndexKey) === undefined)) {
setTabIndexKey(getItemKey(items[0]));
}
}, [items, getItemKey, tabIndexKey, keyToIndexMap]);
/**
* Scrolls to a specific item index and sets it as focused.
* Uses Virtuoso's scrollIntoView method for smooth scrolling.
*/
const scrollToIndex = useCallback(
(index: number, align?: "center" | "end" | "start"): void => {
// Ensure index is within bounds
const clampedIndex = Math.max(0, Math.min(index, items.length - 1));
if (isScrollingToItem.current) {
// If already scrolling to an item drop this request. Adding further requests
// causes the event to bubble up and be handled by other components(unintentional timeline scrolling was observed).
return;
}
if (items[clampedIndex]) {
const key = getItemKey(items[clampedIndex]);
setTabIndexKey(key);
isScrollingToItem.current = true;
virtuosoHandleRef?.current?.scrollIntoView({
index: clampedIndex,
align: align,
behavior: "auto",
done: () => {
isScrollingToItem.current = false;
},
});
}
},
[items, getItemKey],
);
/**
* Scrolls to an item, skipping over non-focusable items if necessary.
* This is used for keyboard navigation to ensure focus lands on valid items.
*/
const scrollToItem = useCallback(
(index: number, isDirectionDown: boolean, align?: "center" | "end" | "start"): void => {
const totalRows = items.length;
let nextIndex: number | undefined;
for (let i = index; isDirectionDown ? i < totalRows : i >= 0; i = i + (isDirectionDown ? 1 : -1)) {
if (isItemFocusable(items[i])) {
nextIndex = i;
break;
}
}
if (nextIndex === undefined) {
return;
}
scrollToIndex(nextIndex, align);
},
[scrollToIndex, items, isItemFocusable],
);
/**
* Handles keyboard navigation for the list.
* Supports Arrow keys, Home, End, Page Up/Down, Enter, and Space.
*/
const keyDownCallback = useCallback(
(e: React.KeyboardEvent) => {
if (!e) return; // Guard against null/undefined events
const currentIndex = tabIndexKey ? keyToIndexMap.get(tabIndexKey) : undefined;
let handled = false;
if (e.code === "ArrowUp" && currentIndex !== undefined) {
scrollToItem(currentIndex - 1, false);
handled = true;
} else if (e.code === "ArrowDown" && currentIndex !== undefined) {
scrollToItem(currentIndex + 1, true);
handled = true;
} else if ((e.code === "Enter" || e.code === "Space") && currentIndex !== undefined) {
const item = items[currentIndex];
onSelectItem(item);
handled = true;
} else if (e.code === "Home") {
scrollToIndex(0);
handled = true;
} else if (e.code === "End") {
scrollToIndex(items.length - 1);
handled = true;
} else if (e.code === "PageDown" && 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 === "PageUp" && visibleRange && currentIndex !== undefined) {
const numberDisplayed = visibleRange.endIndex - visibleRange.startIndex;
scrollToItem(Math.max(currentIndex - numberDisplayed, 0), false, `start`);
handled = true;
}
if (handled) {
e.stopPropagation();
e.preventDefault();
}
},
[scrollToIndex, scrollToItem, tabIndexKey, keyToIndexMap, visibleRange, items, onSelectItem],
);
/**
* Callback ref for the Virtuoso scroller element.
* Stores the reference for use in focus management.
*/
const scrollerRef = useCallback((element: HTMLElement | Window | null) => {
virtuosoDomRef.current = element;
}, []);
const getItemComponentInternal = useCallback(
(index: number, item: Item, context: ListContext<Context>): JSX.Element => {
const onFocus = (e: React.FocusEvent): void => {
// If one of the item components has been focused directly, set the focused and tabIndex state
// and stop propagation so the ListViews onFocus doesn't also handle it.
const key = getItemKey(item);
setIsFocused(true);
setTabIndexKey(key);
e.stopPropagation();
};
return getItemComponent(index, item, context, onFocus);
},
[getItemComponent, getItemKey],
);
/**
* Handles focus events on the list.
* Sets the focused state and scrolls to the focused item if it is not currently visible.
*/
const onFocus = useCallback(
(e?: React.FocusEvent): void => {
if (e?.currentTarget !== virtuosoDomRef.current || typeof tabIndexKey !== "string") {
return;
}
setIsFocused(true);
const index = keyToIndexMap.get(tabIndexKey);
if (
index !== undefined &&
visibleRange &&
(index < visibleRange.startIndex || index > visibleRange.endIndex)
) {
scrollToIndex(index);
}
e?.stopPropagation();
e?.preventDefault();
},
[keyToIndexMap, visibleRange, scrollToIndex, tabIndexKey],
);
const onBlur = useCallback((): void => {
setIsFocused(false);
}, []);
const listContext: ListContext<Context> = {
tabIndexKey: tabIndexKey,
focused: isFocused,
context: props.context || ({} as Context),
};
return (
<Virtuoso
tabIndex={props.tabIndex || undefined} // We don't need to focus the container, so leave it undefined by default
scrollerRef={scrollerRef}
ref={virtuosoHandleRef}
onKeyDown={keyDownCallback}
context={listContext}
rangeChanged={setVisibleRange}
// virtuoso errors internally if you pass undefined.
overscan={props.overscan || 0}
data={props.items}
onFocus={onFocus}
onBlur={onBlur}
itemContent={getItemComponentInternal}
{...virtuosoProps}
/>
);
}
@@ -38,6 +38,8 @@ import { isValid3pidInvite } from "../../../RoomInvite";
import { type ThreePIDInvite } from "../../../models/rooms/ThreePIDInvite";
import { type XOR } from "../../../@types/common";
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
import { Action } from "../../../dispatcher/actions";
import dis from "../../../dispatcher/dispatcher";
type Member = XOR<{ member: RoomMember }, { threePidInvite: ThreePIDInvite }>;
@@ -111,6 +113,7 @@ export interface MemberListViewState {
shouldShowSearch: boolean;
isLoading: boolean;
canInvite: boolean;
onClickMember: (member: RoomMember | ThreePIDInvite) => void;
onInviteButtonClick: (ev: ButtonEvent) => void;
}
export function useMemberListViewModel(roomId: string): MemberListViewState {
@@ -133,6 +136,14 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
*/
const [memberCount, setMemberCount] = useState(0);
const onClickMember = (member: RoomMember | ThreePIDInvite): void => {
dis.dispatch({
action: Action.ViewUser,
member: member,
push: true,
});
};
const loadMembers = useMemo(
() =>
throttle(
@@ -267,6 +278,7 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
isPresenceEnabled,
isLoading,
onInviteButtonClick,
onClickMember,
shouldShowSearch: totalMemberCount >= 20,
canInvite,
};
@@ -5,7 +5,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 { useEffect, useMemo, useState } from "react";
import { useEffect, useState } from "react";
import { RoomStateEvent, type MatrixEvent, EventType } from "matrix-js-sdk/src/matrix";
import { type UserVerificationStatus, CryptoEvent } from "matrix-js-sdk/src/crypto-api";
@@ -16,7 +16,6 @@ import { asyncSome } from "../../../../utils/arrays";
import { getUserDeviceIds } from "../../../../utils/crypto/deviceInfo";
import { type RoomMember } from "../../../../models/rooms/RoomMember";
import { _t, _td, type TranslationKey } from "../../../../languageHandler";
import UserIdentifierCustomisations from "../../../../customisations/UserIdentifier";
import { E2EStatus } from "../../../../utils/ShieldUtils";
interface MemberTileViewModelProps {
@@ -28,16 +27,17 @@ export interface MemberTileViewState extends MemberTileViewModelProps {
e2eStatus?: E2EStatus;
name: string;
onClick: () => void;
title?: string;
userLabel?: string;
}
export enum PowerStatus {
Creator = "creator",
Admin = "admin",
Moderator = "moderator",
}
const PowerLabel: Record<PowerStatus, TranslationKey> = {
[PowerStatus.Creator]: _td("power_level|creator"),
[PowerStatus.Admin]: _td("power_level|admin"),
[PowerStatus.Moderator]: _td("power_level|moderator"),
};
@@ -117,6 +117,7 @@ export function useMemberTileViewModel(props: MemberTileViewModelProps): MemberT
const name = props.member.name;
const powerStatusMap = new Map([
[Infinity, PowerStatus.Creator],
[100, PowerStatus.Admin],
[50, PowerStatus.Moderator],
]);
@@ -130,15 +131,6 @@ export function useMemberTileViewModel(props: MemberTileViewModelProps): MemberT
}
}
const title = useMemo(() => {
return _t("member_list|power_label", {
userName: UserIdentifierCustomisations.getDisplayUserIdentifier(member.userId, {
roomId: member.roomId,
}),
powerLevelNumber: member.powerLevel,
}).trim();
}, [member.powerLevel, member.roomId, member.userId]);
let userLabel;
const powerStatus = powerStatusMap.get(powerLevel);
if (powerStatus) {
@@ -149,7 +141,6 @@ export function useMemberTileViewModel(props: MemberTileViewModelProps): MemberT
}
return {
title,
member,
name,
onClick,
@@ -290,7 +290,7 @@ export default class LoginWithQRFlow extends React.Component<Props> {
data-testid="back-button"
className="mx_LoginWithQR_BackButton"
onClick={this.handleClick(Click.Back)}
title="Back"
title={_t("action|back")}
>
<ChevronLeftIcon />
</AccessibleButton>
+48 -19
View File
@@ -14,6 +14,7 @@ import OnlineOrUnavailableIcon from "@vector-im/compound-design-tokens/assets/we
import OfflineIcon from "@vector-im/compound-design-tokens/assets/web/icons/presence-outline-8x8";
import BusyIcon from "@vector-im/compound-design-tokens/assets/web/icons/presence-strikethrough-8x8";
import classNames from "classnames";
import { Tooltip } from "@vector-im/compound-web";
import RoomAvatar from "./RoomAvatar";
import { AvatarBadgeDecoration, useRoomAvatarViewModel } from "../../viewmodels/avatars/RoomAvatarViewModel";
@@ -37,6 +38,7 @@ export function RoomAvatarView({ room }: RoomAvatarViewProps): JSX.Element {
if (!vm.badgeDecoration) return <RoomAvatar size="32px" room={room} />;
const icon = getAvatarDecoration(vm.badgeDecoration, vm.presence);
const label = getDecorationLabel(vm.badgeDecoration, vm.presence);
// Presence indicator and video/public icons don't have the same size
// We use different masks
@@ -48,22 +50,15 @@ export function RoomAvatarView({ room }: RoomAvatarViewProps): JSX.Element {
return (
<div className="mx_RoomAvatarView">
<RoomAvatar className={classNames("mx_RoomAvatarView_RoomAvatar", maskClass)} size="32px" room={room} />
{icon}
{label ? <Tooltip label={label}>{icon}</Tooltip> : icon}
</div>
);
}
type PresenceDecorationProps = {
/**
* The presence of the user in the DM room.
*/
presence: NonNullable<Presence>;
};
/**
* Component to display the presence of a user in a DM room.
* Get the decoration for the avatar based on the presence.
*/
function PresenceDecoration({ presence }: PresenceDecorationProps): JSX.Element {
function getPresenceDecoration(presence: Presence): JSX.Element {
switch (presence) {
case Presence.Online:
return (
@@ -72,7 +67,7 @@ function PresenceDecoration({ presence }: PresenceDecorationProps): JSX.Element
height="8px"
className="mx_RoomAvatarView_PresenceDecoration"
color="var(--cpd-color-icon-accent-primary)"
aria-label={_t("presence|online")}
aria-label={getPresenceLabel(presence)}
/>
);
case Presence.Away:
@@ -82,7 +77,7 @@ function PresenceDecoration({ presence }: PresenceDecorationProps): JSX.Element
height="8px"
className="mx_RoomAvatarView_PresenceDecoration"
color="var(--cpd-color-icon-quaternary)"
aria-label={_t("presence|away")}
aria-label={getPresenceLabel(presence)}
/>
);
case Presence.Offline:
@@ -92,7 +87,7 @@ function PresenceDecoration({ presence }: PresenceDecorationProps): JSX.Element
height="8px"
className="mx_RoomAvatarView_PresenceDecoration"
color="var(--cpd-color-icon-tertiary)"
aria-label={_t("presence|offline")}
aria-label={getPresenceLabel(presence)}
/>
);
case Presence.Busy:
@@ -102,7 +97,7 @@ function PresenceDecoration({ presence }: PresenceDecorationProps): JSX.Element
height="8px"
className="mx_RoomAvatarView_PresenceDecoration"
color="var(--cpd-color-icon-tertiary)"
aria-label={_t("presence|busy")}
aria-label={getPresenceLabel(presence)}
/>
);
}
@@ -116,7 +111,7 @@ function getAvatarDecoration(decoration: AvatarBadgeDecoration, presence: Presen
height="16px"
className="mx_RoomAvatarView_icon"
color="var(--cpd-color-icon-tertiary)"
aria-label={_t("room|room_is_low_priority")}
aria-label={getDecorationLabel(decoration, presence)}
/>
);
} else if (decoration === AvatarBadgeDecoration.VideoRoom) {
@@ -126,7 +121,7 @@ function getAvatarDecoration(decoration: AvatarBadgeDecoration, presence: Presen
height="16px"
className="mx_RoomAvatarView_icon"
color="var(--cpd-color-icon-tertiary)"
aria-label={_t("room|video_room")}
aria-label={getDecorationLabel(decoration, presence)}
/>
);
} else if (decoration === AvatarBadgeDecoration.PublicRoom) {
@@ -135,11 +130,45 @@ function getAvatarDecoration(decoration: AvatarBadgeDecoration, presence: Presen
width="16px"
height="16px"
className="mx_RoomAvatarView_icon"
color="var(--cpd-color-icon-tertiary)"
aria-label={_t("room|header|room_is_public")}
color="var(--cpd-color-icon-info-primary)"
aria-label={getDecorationLabel(decoration, presence)}
/>
);
} else if (decoration === AvatarBadgeDecoration.Presence) {
return <PresenceDecoration presence={presence!} />;
return getPresenceDecoration(presence!);
}
}
/**
* Get the label for the avatar decoration.
* This is used for the tooltip and a11y label.
*/
function getDecorationLabel(decoration: AvatarBadgeDecoration, presence: Presence | null): string | undefined {
switch (decoration) {
case AvatarBadgeDecoration.LowPriority:
return _t("room|room_is_low_priority");
case AvatarBadgeDecoration.VideoRoom:
return _t("room|video_room");
case AvatarBadgeDecoration.PublicRoom:
return _t("room|header|room_is_public");
case AvatarBadgeDecoration.Presence:
return getPresenceLabel(presence!);
}
}
/**
* Get the label for the presence.
* This is used for the tooltip and a11y label.
*/
function getPresenceLabel(presence: Presence): string {
switch (presence) {
case Presence.Online:
return _t("presence|online");
case Presence.Away:
return _t("presence|away");
case Presence.Offline:
return _t("presence|offline");
case Presence.Busy:
return _t("presence|busy");
}
}
@@ -358,20 +358,22 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
}
this.profilesStore = SdkContextClass.instance.userProfilesStore;
const cli = MatrixClientPeg.safeGet();
const excludedIds = new Set([MatrixClientPeg.safeGet().getUserId()!]);
const excludedIds = new Set([cli.getSafeUserId()]);
if (isRoomInvite(props)) {
const room = MatrixClientPeg.safeGet().getRoom(props.roomId);
const isFederated = room?.currentState.getStateEvents(EventType.RoomCreate, "")?.getContent()["m.federate"];
const room = cli.getRoom(props.roomId);
if (!room) throw new Error("Room ID given to InviteDialog does not look like a room");
const isFederated = room?.currentState.getStateEvents(EventType.RoomCreate, "")?.getContent()["m.federate"];
room.getMembersWithMembership(KnownMembership.Invite).forEach((m) => excludedIds.add(m.userId));
room.getMembersWithMembership(KnownMembership.Join).forEach((m) => excludedIds.add(m.userId));
// add banned users, so we don't try to invite them
room.getMembersWithMembership(KnownMembership.Ban).forEach((m) => excludedIds.add(m.userId));
if (isFederated === false) {
const ourHomeserver = cli.getDomain();
if (isFederated === false && ourHomeserver) {
// If this room isn't federated, we must be on the same server.
// exclude users from external servers
const homeserver = props.roomId.split(":")[1];
this.excludeExternals(homeserver, excludedIds);
this.excludeExternals(ourHomeserver, excludedIds);
}
}
@@ -385,7 +387,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
numSuggestionsShown: INITIAL_ROOMS_SHOWN,
serverResultsMixin: [],
threepidResultsMixin: [],
canUseIdentityServer: !!MatrixClientPeg.safeGet().getIdentityServerUrl(),
canUseIdentityServer: !!cli.getIdentityServerUrl(),
tryingIdentityServer: false,
consultFirst: false,
dialPadValue: "",
@@ -15,23 +15,13 @@ import BaseDialog from "../dialogs/BaseDialog";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import SpaceChildrenPicker from "../spaces/SpaceChildrenPicker";
import { filterBoolean } from "../../../utils/arrays";
import { isOnlyAdmin } from "../../../utils/membership";
interface IProps {
space: Room;
onFinished(leave: boolean, rooms?: Room[]): void;
}
const isOnlyAdmin = (room: Room): boolean => {
const userId = room.client.getSafeUserId();
if (room.getMember(userId)?.powerLevelNorm !== 100) {
return false; // user is not an admin
}
return room.getJoinedMembers().every((member) => {
// return true if every other member has a lower power level (we are highest)
return member.userId === userId || member.powerLevelNorm < 100;
});
};
const LeaveSpaceDialog: React.FC<IProps> = ({ space, onFinished }) => {
const spaceChildren = useMemo(() => {
const roomSet = new Set(SpaceStore.instance.getSpaceFilteredRoomIds(space.roomId));
@@ -19,6 +19,7 @@ import FilteredList from "./FilteredList";
import Spinner from "../../elements/Spinner";
import SyntaxHighlight from "../../elements/SyntaxHighlight";
import { useAsyncMemo } from "../../../../hooks/useAsyncMemo";
import LabelledToggleSwitch from "../../elements/LabelledToggleSwitch";
export const StateEventEditor: React.FC<IEditorProps> = ({ mxEvent, onBack }) => {
const context = useContext(DevtoolsContext);
@@ -114,6 +115,7 @@ const RoomStateExplorerEventType: React.FC<IEventTypeProps> = ({ eventType, onBa
const [query, setQuery] = useState("");
const [event, setEvent] = useState<MatrixEvent | null>(null);
const [history, setHistory] = useState(false);
const [showEmptyState, setShowEmptyState] = useState(true);
const events = context.room.currentState.events.get(eventType)!;
@@ -149,10 +151,17 @@ const RoomStateExplorerEventType: React.FC<IEventTypeProps> = ({ eventType, onBa
return (
<BaseTool onBack={onBack}>
<FilteredList query={query} onChange={setQuery}>
{Array.from(events.entries()).map(([stateKey, ev]) => (
<StateEventButton key={stateKey} label={stateKey} onClick={() => setEvent(ev)} />
))}
{Array.from(events.entries())
.filter(([_, ev]) => showEmptyState || Object.keys(ev.getContent()).length > 0)
.map(([stateKey, ev]) => (
<StateEventButton key={stateKey} label={stateKey} onClick={() => setEvent(ev)} />
))}
</FilteredList>
<LabelledToggleSwitch
label={_t("devtools|show_empty_content_events")}
onChange={setShowEmptyState}
value={showEmptyState}
/>
</BaseTool>
);
};
@@ -14,7 +14,7 @@ import React, { type ChangeEvent, type FormEvent } from "react";
import { type SecretStorage } from "matrix-js-sdk/src/matrix";
import Field from "../../elements/Field";
import { Flex } from "../../../utils/Flex";
import { Flex } from "../../../../shared-components/utils/Flex";
import { _t } from "../../../../languageHandler";
import { EncryptionCard } from "../../settings/encryption/EncryptionCard";
import { EncryptionCardButtons } from "../../settings/encryption/EncryptionCardButtons";
@@ -26,6 +26,12 @@ interface IProps {
mediaEventHelperGet: () => MediaEventHelper | undefined;
}
function useButtonTitle(loading: boolean, isEncrypted: boolean): string {
if (!loading) return _t("action|download");
return isEncrypted ? _t("timeline|download_action_decrypting") : _t("timeline|download_action_downloading");
}
export default function DownloadActionButton({ mxEvent, mediaEventHelperGet }: IProps): ReactElement | null {
const mediaEventHelper = useMemo(() => mediaEventHelperGet(), [mediaEventHelperGet]);
const downloadUrl = mediaEventHelper?.media.srcHttp ?? "";
@@ -33,6 +39,8 @@ export default function DownloadActionButton({ mxEvent, mediaEventHelperGet }: I
const { download, loading, canDownload } = useDownloadMedia(downloadUrl, fileName, mxEvent);
const buttonTitle = useButtonTitle(loading, mediaEventHelper?.media.isEncrypted ?? false);
if (!canDownload) return null;
const spinner = loading ? <Spinner w={18} h={18} /> : undefined;
@@ -45,7 +53,7 @@ export default function DownloadActionButton({ mxEvent, mediaEventHelperGet }: I
return (
<RovingAccessibleButton
className={classes}
title={loading ? _t("timeline|download_action_downloading") : _t("action|download")}
title={buttonTitle}
onClick={download}
disabled={loading}
placement="left"
@@ -120,8 +120,8 @@ export const PollHistoryList: React.FC<PollHistoryListProps> = ({
value={filter}
onFilterChange={onFilterChange}
tabs={[
{ id: "ACTIVE", label: "Active polls" },
{ id: "ENDED", label: "Past polls" },
{ id: "ACTIVE", label: _t("right_panel|poll|active_heading") },
{ id: "ENDED", label: _t("right_panel|poll|past_heading") },
]}
/>
{!!pollStartEvents.length && (
@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
import React, { type ComponentType } from "react";
import { Text } from "@vector-im/compound-web";
import { Flex } from "../../utils/Flex";
import { Flex } from "../../../shared-components/utils/Flex";
interface Props {
Icon: ComponentType<React.SVGAttributes<SVGElement>>;
@@ -46,9 +46,9 @@ import RoomAvatar from "../avatars/RoomAvatar.tsx";
import { E2EStatus } from "../../../utils/ShieldUtils.ts";
import { type RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks.ts";
import RoomName from "../elements/RoomName.tsx";
import { Flex } from "../../utils/Flex.tsx";
import { Flex } from "../../../shared-components/utils/Flex";
import { Linkify, topicToHtml } from "../../../HtmlUtils.tsx";
import { Box } from "../../utils/Box.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";
@@ -80,7 +80,7 @@ const RoomTopic: React.FC<Pick<IProps, "room">> = ({ room }): JSX.Element | null
gap="var(--cpd-space-2x)"
className="mx_RoomSummaryCard_topic"
>
<Box flex="1">
<Box flex="1" className="mx_RoomSummaryCard_topic_box">
<Link kind="primary" onClick={vm.onEditClick}>
<Text size="sm" weight="regular">
{_t("right_panel|add_topic")}
@@ -103,7 +103,7 @@ const RoomTopic: React.FC<Pick<IProps, "room">> = ({ room }): JSX.Element | null
mx_RoomSummaryCard_topic_collapsed: !vm.expanded,
})}
>
<Box flex="1" className="mx_RoomSummaryCard_topic_container">
<Box flex="1" className="mx_RoomSummaryCard_topic_container mx_RoomSummaryCard_topic_box">
<Text size="sm" weight="regular" onClick={vm.onTopicLinkClick}>
{content}
</Text>
@@ -169,8 +169,8 @@ const RoomSummaryCardView: React.FC<IProps> = ({
<Flex as="section" justify="center" gap="var(--cpd-space-2x)" className="mx_RoomSummaryCard_badges">
{!vm.isDirectMessage && vm.roomJoinRule === JoinRule.Public && (
<Badge kind="grey">
<PublicIcon width="1em" />
<Badge kind="blue">
<PublicIcon width="1em" color="var(--cpd-color-icon-info-primary)" />
{_t("common|public_room")}
</Badge>
)}
@@ -183,8 +183,8 @@ const RoomSummaryCardView: React.FC<IProps> = ({
)}
{!vm.isRoomEncrypted && (
<Badge kind="grey">
<LockOffIcon width="1em" />
<Badge kind="blue">
<LockOffIcon width="1em" color="var(--cpd-color-icon-info-primary)" />
{_t("common|unencrypted")}
</Badge>
)}
@@ -12,7 +12,7 @@ import { VerifiedIcon } from "@vector-im/compound-design-tokens/assets/web/icons
import { useUserInfoVerificationViewModel } from "../../../viewmodels/right_panel/user_info/UserInfoHeaderVerificationViewModel";
import { type IDevice } from "../UserInfo";
import { Flex } from "../../../utils/Flex";
import { Flex } from "../../../../shared-components/utils/Flex";
import { _t } from "../../../../languageHandler";
export const UserInfoHeaderVerificationView: React.FC<{
@@ -12,7 +12,7 @@ import { Heading, Tooltip, Text } from "@vector-im/compound-web";
import { useUserfoHeaderViewModel } from "../../../viewmodels/right_panel/user_info/UserInfoHeaderViewModel";
import MemberAvatar from "../../avatars/MemberAvatar";
import { Container, type Member, type IDevice } from "../UserInfo";
import { Flex } from "../../../utils/Flex";
import { Flex } from "../../../../shared-components/utils/Flex";
import PresenceLabel from "../../rooms/PresenceLabel";
import CopyableText from "../../elements/CopyableText";
import { UserInfoHeaderVerificationView } from "./UserInfoHeaderVerificationView";
+8 -5
View File
@@ -729,11 +729,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
if (this.state.shieldColour !== EventShieldColour.NONE) {
let shieldReasonMessage: string;
switch (this.state.shieldReason) {
case null:
case EventShieldReason.UNKNOWN:
shieldReasonMessage = _t("error|unknown");
break;
case EventShieldReason.UNVERIFIED_IDENTITY:
shieldReasonMessage = _t("encryption|event_shield_reason_unverified_identity");
break;
@@ -761,6 +756,14 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
case EventShieldReason.VERIFICATION_VIOLATION:
shieldReasonMessage = _t("timeline|decryption_failure|sender_identity_previously_verified");
break;
case EventShieldReason.MISMATCHED_SENDER:
shieldReasonMessage = _t("encryption|event_shield_reason_mismatched_sender");
break;
default:
shieldReasonMessage = _t("error|unknown");
break;
}
if (this.state.shieldColour === EventShieldColour.GREY) {
@@ -7,10 +7,10 @@ Please see LICENSE files in the repository root for full details.
import { Search, Text, Button, Tooltip, InlineSpinner } from "@vector-im/compound-web";
import React from "react";
import InviteIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-add-solid";
import InviteIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-add";
import { UserAddIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { Flex } from "../../../utils/Flex";
import { Flex } from "../../../../shared-components/utils/Flex";
import { type MemberListViewState } from "../../../viewmodels/memberlist/MemberListViewModel";
import { _t } from "../../../../languageHandler";
@@ -19,10 +19,10 @@ interface TooltipProps {
children: React.ReactNode;
}
const OptionalTooltip: React.FC<TooltipProps> = ({ canInvite, children }) => {
if (canInvite) return children;
const InviteTooltip: React.FC<TooltipProps> = ({ canInvite, children }) => {
const description: string = canInvite ? _t("action|invite") : _t("member_list|invite_button_no_perms_tooltip");
// If the user isn't allowed to invite others to this room, wrap with a relevant tooltip.
return <Tooltip description={_t("member_list|invite_button_no_perms_tooltip")}>{children}</Tooltip>;
return <Tooltip description={description}>{children}</Tooltip>;
};
interface Props {
@@ -42,10 +42,10 @@ const InviteButton: React.FC<Props> = ({ vm }) => {
if (shouldShowSearch) {
/// When rendered alongside a search box, the invite button is just an icon.
return (
<OptionalTooltip canInvite={vm.canInvite}>
<InviteTooltip canInvite={vm.canInvite}>
<Button
className="mx_MemberListHeaderView_invite_small"
kind="primary"
kind="secondary"
onClick={vm.onInviteButtonClick}
size="sm"
iconOnly={true}
@@ -54,13 +54,13 @@ const InviteButton: React.FC<Props> = ({ vm }) => {
aria-label={_t("action|invite")}
type="button"
/>
</OptionalTooltip>
</InviteTooltip>
);
}
// Without a search box, invite button is a full size button.
return (
<OptionalTooltip canInvite={vm.canInvite}>
<InviteTooltip canInvite={vm.canInvite}>
<Button
kind="secondary"
size="sm"
@@ -72,7 +72,7 @@ const InviteButton: React.FC<Props> = ({ vm }) => {
>
{_t("action|invite")}
</Button>
</OptionalTooltip>
</InviteTooltip>
);
};
@@ -6,11 +6,9 @@ Please see LICENSE files in the repository root for full details.
*/
import { Form } from "@vector-im/compound-web";
import React, { type JSX } from "react";
import { List, type ListRowProps } from "react-virtualized/dist/commonjs/List";
import { AutoSizer } from "react-virtualized";
import React, { type JSX, useCallback } from "react";
import { Flex } from "../../../utils/Flex";
import { Flex } from "../../../../shared-components/utils/Flex";
import {
type MemberWithSeparator,
SEPARATOR,
@@ -21,7 +19,7 @@ import { ThreePidInviteTileView } from "./tiles/ThreePidInviteTileView";
import { MemberListHeaderView } from "./MemberListHeaderView";
import BaseCard from "../../right_panel/BaseCard";
import { _t } from "../../../../languageHandler";
import { RovingTabIndexProvider } from "../../../../accessibility/RovingTabIndex";
import { type ListContext, ListView } from "../../../utils/ListView";
interface IProps {
roomId: string;
@@ -30,53 +28,74 @@ interface IProps {
const MemberListView: React.FC<IProps> = (props: IProps) => {
const vm = useMemberListViewModel(props.roomId);
const { isPresenceEnabled, onClickMember, memberCount } = vm;
const totalRows = vm.members.length;
const getRowComponent = (item: MemberWithSeparator): JSX.Element => {
const getItemKey = useCallback((item: MemberWithSeparator): string => {
if (item === SEPARATOR) {
return <hr className="mx_MemberListView_separator" />;
return "separator";
} else if (item.member) {
return <RoomMemberTileView member={item.member} showPresence={vm.isPresenceEnabled} />;
return `member-${item.member.userId}`;
} else {
return <ThreePidInviteTileView threePidInvite={item.threePidInvite} />;
return `threePidInvite-${item.threePidInvite.event.getContent().public_key}`;
}
};
}, []);
const getRowHeight = ({ index }: { index: number }): number => {
if (vm.members[index] === SEPARATOR) {
/**
* This is a separator of 2px height rendered between
* joined and invited members.
*/
return 2;
} else if (totalRows && index === totalRows) {
/**
* The empty spacer div rendered at the bottom should
* have a height of 32px.
*/
return 32;
} else {
/**
* The actual member tiles have a height of 56px.
*/
return 56;
}
};
const getItemComponent = useCallback(
(
index: number,
item: MemberWithSeparator,
context: ListContext<any>,
onFocus: (e: React.FocusEvent) => void,
): JSX.Element => {
const itemKey = getItemKey(item);
const isRovingItem = itemKey === context.tabIndexKey;
const focused = isRovingItem && context.focused;
if (item === SEPARATOR) {
return <hr className="mx_MemberListView_separator" />;
} else if (item.member) {
return (
<RoomMemberTileView
member={item.member}
showPresence={isPresenceEnabled}
focused={focused}
tabIndex={isRovingItem ? 0 : -1}
index={index}
memberCount={memberCount}
onFocus={onFocus}
/>
);
} else {
return (
<ThreePidInviteTileView
threePidInvite={item.threePidInvite}
focused={focused}
tabIndex={isRovingItem ? 0 : -1}
memberIndex={index - 1} // Adjust as invites are below the separator
memberCount={memberCount}
onFocus={onFocus}
/>
);
}
},
[isPresenceEnabled, getItemKey, memberCount],
);
const rowRenderer = ({ key, index, style }: ListRowProps): JSX.Element => {
if (index === totalRows) {
// We've rendered all the members,
// now we render an empty div to add some space to the end of the list.
return <div key={key} style={style} />;
}
const item = vm.members[index];
return (
<div key={key} style={style}>
{getRowComponent(item)}
</div>
);
};
const handleSelectItem = useCallback(
(item: MemberWithSeparator): void => {
if (item !== SEPARATOR) {
if (item.member) {
onClickMember(item.member);
} else {
onClickMember(item.threePidInvite);
}
}
},
[onClickMember],
);
const isItemFocusable = useCallback((item: MemberWithSeparator): boolean => {
return item !== SEPARATOR;
}, []);
return (
<BaseCard
@@ -87,34 +106,20 @@ const MemberListView: React.FC<IProps> = (props: IProps) => {
header={_t("common|people")}
onClose={props.onClose}
>
<RovingTabIndexProvider handleUpDown scrollIntoView>
{({ onKeyDownHandler }) => (
<Flex
align="stretch"
direction="column"
className="mx_MemberListView_container"
onKeyDown={onKeyDownHandler}
>
<Form.Root onSubmit={(e) => e.preventDefault()}>
<MemberListHeaderView vm={vm} />
</Form.Root>
<AutoSizer>
{({ height, width }) => (
<List
rowRenderer={rowRenderer}
rowHeight={getRowHeight}
// The +1 refers to the additional empty div that we render at the end of the list.
rowCount={totalRows + 1}
// Subtract the height of MemberlistHeaderView so that the parent div does not overflow.
height={height - 113}
width={width}
overscanRowCount={15}
/>
)}
</AutoSizer>
</Flex>
)}
</RovingTabIndexProvider>
<Flex align="stretch" direction="column" className="mx_MemberListView_container">
<Form.Root onSubmit={(e) => e.preventDefault()}>
<MemberListHeaderView vm={vm} />
</Form.Root>
<ListView
items={vm.members}
onSelectItem={handleSelectItem}
getItemComponent={getItemComponent}
getItemKey={getItemKey}
isItemFocusable={isItemFocusable}
role="listbox"
aria-label={_t("member_list|list_title")}
/>
</Flex>
</BaseCard>
);
};
@@ -19,7 +19,12 @@ import { InvitedIconView } from "./common/InvitedIconView";
interface IProps {
member: RoomMember;
index: number;
memberCount: number;
showPresence?: boolean;
focused?: boolean;
tabIndex?: number;
onFocus: (e: React.FocusEvent) => void;
}
export function RoomMemberTileView(props: IProps): JSX.Element {
@@ -36,7 +41,7 @@ export function RoomMemberTileView(props: IProps): JSX.Element {
/>
);
const name = vm.name;
const nameJSX = <DisambiguatedProfile member={member} fallbackName={name || ""} />;
const nameJSX = <DisambiguatedProfile withTooltip member={member} fallbackName={name || ""} />;
const presenceState = member.presenceState;
let presenceJSX: JSX.Element | undefined;
@@ -54,13 +59,18 @@ export function RoomMemberTileView(props: IProps): JSX.Element {
return (
<MemberTileView
title={vm.title}
onClick={vm.onClick}
onFocus={props.onFocus}
avatarJsx={av}
presenceJsx={presenceJSX}
nameJsx={nameJSX}
userLabel={vm.userLabel}
ariaLabel={name}
iconJsx={iconJsx}
focused={props.focused}
tabIndex={props.tabIndex}
memberIndex={props.index - (member.isInvite ? 1 : 0)} // Adjust as invites are below the seperator
memberCount={props.memberCount}
/>
);
}
@@ -15,20 +15,32 @@ import { InvitedIconView } from "./common/InvitedIconView";
interface Props {
threePidInvite: ThreePIDInvite;
memberIndex: number;
memberCount: number;
focused?: boolean;
tabIndex?: number;
onFocus: (e: React.FocusEvent) => void;
}
export function ThreePidInviteTileView(props: Props): JSX.Element {
const vm = useThreePidTileViewModel(props);
const av = <BaseAvatar name={vm.name} size="32px" aria-hidden="true" />;
const iconJsx = <InvitedIconView isThreePid={true} />;
const name = vm.name;
return (
<MemberTileView
nameJsx={vm.name}
nameJsx={name}
avatarJsx={av}
onClick={vm.onClick}
memberIndex={props.memberIndex}
memberCount={props.memberCount}
ariaLabel={name}
userLabel={vm.userLabel}
iconJsx={iconJsx}
focused={props.focused}
tabIndex={props.tabIndex}
onFocus={props.onFocus}
/>
);
}
@@ -9,7 +9,7 @@ import React, { type JSX } from "react";
import EmailIcon from "@vector-im/compound-design-tokens/assets/web/icons/email-solid";
import UserAddIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-add-solid";
import { Flex } from "../../../../../utils/Flex";
import { Flex } from "../../../../../../shared-components/utils/Flex";
interface Props {
isThreePid: boolean;
@@ -5,18 +5,23 @@ 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 } from "react";
import React, { useEffect, useRef, type JSX } from "react";
import { RovingAccessibleButton } from "../../../../../../accessibility/RovingTabIndex";
import AccessibleButton from "../../../../elements/AccessibleButton";
interface Props {
avatarJsx: JSX.Element;
nameJsx: JSX.Element | string;
onClick: () => void;
title?: string;
onFocus: (e: React.FocusEvent) => void;
memberIndex: number;
memberCount: number;
ariaLabel?: string;
presenceJsx?: JSX.Element;
userLabel?: React.ReactNode;
iconJsx?: JSX.Element;
tabIndex?: number;
focused?: boolean;
}
export function MemberTileView(props: Props): JSX.Element {
@@ -24,22 +29,37 @@ export function MemberTileView(props: Props): JSX.Element {
if (props.userLabel) {
userLabelJsx = <div className="mx_MemberTileView_userLabel">{props.userLabel}</div>;
}
const ref = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (props.focused) {
ref.current?.focus({ preventScroll: true, focusVisible: true });
}
}, [props.focused]);
return (
// The wrapping div is required to make the magic mouse listener work, for some reason.
<div>
<RovingAccessibleButton className="mx_MemberTileView" title={props.title} onClick={props.onClick}>
<div className="mx_MemberTileView_left">
<AccessibleButton
ref={ref}
className="mx_MemberTileView"
onClick={props.onClick}
onFocus={props.onFocus}
aria-label={props?.ariaLabel}
tabIndex={props.tabIndex}
role="option"
aria-posinset={props.memberIndex + 1}
aria-setsize={props.memberCount}
>
<div aria-hidden className="mx_MemberTileView_left">
<div className="mx_MemberTileView_avatar">
{props.avatarJsx} {props.presenceJsx}
</div>
<div className="mx_MemberTileView_name">{props.nameJsx}</div>
</div>
<div className="mx_MemberTileView_right">
<div aria-hidden className="mx_MemberTileView_right">
{userLabelJsx}
{props.iconJsx}
</div>
</RovingAccessibleButton>
</AccessibleButton>
</div>
);
}
@@ -13,7 +13,7 @@ import VideoCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/vi
import EmailIcon from "@vector-im/compound-design-tokens/assets/web/icons/email-solid";
import { UnreadCounter, Unread } from "@vector-im/compound-web";
import { Flex } from "../../utils/Flex";
import { Flex } from "../../../shared-components/utils/Flex";
import { type RoomNotificationState } from "../../../stores/notifications/RoomNotificationState";
import { useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
@@ -25,8 +25,8 @@ import { RightPanelPhases } from "../../../../stores/right-panel/RightPanelStore
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext.tsx";
import { useRoomMemberCount, useRoomMembers } from "../../../../hooks/useRoomMembers.ts";
import { _t } from "../../../../languageHandler.tsx";
import { Flex } from "../../../utils/Flex.tsx";
import { Box } from "../../../utils/Box.tsx";
import { Flex } from "../../../../shared-components/utils/Flex";
import { Box } from "../../../../shared-components/utils/Box";
import { getPlatformCallTypeProps, useRoomCall } from "../../../../hooks/room/useRoomCall.tsx";
import { useRoomThreadNotifications } from "../../../../hooks/room/useRoomThreadNotifications.ts";
import { useGlobalNotificationState } from "../../../../hooks/useGlobalNotificationState.ts";
@@ -286,7 +286,8 @@ export default function RoomHeader({
<PublicIcon
width="16px"
height="16px"
className="mx_RoomHeader_icon text-secondary"
className="mx_RoomHeader_icon"
color="var(--cpd-color-icon-info-primary)"
aria-label={_t("common|public_room")}
/>
</Tooltip>
@@ -7,11 +7,11 @@
import React, { type JSX, type PropsWithChildren } from "react";
import { Button } from "@vector-im/compound-web";
import UserAddIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-add";
import ChatIcon from "@vector-im/compound-design-tokens/assets/web/icons/chat";
import RoomIcon from "@vector-im/compound-design-tokens/assets/web/icons/room";
import type { RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel";
import { Flex } from "../../../utils/Flex";
import { Flex } from "../../../../shared-components/utils/Flex";
import { _t } from "../../../../languageHandler";
import { FilterKey } from "../../../../stores/room-list-v3/skip-list/filters";
import { type PrimaryFilter } from "../../../viewmodels/roomlist/useFilteredRooms";
@@ -148,8 +148,8 @@ function DefaultPlaceholder({ vm }: DefaultPlaceholderProps): JSX.Element {
direction="column"
gap="var(--cpd-space-4x)"
>
<Button size="sm" kind="secondary" Icon={UserAddIcon} onClick={vm.createChatRoom}>
{_t("action|new_message")}
<Button size="sm" kind="secondary" Icon={ChatIcon} onClick={vm.createChatRoom}>
{_t("action|start_chat")}
</Button>
{vm.canCreateRoom && (
<Button size="sm" kind="secondary" Icon={RoomIcon} onClick={vm.createRoom}>
@@ -14,9 +14,10 @@ import HomeIcon from "@vector-im/compound-design-tokens/assets/web/icons/home";
import PreferencesIcon from "@vector-im/compound-design-tokens/assets/web/icons/preferences";
import SettingsIcon from "@vector-im/compound-design-tokens/assets/web/icons/settings";
import VideoCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/video-call";
import ChatIcon from "@vector-im/compound-design-tokens/assets/web/icons/chat";
import { _t } from "../../../../languageHandler";
import { Flex } from "../../../utils/Flex";
import { Flex } from "../../../../shared-components/utils/Flex";
import {
type RoomListHeaderViewState,
useRoomListHeaderViewModel,
@@ -49,7 +50,7 @@ export function RoomListHeaderView(): JSX.Element {
{vm.displayComposeMenu ? (
<ComposeMenu vm={vm} />
) : (
<IconButton aria-label={_t("action|new_message")} onClick={(e) => vm.createChatRoom(e.nativeEvent)}>
<IconButton aria-label={_t("action|start_chat")} onClick={(e) => vm.createChatRoom(e.nativeEvent)}>
<ComposeIcon color="var(--cpd-color-icon-secondary)" />
</IconButton>
)}
@@ -143,12 +144,7 @@ function ComposeMenu({ vm }: ComposeMenuProps): JSX.Element {
</IconButton>
}
>
<MenuItem
Icon={UserAddIcon}
label={_t("action|new_message")}
onSelect={vm.createChatRoom}
hideChevron={true}
/>
<MenuItem Icon={ChatIcon} label={_t("action|start_chat")} onSelect={vm.createChatRoom} hideChevron={true} />
{vm.canCreateRoom && (
<MenuItem Icon={RoomIcon} label={_t("action|new_room")} onSelect={vm.createRoom} hideChevron={true} />
)}
@@ -21,7 +21,7 @@ import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check"
import { type Room } from "matrix-js-sdk/src/matrix";
import { _t } from "../../../../languageHandler";
import { Flex } from "../../../utils/Flex";
import { Flex } from "../../../../shared-components/utils/Flex";
import {
type RoomListItemMenuViewState,
useRoomListItemMenuViewModel,
@@ -10,7 +10,7 @@ import { type Room } from "matrix-js-sdk/src/matrix";
import classNames from "classnames";
import { useRoomListItemViewModel } from "../../../viewmodels/roomlist/RoomListItemViewModel";
import { Flex } from "../../../utils/Flex";
import { Flex } from "../../../../shared-components/utils/Flex";
import { RoomListItemMenuView } from "./RoomListItemMenuView";
import { NotificationDecoration } from "../NotificationDecoration";
import { RoomAvatarView } from "../../avatars/RoomAvatarView";
@@ -12,7 +12,7 @@ import { UIComponent } from "../../../../settings/UIFeature";
import { RoomListSearch } from "./RoomListSearch";
import { RoomListHeaderView } from "./RoomListHeaderView";
import { RoomListView } from "./RoomListView";
import { Flex } from "../../../utils/Flex";
import { Flex } from "../../../../shared-components/utils/Flex";
import { _t } from "../../../../languageHandler";
type RoomListPanelProps = {
@@ -10,7 +10,7 @@ import { ChatFilter, IconButton } from "@vector-im/compound-web";
import ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down";
import type { RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel";
import { Flex } from "../../../utils/Flex";
import { Flex } from "../../../../shared-components/utils/Flex";
import { _t } from "../../../../languageHandler";
interface RoomListPrimaryFiltersProps {
@@ -20,7 +20,7 @@ import { MetaSpace } from "../../../../stores/spaces";
import { Action } from "../../../../dispatcher/actions";
import PosthogTrackers from "../../../../PosthogTrackers";
import defaultDispatcher from "../../../../dispatcher/dispatcher";
import { Flex } from "../../../utils/Flex";
import { Flex } from "../../../../shared-components/utils/Flex";
import { useTypedEventEmitterState } from "../../../../hooks/useEventEmitter";
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../../../LegacyCallHandler";
@@ -61,7 +61,6 @@ export function RoomListSearch({ activeSpace }: RoomListSearchProps): JSX.Elemen
</Button>
{displayDialButton && (
<Button
className="mx_RoomListSearch_button"
kind="secondary"
size="sm"
Icon={DialPadIcon}
@@ -74,7 +73,6 @@ export function RoomListSearch({ activeSpace }: RoomListSearchProps): JSX.Elemen
)}
{displayExploreButton && (
<Button
className="mx_RoomListSearch_button"
kind="secondary"
size="sm"
Icon={ExploreIcon}
@@ -19,7 +19,7 @@ import { isValid3pidInvite } from "../../../RoomInvite";
import { Action } from "../../../dispatcher/actions";
import ErrorDialog from "../dialogs/ErrorDialog";
import BaseCard from "../right_panel/BaseCard";
import { Flex } from "../../utils/Flex";
import { Flex } from "../../../shared-components/utils/Flex";
interface IProps {
event: MatrixEvent;
@@ -26,7 +26,7 @@ import AccessibleButton from "../elements/AccessibleButton";
import LogoutDialog, { shouldShowLogoutDialog } from "../dialogs/LogoutDialog";
import Modal from "../../../Modal";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { Flex } from "../../utils/Flex";
import { Flex } from "../../../shared-components/utils/Flex";
const SpinnerToast: React.FC<{ children?: ReactNode }> = ({ children }) => (
<>
@@ -7,7 +7,7 @@
import React, { type JSX, type PropsWithChildren } from "react";
import { Flex } from "../../../utils/Flex";
import { Flex } from "../../../../shared-components/utils/Flex";
/**
* A component for emphasised text within an {@link EncryptionCard}
@@ -348,10 +348,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps, RolesR
powerLevelDescriptors.users_default.defaultValue,
);
let currentUserLevel = userLevels[client.getUserId()!];
if (currentUserLevel === undefined) {
currentUserLevel = defaultUserLevel;
}
const currentUserLevel = room.getMember(client.getSafeUserId())?.powerLevel ?? defaultUserLevel;
this.populateDefaultPlEvents(
eventsLevels,
-1
View File
@@ -3376,7 +3376,6 @@
"unable_to_decrypt": "Zprávu nelze dešifrovat"
},
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Dešifrování",
"download_action_downloading": "Stahování",
"download_failed": "Stažení se nezdařilo",
"download_failed_description": "Při stahování tohoto souboru došlo k chybě",
-1
View File
@@ -3341,7 +3341,6 @@
"unable_to_decrypt": "Methu dadgryptio'r neges"
},
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Dadgryptio",
"download_action_downloading": "Yn llwytho i lawr",
"download_failed": "Methodd y llwytho i lawr",
"download_failed_description": "Bu gwall wrth lawrlwytho'r ffeil hon",
-1
View File
@@ -3334,7 +3334,6 @@
"unable_to_decrypt": "Entschlüsselung der Nachricht nicht möglich"
},
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Entschlüsseln",
"download_action_downloading": "Herunterladen",
"download_failed": "Herunterladen fehlgeschlagen",
"download_failed_description": "Beim Herunterladen dieser Datei ist ein Fehler aufgetreten",
-1
View File
@@ -2688,7 +2688,6 @@
},
"creation_summary_dm": "Ο/η %(creator)s δημιούργησε αυτό το απευθείας μήνυμα.",
"creation_summary_room": "Ο/η %(creator)s δημιούργησε και διαμόρφωσε το δωμάτιο.",
"download_action_decrypting": "Αποκρυπτογράφηση",
"download_action_downloading": "Γίνεται λήψη",
"edits": {
"tooltip_label": "Επεξεργάστηκε στις %(date)s. Κάντε κλικ για να δείτε τις τροποποιήσεις.",
+9 -3
View File
@@ -93,7 +93,6 @@
"maximise": "Maximise",
"mention": "Mention",
"minimise": "Minimise",
"new_message": "New message",
"new_room": "New room",
"new_video_room": "New video room",
"next": "Next",
@@ -863,6 +862,7 @@
"elementCallUrl": "Element Call URL"
},
"settings_explorer": "Settings explorer",
"show_empty_content_events": "Show events with empty content",
"show_hidden_events": "Show hidden events in timeline",
"spaces": {
"one": "<space>",
@@ -933,6 +933,7 @@
"cross_signing_user_warning": "This user has not verified all of their sessions.",
"enter_recovery_key": "Enter recovery key",
"event_shield_reason_authenticity_not_guaranteed": "The authenticity of this encrypted message can't be guaranteed on this device.",
"event_shield_reason_mismatched_sender": "The sender of the event does not match the owner of the device that sent it.",
"event_shield_reason_mismatched_sender_key": "Encrypted by an unverified session",
"event_shield_reason_unknown_device": "Encrypted by an unknown or deleted device.",
"event_shield_reason_unsigned_device": "Encrypted by a device not verified by its owner.",
@@ -1532,6 +1533,9 @@
"render_reaction_images_description": "Sometimes referred to as \"custom emojis\".",
"report_to_moderators": "Report to moderators",
"report_to_moderators_description": "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.",
"share_history_on_invite": "Share encrypted history with new members",
"share_history_on_invite_description": "When inviting a user to an encrypted room that has history visibility set to \"shared\", share encrypted history with that user, and accept encrypted history when you are invited to such a room.",
"share_history_on_invite_warning": "This feature is EXPERIMENTAL and not all security precautions are implemented. Do not enable on production accounts.",
"sliding_sync": "Sliding Sync mode",
"sliding_sync_description": "Under active development, cannot be disabled.",
"sliding_sync_disabled_notice": "Log out and back in to disable",
@@ -1653,8 +1657,8 @@
"filter_placeholder": "Search room members",
"invite_button_no_perms_tooltip": "You do not have permission to invite users",
"invited_label": "Invited",
"no_matches": "No matches",
"power_label": "%(userName)s (power %(powerLevelNumber)s)"
"list_title": "Member list",
"no_matches": "No matches"
},
"member_list_back_action_label": "Room members",
"message_edit_dialog_title": "Message edits",
@@ -1758,6 +1762,7 @@
},
"power_level": {
"admin": "Admin",
"creator": "Owner",
"custom": "Custom (%(level)s)",
"custom_level": "Custom level",
"default": "Default",
@@ -3371,6 +3376,7 @@
"unable_to_decrypt": "Unable to decrypt message"
},
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Decrypting",
"download_action_downloading": "Downloading",
"download_failed": "Download failed",
"download_failed_description": "An error occurred while downloading this file",
-1
View File
@@ -2091,7 +2091,6 @@
},
"creation_summary_dm": "%(creator)s kreis ĉi tiun individuan ĉambron.",
"creation_summary_room": "%(creator)s kreis kaj agordis la ĉambron.",
"download_action_decrypting": "Malĉifrante",
"edits": {
"tooltip_label": "Redaktita je %(date)s. Klaku por vidi redaktojn.",
"tooltip_sub": "Klaku por vidi redaktojn",
-1
View File
@@ -2672,7 +2672,6 @@
},
"creation_summary_dm": "%(creator)s creó este mensaje directo.",
"creation_summary_room": "Sala creada y configurada por %(creator)s.",
"download_action_decrypting": "Descifrando",
"download_action_downloading": "Descargando",
"edits": {
"tooltip_label": "Última vez editado: %(date)s. Haz clic para ver los cambios.",
+1 -1
View File
@@ -1450,6 +1450,7 @@
"room_list_navigate_down": "Suundu jututubade loendis alla",
"room_list_navigate_up": "Suundu jututubade loendis üles",
"room_list_select_room": "Vali tubade loendist jututuba",
"save": "Salvesta",
"scroll_down_timeline": "Liigu ajajoonel alla",
"scroll_up_timeline": "Liigu ajajoonel üles",
"search": "Otsing (peab olema lubatud)",
@@ -3370,7 +3371,6 @@
"unable_to_decrypt": "Sõnumi dekrüptimine ei õnnestu"
},
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Dekrüptin sisu",
"download_action_downloading": "Laadin alla",
"download_failed": "Allalaadimine ei õnnestunud",
"download_failed_description": "Selle faili allalaadimisel tekkis viga",
+5 -2
View File
@@ -708,7 +708,8 @@
"twemoji_colr": "<colr>twemoji-colr</colr>-fontti © <author>Mozilla Foundation</author>, käytössä <terms>Apache 2.0</terms>:n ehtojen mukaisesti."
},
"decline_invitation_dialog": {
"reason_description": "Kerro syy huoneen ilmoittamiseen."
"reason_description": "Kerro syy huoneen ilmoittamiseen.",
"title": "Kieltäydy kutsusta"
},
"desktop_default_device_name": "%(brand)sin työpöytäversio: %(platformName)s",
"devtools": {
@@ -1240,6 +1241,7 @@
"room_list_navigate_down": "Liiku alas huoneluettelossa",
"room_list_navigate_up": "Liiku ylös huoneluettelossa",
"room_list_select_room": "Valitse huone huoneluettelosta",
"save": "Tallenna",
"scroll_down_timeline": "Vieritä alas aikajanalla",
"scroll_up_timeline": "Vieritä ylös aikajanalla",
"search": "Haku (pitää olla käytössä)",
@@ -2356,6 +2358,7 @@
"notifications": {
"desktop_notification_message_preview": "Näytä viestin esikatselu työpöytäilmoituksessa",
"dialog_title": "<strong>Asetukset:</strong> Ilmoitukset",
"email_section": "Sähköpostin yhteenveto",
"enable_audible_notifications_session": "Ota käyttöön ääni-ilmoitukset tälle istunnolle",
"enable_desktop_notifications_session": "Ota käyttöön työpöytäilmoitukset tälle istunnolle",
"enable_email_notifications": "Sähköposti-ilmoitukset osoitteeseen %(email)s",
@@ -2548,6 +2551,7 @@
"show_chat_effects": "Näytä keskustelutehosteet (animaatiot, kun saat esim. konfettia)",
"show_displayname_changes": "Näytä näyttönimien muutokset",
"show_join_leave": "Näytä liittymis- ja poistumisviestit (ei vaikutusta kutsuihin, poistamisiin ja porttikieltoihin)",
"show_message_previews": "Näytä viestien esikatselut",
"show_nsfw_content": "Näytä NSFW-sisältö",
"show_read_receipts": "Näytä muiden käyttäjien lukukuittaukset",
"show_redaction_placeholder": "Näytä paikanpitäjä poistetuille viesteille",
@@ -2886,7 +2890,6 @@
"unable_to_decrypt": "Viestin salausta ei voi purkaa"
},
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Puretaan salausta",
"download_action_downloading": "Ladataan",
"download_failed": "Lataus epäonnistui",
"download_failed_description": "Tiedostoa ladattaessa tapahtui virhe",
+4 -3
View File
@@ -568,7 +568,7 @@
"room_name": "Nom du salon",
"rooms": "Salons",
"save": "Enregistrer",
"saved": "Sauvegardé",
"saved": "Enregistré",
"saving": "Enregistrement…",
"secure_backup": "Sauvegarde sécurisée",
"select_all": "Tout sélectionner",
@@ -863,6 +863,7 @@
"elementCallUrl": "Lien d'Element Call"
},
"settings_explorer": "Explorateur de paramètres",
"show_empty_content_events": "Afficher les événements sans contenu",
"show_hidden_events": "Afficher les évènements cachés dans le fil de discussion",
"spaces": {
"one": "<espace>",
@@ -1450,6 +1451,7 @@
"room_list_navigate_down": "Descendre dans la liste des salons",
"room_list_navigate_up": "Remonter dans la liste des salons",
"room_list_select_room": "Sélectionner un salon de la liste des salons",
"save": "Enregistrer",
"scroll_down_timeline": "Faire défiler le fil de discussion vers le bas",
"scroll_up_timeline": "Faire défiler le fil de discussion vers le haut",
"search": "Recherche (si activée)",
@@ -2730,7 +2732,7 @@
"set_phrase_again": "Retournez en arrière pour la redéfinir.",
"settings_reminder": "Vous pouvez aussi configurer la sauvegarde sécurisée et gérer vos clés depuis les paramètres.",
"title_confirm_phrase": "Confirmer la phrase de sécurité",
"title_save_key": "Sauvegarder votre clé de récupération",
"title_save_key": "Enregistrer votre clé de récupération",
"title_set_phrase": "Définir une phrase de sécurité",
"unable_to_setup": "Impossible de configurer le coffre secret",
"use_different_passphrase": "Utiliser une phrase secrète différente ?",
@@ -3369,7 +3371,6 @@
"unable_to_decrypt": "Impossible de déchiffrer le message"
},
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Déchiffrement",
"download_action_downloading": "Téléchargement en cours",
"download_failed": "Échec du téléchargement",
"download_failed_description": "Une erreur s'est produite lors du téléchargement de ce fichier",
-1
View File
@@ -2435,7 +2435,6 @@
},
"creation_summary_dm": "%(creator)s creou esta MD.",
"creation_summary_room": "%(creator)s creou e configurou a sala.",
"download_action_decrypting": "Descifrando",
"download_action_downloading": "Descargando",
"edits": {
"tooltip_label": "Editada o %(date)s. Preme para ver edicións.",
-1
View File
@@ -1985,7 +1985,6 @@
},
"creation_summary_dm": "%(creator)s יצר את DM הזה.",
"creation_summary_room": "%(creator)s יצר/ה והגדיר/ה את החדר.",
"download_action_decrypting": "מפענח",
"download_action_downloading": "מוריד",
"edits": {
"tooltip_label": "נערך ב-%(date)s. לחץ לצפייה בעריכות.",
-1
View File
@@ -3344,7 +3344,6 @@
"unable_to_decrypt": "Nem sikerült visszafejteni az üzenetet"
},
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Visszafejtés",
"download_action_downloading": "Letöltés",
"download_failed": "Letöltés sikertelen",
"download_failed_description": "Hiba történt a fájl letöltése közben",
-1
View File
@@ -3362,7 +3362,6 @@
"unable_to_decrypt": "Tidak dapat mendekripsi pesan"
},
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Mendekripsi",
"download_action_downloading": "Mengunduh",
"download_failed": "Pengunduhan gagal",
"download_failed_description": "Terjadi kesalahan saat mengunduh berkas ini",
-1
View File
@@ -2360,7 +2360,6 @@
},
"creation_summary_dm": "%(creator)s bjó til oþessi beinu skilaboð.",
"creation_summary_room": "%(creator)s bjó til og stillti spjallrásina.",
"download_action_decrypting": "Afkóðun",
"download_action_downloading": "Sæki",
"edits": {
"tooltip_label": "Breytt þann %(date)s. Smelltu hér til að skoða breytingar.",
-1
View File
@@ -2906,7 +2906,6 @@
"creation_summary_dm": "%(creator)s ha creato questo MD.",
"creation_summary_room": "%(creator)s ha creato e configurato la stanza.",
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Decifrazione",
"download_action_downloading": "Scaricamento",
"edits": {
"tooltip_label": "Modificato alle %(date)s. Clicca per vedere le modifiche.",
-1
View File
@@ -2649,7 +2649,6 @@
"creation_summary_dm": "%(creator)sがこのダイレクトメッセージを作成しました。",
"creation_summary_room": "%(creator)sがルームを作成し設定しました。",
"disambiguated_profile": "%(displayName)s(%(matrixId)s)",
"download_action_decrypting": "復号化しています",
"download_action_downloading": "ダウンロードしています",
"edits": {
"tooltip_label": "%(date)sに編集済。クリックすると変更履歴を表示。",
-1
View File
@@ -2262,7 +2262,6 @@
"creation_summary_dm": "%(creator)sშექმნა ეს DM.",
"creation_summary_room": "%(creator)sოთახის შექმნა და კონფიგურაცია.",
"disambiguated_profile": "%(displayName)s(%(matrixId)s )",
"download_action_decrypting": "გაშიფვრა",
"download_action_downloading": "ჩამოტვირთვა",
"edits": {
"tooltip_label": "რედაქტირებულია%(date)s . დააწკაპუნეთ რედაქტირების სანახავად.",
-1
View File
@@ -2342,7 +2342,6 @@
},
"creation_summary_dm": "%(creator)s ສ້າງ DM ນີ້.",
"creation_summary_room": "%(creator)s ສ້າງ ແລະ ກຳນົດຄ່າຫ້ອງ.",
"download_action_decrypting": "ການຖອດລະຫັດ",
"download_action_downloading": "ກຳລັງດາວໂຫຼດ",
"edits": {
"tooltip_label": "ແກ້ໄຂເມື່ອ %(date)s. ກົດເພື່ອເບິ່ງການແກ້ໄຂ.",
-1
View File
@@ -1827,7 +1827,6 @@
},
"creation_summary_dm": "%(creator)s sukūrė šį tiesioginio susirašymo kambarį.",
"creation_summary_room": "%(creator)s sukūrė ir sukonfigūravo kambarį.",
"download_action_decrypting": "Iššifruojama",
"download_action_downloading": "Atsiunčiama",
"edits": {
"tooltip_label": "Keista %(date)s. Spustelėkite kad peržiūrėti pakeitimus.",
-1
View File
@@ -2836,7 +2836,6 @@
"historical_event_unverified_device": "Ir jāapliecina šī ierīce, lai piekļūtu senākām ziņām."
},
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Atšifrē",
"download_action_downloading": "Lejupielādē",
"edits": {
"tooltip_label": "Labots %(date)s. Klikšķināt, lai skatītu labojumus.",
-1
View File
@@ -2889,7 +2889,6 @@
"creation_summary_dm": "%(creator)snamorona ity DM ity.",
"creation_summary_room": "%(creator)snamorona sy nanamboatra ny efitrano.",
"disambiguated_profile": "%(displayName)s(%(matrixId)s )",
"download_action_decrypting": "Decryptering",
"download_action_downloading": "Misintona",
"edits": {
"tooltip_label": "Namboarina tamin'ny%(Rendez-vous . Kitiho raha hijery ny fanovana.",
+1 -1
View File
@@ -1450,6 +1450,7 @@
"room_list_navigate_down": "Naviger ned i romlisten",
"room_list_navigate_up": "Naviger opp i romlisten",
"room_list_select_room": "Velg rom fra romlisten",
"save": "Lagre",
"scroll_down_timeline": "Rull ned i tidslinjen",
"scroll_up_timeline": "Rull opp i tidslinjen",
"search": "Søk (må være aktivert)",
@@ -3369,7 +3370,6 @@
"unable_to_decrypt": "Kan ikke dekryptere meldingen"
},
"disambiguated_profile": "%(displayName)s(%(matrixId)s)",
"download_action_decrypting": "Dekrypterer",
"download_action_downloading": "Laster ned",
"download_failed": "Nedlasting mislyktes",
"download_failed_description": "Det oppstod en feil under nedlasting av denne filen",
-1
View File
@@ -2484,7 +2484,6 @@
},
"creation_summary_dm": "%(creator)s maakte deze directe chat.",
"creation_summary_room": "Kamer gestart en ingesteld door %(creator)s.",
"download_action_decrypting": "Ontsleutelen",
"download_action_downloading": "Downloaden",
"edits": {
"tooltip_label": "Bewerkt op %(date)s. Klik om de bewerkingen te bekijken.",
+48 -2
View File
@@ -159,6 +159,7 @@
"view_message": "Wyświetl wiadomość",
"view_source": "Wyświetl źródło",
"yes": "Tak",
"yes_dismiss": "Tak, odrzuć",
"zoom_in": "Powiększ",
"zoom_out": "Pomniejsz"
},
@@ -788,6 +789,7 @@
"cross_signing_status": "Status weryfikacji krzyżowej:",
"cross_signing_untrusted": "Twoje konto ma tożsamość weryfikacji krzyżowej w sekretnym magazynie, ale nie jest jeszcze zaufane przez tę sesję.",
"crypto_not_available": "Moduł kryptograficzny nie jest dostępny",
"device_id": "ID urządzenia",
"key_backup_active_version": "Aktywna wersja kopii zapasowej:",
"key_backup_active_version_none": "Brak",
"key_backup_inactive_warning": "Twoje klucze nie są przywracane na tej sesji.",
@@ -800,6 +802,8 @@
"secret_storage_ready": "gotowe",
"secret_storage_status": "Sekretny magazyn:",
"self_signing_private_key_cached_status": "Samopodpisujący klucz prywatny:",
"session": "Sesja",
"session_fingerprint": "Odcisk palca (klucz sesji)",
"title": "Szyfrowanie end-to-end",
"user_signing_private_key_cached_status": "Podpisany przez użytkownika klucz prywatny:"
},
@@ -825,6 +829,7 @@
"low_bandwidth_mode": "Tryb niskiej przepustowości",
"low_bandwidth_mode_description": "Wymaga kompatybilnego serwera domowego.",
"main_timeline": "Główna oś czasu",
"manual_device_verification": "Ręczna weryfikacja urządzenia",
"no_receipt_found": "Nie znaleziono potwierdzenia",
"notification_state": "Status powiadomień <strong>%(notificationState)s</strong>",
"notifications_debug": "Debug powiadomień",
@@ -915,9 +920,11 @@
"empty_room_was_name": "Pusty pokój (poprzednio %(oldName)s)",
"encryption": {
"access_secret_storage_dialog": {
"alternatives": "Możesz podać również klucz lub frazę bezpieczeństwa.",
"key_validation_text": {
"wrong_security_key": "Wprowadzony klucz przywracania nie jest poprawny."
},
"privacy_warning": "Upewnij się, że nikt nie widzi tego ekranu!",
"restoring": "Przywracanie kluczy z kopii zapasowej",
"security_key_title": "Klucz przywracania"
},
@@ -971,6 +978,8 @@
"setup_secure_backup": {
"explainer": "Utwórz kopię zapasową kluczy przed wylogowaniem, aby ich nie utracić."
},
"turn_on_key_storage": "Włącz magazyn kluczy",
"turn_on_key_storage_description": "Bezpiecznie przechowuj swoją tożsamość kryptograficzną i klucze wiadomości na serwerze. Umożliwi to przeglądanie historii wiadomości na każdym nowym urządzeniu.",
"udd": {
"interactive_verification_button": "Zweryfikuj interaktywnie za pomocą emoji",
"other_ask_verify_text": "Poproś go/ją o zweryfikowanie tej sesji bądź zweryfikuj ją osobiście poniżej.",
@@ -1005,6 +1014,21 @@
"incoming_sas_dialog_waiting": "Oczekiwanie na potwierdzenie partnera…",
"incoming_sas_user_dialog_text_1": "Zweryfikuj tego użytkownika, aby oznaczyć go jako zaufanego. Użytkownicy zaufani dodają większej pewności, gdy korzystasz z wiadomości szyfrowanych end-to-end.",
"incoming_sas_user_dialog_text_2": "Weryfikacja tego użytkownika oznaczy Twoją i jego sesję jako zaufaną.",
"manual": {
"already_verified": "To urządzenie zostało już zweryfikowane.",
"already_verified_and_wrong_fingerprint": "Podany odcinek palca nie pasuje, ale urządzenie zostało już zweryfikowane!",
"device_id": "ID urządzenia",
"failure_description": "Nie udało się zweryfikować '%(deviceId)s': %(error)s",
"failure_title": "Weryfikacja nie powiodła się",
"fingerprint": "Odcisk palca (klucz sesji)",
"no_crypto": "Nie można zweryfikować urządzenia - krypto nie jest włączone",
"no_device": "Nie można zweryfikować urządzenia - urządzenie '%(deviceId)s' nie zostało znalezione",
"no_userid": "Nie można zweryfikować urządzenia - nie udało się znaleźć ID użytkownika",
"success_description": "Urządzenie (%(deviceId)s) jest teraz podpisane krzyżowo",
"success_title": "Zweryfikowano pomyślnie",
"text": "Podaj ID i odcisk palca jednego ze swoich urządzeń, aby zweryfikować. UWAGA umożliwi to drugiemu urządzeniu wysyłanie i odbieranie wiadomości w Twoim imieniu. JEŚLI KTOŚ POWIEDZIAŁ CI, ŻEBYŚ COŚ TU WKLEIŁ, PRAWDOPODOBNIE JESTEŚ OSZUKIWANY!",
"wrong_fingerprint": "Nie można zweryfikować urządzenia '%(deviceId)s' - podany odcisk palca '%(fingerprint)s' nie pasuje do odcisku palca na urządzeniu, '%(fprint)s'"
},
"no_key_or_device": "Wygląda na to, że nie masz klucza przywracania ani żadnych innych urządzeń, które mogłyby zweryfikować Twoją tożsamość. To urządzenie nie będzie mogło odczytać wcześniej zaszyfrowanych wiadomości. Aby zweryfikować swoją tożsamość na tym urządzeniu, musisz zresetować klucze weryfikacyjne.",
"no_support_qr_emoji": "Urządzenie, które próbujesz zweryfikować nie wspiera skanowania kodu QR lub weryfikacji emoji, czyli tego co obsługuje %(brand)s. Spróbuj użyć innego klienta.",
"other_party_cancelled": "Druga strona anulowała weryfikację.",
@@ -1961,6 +1985,7 @@
},
"face_pile_tooltip_shortcut": "Włączając %(commaSeparatedMembers)s",
"face_pile_tooltip_shortcut_joined": "Włączając Ciebie, %(commaSeparatedMembers)s",
"failed_determine_user": "Nie można określić, którego użytkownika należy zignorować, ponieważ jego wydarzenie uległo zmianie.",
"failed_reject_invite": "Nie udało się odrzucić zaproszenia",
"forget_room": "Zapomnij o tym pokoju",
"forget_space": "Zapomnij tą przestrzeń",
@@ -2053,6 +2078,8 @@
"read_topic": "Kliknij, aby przeczytać temat",
"rejecting": "Odrzucanie zaproszenia…",
"rejoin_button": "Dołącz ponownie",
"room_content": "Zawartość pokoju",
"room_is_low_priority": "To jest pokój o niskim priorytecie",
"search": {
"all_rooms_button": "Wyszukaj wszystkie pokoje",
"placeholder": "Szukaj wiadomości...",
@@ -2102,24 +2129,33 @@
"add_space_label": "Dodaj przestrzeń",
"breadcrumbs_empty": "Brak ostatnio odwiedzonych pokojów",
"breadcrumbs_label": "Ostatnio odwiedzane pokoje",
"collapse_filters": "Zwiń listę filtrów",
"empty": {
"no_chats": "Nie ma jeszcze czatów",
"no_chats_description": "Zacznij od wysłania wiadomości lub utworzenia pokoju",
"no_chats_description_no_room_rights": "Wyślij komuś wiadomość, aby rozpocząć.",
"no_favourites": "Nie masz jeszcze ulubionego czatu",
"no_favourites_description": "Dodaj czat do ulubionych w ustawieniach czatu",
"no_invites": "Nie masz żadnych nieprzeczytanych zaproszeń",
"no_lowpriority": "Nie masz pokoi o niskim priorytecie",
"no_mentions": "Nie masz żadnych nieprzeczytanych wzmianek",
"no_people": "Nie prowadzisz jeszcze z nikim czatów prywatnych",
"no_people_description": "Wyczyść filtry, aby zobaczyć pozostałe czaty",
"no_rooms": "Nie jesteś jeszcze w żadnym pokoju",
"no_rooms_description": "Wyczyść filtry, aby zobaczyć pozostałe czaty",
"no_unread": "Brawo! Nie masz żadnych nieprzeczytanych wiadomości",
"show_activity": "Wyświetl całą aktywność",
"show_chats": "Pokaż wszystkie czaty"
},
"expand_filters": "Rozwiń listę filtrów",
"failed_add_tag": "Nie można dodać tagu %(tagName)s do pokoju",
"failed_remove_tag": "Nie udało się usunąć tagu %(tagName)s z pokoju",
"failed_set_dm_tag": "Nie udało się ustawić tagu wiadomości prywatnych",
"filters": {
"favourite": "Ulubione",
"invites": "Zaproszenia",
"low_priority": "Niski priorytet",
"mentions": "Wzmianki",
"people": "Osoby",
"rooms": "Pokoje",
"unread": "Nieprzeczytane"
@@ -2550,6 +2586,8 @@
"session_key": "Klucz sesji:",
"title": "Zaawansowane"
},
"confirm_key_storage_off": "Czy na pewno pozostawić wyłączony magazyn kluczy?",
"confirm_key_storage_off_description": "Jeśli wylogujesz się ze wszystkich swoich urządzeń, utracisz historię wiadomości i będziesz musiał ponownie zweryfikować wszystkie istniejące kontakty. <a>Dowiedz się więcej</a>",
"delete_key_storage": {
"breadcrumb_page": "Usuń magazyn kluczy",
"confirm": "Usuń magazyn kluczy",
@@ -2685,6 +2723,9 @@
"inline_url_previews_room": "Włącz domyślny podgląd URL dla uczestników w tym pokoju",
"inline_url_previews_room_account": "Włącz podgląd URL dla tego pokoju (dotyczy tylko Ciebie)",
"insert_trailing_colon_mentions": "Wstawiaj dwukropek po wzmiance użytkownika na początku wiadomości",
"invite_controls": {
"default_label": "Zezwól użytkownikom na zapraszanie Cię do pokojów"
},
"jump_to_bottom_on_send": "Przejdź na dół osi czasu po wysłaniu wiadomości",
"key_backup": {
"setup_secure_backup": {
@@ -2746,6 +2787,7 @@
"show_in_private": "W pokojach prywatnych",
"show_media": "Zawsze pokazuj"
},
"not_supported": "Twój serwer nie obsługuje tej funkcji.",
"notifications": {
"default_setting_description": "To ustawienie zastosuje się do wszystkich Twoich pokoi.",
"default_setting_section": "Chce otrzymywać powiadomienia (Domyślne ustawienie)",
@@ -2803,6 +2845,7 @@
"voip": "Połączenia audio i wideo"
},
"preferences": {
"Electron.enableContentProtection": "Zapobiegaj przechwytywaniu zawartości okna przez inne aplikacje",
"Electron.enableHardwareAcceleration": "Włącz akcelerację sprzętową (uruchom ponownie %(appName)s, aby zastosować zmiany).",
"always_show_menu_bar": "Zawsze pokazuj pasek menu okna",
"autocomplete_delay": "Opóźnienie autouzupełniania (ms)",
@@ -2811,6 +2854,7 @@
"composer_heading": "Kompozytor",
"default_timezone": "Ustawienie przeglądarki (%(timezone)s)",
"dialog_title": "<strong>Ustawienia:</strong> Preferencje",
"enable_content_protection": "Włącz ochronę treści",
"enable_hardware_acceleration": "Włącz przyspieszenie sprzętowe",
"enable_tray_icon": "Pokaż ikonę w zasobniku systemowym i zminimalizuj okno do niej zamiast zamknięcia",
"keyboard_heading": "Skróty klawiszowe",
@@ -2974,6 +3018,7 @@
"show_chat_effects": "Pokaż efekty czatu (animacje po odebraniu np. confetti)",
"show_displayname_changes": "Pokaż zmiany wyświetlanej nazwy",
"show_join_leave": "Pokaż wiadomości dołączenia/opuszczenia pokoju (nie dotyczy wiadomości zaproszenia/wyrzucenia/banów)",
"show_message_previews": "Pokaż podglądy wiadomości",
"show_nsfw_content": "Pokaż zawartość NSFW",
"show_read_receipts": "Pokaż potwierdzenia odczytania wysyłane przez innych użytkowników",
"show_redaction_placeholder": "Pokaż symbol zastępczy dla usuniętych wiadomości",
@@ -3080,6 +3125,8 @@
"jumptodate": "Przeskocz do podanej daty w linii czasu",
"jumptodate_invalid_input": "Nie jesteśmy w stanie rozpoznać podanej daty (%(inputDate)s). Spróbuj użyć formatu RRRR-MM-DD.",
"lenny": "Dodaje ( ͡° ͜ʖ ͡°) na początku wiadomości tekstowej",
"manual_device_verification_confirm_description": "Umożliwi to drugiemu urządzeniu wysyłanie i odbieranie wiadomości w Twoim imieniu. JEŚLI KTOŚ POWIEDZIAŁ CI, ŻEBYŚ COŚ TU WKLEIŁ, PRAWDOPODOBNIE JESTEŚ OSZUKIWANY! Czy na pewno chcesz zweryfikować drugie urządzenie?",
"manual_device_verification_confirm_title": "Uwaga: ręczna weryfikacja urządzenia",
"me": "Wyświetla akcję",
"msg": "Wysyła wiadomość do wybranego użytkownika",
"myavatar": "Zdjęcie profilowe zostanie zmienione we wszystkich pokojach",
@@ -3120,7 +3167,7 @@
"upgraderoom": "Ulepsza pokój do nowej wersji",
"upgraderoom_permission_error": "Nie posiadasz wymaganych uprawnień do użycia tego polecenia.",
"usage": "Użycie",
"verify": "Weryfikuje użytkownika, sesję oraz klucz publiczny",
"verify": "Zweryfikuj ręcznie jedno ze swoich urządzeń",
"view": "Przegląda pokój z podanym adresem",
"whois": "Pokazuje informacje na temat użytkownika"
},
@@ -3340,7 +3387,6 @@
"unable_to_decrypt": "Nie można rozszyfrować wiadomości"
},
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Rozszyfrowuję",
"download_action_downloading": "Pobieranie",
"download_failed": "Błąd pobierania",
"download_failed_description": "Wystąpił błąd podczas pobierania tego pliku",

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