Merge branch 'element-hq:master' into master
@@ -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'] }}"
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ const config: StorybookConfig = {
|
||||
},
|
||||
// Needed for counterpart to work
|
||||
plugins: [nodePolyfills({ include: ["process", "util"] })],
|
||||
server: {
|
||||
allowedHosts: ["localhost", ".docker.internal"],
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.0 KiB |
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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ě",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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. Κάντε κλικ για να δείτε τις τροποποιήσεις.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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. לחץ לצפייה בעריכות.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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に編集済。クリックすると変更履歴を表示。",
|
||||
|
||||
@@ -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 . დააწკაპუნეთ რედაქტირების სანახავად.",
|
||||
|
||||
@@ -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. ກົດເພື່ອເບິ່ງການແກ້ໄຂ.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||