Merge branch 'element-hq:master' into master
@@ -139,3 +139,16 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
repository: vectorim/element-web
|
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
|
// Needed for counterpart to work
|
||||||
plugins: [nodePolyfills({ include: ["process", "util"] })],
|
plugins: [nodePolyfills({ include: ["process", "util"] })],
|
||||||
|
server: {
|
||||||
|
allowedHosts: ["localhost", ".docker.internal"],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import "../res/css/shared.pcss";
|
|||||||
import "./preview.css";
|
import "./preview.css";
|
||||||
import React, { useLayoutEffect } from "react";
|
import React, { useLayoutEffect } from "react";
|
||||||
import { FORCE_RE_RENDER } from "storybook/internal/core-events";
|
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 = {
|
export const globalTypes = {
|
||||||
theme: {
|
theme: {
|
||||||
@@ -82,9 +83,24 @@ export const withLanguageProvider: Decorator = (Story, context) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const withTooltipProvider: Decorator = (Story) => {
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Story />
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const preview: Preview = {
|
const preview: Preview = {
|
||||||
tags: ["autodocs"],
|
tags: ["autodocs"],
|
||||||
decorators: [withThemeProvider, withLanguageProvider],
|
decorators: [withThemeProvider, withLanguageProvider, withTooltipProvider],
|
||||||
|
parameters: {
|
||||||
|
options: {
|
||||||
|
storySort: {
|
||||||
|
method: "alphabetical",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default preview;
|
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)
|
Changes in [1.11.108](https://github.com/element-hq/element-web/releases/tag/v1.11.108) (2025-07-30)
|
||||||
====================================================================================================
|
====================================================================================================
|
||||||
## 🐛 Bug Fixes
|
## 🐛 Bug Fixes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# syntax=docker.io/docker/dockerfile:1.17-labs@sha256:9187104f31e3a002a8a6a3209ea1f937fb7486c093cbbde1e14b0fa0d7e4f1b5
|
# syntax=docker.io/docker/dockerfile:1.17-labs@sha256:9187104f31e3a002a8a6a3209ea1f937fb7486c093cbbde1e14b0fa0d7e4f1b5
|
||||||
|
|
||||||
# Builder
|
# 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.
|
# Support custom branch of the js-sdk. This also helps us build images of element-web develop.
|
||||||
ARG USE_CUSTOM_SDKS=false
|
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
|
RUN cp /src/config.sample.json /src/webapp/config.json
|
||||||
|
|
||||||
# App
|
# 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
|
# Need root user to install packages & manipulate the usr directory
|
||||||
USER root
|
USER root
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ const config: Config = {
|
|||||||
setupFiles: ["jest-canvas-mock", "web-streams-polyfill/polyfill"],
|
setupFiles: ["jest-canvas-mock", "web-streams-polyfill/polyfill"],
|
||||||
setupFilesAfterEnv: ["<rootDir>/test/setupTests.ts"],
|
setupFilesAfterEnv: ["<rootDir>/test/setupTests.ts"],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
|
// Support CSS module
|
||||||
|
"\\.(module.css)$": "identity-obj-proxy",
|
||||||
"\\.(css|scss|pcss)$": "<rootDir>/__mocks__/cssMock.js",
|
"\\.(css|scss|pcss)$": "<rootDir>/__mocks__/cssMock.js",
|
||||||
"\\.(gif|png|ttf|woff2)$": "<rootDir>/__mocks__/imageMock.js",
|
"\\.(gif|png|ttf|woff2)$": "<rootDir>/__mocks__/imageMock.js",
|
||||||
"\\.svg$": "<rootDir>/__mocks__/svg.js",
|
"\\.svg$": "<rootDir>/__mocks__/svg.js",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "element-web",
|
"name": "element-web",
|
||||||
"version": "1.11.108",
|
"version": "1.11.109",
|
||||||
"description": "Element: the future of secure communication",
|
"description": "Element: the future of secure communication",
|
||||||
"author": "New Vector Ltd.",
|
"author": "New Vector Ltd.",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -69,11 +69,12 @@
|
|||||||
"storybook": "storybook dev -p 6007",
|
"storybook": "storybook dev -p 6007",
|
||||||
"build-storybook": "storybook build",
|
"build-storybook": "storybook build",
|
||||||
"test:storybook": "test-storybook --url http://localhost:6007/",
|
"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": {
|
"resolutions": {
|
||||||
"**/pretty-format/react-is": "19.1.0",
|
"**/pretty-format/react-is": "19.1.0",
|
||||||
"@playwright/test": "1.53.2",
|
"@playwright/test": "1.54.1",
|
||||||
"@types/react": "19.1.8",
|
"@types/react": "19.1.8",
|
||||||
"@types/react-dom": "19.1.6",
|
"@types/react-dom": "19.1.6",
|
||||||
"oidc-client-ts": "3.3.0",
|
"oidc-client-ts": "3.3.0",
|
||||||
@@ -98,7 +99,7 @@
|
|||||||
"@types/react-virtualized": "^9.21.30",
|
"@types/react-virtualized": "^9.21.30",
|
||||||
"@vector-im/compound-design-tokens": "^5.0.0",
|
"@vector-im/compound-design-tokens": "^5.0.0",
|
||||||
"@vector-im/compound-web": "^8.1.2",
|
"@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/core": "^3.0.4",
|
||||||
"@zxcvbn-ts/language-common": "^3.0.4",
|
"@zxcvbn-ts/language-common": "^3.0.4",
|
||||||
"@zxcvbn-ts/language-en": "^3.0.2",
|
"@zxcvbn-ts/language-en": "^3.0.2",
|
||||||
@@ -116,7 +117,7 @@
|
|||||||
"emojibase-regex": "15.3.2",
|
"emojibase-regex": "15.3.2",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"filesize": "10.1.6",
|
"filesize": "11.0.2",
|
||||||
"github-markdown-css": "^5.5.1",
|
"github-markdown-css": "^5.5.1",
|
||||||
"glob-to-regexp": "^0.4.1",
|
"glob-to-regexp": "^0.4.1",
|
||||||
"highlight.js": "^11.3.1",
|
"highlight.js": "^11.3.1",
|
||||||
@@ -127,14 +128,14 @@
|
|||||||
"jsrsasign": "^11.0.0",
|
"jsrsasign": "^11.0.0",
|
||||||
"jszip": "^3.7.0",
|
"jszip": "^3.7.0",
|
||||||
"katex": "^0.16.0",
|
"katex": "^0.16.0",
|
||||||
"linkify-react": "4.3.1",
|
"linkify-react": "4.3.2",
|
||||||
"linkify-string": "4.3.1",
|
"linkify-string": "4.3.2",
|
||||||
"linkifyjs": "4.3.1",
|
"linkifyjs": "4.3.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"maplibre-gl": "^5.0.0",
|
"maplibre-gl": "^5.0.0",
|
||||||
"matrix-encrypt-attachment": "^1.0.3",
|
"matrix-encrypt-attachment": "^1.0.3",
|
||||||
"matrix-events-sdk": "0.0.1",
|
"matrix-events-sdk": "0.0.1",
|
||||||
"matrix-js-sdk": "37.12.0",
|
"matrix-js-sdk": "37.13.0",
|
||||||
"matrix-widget-api": "^1.10.0",
|
"matrix-widget-api": "^1.10.0",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
"mime": "^4.0.4",
|
"mime": "^4.0.4",
|
||||||
@@ -142,7 +143,7 @@
|
|||||||
"opus-recorder": "^8.0.3",
|
"opus-recorder": "^8.0.3",
|
||||||
"pako": "^2.0.3",
|
"pako": "^2.0.3",
|
||||||
"png-chunks-extract": "^1.0.0",
|
"png-chunks-extract": "^1.0.0",
|
||||||
"posthog-js": "1.256.2",
|
"posthog-js": "1.257.0",
|
||||||
"qrcode": "1.5.4",
|
"qrcode": "1.5.4",
|
||||||
"re-resizable": "6.11.2",
|
"re-resizable": "6.11.2",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
@@ -153,6 +154,7 @@
|
|||||||
"react-string-replace": "^1.1.1",
|
"react-string-replace": "^1.1.1",
|
||||||
"react-transition-group": "^4.4.1",
|
"react-transition-group": "^4.4.1",
|
||||||
"react-virtualized": "^9.22.5",
|
"react-virtualized": "^9.22.5",
|
||||||
|
"react-virtuoso": "^4.12.6",
|
||||||
"rfc4648": "^1.4.0",
|
"rfc4648": "^1.4.0",
|
||||||
"sanitize-filename": "^1.6.3",
|
"sanitize-filename": "^1.6.3",
|
||||||
"sanitize-html": "2.17.0",
|
"sanitize-html": "2.17.0",
|
||||||
@@ -185,12 +187,12 @@
|
|||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@casualbot/jest-sonar-reporter": "2.2.7",
|
"@casualbot/jest-sonar-reporter": "2.2.7",
|
||||||
"@element-hq/element-call-embedded": "0.13.1",
|
"@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",
|
"@peculiar/webcrypto": "^1.4.3",
|
||||||
"@playwright/test": "^1.50.1",
|
"@playwright/test": "^1.50.1",
|
||||||
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
|
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
|
||||||
"@rrweb/types": "^2.0.0-alpha.18",
|
"@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-designs": "^10.0.1",
|
||||||
"@storybook/addon-docs": "^9.0.12",
|
"@storybook/addon-docs": "^9.0.12",
|
||||||
"@storybook/icons": "^1.4.0",
|
"@storybook/icons": "^1.4.0",
|
||||||
@@ -264,6 +266,7 @@
|
|||||||
"file-loader": "^6.0.0",
|
"file-loader": "^6.0.0",
|
||||||
"html-webpack-plugin": "^5.5.3",
|
"html-webpack-plugin": "^5.5.3",
|
||||||
"husky": "^9.0.0",
|
"husky": "^9.0.0",
|
||||||
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jest": "^29.6.2",
|
"jest": "^29.6.2",
|
||||||
"jest-canvas-mock": "^2.5.2",
|
"jest-canvas-mock": "^2.5.2",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
|
|||||||
@@ -124,6 +124,10 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
|||||||
const toasts = new Toasts(page);
|
const toasts = new Toasts(page);
|
||||||
await toasts.rejectToast("Notifications");
|
await toasts.rejectToast("Notifications");
|
||||||
await toasts.assertNoToasts();
|
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 }) => {
|
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 }) => {
|
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++) {
|
for (let i = 1; i <= 10; i++) {
|
||||||
const displayName = `Charly #${i}`;
|
const displayName = `Charly #${i}`;
|
||||||
const bot = new Bot(page, homeserver, { displayName, startClient: false, autoAcceptInvites: false });
|
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");
|
await expect(page.getByRole("menu")).toMatchScreenshot("room-list-header-compose-menu.png");
|
||||||
|
|
||||||
// New message should open the direct messages dialog
|
// Start chat should open the direct messages dialog
|
||||||
await page.getByRole("menuitem", { name: "New message" }).click();
|
await page.getByRole("menuitem", { name: "Start chat" }).click();
|
||||||
await expect(page.getByRole("heading", { name: "Direct Messages" })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Direct Messages" })).toBeVisible();
|
||||||
await app.closeDialog();
|
await app.closeDialog();
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,32 @@ import { Bot } from "../../pages/bot";
|
|||||||
const ROOM_NAME = "Test room";
|
const ROOM_NAME = "Test room";
|
||||||
const NAME = "Alice";
|
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({
|
test.use({
|
||||||
synapseConfig: {
|
synapseConfig: {
|
||||||
presence: {
|
presence: {
|
||||||
@@ -25,17 +51,8 @@ test.use({
|
|||||||
test.describe("Memberlist", () => {
|
test.describe("Memberlist", () => {
|
||||||
test.beforeEach(async ({ app, user, page, homeserver }, testInfo) => {
|
test.beforeEach(async ({ app, user, page, homeserver }, testInfo) => {
|
||||||
testInfo.setTimeout(testInfo.timeout + 30_000);
|
testInfo.setTimeout(testInfo.timeout + 30_000);
|
||||||
const id = await app.client.createRoom({ name: ROOM_NAME });
|
|
||||||
const newBots: Bot[] = [];
|
|
||||||
const names = ["Bob", "Bob", "Susan"];
|
const names = ["Bob", "Bob", "Susan"];
|
||||||
for (let i = 0; i < 3; i++) {
|
await setupRoomWithMembers(app, page, homeserver, ROOM_NAME, names);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Renders correctly", { tag: "@screenshot" }, async ({ page, app }) => {
|
test("Renders correctly", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||||
@@ -45,4 +62,37 @@ test.describe("Memberlist", () => {
|
|||||||
await expect(memberlist.getByText("Invited")).toHaveCount(1);
|
await expect(memberlist.getByText("Invited")).toHaveCount(1);
|
||||||
await expect(page.locator(".mx_MemberListView")).toMatchScreenshot("with-four-members.png");
|
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();
|
const rightPanel = await app.toggleRoomInfoPanel();
|
||||||
await rightPanel.getByRole("menuitem", { name: "People" }).click();
|
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();
|
await rightPanel.getByRole("button", { name: "Share profile" }).click();
|
||||||
|
|
||||||
const dialog = page.getByRole("dialog", { name: "Share User" });
|
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");
|
return page.locator(".mx_SpacePanel_contextMenu");
|
||||||
}
|
}
|
||||||
|
|
||||||
function spaceCreateOptions(spaceName: string, roomIds: string[] = []): ICreateRoomOpts {
|
function spaceCreateOptions(serverName: string, spaceName: string, roomIds: string[] = []): ICreateRoomOpts {
|
||||||
return {
|
return {
|
||||||
creation_content: {
|
creation_content: {
|
||||||
type: "m.space",
|
type: "m.space",
|
||||||
@@ -35,17 +35,21 @@ function spaceCreateOptions(spaceName: string, roomIds: string[] = []): ICreateR
|
|||||||
name: spaceName,
|
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 {
|
return {
|
||||||
type: "m.space.child",
|
type: "m.space.child",
|
||||||
state_key: roomId,
|
state_key: roomId,
|
||||||
content: {
|
content: {
|
||||||
via: [roomId.split(":")[1]],
|
via: [serverName],
|
||||||
order,
|
order,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -240,7 +244,7 @@ test.describe("Spaces", () => {
|
|||||||
});
|
});
|
||||||
await expect(await app.getSpacePanelButton("My Space")).toBeVisible();
|
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);
|
await bot.inviteUser(roomId, user.userId);
|
||||||
|
|
||||||
// Assert that `Space Space` is above `My Space` due to it being an invite
|
// 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";
|
const spaceName = "Spacey Mc. Space Space";
|
||||||
await app.client.createSpace({
|
await app.client.createSpace({
|
||||||
name: spaceName,
|
name: spaceName,
|
||||||
initial_state: [spaceChildInitialState(roomId1), spaceChildInitialState(roomId2)],
|
initial_state: [
|
||||||
|
spaceChildInitialState(user.homeServer, roomId1),
|
||||||
|
spaceChildInitialState(user.homeServer, roomId2),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
await app.viewSpaceHomeByName(spaceName);
|
await app.viewSpaceHomeByName(spaceName);
|
||||||
@@ -287,7 +294,7 @@ test.describe("Spaces", () => {
|
|||||||
});
|
});
|
||||||
await app.client.createSpace({
|
await app.client.createSpace({
|
||||||
name: "Root Space",
|
name: "Root Space",
|
||||||
initial_state: [spaceChildInitialState(childSpaceId)],
|
initial_state: [spaceChildInitialState(user.homeServer, childSpaceId)],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Find collapsed Space panel
|
// Find collapsed Space panel
|
||||||
@@ -323,7 +330,7 @@ test.describe("Spaces", () => {
|
|||||||
name: "Test Room",
|
name: "Test Room",
|
||||||
topic: "This is a topic https://github.com/matrix-org/matrix-react-sdk/pull/10060 with a link",
|
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 bot.inviteUser(spaceId, user.userId);
|
||||||
|
|
||||||
await expect(await app.getSpacePanelButton("Test Space")).toBeVisible();
|
await expect(await app.getSpacePanelButton("Test Space")).toBeVisible();
|
||||||
@@ -361,9 +368,9 @@ test.describe("Spaces", () => {
|
|||||||
await app.client.createSpace({
|
await app.client.createSpace({
|
||||||
name: "Root Space",
|
name: "Root Space",
|
||||||
initial_state: [
|
initial_state: [
|
||||||
spaceChildInitialState(childSpaceId1, "a"),
|
spaceChildInitialState(user.homeServer, childSpaceId1, "a"),
|
||||||
spaceChildInitialState(childSpaceId2, "b"),
|
spaceChildInitialState(user.homeServer, childSpaceId2, "b"),
|
||||||
spaceChildInitialState(childSpaceId3, "c"),
|
spaceChildInitialState(user.homeServer, childSpaceId3, "c"),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
await app.viewSpaceByName("Root Space");
|
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";
|
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,
|
* SynapseContainer which freezes the docker digest to stabilise tests,
|
||||||
|
|||||||
@@ -53,8 +53,6 @@
|
|||||||
@import "./components/views/settings/shared/_SettingsSubsectionHeading.pcss";
|
@import "./components/views/settings/shared/_SettingsSubsectionHeading.pcss";
|
||||||
@import "./components/views/spaces/_QuickThemeSwitcher.pcss";
|
@import "./components/views/spaces/_QuickThemeSwitcher.pcss";
|
||||||
@import "./components/views/typography/_Caption.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/_Icon.pcss";
|
||||||
@import "./compound/_SuccessDialog.pcss";
|
@import "./compound/_SuccessDialog.pcss";
|
||||||
@import "./structures/_AutoHideScrollbar.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);
|
color: var(--cpd-color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Flex {
|
.mx_ErrorView_flexContainer {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
max-width: max-content;
|
max-width: max-content;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
color: var(--cpd-color-text-secondary);
|
color: var(--cpd-color-text-secondary);
|
||||||
|
|
||||||
.mx_Box {
|
.mx_RoomSummaryCard_topic_box {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -108,11 +108,6 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: $font-20px;
|
font-size: $font-20px;
|
||||||
line-height: $font-25px;
|
line-height: $font-25px;
|
||||||
|
|
||||||
/* E2E icon wrapper */
|
|
||||||
.mx_Flex > span {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_UserInfo_profile_name {
|
.mx_UserInfo_profile_name {
|
||||||
|
|||||||
@@ -12,10 +12,6 @@
|
|||||||
border-bottom: var(--cpd-border-width-1) solid var(--cpd-color-bg-subtle-primary);
|
border-bottom: var(--cpd-border-width-1) solid var(--cpd-color-bg-subtle-primary);
|
||||||
padding: 0 var(--cpd-space-3x);
|
padding: 0 var(--cpd-space-3x);
|
||||||
|
|
||||||
svg {
|
|
||||||
fill: var(--cpd-color-icon-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomListSearch_search {
|
.mx_RoomListSearch_search {
|
||||||
/* The search button should take all the remaining space */
|
/* The search button should take all the remaining space */
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -23,6 +19,10 @@
|
|||||||
color: var(--cpd-color-text-secondary);
|
color: var(--cpd-color-text-secondary);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: var(--cpd-color-icon-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
flex: 1;
|
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();
|
await getStoredSessionVars();
|
||||||
|
|
||||||
if (hasAccessToken && !accessToken) {
|
if (hasAccessToken && !accessToken) {
|
||||||
|
logger.warn(
|
||||||
|
"restoreSessionFromStorage: storage indicates we should have an access token, but we do not. Displaying StorageEvictedDialog",
|
||||||
|
);
|
||||||
await abortLogin();
|
await abortLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -823,6 +826,7 @@ async function doSetLoggedIn(
|
|||||||
// crypto store, we'll be generally confused when handling encrypted data.
|
// crypto store, we'll be generally confused when handling encrypted data.
|
||||||
// Show a modal recommending a full reset of storage.
|
// Show a modal recommending a full reset of storage.
|
||||||
if (results.dataInLocalStorage && results.cryptoInited && !results.dataInCryptoStore) {
|
if (results.dataInLocalStorage && results.cryptoInited && !results.dataInCryptoStore) {
|
||||||
|
logger.warn("doSetLoggedIn: StorageManager consistency check failed; displaying StorageEvictedDialog.");
|
||||||
await abortLogin();
|
await abortLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export const Commands = [
|
|||||||
command: "upgraderoom",
|
command: "upgraderoom",
|
||||||
args: "<new_version>",
|
args: "<new_version>",
|
||||||
description: _td("slash_command|upgraderoom"),
|
description: _td("slash_command|upgraderoom"),
|
||||||
isEnabled: (cli) => !isCurrentLocalRoom(cli) && SettingsStore.getValue("developerMode"),
|
isEnabled: (cli) => !isCurrentLocalRoom(cli),
|
||||||
runFn: function (cli, roomId, threadId, args) {
|
runFn: function (cli, roomId, threadId, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
const room = cli.getRoom(roomId);
|
const room = cli.getRoom(roomId);
|
||||||
|
|||||||
@@ -572,8 +572,11 @@ function textForPinnedEvent(event: MatrixEvent, client: MatrixClient, allowJSX:
|
|||||||
const senderName = getSenderName(event);
|
const senderName = getSenderName(event);
|
||||||
const roomId = event.getRoomId()!;
|
const roomId = event.getRoomId()!;
|
||||||
|
|
||||||
const pinned = event.getContent<{ pinned: string[] }>().pinned ?? [];
|
const content = event.getContent<{ pinned: string[] }>();
|
||||||
const previouslyPinned: string[] = event.getPrevContent().pinned ?? [];
|
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 newlyPinned = pinned.filter((item) => previouslyPinned.indexOf(item) < 0);
|
||||||
const newlyUnpinned = previouslyPinned.filter((item) => pinned.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;
|
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 {
|
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 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 { IS_MAC, IS_ELECTRON, Key } from "../Keyboard";
|
||||||
import { type IBaseSetting } from "../settings/Settings";
|
import { type IBaseSetting } from "../settings/Settings";
|
||||||
import { type KeyCombo } from "../KeyBindingsManager";
|
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 PopOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/pop-out";
|
||||||
|
|
||||||
import SdkConfig from "../../SdkConfig";
|
import SdkConfig from "../../SdkConfig";
|
||||||
import { Flex } from "../../components/utils/Flex";
|
import { Flex } from "../../shared-components/utils/Flex";
|
||||||
import { _t } from "../../languageHandler";
|
import { _t } from "../../languageHandler";
|
||||||
import { Icon as AppleIcon } from "../../../res/themes/element/img/compound/apple.svg";
|
import { Icon as AppleIcon } from "../../../res/themes/element/img/compound/apple.svg";
|
||||||
import { Icon as MicrosoftIcon } from "../../../res/themes/element/img/compound/microsoft.svg";
|
import { Icon as MicrosoftIcon } from "../../../res/themes/element/img/compound/microsoft.svg";
|
||||||
@@ -58,7 +58,7 @@ const MobileAppLinks: React.FC<{
|
|||||||
googlePlayUrl?: string;
|
googlePlayUrl?: string;
|
||||||
fdroidUrl?: string;
|
fdroidUrl?: string;
|
||||||
}> = ({ appleAppStoreUrl, googlePlayUrl, fdroidUrl }) => (
|
}> = ({ appleAppStoreUrl, googlePlayUrl, fdroidUrl }) => (
|
||||||
<Flex gap="var(--cpd-space-6x)">
|
<Flex gap="var(--cpd-space-6x)" className="mx_ErrorView_flexContainer">
|
||||||
{appleAppStoreUrl && (
|
{appleAppStoreUrl && (
|
||||||
<a href={appleAppStoreUrl} target="_blank" rel="noreferrer noopener">
|
<a href={appleAppStoreUrl} target="_blank" rel="noreferrer noopener">
|
||||||
<img height="64" src="themes/element/img/download/apple.svg" alt="Apple App Store" />
|
<img height="64" src="themes/element/img/download/apple.svg" alt="Apple App Store" />
|
||||||
@@ -84,7 +84,7 @@ const DesktopAppLinks: React.FC<{
|
|||||||
linuxUrl?: string;
|
linuxUrl?: string;
|
||||||
}> = ({ macOsUrl, win64Url, win64ArmUrl, linuxUrl }) => {
|
}> = ({ macOsUrl, win64Url, win64ArmUrl, linuxUrl }) => {
|
||||||
return (
|
return (
|
||||||
<Flex gap="var(--cpd-space-4x)">
|
<Flex gap="var(--cpd-space-4x)" className="mx_ErrorView_flexContainer">
|
||||||
{macOsUrl && (
|
{macOsUrl && (
|
||||||
<Button as="a" href={macOsUrl} kind="secondary" Icon={AppleIcon}>
|
<Button as="a" href={macOsUrl} kind="secondary" Icon={AppleIcon}>
|
||||||
{_t("incompatible_browser|macos")}
|
{_t("incompatible_browser|macos")}
|
||||||
@@ -193,7 +193,7 @@ export const UnsupportedBrowserView: React.FC<{
|
|||||||
)}
|
)}
|
||||||
</Text>
|
</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">
|
<Button Icon={PopOutIcon} kind="secondary" size="sm">
|
||||||
{_t("incompatible_browser|learn_more")}
|
{_t("incompatible_browser|learn_more")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ import { type OpenForwardDialogPayload } from "../../dispatcher/payloads/OpenFor
|
|||||||
import { ShareFormat, type SharePayload } from "../../dispatcher/payloads/SharePayload";
|
import { ShareFormat, type SharePayload } from "../../dispatcher/payloads/SharePayload";
|
||||||
import Markdown from "../../Markdown";
|
import Markdown from "../../Markdown";
|
||||||
import { sanitizeHtmlParams } from "../../Linkify";
|
import { sanitizeHtmlParams } from "../../Linkify";
|
||||||
|
import { isOnlyAdmin } from "../../utils/membership";
|
||||||
|
|
||||||
// legacy export
|
// legacy export
|
||||||
export { default as Views } from "../../Views";
|
export { default as Views } from "../../Views";
|
||||||
@@ -1255,29 +1256,22 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
if (client && roomToLeave) {
|
if (client && roomToLeave) {
|
||||||
const plEvent = roomToLeave.currentState.getStateEvents(EventType.RoomPowerLevels, "");
|
// If the user is the only user with highest power level
|
||||||
const plContent = plEvent ? plEvent.getContent() : {};
|
if (isOnlyAdmin(roomToLeave)) {
|
||||||
const userLevels = plContent.users || {};
|
const userLevelValues = roomToLeave.getJoinedMembers().map((m) => m.powerLevel);
|
||||||
const currentUserLevel = userLevels[client.getUserId()!];
|
|
||||||
const userLevelValues = Object.values(userLevels);
|
|
||||||
if (userLevelValues.every((x) => typeof x === "number")) {
|
|
||||||
const maxUserLevel = Math.max(...(userLevelValues as number[]));
|
const maxUserLevel = Math.max(...(userLevelValues as number[]));
|
||||||
// If the user is the only user with highest power level
|
|
||||||
if (
|
const warning =
|
||||||
maxUserLevel === currentUserLevel &&
|
maxUserLevel >= 100
|
||||||
userLevelValues.lastIndexOf(maxUserLevel) == userLevelValues.indexOf(maxUserLevel)
|
? _t("leave_room_dialog|room_leave_admin_warning")
|
||||||
) {
|
: _t("leave_room_dialog|room_leave_mod_warning");
|
||||||
const warning =
|
warnings.push(
|
||||||
maxUserLevel >= 100
|
<strong className="warning" key="last_admin_warning">
|
||||||
? _t("leave_room_dialog|room_leave_admin_warning")
|
{" " /* Whitespace, otherwise the sentences get smashed together */}
|
||||||
: _t("leave_room_dialog|room_leave_mod_warning");
|
{warning}
|
||||||
warnings.push(
|
</strong>,
|
||||||
<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 ThreePIDInvite } from "../../../models/rooms/ThreePIDInvite";
|
||||||
import { type XOR } from "../../../@types/common";
|
import { type XOR } from "../../../@types/common";
|
||||||
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
|
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
|
||||||
|
import { Action } from "../../../dispatcher/actions";
|
||||||
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
|
|
||||||
type Member = XOR<{ member: RoomMember }, { threePidInvite: ThreePIDInvite }>;
|
type Member = XOR<{ member: RoomMember }, { threePidInvite: ThreePIDInvite }>;
|
||||||
|
|
||||||
@@ -111,6 +113,7 @@ export interface MemberListViewState {
|
|||||||
shouldShowSearch: boolean;
|
shouldShowSearch: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
canInvite: boolean;
|
canInvite: boolean;
|
||||||
|
onClickMember: (member: RoomMember | ThreePIDInvite) => void;
|
||||||
onInviteButtonClick: (ev: ButtonEvent) => void;
|
onInviteButtonClick: (ev: ButtonEvent) => void;
|
||||||
}
|
}
|
||||||
export function useMemberListViewModel(roomId: string): MemberListViewState {
|
export function useMemberListViewModel(roomId: string): MemberListViewState {
|
||||||
@@ -133,6 +136,14 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
|
|||||||
*/
|
*/
|
||||||
const [memberCount, setMemberCount] = useState(0);
|
const [memberCount, setMemberCount] = useState(0);
|
||||||
|
|
||||||
|
const onClickMember = (member: RoomMember | ThreePIDInvite): void => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: Action.ViewUser,
|
||||||
|
member: member,
|
||||||
|
push: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const loadMembers = useMemo(
|
const loadMembers = useMemo(
|
||||||
() =>
|
() =>
|
||||||
throttle(
|
throttle(
|
||||||
@@ -267,6 +278,7 @@ export function useMemberListViewModel(roomId: string): MemberListViewState {
|
|||||||
isPresenceEnabled,
|
isPresenceEnabled,
|
||||||
isLoading,
|
isLoading,
|
||||||
onInviteButtonClick,
|
onInviteButtonClick,
|
||||||
|
onClickMember,
|
||||||
shouldShowSearch: totalMemberCount >= 20,
|
shouldShowSearch: totalMemberCount >= 20,
|
||||||
canInvite,
|
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.
|
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 { RoomStateEvent, type MatrixEvent, EventType } from "matrix-js-sdk/src/matrix";
|
||||||
import { type UserVerificationStatus, CryptoEvent } from "matrix-js-sdk/src/crypto-api";
|
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 { getUserDeviceIds } from "../../../../utils/crypto/deviceInfo";
|
||||||
import { type RoomMember } from "../../../../models/rooms/RoomMember";
|
import { type RoomMember } from "../../../../models/rooms/RoomMember";
|
||||||
import { _t, _td, type TranslationKey } from "../../../../languageHandler";
|
import { _t, _td, type TranslationKey } from "../../../../languageHandler";
|
||||||
import UserIdentifierCustomisations from "../../../../customisations/UserIdentifier";
|
|
||||||
import { E2EStatus } from "../../../../utils/ShieldUtils";
|
import { E2EStatus } from "../../../../utils/ShieldUtils";
|
||||||
|
|
||||||
interface MemberTileViewModelProps {
|
interface MemberTileViewModelProps {
|
||||||
@@ -28,16 +27,17 @@ export interface MemberTileViewState extends MemberTileViewModelProps {
|
|||||||
e2eStatus?: E2EStatus;
|
e2eStatus?: E2EStatus;
|
||||||
name: string;
|
name: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
title?: string;
|
|
||||||
userLabel?: string;
|
userLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PowerStatus {
|
export enum PowerStatus {
|
||||||
|
Creator = "creator",
|
||||||
Admin = "admin",
|
Admin = "admin",
|
||||||
Moderator = "moderator",
|
Moderator = "moderator",
|
||||||
}
|
}
|
||||||
|
|
||||||
const PowerLabel: Record<PowerStatus, TranslationKey> = {
|
const PowerLabel: Record<PowerStatus, TranslationKey> = {
|
||||||
|
[PowerStatus.Creator]: _td("power_level|creator"),
|
||||||
[PowerStatus.Admin]: _td("power_level|admin"),
|
[PowerStatus.Admin]: _td("power_level|admin"),
|
||||||
[PowerStatus.Moderator]: _td("power_level|moderator"),
|
[PowerStatus.Moderator]: _td("power_level|moderator"),
|
||||||
};
|
};
|
||||||
@@ -117,6 +117,7 @@ export function useMemberTileViewModel(props: MemberTileViewModelProps): MemberT
|
|||||||
const name = props.member.name;
|
const name = props.member.name;
|
||||||
|
|
||||||
const powerStatusMap = new Map([
|
const powerStatusMap = new Map([
|
||||||
|
[Infinity, PowerStatus.Creator],
|
||||||
[100, PowerStatus.Admin],
|
[100, PowerStatus.Admin],
|
||||||
[50, PowerStatus.Moderator],
|
[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;
|
let userLabel;
|
||||||
const powerStatus = powerStatusMap.get(powerLevel);
|
const powerStatus = powerStatusMap.get(powerLevel);
|
||||||
if (powerStatus) {
|
if (powerStatus) {
|
||||||
@@ -149,7 +141,6 @@ export function useMemberTileViewModel(props: MemberTileViewModelProps): MemberT
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
|
||||||
member,
|
member,
|
||||||
name,
|
name,
|
||||||
onClick,
|
onClick,
|
||||||
|
|||||||
@@ -290,7 +290,7 @@ export default class LoginWithQRFlow extends React.Component<Props> {
|
|||||||
data-testid="back-button"
|
data-testid="back-button"
|
||||||
className="mx_LoginWithQR_BackButton"
|
className="mx_LoginWithQR_BackButton"
|
||||||
onClick={this.handleClick(Click.Back)}
|
onClick={this.handleClick(Click.Back)}
|
||||||
title="Back"
|
title={_t("action|back")}
|
||||||
>
|
>
|
||||||
<ChevronLeftIcon />
|
<ChevronLeftIcon />
|
||||||
</AccessibleButton>
|
</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 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 BusyIcon from "@vector-im/compound-design-tokens/assets/web/icons/presence-strikethrough-8x8";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { Tooltip } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import RoomAvatar from "./RoomAvatar";
|
import RoomAvatar from "./RoomAvatar";
|
||||||
import { AvatarBadgeDecoration, useRoomAvatarViewModel } from "../../viewmodels/avatars/RoomAvatarViewModel";
|
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} />;
|
if (!vm.badgeDecoration) return <RoomAvatar size="32px" room={room} />;
|
||||||
|
|
||||||
const icon = getAvatarDecoration(vm.badgeDecoration, vm.presence);
|
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
|
// Presence indicator and video/public icons don't have the same size
|
||||||
// We use different masks
|
// We use different masks
|
||||||
@@ -48,22 +50,15 @@ export function RoomAvatarView({ room }: RoomAvatarViewProps): JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<div className="mx_RoomAvatarView">
|
<div className="mx_RoomAvatarView">
|
||||||
<RoomAvatar className={classNames("mx_RoomAvatarView_RoomAvatar", maskClass)} size="32px" room={room} />
|
<RoomAvatar className={classNames("mx_RoomAvatarView_RoomAvatar", maskClass)} size="32px" room={room} />
|
||||||
{icon}
|
{label ? <Tooltip label={label}>{icon}</Tooltip> : icon}
|
||||||
</div>
|
</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) {
|
switch (presence) {
|
||||||
case Presence.Online:
|
case Presence.Online:
|
||||||
return (
|
return (
|
||||||
@@ -72,7 +67,7 @@ function PresenceDecoration({ presence }: PresenceDecorationProps): JSX.Element
|
|||||||
height="8px"
|
height="8px"
|
||||||
className="mx_RoomAvatarView_PresenceDecoration"
|
className="mx_RoomAvatarView_PresenceDecoration"
|
||||||
color="var(--cpd-color-icon-accent-primary)"
|
color="var(--cpd-color-icon-accent-primary)"
|
||||||
aria-label={_t("presence|online")}
|
aria-label={getPresenceLabel(presence)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case Presence.Away:
|
case Presence.Away:
|
||||||
@@ -82,7 +77,7 @@ function PresenceDecoration({ presence }: PresenceDecorationProps): JSX.Element
|
|||||||
height="8px"
|
height="8px"
|
||||||
className="mx_RoomAvatarView_PresenceDecoration"
|
className="mx_RoomAvatarView_PresenceDecoration"
|
||||||
color="var(--cpd-color-icon-quaternary)"
|
color="var(--cpd-color-icon-quaternary)"
|
||||||
aria-label={_t("presence|away")}
|
aria-label={getPresenceLabel(presence)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case Presence.Offline:
|
case Presence.Offline:
|
||||||
@@ -92,7 +87,7 @@ function PresenceDecoration({ presence }: PresenceDecorationProps): JSX.Element
|
|||||||
height="8px"
|
height="8px"
|
||||||
className="mx_RoomAvatarView_PresenceDecoration"
|
className="mx_RoomAvatarView_PresenceDecoration"
|
||||||
color="var(--cpd-color-icon-tertiary)"
|
color="var(--cpd-color-icon-tertiary)"
|
||||||
aria-label={_t("presence|offline")}
|
aria-label={getPresenceLabel(presence)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case Presence.Busy:
|
case Presence.Busy:
|
||||||
@@ -102,7 +97,7 @@ function PresenceDecoration({ presence }: PresenceDecorationProps): JSX.Element
|
|||||||
height="8px"
|
height="8px"
|
||||||
className="mx_RoomAvatarView_PresenceDecoration"
|
className="mx_RoomAvatarView_PresenceDecoration"
|
||||||
color="var(--cpd-color-icon-tertiary)"
|
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"
|
height="16px"
|
||||||
className="mx_RoomAvatarView_icon"
|
className="mx_RoomAvatarView_icon"
|
||||||
color="var(--cpd-color-icon-tertiary)"
|
color="var(--cpd-color-icon-tertiary)"
|
||||||
aria-label={_t("room|room_is_low_priority")}
|
aria-label={getDecorationLabel(decoration, presence)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (decoration === AvatarBadgeDecoration.VideoRoom) {
|
} else if (decoration === AvatarBadgeDecoration.VideoRoom) {
|
||||||
@@ -126,7 +121,7 @@ function getAvatarDecoration(decoration: AvatarBadgeDecoration, presence: Presen
|
|||||||
height="16px"
|
height="16px"
|
||||||
className="mx_RoomAvatarView_icon"
|
className="mx_RoomAvatarView_icon"
|
||||||
color="var(--cpd-color-icon-tertiary)"
|
color="var(--cpd-color-icon-tertiary)"
|
||||||
aria-label={_t("room|video_room")}
|
aria-label={getDecorationLabel(decoration, presence)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (decoration === AvatarBadgeDecoration.PublicRoom) {
|
} else if (decoration === AvatarBadgeDecoration.PublicRoom) {
|
||||||
@@ -135,11 +130,45 @@ function getAvatarDecoration(decoration: AvatarBadgeDecoration, presence: Presen
|
|||||||
width="16px"
|
width="16px"
|
||||||
height="16px"
|
height="16px"
|
||||||
className="mx_RoomAvatarView_icon"
|
className="mx_RoomAvatarView_icon"
|
||||||
color="var(--cpd-color-icon-tertiary)"
|
color="var(--cpd-color-icon-info-primary)"
|
||||||
aria-label={_t("room|header|room_is_public")}
|
aria-label={getDecorationLabel(decoration, presence)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (decoration === AvatarBadgeDecoration.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;
|
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)) {
|
if (isRoomInvite(props)) {
|
||||||
const room = MatrixClientPeg.safeGet().getRoom(props.roomId);
|
const room = cli.getRoom(props.roomId);
|
||||||
const isFederated = room?.currentState.getStateEvents(EventType.RoomCreate, "")?.getContent()["m.federate"];
|
|
||||||
if (!room) throw new Error("Room ID given to InviteDialog does not look like a room");
|
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.Invite).forEach((m) => excludedIds.add(m.userId));
|
||||||
room.getMembersWithMembership(KnownMembership.Join).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
|
// add banned users, so we don't try to invite them
|
||||||
room.getMembersWithMembership(KnownMembership.Ban).forEach((m) => excludedIds.add(m.userId));
|
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
|
// exclude users from external servers
|
||||||
const homeserver = props.roomId.split(":")[1];
|
this.excludeExternals(ourHomeserver, excludedIds);
|
||||||
this.excludeExternals(homeserver, excludedIds);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,7 +387,7 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
|||||||
numSuggestionsShown: INITIAL_ROOMS_SHOWN,
|
numSuggestionsShown: INITIAL_ROOMS_SHOWN,
|
||||||
serverResultsMixin: [],
|
serverResultsMixin: [],
|
||||||
threepidResultsMixin: [],
|
threepidResultsMixin: [],
|
||||||
canUseIdentityServer: !!MatrixClientPeg.safeGet().getIdentityServerUrl(),
|
canUseIdentityServer: !!cli.getIdentityServerUrl(),
|
||||||
tryingIdentityServer: false,
|
tryingIdentityServer: false,
|
||||||
consultFirst: false,
|
consultFirst: false,
|
||||||
dialPadValue: "",
|
dialPadValue: "",
|
||||||
|
|||||||
@@ -15,23 +15,13 @@ import BaseDialog from "../dialogs/BaseDialog";
|
|||||||
import SpaceStore from "../../../stores/spaces/SpaceStore";
|
import SpaceStore from "../../../stores/spaces/SpaceStore";
|
||||||
import SpaceChildrenPicker from "../spaces/SpaceChildrenPicker";
|
import SpaceChildrenPicker from "../spaces/SpaceChildrenPicker";
|
||||||
import { filterBoolean } from "../../../utils/arrays";
|
import { filterBoolean } from "../../../utils/arrays";
|
||||||
|
import { isOnlyAdmin } from "../../../utils/membership";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
space: Room;
|
space: Room;
|
||||||
onFinished(leave: boolean, rooms?: Room[]): void;
|
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 LeaveSpaceDialog: React.FC<IProps> = ({ space, onFinished }) => {
|
||||||
const spaceChildren = useMemo(() => {
|
const spaceChildren = useMemo(() => {
|
||||||
const roomSet = new Set(SpaceStore.instance.getSpaceFilteredRoomIds(space.roomId));
|
const roomSet = new Set(SpaceStore.instance.getSpaceFilteredRoomIds(space.roomId));
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import FilteredList from "./FilteredList";
|
|||||||
import Spinner from "../../elements/Spinner";
|
import Spinner from "../../elements/Spinner";
|
||||||
import SyntaxHighlight from "../../elements/SyntaxHighlight";
|
import SyntaxHighlight from "../../elements/SyntaxHighlight";
|
||||||
import { useAsyncMemo } from "../../../../hooks/useAsyncMemo";
|
import { useAsyncMemo } from "../../../../hooks/useAsyncMemo";
|
||||||
|
import LabelledToggleSwitch from "../../elements/LabelledToggleSwitch";
|
||||||
|
|
||||||
export const StateEventEditor: React.FC<IEditorProps> = ({ mxEvent, onBack }) => {
|
export const StateEventEditor: React.FC<IEditorProps> = ({ mxEvent, onBack }) => {
|
||||||
const context = useContext(DevtoolsContext);
|
const context = useContext(DevtoolsContext);
|
||||||
@@ -114,6 +115,7 @@ const RoomStateExplorerEventType: React.FC<IEventTypeProps> = ({ eventType, onBa
|
|||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [event, setEvent] = useState<MatrixEvent | null>(null);
|
const [event, setEvent] = useState<MatrixEvent | null>(null);
|
||||||
const [history, setHistory] = useState(false);
|
const [history, setHistory] = useState(false);
|
||||||
|
const [showEmptyState, setShowEmptyState] = useState(true);
|
||||||
|
|
||||||
const events = context.room.currentState.events.get(eventType)!;
|
const events = context.room.currentState.events.get(eventType)!;
|
||||||
|
|
||||||
@@ -149,10 +151,17 @@ const RoomStateExplorerEventType: React.FC<IEventTypeProps> = ({ eventType, onBa
|
|||||||
return (
|
return (
|
||||||
<BaseTool onBack={onBack}>
|
<BaseTool onBack={onBack}>
|
||||||
<FilteredList query={query} onChange={setQuery}>
|
<FilteredList query={query} onChange={setQuery}>
|
||||||
{Array.from(events.entries()).map(([stateKey, ev]) => (
|
{Array.from(events.entries())
|
||||||
<StateEventButton key={stateKey} label={stateKey} onClick={() => setEvent(ev)} />
|
.filter(([_, ev]) => showEmptyState || Object.keys(ev.getContent()).length > 0)
|
||||||
))}
|
.map(([stateKey, ev]) => (
|
||||||
|
<StateEventButton key={stateKey} label={stateKey} onClick={() => setEvent(ev)} />
|
||||||
|
))}
|
||||||
</FilteredList>
|
</FilteredList>
|
||||||
|
<LabelledToggleSwitch
|
||||||
|
label={_t("devtools|show_empty_content_events")}
|
||||||
|
onChange={setShowEmptyState}
|
||||||
|
value={showEmptyState}
|
||||||
|
/>
|
||||||
</BaseTool>
|
</BaseTool>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import React, { type ChangeEvent, type FormEvent } from "react";
|
|||||||
import { type SecretStorage } from "matrix-js-sdk/src/matrix";
|
import { type SecretStorage } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import Field from "../../elements/Field";
|
import Field from "../../elements/Field";
|
||||||
import { Flex } from "../../../utils/Flex";
|
import { Flex } from "../../../../shared-components/utils/Flex";
|
||||||
import { _t } from "../../../../languageHandler";
|
import { _t } from "../../../../languageHandler";
|
||||||
import { EncryptionCard } from "../../settings/encryption/EncryptionCard";
|
import { EncryptionCard } from "../../settings/encryption/EncryptionCard";
|
||||||
import { EncryptionCardButtons } from "../../settings/encryption/EncryptionCardButtons";
|
import { EncryptionCardButtons } from "../../settings/encryption/EncryptionCardButtons";
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ interface IProps {
|
|||||||
mediaEventHelperGet: () => MediaEventHelper | undefined;
|
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 {
|
export default function DownloadActionButton({ mxEvent, mediaEventHelperGet }: IProps): ReactElement | null {
|
||||||
const mediaEventHelper = useMemo(() => mediaEventHelperGet(), [mediaEventHelperGet]);
|
const mediaEventHelper = useMemo(() => mediaEventHelperGet(), [mediaEventHelperGet]);
|
||||||
const downloadUrl = mediaEventHelper?.media.srcHttp ?? "";
|
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 { download, loading, canDownload } = useDownloadMedia(downloadUrl, fileName, mxEvent);
|
||||||
|
|
||||||
|
const buttonTitle = useButtonTitle(loading, mediaEventHelper?.media.isEncrypted ?? false);
|
||||||
|
|
||||||
if (!canDownload) return null;
|
if (!canDownload) return null;
|
||||||
|
|
||||||
const spinner = loading ? <Spinner w={18} h={18} /> : undefined;
|
const spinner = loading ? <Spinner w={18} h={18} /> : undefined;
|
||||||
@@ -45,7 +53,7 @@ export default function DownloadActionButton({ mxEvent, mediaEventHelperGet }: I
|
|||||||
return (
|
return (
|
||||||
<RovingAccessibleButton
|
<RovingAccessibleButton
|
||||||
className={classes}
|
className={classes}
|
||||||
title={loading ? _t("timeline|download_action_downloading") : _t("action|download")}
|
title={buttonTitle}
|
||||||
onClick={download}
|
onClick={download}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
placement="left"
|
placement="left"
|
||||||
|
|||||||
@@ -120,8 +120,8 @@ export const PollHistoryList: React.FC<PollHistoryListProps> = ({
|
|||||||
value={filter}
|
value={filter}
|
||||||
onFilterChange={onFilterChange}
|
onFilterChange={onFilterChange}
|
||||||
tabs={[
|
tabs={[
|
||||||
{ id: "ACTIVE", label: "Active polls" },
|
{ id: "ACTIVE", label: _t("right_panel|poll|active_heading") },
|
||||||
{ id: "ENDED", label: "Past polls" },
|
{ id: "ENDED", label: _t("right_panel|poll|past_heading") },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{!!pollStartEvents.length && (
|
{!!pollStartEvents.length && (
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
import React, { type ComponentType } from "react";
|
import React, { type ComponentType } from "react";
|
||||||
import { Text } from "@vector-im/compound-web";
|
import { Text } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import { Flex } from "../../utils/Flex";
|
import { Flex } from "../../../shared-components/utils/Flex";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
Icon: ComponentType<React.SVGAttributes<SVGElement>>;
|
Icon: ComponentType<React.SVGAttributes<SVGElement>>;
|
||||||
|
|||||||
@@ -46,9 +46,9 @@ import RoomAvatar from "../avatars/RoomAvatar.tsx";
|
|||||||
import { E2EStatus } from "../../../utils/ShieldUtils.ts";
|
import { E2EStatus } from "../../../utils/ShieldUtils.ts";
|
||||||
import { type RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks.ts";
|
import { type RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks.ts";
|
||||||
import RoomName from "../elements/RoomName.tsx";
|
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 { 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 { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement.tsx";
|
||||||
import { useRoomSummaryCardViewModel } from "../../viewmodels/right_panel/RoomSummaryCardViewModel.tsx";
|
import { useRoomSummaryCardViewModel } from "../../viewmodels/right_panel/RoomSummaryCardViewModel.tsx";
|
||||||
import { useRoomTopicViewModel } from "../../viewmodels/right_panel/RoomSummaryCardTopicViewModel.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)"
|
gap="var(--cpd-space-2x)"
|
||||||
className="mx_RoomSummaryCard_topic"
|
className="mx_RoomSummaryCard_topic"
|
||||||
>
|
>
|
||||||
<Box flex="1">
|
<Box flex="1" className="mx_RoomSummaryCard_topic_box">
|
||||||
<Link kind="primary" onClick={vm.onEditClick}>
|
<Link kind="primary" onClick={vm.onEditClick}>
|
||||||
<Text size="sm" weight="regular">
|
<Text size="sm" weight="regular">
|
||||||
{_t("right_panel|add_topic")}
|
{_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,
|
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}>
|
<Text size="sm" weight="regular" onClick={vm.onTopicLinkClick}>
|
||||||
{content}
|
{content}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -169,8 +169,8 @@ const RoomSummaryCardView: React.FC<IProps> = ({
|
|||||||
|
|
||||||
<Flex as="section" justify="center" gap="var(--cpd-space-2x)" className="mx_RoomSummaryCard_badges">
|
<Flex as="section" justify="center" gap="var(--cpd-space-2x)" className="mx_RoomSummaryCard_badges">
|
||||||
{!vm.isDirectMessage && vm.roomJoinRule === JoinRule.Public && (
|
{!vm.isDirectMessage && vm.roomJoinRule === JoinRule.Public && (
|
||||||
<Badge kind="grey">
|
<Badge kind="blue">
|
||||||
<PublicIcon width="1em" />
|
<PublicIcon width="1em" color="var(--cpd-color-icon-info-primary)" />
|
||||||
{_t("common|public_room")}
|
{_t("common|public_room")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
@@ -183,8 +183,8 @@ const RoomSummaryCardView: React.FC<IProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!vm.isRoomEncrypted && (
|
{!vm.isRoomEncrypted && (
|
||||||
<Badge kind="grey">
|
<Badge kind="blue">
|
||||||
<LockOffIcon width="1em" />
|
<LockOffIcon width="1em" color="var(--cpd-color-icon-info-primary)" />
|
||||||
{_t("common|unencrypted")}
|
{_t("common|unencrypted")}
|
||||||
</Badge>
|
</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 { useUserInfoVerificationViewModel } from "../../../viewmodels/right_panel/user_info/UserInfoHeaderVerificationViewModel";
|
||||||
import { type IDevice } from "../UserInfo";
|
import { type IDevice } from "../UserInfo";
|
||||||
import { Flex } from "../../../utils/Flex";
|
import { Flex } from "../../../../shared-components/utils/Flex";
|
||||||
import { _t } from "../../../../languageHandler";
|
import { _t } from "../../../../languageHandler";
|
||||||
|
|
||||||
export const UserInfoHeaderVerificationView: React.FC<{
|
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 { useUserfoHeaderViewModel } from "../../../viewmodels/right_panel/user_info/UserInfoHeaderViewModel";
|
||||||
import MemberAvatar from "../../avatars/MemberAvatar";
|
import MemberAvatar from "../../avatars/MemberAvatar";
|
||||||
import { Container, type Member, type IDevice } from "../UserInfo";
|
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 PresenceLabel from "../../rooms/PresenceLabel";
|
||||||
import CopyableText from "../../elements/CopyableText";
|
import CopyableText from "../../elements/CopyableText";
|
||||||
import { UserInfoHeaderVerificationView } from "./UserInfoHeaderVerificationView";
|
import { UserInfoHeaderVerificationView } from "./UserInfoHeaderVerificationView";
|
||||||
|
|||||||
@@ -729,11 +729,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
if (this.state.shieldColour !== EventShieldColour.NONE) {
|
if (this.state.shieldColour !== EventShieldColour.NONE) {
|
||||||
let shieldReasonMessage: string;
|
let shieldReasonMessage: string;
|
||||||
switch (this.state.shieldReason) {
|
switch (this.state.shieldReason) {
|
||||||
case null:
|
|
||||||
case EventShieldReason.UNKNOWN:
|
|
||||||
shieldReasonMessage = _t("error|unknown");
|
|
||||||
break;
|
|
||||||
|
|
||||||
case EventShieldReason.UNVERIFIED_IDENTITY:
|
case EventShieldReason.UNVERIFIED_IDENTITY:
|
||||||
shieldReasonMessage = _t("encryption|event_shield_reason_unverified_identity");
|
shieldReasonMessage = _t("encryption|event_shield_reason_unverified_identity");
|
||||||
break;
|
break;
|
||||||
@@ -761,6 +756,14 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
case EventShieldReason.VERIFICATION_VIOLATION:
|
case EventShieldReason.VERIFICATION_VIOLATION:
|
||||||
shieldReasonMessage = _t("timeline|decryption_failure|sender_identity_previously_verified");
|
shieldReasonMessage = _t("timeline|decryption_failure|sender_identity_previously_verified");
|
||||||
break;
|
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) {
|
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 { Search, Text, Button, Tooltip, InlineSpinner } from "@vector-im/compound-web";
|
||||||
import React from "react";
|
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 { 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 { type MemberListViewState } from "../../../viewmodels/memberlist/MemberListViewModel";
|
||||||
import { _t } from "../../../../languageHandler";
|
import { _t } from "../../../../languageHandler";
|
||||||
|
|
||||||
@@ -19,10 +19,10 @@ interface TooltipProps {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OptionalTooltip: React.FC<TooltipProps> = ({ canInvite, children }) => {
|
const InviteTooltip: React.FC<TooltipProps> = ({ canInvite, children }) => {
|
||||||
if (canInvite) return 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.
|
// 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 {
|
interface Props {
|
||||||
@@ -42,10 +42,10 @@ const InviteButton: React.FC<Props> = ({ vm }) => {
|
|||||||
if (shouldShowSearch) {
|
if (shouldShowSearch) {
|
||||||
/// When rendered alongside a search box, the invite button is just an icon.
|
/// When rendered alongside a search box, the invite button is just an icon.
|
||||||
return (
|
return (
|
||||||
<OptionalTooltip canInvite={vm.canInvite}>
|
<InviteTooltip canInvite={vm.canInvite}>
|
||||||
<Button
|
<Button
|
||||||
className="mx_MemberListHeaderView_invite_small"
|
className="mx_MemberListHeaderView_invite_small"
|
||||||
kind="primary"
|
kind="secondary"
|
||||||
onClick={vm.onInviteButtonClick}
|
onClick={vm.onInviteButtonClick}
|
||||||
size="sm"
|
size="sm"
|
||||||
iconOnly={true}
|
iconOnly={true}
|
||||||
@@ -54,13 +54,13 @@ const InviteButton: React.FC<Props> = ({ vm }) => {
|
|||||||
aria-label={_t("action|invite")}
|
aria-label={_t("action|invite")}
|
||||||
type="button"
|
type="button"
|
||||||
/>
|
/>
|
||||||
</OptionalTooltip>
|
</InviteTooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Without a search box, invite button is a full size button.
|
// Without a search box, invite button is a full size button.
|
||||||
return (
|
return (
|
||||||
<OptionalTooltip canInvite={vm.canInvite}>
|
<InviteTooltip canInvite={vm.canInvite}>
|
||||||
<Button
|
<Button
|
||||||
kind="secondary"
|
kind="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -72,7 +72,7 @@ const InviteButton: React.FC<Props> = ({ vm }) => {
|
|||||||
>
|
>
|
||||||
{_t("action|invite")}
|
{_t("action|invite")}
|
||||||
</Button>
|
</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 { Form } from "@vector-im/compound-web";
|
||||||
import React, { type JSX } from "react";
|
import React, { type JSX, useCallback } from "react";
|
||||||
import { List, type ListRowProps } from "react-virtualized/dist/commonjs/List";
|
|
||||||
import { AutoSizer } from "react-virtualized";
|
|
||||||
|
|
||||||
import { Flex } from "../../../utils/Flex";
|
import { Flex } from "../../../../shared-components/utils/Flex";
|
||||||
import {
|
import {
|
||||||
type MemberWithSeparator,
|
type MemberWithSeparator,
|
||||||
SEPARATOR,
|
SEPARATOR,
|
||||||
@@ -21,7 +19,7 @@ import { ThreePidInviteTileView } from "./tiles/ThreePidInviteTileView";
|
|||||||
import { MemberListHeaderView } from "./MemberListHeaderView";
|
import { MemberListHeaderView } from "./MemberListHeaderView";
|
||||||
import BaseCard from "../../right_panel/BaseCard";
|
import BaseCard from "../../right_panel/BaseCard";
|
||||||
import { _t } from "../../../../languageHandler";
|
import { _t } from "../../../../languageHandler";
|
||||||
import { RovingTabIndexProvider } from "../../../../accessibility/RovingTabIndex";
|
import { type ListContext, ListView } from "../../../utils/ListView";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
@@ -30,53 +28,74 @@ interface IProps {
|
|||||||
|
|
||||||
const MemberListView: React.FC<IProps> = (props: IProps) => {
|
const MemberListView: React.FC<IProps> = (props: IProps) => {
|
||||||
const vm = useMemberListViewModel(props.roomId);
|
const vm = useMemberListViewModel(props.roomId);
|
||||||
|
const { isPresenceEnabled, onClickMember, memberCount } = vm;
|
||||||
|
|
||||||
const totalRows = vm.members.length;
|
const getItemKey = useCallback((item: MemberWithSeparator): string => {
|
||||||
|
|
||||||
const getRowComponent = (item: MemberWithSeparator): JSX.Element => {
|
|
||||||
if (item === SEPARATOR) {
|
if (item === SEPARATOR) {
|
||||||
return <hr className="mx_MemberListView_separator" />;
|
return "separator";
|
||||||
} else if (item.member) {
|
} else if (item.member) {
|
||||||
return <RoomMemberTileView member={item.member} showPresence={vm.isPresenceEnabled} />;
|
return `member-${item.member.userId}`;
|
||||||
} else {
|
} else {
|
||||||
return <ThreePidInviteTileView threePidInvite={item.threePidInvite} />;
|
return `threePidInvite-${item.threePidInvite.event.getContent().public_key}`;
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const getRowHeight = ({ index }: { index: number }): number => {
|
const getItemComponent = useCallback(
|
||||||
if (vm.members[index] === SEPARATOR) {
|
(
|
||||||
/**
|
index: number,
|
||||||
* This is a separator of 2px height rendered between
|
item: MemberWithSeparator,
|
||||||
* joined and invited members.
|
context: ListContext<any>,
|
||||||
*/
|
onFocus: (e: React.FocusEvent) => void,
|
||||||
return 2;
|
): JSX.Element => {
|
||||||
} else if (totalRows && index === totalRows) {
|
const itemKey = getItemKey(item);
|
||||||
/**
|
const isRovingItem = itemKey === context.tabIndexKey;
|
||||||
* The empty spacer div rendered at the bottom should
|
const focused = isRovingItem && context.focused;
|
||||||
* have a height of 32px.
|
if (item === SEPARATOR) {
|
||||||
*/
|
return <hr className="mx_MemberListView_separator" />;
|
||||||
return 32;
|
} else if (item.member) {
|
||||||
} else {
|
return (
|
||||||
/**
|
<RoomMemberTileView
|
||||||
* The actual member tiles have a height of 56px.
|
member={item.member}
|
||||||
*/
|
showPresence={isPresenceEnabled}
|
||||||
return 56;
|
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 => {
|
const handleSelectItem = useCallback(
|
||||||
if (index === totalRows) {
|
(item: MemberWithSeparator): void => {
|
||||||
// We've rendered all the members,
|
if (item !== SEPARATOR) {
|
||||||
// now we render an empty div to add some space to the end of the list.
|
if (item.member) {
|
||||||
return <div key={key} style={style} />;
|
onClickMember(item.member);
|
||||||
}
|
} else {
|
||||||
const item = vm.members[index];
|
onClickMember(item.threePidInvite);
|
||||||
return (
|
}
|
||||||
<div key={key} style={style}>
|
}
|
||||||
{getRowComponent(item)}
|
},
|
||||||
</div>
|
[onClickMember],
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
const isItemFocusable = useCallback((item: MemberWithSeparator): boolean => {
|
||||||
|
return item !== SEPARATOR;
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseCard
|
<BaseCard
|
||||||
@@ -87,34 +106,20 @@ const MemberListView: React.FC<IProps> = (props: IProps) => {
|
|||||||
header={_t("common|people")}
|
header={_t("common|people")}
|
||||||
onClose={props.onClose}
|
onClose={props.onClose}
|
||||||
>
|
>
|
||||||
<RovingTabIndexProvider handleUpDown scrollIntoView>
|
<Flex align="stretch" direction="column" className="mx_MemberListView_container">
|
||||||
{({ onKeyDownHandler }) => (
|
<Form.Root onSubmit={(e) => e.preventDefault()}>
|
||||||
<Flex
|
<MemberListHeaderView vm={vm} />
|
||||||
align="stretch"
|
</Form.Root>
|
||||||
direction="column"
|
<ListView
|
||||||
className="mx_MemberListView_container"
|
items={vm.members}
|
||||||
onKeyDown={onKeyDownHandler}
|
onSelectItem={handleSelectItem}
|
||||||
>
|
getItemComponent={getItemComponent}
|
||||||
<Form.Root onSubmit={(e) => e.preventDefault()}>
|
getItemKey={getItemKey}
|
||||||
<MemberListHeaderView vm={vm} />
|
isItemFocusable={isItemFocusable}
|
||||||
</Form.Root>
|
role="listbox"
|
||||||
<AutoSizer>
|
aria-label={_t("member_list|list_title")}
|
||||||
{({ height, width }) => (
|
/>
|
||||||
<List
|
</Flex>
|
||||||
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>
|
|
||||||
</BaseCard>
|
</BaseCard>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ import { InvitedIconView } from "./common/InvitedIconView";
|
|||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
member: RoomMember;
|
member: RoomMember;
|
||||||
|
index: number;
|
||||||
|
memberCount: number;
|
||||||
showPresence?: boolean;
|
showPresence?: boolean;
|
||||||
|
focused?: boolean;
|
||||||
|
tabIndex?: number;
|
||||||
|
onFocus: (e: React.FocusEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RoomMemberTileView(props: IProps): JSX.Element {
|
export function RoomMemberTileView(props: IProps): JSX.Element {
|
||||||
@@ -36,7 +41,7 @@ export function RoomMemberTileView(props: IProps): JSX.Element {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
const name = vm.name;
|
const name = vm.name;
|
||||||
const nameJSX = <DisambiguatedProfile member={member} fallbackName={name || ""} />;
|
const nameJSX = <DisambiguatedProfile withTooltip member={member} fallbackName={name || ""} />;
|
||||||
|
|
||||||
const presenceState = member.presenceState;
|
const presenceState = member.presenceState;
|
||||||
let presenceJSX: JSX.Element | undefined;
|
let presenceJSX: JSX.Element | undefined;
|
||||||
@@ -54,13 +59,18 @@ export function RoomMemberTileView(props: IProps): JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MemberTileView
|
<MemberTileView
|
||||||
title={vm.title}
|
|
||||||
onClick={vm.onClick}
|
onClick={vm.onClick}
|
||||||
|
onFocus={props.onFocus}
|
||||||
avatarJsx={av}
|
avatarJsx={av}
|
||||||
presenceJsx={presenceJSX}
|
presenceJsx={presenceJSX}
|
||||||
nameJsx={nameJSX}
|
nameJsx={nameJSX}
|
||||||
userLabel={vm.userLabel}
|
userLabel={vm.userLabel}
|
||||||
|
ariaLabel={name}
|
||||||
iconJsx={iconJsx}
|
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 {
|
interface Props {
|
||||||
threePidInvite: ThreePIDInvite;
|
threePidInvite: ThreePIDInvite;
|
||||||
|
memberIndex: number;
|
||||||
|
memberCount: number;
|
||||||
|
focused?: boolean;
|
||||||
|
tabIndex?: number;
|
||||||
|
onFocus: (e: React.FocusEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ThreePidInviteTileView(props: Props): JSX.Element {
|
export function ThreePidInviteTileView(props: Props): JSX.Element {
|
||||||
const vm = useThreePidTileViewModel(props);
|
const vm = useThreePidTileViewModel(props);
|
||||||
const av = <BaseAvatar name={vm.name} size="32px" aria-hidden="true" />;
|
const av = <BaseAvatar name={vm.name} size="32px" aria-hidden="true" />;
|
||||||
const iconJsx = <InvitedIconView isThreePid={true} />;
|
const iconJsx = <InvitedIconView isThreePid={true} />;
|
||||||
|
const name = vm.name;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MemberTileView
|
<MemberTileView
|
||||||
nameJsx={vm.name}
|
nameJsx={name}
|
||||||
avatarJsx={av}
|
avatarJsx={av}
|
||||||
onClick={vm.onClick}
|
onClick={vm.onClick}
|
||||||
|
memberIndex={props.memberIndex}
|
||||||
|
memberCount={props.memberCount}
|
||||||
|
ariaLabel={name}
|
||||||
userLabel={vm.userLabel}
|
userLabel={vm.userLabel}
|
||||||
iconJsx={iconJsx}
|
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 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 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 {
|
interface Props {
|
||||||
isThreePid: boolean;
|
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.
|
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 {
|
interface Props {
|
||||||
avatarJsx: JSX.Element;
|
avatarJsx: JSX.Element;
|
||||||
nameJsx: JSX.Element | string;
|
nameJsx: JSX.Element | string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
title?: string;
|
onFocus: (e: React.FocusEvent) => void;
|
||||||
|
memberIndex: number;
|
||||||
|
memberCount: number;
|
||||||
|
ariaLabel?: string;
|
||||||
presenceJsx?: JSX.Element;
|
presenceJsx?: JSX.Element;
|
||||||
userLabel?: React.ReactNode;
|
userLabel?: React.ReactNode;
|
||||||
iconJsx?: JSX.Element;
|
iconJsx?: JSX.Element;
|
||||||
|
tabIndex?: number;
|
||||||
|
focused?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MemberTileView(props: Props): JSX.Element {
|
export function MemberTileView(props: Props): JSX.Element {
|
||||||
@@ -24,22 +29,37 @@ export function MemberTileView(props: Props): JSX.Element {
|
|||||||
if (props.userLabel) {
|
if (props.userLabel) {
|
||||||
userLabelJsx = <div className="mx_MemberTileView_userLabel">{props.userLabel}</div>;
|
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 (
|
return (
|
||||||
// The wrapping div is required to make the magic mouse listener work, for some reason.
|
// The wrapping div is required to make the magic mouse listener work, for some reason.
|
||||||
<div>
|
<div>
|
||||||
<RovingAccessibleButton className="mx_MemberTileView" title={props.title} onClick={props.onClick}>
|
<AccessibleButton
|
||||||
<div className="mx_MemberTileView_left">
|
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">
|
<div className="mx_MemberTileView_avatar">
|
||||||
{props.avatarJsx} {props.presenceJsx}
|
{props.avatarJsx} {props.presenceJsx}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_MemberTileView_name">{props.nameJsx}</div>
|
<div className="mx_MemberTileView_name">{props.nameJsx}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_MemberTileView_right">
|
<div aria-hidden className="mx_MemberTileView_right">
|
||||||
{userLabelJsx}
|
{userLabelJsx}
|
||||||
{props.iconJsx}
|
{props.iconJsx}
|
||||||
</div>
|
</div>
|
||||||
</RovingAccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</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 EmailIcon from "@vector-im/compound-design-tokens/assets/web/icons/email-solid";
|
||||||
import { UnreadCounter, Unread } from "@vector-im/compound-web";
|
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 { type RoomNotificationState } from "../../../stores/notifications/RoomNotificationState";
|
||||||
import { useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
|
import { useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||||
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
|
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ import { RightPanelPhases } from "../../../../stores/right-panel/RightPanelStore
|
|||||||
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext.tsx";
|
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext.tsx";
|
||||||
import { useRoomMemberCount, useRoomMembers } from "../../../../hooks/useRoomMembers.ts";
|
import { useRoomMemberCount, useRoomMembers } from "../../../../hooks/useRoomMembers.ts";
|
||||||
import { _t } from "../../../../languageHandler.tsx";
|
import { _t } from "../../../../languageHandler.tsx";
|
||||||
import { Flex } from "../../../utils/Flex.tsx";
|
import { Flex } from "../../../../shared-components/utils/Flex";
|
||||||
import { Box } from "../../../utils/Box.tsx";
|
import { Box } from "../../../../shared-components/utils/Box";
|
||||||
import { getPlatformCallTypeProps, useRoomCall } from "../../../../hooks/room/useRoomCall.tsx";
|
import { getPlatformCallTypeProps, useRoomCall } from "../../../../hooks/room/useRoomCall.tsx";
|
||||||
import { useRoomThreadNotifications } from "../../../../hooks/room/useRoomThreadNotifications.ts";
|
import { useRoomThreadNotifications } from "../../../../hooks/room/useRoomThreadNotifications.ts";
|
||||||
import { useGlobalNotificationState } from "../../../../hooks/useGlobalNotificationState.ts";
|
import { useGlobalNotificationState } from "../../../../hooks/useGlobalNotificationState.ts";
|
||||||
@@ -286,7 +286,8 @@ export default function RoomHeader({
|
|||||||
<PublicIcon
|
<PublicIcon
|
||||||
width="16px"
|
width="16px"
|
||||||
height="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")}
|
aria-label={_t("common|public_room")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -7,11 +7,11 @@
|
|||||||
|
|
||||||
import React, { type JSX, type PropsWithChildren } from "react";
|
import React, { type JSX, type PropsWithChildren } from "react";
|
||||||
import { Button } from "@vector-im/compound-web";
|
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 RoomIcon from "@vector-im/compound-design-tokens/assets/web/icons/room";
|
||||||
|
|
||||||
import type { RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel";
|
import type { RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel";
|
||||||
import { Flex } from "../../../utils/Flex";
|
import { Flex } from "../../../../shared-components/utils/Flex";
|
||||||
import { _t } from "../../../../languageHandler";
|
import { _t } from "../../../../languageHandler";
|
||||||
import { FilterKey } from "../../../../stores/room-list-v3/skip-list/filters";
|
import { FilterKey } from "../../../../stores/room-list-v3/skip-list/filters";
|
||||||
import { type PrimaryFilter } from "../../../viewmodels/roomlist/useFilteredRooms";
|
import { type PrimaryFilter } from "../../../viewmodels/roomlist/useFilteredRooms";
|
||||||
@@ -148,8 +148,8 @@ function DefaultPlaceholder({ vm }: DefaultPlaceholderProps): JSX.Element {
|
|||||||
direction="column"
|
direction="column"
|
||||||
gap="var(--cpd-space-4x)"
|
gap="var(--cpd-space-4x)"
|
||||||
>
|
>
|
||||||
<Button size="sm" kind="secondary" Icon={UserAddIcon} onClick={vm.createChatRoom}>
|
<Button size="sm" kind="secondary" Icon={ChatIcon} onClick={vm.createChatRoom}>
|
||||||
{_t("action|new_message")}
|
{_t("action|start_chat")}
|
||||||
</Button>
|
</Button>
|
||||||
{vm.canCreateRoom && (
|
{vm.canCreateRoom && (
|
||||||
<Button size="sm" kind="secondary" Icon={RoomIcon} onClick={vm.createRoom}>
|
<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 PreferencesIcon from "@vector-im/compound-design-tokens/assets/web/icons/preferences";
|
||||||
import SettingsIcon from "@vector-im/compound-design-tokens/assets/web/icons/settings";
|
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 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 { _t } from "../../../../languageHandler";
|
||||||
import { Flex } from "../../../utils/Flex";
|
import { Flex } from "../../../../shared-components/utils/Flex";
|
||||||
import {
|
import {
|
||||||
type RoomListHeaderViewState,
|
type RoomListHeaderViewState,
|
||||||
useRoomListHeaderViewModel,
|
useRoomListHeaderViewModel,
|
||||||
@@ -49,7 +50,7 @@ export function RoomListHeaderView(): JSX.Element {
|
|||||||
{vm.displayComposeMenu ? (
|
{vm.displayComposeMenu ? (
|
||||||
<ComposeMenu vm={vm} />
|
<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)" />
|
<ComposeIcon color="var(--cpd-color-icon-secondary)" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
@@ -143,12 +144,7 @@ function ComposeMenu({ vm }: ComposeMenuProps): JSX.Element {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MenuItem
|
<MenuItem Icon={ChatIcon} label={_t("action|start_chat")} onSelect={vm.createChatRoom} hideChevron={true} />
|
||||||
Icon={UserAddIcon}
|
|
||||||
label={_t("action|new_message")}
|
|
||||||
onSelect={vm.createChatRoom}
|
|
||||||
hideChevron={true}
|
|
||||||
/>
|
|
||||||
{vm.canCreateRoom && (
|
{vm.canCreateRoom && (
|
||||||
<MenuItem Icon={RoomIcon} label={_t("action|new_room")} onSelect={vm.createRoom} hideChevron={true} />
|
<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 { type Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { _t } from "../../../../languageHandler";
|
import { _t } from "../../../../languageHandler";
|
||||||
import { Flex } from "../../../utils/Flex";
|
import { Flex } from "../../../../shared-components/utils/Flex";
|
||||||
import {
|
import {
|
||||||
type RoomListItemMenuViewState,
|
type RoomListItemMenuViewState,
|
||||||
useRoomListItemMenuViewModel,
|
useRoomListItemMenuViewModel,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { type Room } from "matrix-js-sdk/src/matrix";
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import { useRoomListItemViewModel } from "../../../viewmodels/roomlist/RoomListItemViewModel";
|
import { useRoomListItemViewModel } from "../../../viewmodels/roomlist/RoomListItemViewModel";
|
||||||
import { Flex } from "../../../utils/Flex";
|
import { Flex } from "../../../../shared-components/utils/Flex";
|
||||||
import { RoomListItemMenuView } from "./RoomListItemMenuView";
|
import { RoomListItemMenuView } from "./RoomListItemMenuView";
|
||||||
import { NotificationDecoration } from "../NotificationDecoration";
|
import { NotificationDecoration } from "../NotificationDecoration";
|
||||||
import { RoomAvatarView } from "../../avatars/RoomAvatarView";
|
import { RoomAvatarView } from "../../avatars/RoomAvatarView";
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { UIComponent } from "../../../../settings/UIFeature";
|
|||||||
import { RoomListSearch } from "./RoomListSearch";
|
import { RoomListSearch } from "./RoomListSearch";
|
||||||
import { RoomListHeaderView } from "./RoomListHeaderView";
|
import { RoomListHeaderView } from "./RoomListHeaderView";
|
||||||
import { RoomListView } from "./RoomListView";
|
import { RoomListView } from "./RoomListView";
|
||||||
import { Flex } from "../../../utils/Flex";
|
import { Flex } from "../../../../shared-components/utils/Flex";
|
||||||
import { _t } from "../../../../languageHandler";
|
import { _t } from "../../../../languageHandler";
|
||||||
|
|
||||||
type RoomListPanelProps = {
|
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 ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down";
|
||||||
|
|
||||||
import type { RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel";
|
import type { RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel";
|
||||||
import { Flex } from "../../../utils/Flex";
|
import { Flex } from "../../../../shared-components/utils/Flex";
|
||||||
import { _t } from "../../../../languageHandler";
|
import { _t } from "../../../../languageHandler";
|
||||||
|
|
||||||
interface RoomListPrimaryFiltersProps {
|
interface RoomListPrimaryFiltersProps {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { MetaSpace } from "../../../../stores/spaces";
|
|||||||
import { Action } from "../../../../dispatcher/actions";
|
import { Action } from "../../../../dispatcher/actions";
|
||||||
import PosthogTrackers from "../../../../PosthogTrackers";
|
import PosthogTrackers from "../../../../PosthogTrackers";
|
||||||
import defaultDispatcher from "../../../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../../../dispatcher/dispatcher";
|
||||||
import { Flex } from "../../../utils/Flex";
|
import { Flex } from "../../../../shared-components/utils/Flex";
|
||||||
import { useTypedEventEmitterState } from "../../../../hooks/useEventEmitter";
|
import { useTypedEventEmitterState } from "../../../../hooks/useEventEmitter";
|
||||||
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../../../LegacyCallHandler";
|
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../../../LegacyCallHandler";
|
||||||
|
|
||||||
@@ -61,7 +61,6 @@ export function RoomListSearch({ activeSpace }: RoomListSearchProps): JSX.Elemen
|
|||||||
</Button>
|
</Button>
|
||||||
{displayDialButton && (
|
{displayDialButton && (
|
||||||
<Button
|
<Button
|
||||||
className="mx_RoomListSearch_button"
|
|
||||||
kind="secondary"
|
kind="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
Icon={DialPadIcon}
|
Icon={DialPadIcon}
|
||||||
@@ -74,7 +73,6 @@ export function RoomListSearch({ activeSpace }: RoomListSearchProps): JSX.Elemen
|
|||||||
)}
|
)}
|
||||||
{displayExploreButton && (
|
{displayExploreButton && (
|
||||||
<Button
|
<Button
|
||||||
className="mx_RoomListSearch_button"
|
|
||||||
kind="secondary"
|
kind="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
Icon={ExploreIcon}
|
Icon={ExploreIcon}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { isValid3pidInvite } from "../../../RoomInvite";
|
|||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||||
import BaseCard from "../right_panel/BaseCard";
|
import BaseCard from "../right_panel/BaseCard";
|
||||||
import { Flex } from "../../utils/Flex";
|
import { Flex } from "../../../shared-components/utils/Flex";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
event: MatrixEvent;
|
event: MatrixEvent;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import AccessibleButton from "../elements/AccessibleButton";
|
|||||||
import LogoutDialog, { shouldShowLogoutDialog } from "../dialogs/LogoutDialog";
|
import LogoutDialog, { shouldShowLogoutDialog } from "../dialogs/LogoutDialog";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
import { Flex } from "../../utils/Flex";
|
import { Flex } from "../../../shared-components/utils/Flex";
|
||||||
|
|
||||||
const SpinnerToast: React.FC<{ children?: ReactNode }> = ({ children }) => (
|
const SpinnerToast: React.FC<{ children?: ReactNode }> = ({ children }) => (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import React, { type JSX, type PropsWithChildren } from "react";
|
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}
|
* 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,
|
powerLevelDescriptors.users_default.defaultValue,
|
||||||
);
|
);
|
||||||
|
|
||||||
let currentUserLevel = userLevels[client.getUserId()!];
|
const currentUserLevel = room.getMember(client.getSafeUserId())?.powerLevel ?? defaultUserLevel;
|
||||||
if (currentUserLevel === undefined) {
|
|
||||||
currentUserLevel = defaultUserLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.populateDefaultPlEvents(
|
this.populateDefaultPlEvents(
|
||||||
eventsLevels,
|
eventsLevels,
|
||||||
|
|||||||
@@ -3376,7 +3376,6 @@
|
|||||||
"unable_to_decrypt": "Zprávu nelze dešifrovat"
|
"unable_to_decrypt": "Zprávu nelze dešifrovat"
|
||||||
},
|
},
|
||||||
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
||||||
"download_action_decrypting": "Dešifrování",
|
|
||||||
"download_action_downloading": "Stahování",
|
"download_action_downloading": "Stahování",
|
||||||
"download_failed": "Stažení se nezdařilo",
|
"download_failed": "Stažení se nezdařilo",
|
||||||
"download_failed_description": "Při stahování tohoto souboru došlo k chybě",
|
"download_failed_description": "Při stahování tohoto souboru došlo k chybě",
|
||||||
|
|||||||
@@ -3341,7 +3341,6 @@
|
|||||||
"unable_to_decrypt": "Methu dadgryptio'r neges"
|
"unable_to_decrypt": "Methu dadgryptio'r neges"
|
||||||
},
|
},
|
||||||
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
||||||
"download_action_decrypting": "Dadgryptio",
|
|
||||||
"download_action_downloading": "Yn llwytho i lawr",
|
"download_action_downloading": "Yn llwytho i lawr",
|
||||||
"download_failed": "Methodd y llwytho i lawr",
|
"download_failed": "Methodd y llwytho i lawr",
|
||||||
"download_failed_description": "Bu gwall wrth lawrlwytho'r ffeil hon",
|
"download_failed_description": "Bu gwall wrth lawrlwytho'r ffeil hon",
|
||||||
|
|||||||
@@ -3334,7 +3334,6 @@
|
|||||||
"unable_to_decrypt": "Entschlüsselung der Nachricht nicht möglich"
|
"unable_to_decrypt": "Entschlüsselung der Nachricht nicht möglich"
|
||||||
},
|
},
|
||||||
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
||||||
"download_action_decrypting": "Entschlüsseln",
|
|
||||||
"download_action_downloading": "Herunterladen",
|
"download_action_downloading": "Herunterladen",
|
||||||
"download_failed": "Herunterladen fehlgeschlagen",
|
"download_failed": "Herunterladen fehlgeschlagen",
|
||||||
"download_failed_description": "Beim Herunterladen dieser Datei ist ein Fehler aufgetreten",
|
"download_failed_description": "Beim Herunterladen dieser Datei ist ein Fehler aufgetreten",
|
||||||
|
|||||||
@@ -2688,7 +2688,6 @@
|
|||||||
},
|
},
|
||||||
"creation_summary_dm": "Ο/η %(creator)s δημιούργησε αυτό το απευθείας μήνυμα.",
|
"creation_summary_dm": "Ο/η %(creator)s δημιούργησε αυτό το απευθείας μήνυμα.",
|
||||||
"creation_summary_room": "Ο/η %(creator)s δημιούργησε και διαμόρφωσε το δωμάτιο.",
|
"creation_summary_room": "Ο/η %(creator)s δημιούργησε και διαμόρφωσε το δωμάτιο.",
|
||||||
"download_action_decrypting": "Αποκρυπτογράφηση",
|
|
||||||
"download_action_downloading": "Γίνεται λήψη",
|
"download_action_downloading": "Γίνεται λήψη",
|
||||||
"edits": {
|
"edits": {
|
||||||
"tooltip_label": "Επεξεργάστηκε στις %(date)s. Κάντε κλικ για να δείτε τις τροποποιήσεις.",
|
"tooltip_label": "Επεξεργάστηκε στις %(date)s. Κάντε κλικ για να δείτε τις τροποποιήσεις.",
|
||||||
|
|||||||
@@ -93,7 +93,6 @@
|
|||||||
"maximise": "Maximise",
|
"maximise": "Maximise",
|
||||||
"mention": "Mention",
|
"mention": "Mention",
|
||||||
"minimise": "Minimise",
|
"minimise": "Minimise",
|
||||||
"new_message": "New message",
|
|
||||||
"new_room": "New room",
|
"new_room": "New room",
|
||||||
"new_video_room": "New video room",
|
"new_video_room": "New video room",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
@@ -863,6 +862,7 @@
|
|||||||
"elementCallUrl": "Element Call URL"
|
"elementCallUrl": "Element Call URL"
|
||||||
},
|
},
|
||||||
"settings_explorer": "Settings explorer",
|
"settings_explorer": "Settings explorer",
|
||||||
|
"show_empty_content_events": "Show events with empty content",
|
||||||
"show_hidden_events": "Show hidden events in timeline",
|
"show_hidden_events": "Show hidden events in timeline",
|
||||||
"spaces": {
|
"spaces": {
|
||||||
"one": "<space>",
|
"one": "<space>",
|
||||||
@@ -933,6 +933,7 @@
|
|||||||
"cross_signing_user_warning": "This user has not verified all of their sessions.",
|
"cross_signing_user_warning": "This user has not verified all of their sessions.",
|
||||||
"enter_recovery_key": "Enter recovery key",
|
"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_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_mismatched_sender_key": "Encrypted by an unverified session",
|
||||||
"event_shield_reason_unknown_device": "Encrypted by an unknown or deleted device.",
|
"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.",
|
"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\".",
|
"render_reaction_images_description": "Sometimes referred to as \"custom emojis\".",
|
||||||
"report_to_moderators": "Report to moderators",
|
"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.",
|
"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": "Sliding Sync mode",
|
||||||
"sliding_sync_description": "Under active development, cannot be disabled.",
|
"sliding_sync_description": "Under active development, cannot be disabled.",
|
||||||
"sliding_sync_disabled_notice": "Log out and back in to disable",
|
"sliding_sync_disabled_notice": "Log out and back in to disable",
|
||||||
@@ -1653,8 +1657,8 @@
|
|||||||
"filter_placeholder": "Search room members",
|
"filter_placeholder": "Search room members",
|
||||||
"invite_button_no_perms_tooltip": "You do not have permission to invite users",
|
"invite_button_no_perms_tooltip": "You do not have permission to invite users",
|
||||||
"invited_label": "Invited",
|
"invited_label": "Invited",
|
||||||
"no_matches": "No matches",
|
"list_title": "Member list",
|
||||||
"power_label": "%(userName)s (power %(powerLevelNumber)s)"
|
"no_matches": "No matches"
|
||||||
},
|
},
|
||||||
"member_list_back_action_label": "Room members",
|
"member_list_back_action_label": "Room members",
|
||||||
"message_edit_dialog_title": "Message edits",
|
"message_edit_dialog_title": "Message edits",
|
||||||
@@ -1758,6 +1762,7 @@
|
|||||||
},
|
},
|
||||||
"power_level": {
|
"power_level": {
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
|
"creator": "Owner",
|
||||||
"custom": "Custom (%(level)s)",
|
"custom": "Custom (%(level)s)",
|
||||||
"custom_level": "Custom level",
|
"custom_level": "Custom level",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
@@ -3371,6 +3376,7 @@
|
|||||||
"unable_to_decrypt": "Unable to decrypt message"
|
"unable_to_decrypt": "Unable to decrypt message"
|
||||||
},
|
},
|
||||||
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
||||||
|
"download_action_decrypting": "Decrypting",
|
||||||
"download_action_downloading": "Downloading",
|
"download_action_downloading": "Downloading",
|
||||||
"download_failed": "Download failed",
|
"download_failed": "Download failed",
|
||||||
"download_failed_description": "An error occurred while downloading this file",
|
"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_dm": "%(creator)s kreis ĉi tiun individuan ĉambron.",
|
||||||
"creation_summary_room": "%(creator)s kreis kaj agordis la ĉambron.",
|
"creation_summary_room": "%(creator)s kreis kaj agordis la ĉambron.",
|
||||||
"download_action_decrypting": "Malĉifrante",
|
|
||||||
"edits": {
|
"edits": {
|
||||||
"tooltip_label": "Redaktita je %(date)s. Klaku por vidi redaktojn.",
|
"tooltip_label": "Redaktita je %(date)s. Klaku por vidi redaktojn.",
|
||||||
"tooltip_sub": "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_dm": "%(creator)s creó este mensaje directo.",
|
||||||
"creation_summary_room": "Sala creada y configurada por %(creator)s.",
|
"creation_summary_room": "Sala creada y configurada por %(creator)s.",
|
||||||
"download_action_decrypting": "Descifrando",
|
|
||||||
"download_action_downloading": "Descargando",
|
"download_action_downloading": "Descargando",
|
||||||
"edits": {
|
"edits": {
|
||||||
"tooltip_label": "Última vez editado: %(date)s. Haz clic para ver los cambios.",
|
"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_down": "Suundu jututubade loendis alla",
|
||||||
"room_list_navigate_up": "Suundu jututubade loendis üles",
|
"room_list_navigate_up": "Suundu jututubade loendis üles",
|
||||||
"room_list_select_room": "Vali tubade loendist jututuba",
|
"room_list_select_room": "Vali tubade loendist jututuba",
|
||||||
|
"save": "Salvesta",
|
||||||
"scroll_down_timeline": "Liigu ajajoonel alla",
|
"scroll_down_timeline": "Liigu ajajoonel alla",
|
||||||
"scroll_up_timeline": "Liigu ajajoonel üles",
|
"scroll_up_timeline": "Liigu ajajoonel üles",
|
||||||
"search": "Otsing (peab olema lubatud)",
|
"search": "Otsing (peab olema lubatud)",
|
||||||
@@ -3370,7 +3371,6 @@
|
|||||||
"unable_to_decrypt": "Sõnumi dekrüptimine ei õnnestu"
|
"unable_to_decrypt": "Sõnumi dekrüptimine ei õnnestu"
|
||||||
},
|
},
|
||||||
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
||||||
"download_action_decrypting": "Dekrüptin sisu",
|
|
||||||
"download_action_downloading": "Laadin alla",
|
"download_action_downloading": "Laadin alla",
|
||||||
"download_failed": "Allalaadimine ei õnnestunud",
|
"download_failed": "Allalaadimine ei õnnestunud",
|
||||||
"download_failed_description": "Selle faili allalaadimisel tekkis viga",
|
"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."
|
"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": {
|
"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",
|
"desktop_default_device_name": "%(brand)sin työpöytäversio: %(platformName)s",
|
||||||
"devtools": {
|
"devtools": {
|
||||||
@@ -1240,6 +1241,7 @@
|
|||||||
"room_list_navigate_down": "Liiku alas huoneluettelossa",
|
"room_list_navigate_down": "Liiku alas huoneluettelossa",
|
||||||
"room_list_navigate_up": "Liiku ylös huoneluettelossa",
|
"room_list_navigate_up": "Liiku ylös huoneluettelossa",
|
||||||
"room_list_select_room": "Valitse huone huoneluettelosta",
|
"room_list_select_room": "Valitse huone huoneluettelosta",
|
||||||
|
"save": "Tallenna",
|
||||||
"scroll_down_timeline": "Vieritä alas aikajanalla",
|
"scroll_down_timeline": "Vieritä alas aikajanalla",
|
||||||
"scroll_up_timeline": "Vieritä ylös aikajanalla",
|
"scroll_up_timeline": "Vieritä ylös aikajanalla",
|
||||||
"search": "Haku (pitää olla käytössä)",
|
"search": "Haku (pitää olla käytössä)",
|
||||||
@@ -2356,6 +2358,7 @@
|
|||||||
"notifications": {
|
"notifications": {
|
||||||
"desktop_notification_message_preview": "Näytä viestin esikatselu työpöytäilmoituksessa",
|
"desktop_notification_message_preview": "Näytä viestin esikatselu työpöytäilmoituksessa",
|
||||||
"dialog_title": "<strong>Asetukset:</strong> Ilmoitukset",
|
"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_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_desktop_notifications_session": "Ota käyttöön työpöytäilmoitukset tälle istunnolle",
|
||||||
"enable_email_notifications": "Sähköposti-ilmoitukset osoitteeseen %(email)s",
|
"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_chat_effects": "Näytä keskustelutehosteet (animaatiot, kun saat esim. konfettia)",
|
||||||
"show_displayname_changes": "Näytä näyttönimien muutokset",
|
"show_displayname_changes": "Näytä näyttönimien muutokset",
|
||||||
"show_join_leave": "Näytä liittymis- ja poistumisviestit (ei vaikutusta kutsuihin, poistamisiin ja porttikieltoihin)",
|
"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_nsfw_content": "Näytä NSFW-sisältö",
|
||||||
"show_read_receipts": "Näytä muiden käyttäjien lukukuittaukset",
|
"show_read_receipts": "Näytä muiden käyttäjien lukukuittaukset",
|
||||||
"show_redaction_placeholder": "Näytä paikanpitäjä poistetuille viesteille",
|
"show_redaction_placeholder": "Näytä paikanpitäjä poistetuille viesteille",
|
||||||
@@ -2886,7 +2890,6 @@
|
|||||||
"unable_to_decrypt": "Viestin salausta ei voi purkaa"
|
"unable_to_decrypt": "Viestin salausta ei voi purkaa"
|
||||||
},
|
},
|
||||||
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
||||||
"download_action_decrypting": "Puretaan salausta",
|
|
||||||
"download_action_downloading": "Ladataan",
|
"download_action_downloading": "Ladataan",
|
||||||
"download_failed": "Lataus epäonnistui",
|
"download_failed": "Lataus epäonnistui",
|
||||||
"download_failed_description": "Tiedostoa ladattaessa tapahtui virhe",
|
"download_failed_description": "Tiedostoa ladattaessa tapahtui virhe",
|
||||||
|
|||||||
@@ -568,7 +568,7 @@
|
|||||||
"room_name": "Nom du salon",
|
"room_name": "Nom du salon",
|
||||||
"rooms": "Salons",
|
"rooms": "Salons",
|
||||||
"save": "Enregistrer",
|
"save": "Enregistrer",
|
||||||
"saved": "Sauvegardé",
|
"saved": "Enregistré",
|
||||||
"saving": "Enregistrement…",
|
"saving": "Enregistrement…",
|
||||||
"secure_backup": "Sauvegarde sécurisée",
|
"secure_backup": "Sauvegarde sécurisée",
|
||||||
"select_all": "Tout sélectionner",
|
"select_all": "Tout sélectionner",
|
||||||
@@ -863,6 +863,7 @@
|
|||||||
"elementCallUrl": "Lien d'Element Call"
|
"elementCallUrl": "Lien d'Element Call"
|
||||||
},
|
},
|
||||||
"settings_explorer": "Explorateur de paramètres",
|
"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",
|
"show_hidden_events": "Afficher les évènements cachés dans le fil de discussion",
|
||||||
"spaces": {
|
"spaces": {
|
||||||
"one": "<espace>",
|
"one": "<espace>",
|
||||||
@@ -1450,6 +1451,7 @@
|
|||||||
"room_list_navigate_down": "Descendre dans la liste des salons",
|
"room_list_navigate_down": "Descendre dans la liste des salons",
|
||||||
"room_list_navigate_up": "Remonter 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",
|
"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_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",
|
"scroll_up_timeline": "Faire défiler le fil de discussion vers le haut",
|
||||||
"search": "Recherche (si activée)",
|
"search": "Recherche (si activée)",
|
||||||
@@ -2730,7 +2732,7 @@
|
|||||||
"set_phrase_again": "Retournez en arrière pour la redéfinir.",
|
"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.",
|
"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_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é",
|
"title_set_phrase": "Définir une phrase de sécurité",
|
||||||
"unable_to_setup": "Impossible de configurer le coffre secret",
|
"unable_to_setup": "Impossible de configurer le coffre secret",
|
||||||
"use_different_passphrase": "Utiliser une phrase secrète différente ?",
|
"use_different_passphrase": "Utiliser une phrase secrète différente ?",
|
||||||
@@ -3369,7 +3371,6 @@
|
|||||||
"unable_to_decrypt": "Impossible de déchiffrer le message"
|
"unable_to_decrypt": "Impossible de déchiffrer le message"
|
||||||
},
|
},
|
||||||
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
||||||
"download_action_decrypting": "Déchiffrement",
|
|
||||||
"download_action_downloading": "Téléchargement en cours",
|
"download_action_downloading": "Téléchargement en cours",
|
||||||
"download_failed": "Échec du téléchargement",
|
"download_failed": "Échec du téléchargement",
|
||||||
"download_failed_description": "Une erreur s'est produite lors du téléchargement de ce fichier",
|
"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_dm": "%(creator)s creou esta MD.",
|
||||||
"creation_summary_room": "%(creator)s creou e configurou a sala.",
|
"creation_summary_room": "%(creator)s creou e configurou a sala.",
|
||||||
"download_action_decrypting": "Descifrando",
|
|
||||||
"download_action_downloading": "Descargando",
|
"download_action_downloading": "Descargando",
|
||||||
"edits": {
|
"edits": {
|
||||||
"tooltip_label": "Editada o %(date)s. Preme para ver edicións.",
|
"tooltip_label": "Editada o %(date)s. Preme para ver edicións.",
|
||||||
|
|||||||
@@ -1985,7 +1985,6 @@
|
|||||||
},
|
},
|
||||||
"creation_summary_dm": "%(creator)s יצר את DM הזה.",
|
"creation_summary_dm": "%(creator)s יצר את DM הזה.",
|
||||||
"creation_summary_room": "%(creator)s יצר/ה והגדיר/ה את החדר.",
|
"creation_summary_room": "%(creator)s יצר/ה והגדיר/ה את החדר.",
|
||||||
"download_action_decrypting": "מפענח",
|
|
||||||
"download_action_downloading": "מוריד",
|
"download_action_downloading": "מוריד",
|
||||||
"edits": {
|
"edits": {
|
||||||
"tooltip_label": "נערך ב-%(date)s. לחץ לצפייה בעריכות.",
|
"tooltip_label": "נערך ב-%(date)s. לחץ לצפייה בעריכות.",
|
||||||
|
|||||||
@@ -3344,7 +3344,6 @@
|
|||||||
"unable_to_decrypt": "Nem sikerült visszafejteni az üzenetet"
|
"unable_to_decrypt": "Nem sikerült visszafejteni az üzenetet"
|
||||||
},
|
},
|
||||||
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
||||||
"download_action_decrypting": "Visszafejtés",
|
|
||||||
"download_action_downloading": "Letöltés",
|
"download_action_downloading": "Letöltés",
|
||||||
"download_failed": "Letöltés sikertelen",
|
"download_failed": "Letöltés sikertelen",
|
||||||
"download_failed_description": "Hiba történt a fájl letöltése közben",
|
"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"
|
"unable_to_decrypt": "Tidak dapat mendekripsi pesan"
|
||||||
},
|
},
|
||||||
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
||||||
"download_action_decrypting": "Mendekripsi",
|
|
||||||
"download_action_downloading": "Mengunduh",
|
"download_action_downloading": "Mengunduh",
|
||||||
"download_failed": "Pengunduhan gagal",
|
"download_failed": "Pengunduhan gagal",
|
||||||
"download_failed_description": "Terjadi kesalahan saat mengunduh berkas ini",
|
"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_dm": "%(creator)s bjó til oþessi beinu skilaboð.",
|
||||||
"creation_summary_room": "%(creator)s bjó til og stillti spjallrásina.",
|
"creation_summary_room": "%(creator)s bjó til og stillti spjallrásina.",
|
||||||
"download_action_decrypting": "Afkóðun",
|
|
||||||
"download_action_downloading": "Sæki",
|
"download_action_downloading": "Sæki",
|
||||||
"edits": {
|
"edits": {
|
||||||
"tooltip_label": "Breytt þann %(date)s. Smelltu hér til að skoða breytingar.",
|
"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_dm": "%(creator)s ha creato questo MD.",
|
||||||
"creation_summary_room": "%(creator)s ha creato e configurato la stanza.",
|
"creation_summary_room": "%(creator)s ha creato e configurato la stanza.",
|
||||||
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
||||||
"download_action_decrypting": "Decifrazione",
|
|
||||||
"download_action_downloading": "Scaricamento",
|
"download_action_downloading": "Scaricamento",
|
||||||
"edits": {
|
"edits": {
|
||||||
"tooltip_label": "Modificato alle %(date)s. Clicca per vedere le modifiche.",
|
"tooltip_label": "Modificato alle %(date)s. Clicca per vedere le modifiche.",
|
||||||
|
|||||||
@@ -2649,7 +2649,6 @@
|
|||||||
"creation_summary_dm": "%(creator)sがこのダイレクトメッセージを作成しました。",
|
"creation_summary_dm": "%(creator)sがこのダイレクトメッセージを作成しました。",
|
||||||
"creation_summary_room": "%(creator)sがルームを作成し設定しました。",
|
"creation_summary_room": "%(creator)sがルームを作成し設定しました。",
|
||||||
"disambiguated_profile": "%(displayName)s(%(matrixId)s)",
|
"disambiguated_profile": "%(displayName)s(%(matrixId)s)",
|
||||||
"download_action_decrypting": "復号化しています",
|
|
||||||
"download_action_downloading": "ダウンロードしています",
|
"download_action_downloading": "ダウンロードしています",
|
||||||
"edits": {
|
"edits": {
|
||||||
"tooltip_label": "%(date)sに編集済。クリックすると変更履歴を表示。",
|
"tooltip_label": "%(date)sに編集済。クリックすると変更履歴を表示。",
|
||||||
|
|||||||
@@ -2262,7 +2262,6 @@
|
|||||||
"creation_summary_dm": "%(creator)sშექმნა ეს DM.",
|
"creation_summary_dm": "%(creator)sშექმნა ეს DM.",
|
||||||
"creation_summary_room": "%(creator)sოთახის შექმნა და კონფიგურაცია.",
|
"creation_summary_room": "%(creator)sოთახის შექმნა და კონფიგურაცია.",
|
||||||
"disambiguated_profile": "%(displayName)s(%(matrixId)s )",
|
"disambiguated_profile": "%(displayName)s(%(matrixId)s )",
|
||||||
"download_action_decrypting": "გაშიფვრა",
|
|
||||||
"download_action_downloading": "ჩამოტვირთვა",
|
"download_action_downloading": "ჩამოტვირთვა",
|
||||||
"edits": {
|
"edits": {
|
||||||
"tooltip_label": "რედაქტირებულია%(date)s . დააწკაპუნეთ რედაქტირების სანახავად.",
|
"tooltip_label": "რედაქტირებულია%(date)s . დააწკაპუნეთ რედაქტირების სანახავად.",
|
||||||
|
|||||||
@@ -2342,7 +2342,6 @@
|
|||||||
},
|
},
|
||||||
"creation_summary_dm": "%(creator)s ສ້າງ DM ນີ້.",
|
"creation_summary_dm": "%(creator)s ສ້າງ DM ນີ້.",
|
||||||
"creation_summary_room": "%(creator)s ສ້າງ ແລະ ກຳນົດຄ່າຫ້ອງ.",
|
"creation_summary_room": "%(creator)s ສ້າງ ແລະ ກຳນົດຄ່າຫ້ອງ.",
|
||||||
"download_action_decrypting": "ການຖອດລະຫັດ",
|
|
||||||
"download_action_downloading": "ກຳລັງດາວໂຫຼດ",
|
"download_action_downloading": "ກຳລັງດາວໂຫຼດ",
|
||||||
"edits": {
|
"edits": {
|
||||||
"tooltip_label": "ແກ້ໄຂເມື່ອ %(date)s. ກົດເພື່ອເບິ່ງການແກ້ໄຂ.",
|
"tooltip_label": "ແກ້ໄຂເມື່ອ %(date)s. ກົດເພື່ອເບິ່ງການແກ້ໄຂ.",
|
||||||
|
|||||||
@@ -1827,7 +1827,6 @@
|
|||||||
},
|
},
|
||||||
"creation_summary_dm": "%(creator)s sukūrė šį tiesioginio susirašymo kambarį.",
|
"creation_summary_dm": "%(creator)s sukūrė šį tiesioginio susirašymo kambarį.",
|
||||||
"creation_summary_room": "%(creator)s sukūrė ir sukonfigūravo kambarį.",
|
"creation_summary_room": "%(creator)s sukūrė ir sukonfigūravo kambarį.",
|
||||||
"download_action_decrypting": "Iššifruojama",
|
|
||||||
"download_action_downloading": "Atsiunčiama",
|
"download_action_downloading": "Atsiunčiama",
|
||||||
"edits": {
|
"edits": {
|
||||||
"tooltip_label": "Keista %(date)s. Spustelėkite kad peržiūrėti pakeitimus.",
|
"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."
|
"historical_event_unverified_device": "Ir jāapliecina šī ierīce, lai piekļūtu senākām ziņām."
|
||||||
},
|
},
|
||||||
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
||||||
"download_action_decrypting": "Atšifrē",
|
|
||||||
"download_action_downloading": "Lejupielādē",
|
"download_action_downloading": "Lejupielādē",
|
||||||
"edits": {
|
"edits": {
|
||||||
"tooltip_label": "Labots %(date)s. Klikšķināt, lai skatītu labojumus.",
|
"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_dm": "%(creator)snamorona ity DM ity.",
|
||||||
"creation_summary_room": "%(creator)snamorona sy nanamboatra ny efitrano.",
|
"creation_summary_room": "%(creator)snamorona sy nanamboatra ny efitrano.",
|
||||||
"disambiguated_profile": "%(displayName)s(%(matrixId)s )",
|
"disambiguated_profile": "%(displayName)s(%(matrixId)s )",
|
||||||
"download_action_decrypting": "Decryptering",
|
|
||||||
"download_action_downloading": "Misintona",
|
"download_action_downloading": "Misintona",
|
||||||
"edits": {
|
"edits": {
|
||||||
"tooltip_label": "Namboarina tamin'ny%(Rendez-vous . Kitiho raha hijery ny fanovana.",
|
"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_down": "Naviger ned i romlisten",
|
||||||
"room_list_navigate_up": "Naviger opp i romlisten",
|
"room_list_navigate_up": "Naviger opp i romlisten",
|
||||||
"room_list_select_room": "Velg rom fra romlisten",
|
"room_list_select_room": "Velg rom fra romlisten",
|
||||||
|
"save": "Lagre",
|
||||||
"scroll_down_timeline": "Rull ned i tidslinjen",
|
"scroll_down_timeline": "Rull ned i tidslinjen",
|
||||||
"scroll_up_timeline": "Rull opp i tidslinjen",
|
"scroll_up_timeline": "Rull opp i tidslinjen",
|
||||||
"search": "Søk (må være aktivert)",
|
"search": "Søk (må være aktivert)",
|
||||||
@@ -3369,7 +3370,6 @@
|
|||||||
"unable_to_decrypt": "Kan ikke dekryptere meldingen"
|
"unable_to_decrypt": "Kan ikke dekryptere meldingen"
|
||||||
},
|
},
|
||||||
"disambiguated_profile": "%(displayName)s(%(matrixId)s)",
|
"disambiguated_profile": "%(displayName)s(%(matrixId)s)",
|
||||||
"download_action_decrypting": "Dekrypterer",
|
|
||||||
"download_action_downloading": "Laster ned",
|
"download_action_downloading": "Laster ned",
|
||||||
"download_failed": "Nedlasting mislyktes",
|
"download_failed": "Nedlasting mislyktes",
|
||||||
"download_failed_description": "Det oppstod en feil under nedlasting av denne filen",
|
"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_dm": "%(creator)s maakte deze directe chat.",
|
||||||
"creation_summary_room": "Kamer gestart en ingesteld door %(creator)s.",
|
"creation_summary_room": "Kamer gestart en ingesteld door %(creator)s.",
|
||||||
"download_action_decrypting": "Ontsleutelen",
|
|
||||||
"download_action_downloading": "Downloaden",
|
"download_action_downloading": "Downloaden",
|
||||||
"edits": {
|
"edits": {
|
||||||
"tooltip_label": "Bewerkt op %(date)s. Klik om de bewerkingen te bekijken.",
|
"tooltip_label": "Bewerkt op %(date)s. Klik om de bewerkingen te bekijken.",
|
||||||
|
|||||||
@@ -159,6 +159,7 @@
|
|||||||
"view_message": "Wyświetl wiadomość",
|
"view_message": "Wyświetl wiadomość",
|
||||||
"view_source": "Wyświetl źródło",
|
"view_source": "Wyświetl źródło",
|
||||||
"yes": "Tak",
|
"yes": "Tak",
|
||||||
|
"yes_dismiss": "Tak, odrzuć",
|
||||||
"zoom_in": "Powiększ",
|
"zoom_in": "Powiększ",
|
||||||
"zoom_out": "Pomniejsz"
|
"zoom_out": "Pomniejsz"
|
||||||
},
|
},
|
||||||
@@ -788,6 +789,7 @@
|
|||||||
"cross_signing_status": "Status weryfikacji krzyżowej:",
|
"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ę.",
|
"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",
|
"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": "Aktywna wersja kopii zapasowej:",
|
||||||
"key_backup_active_version_none": "Brak",
|
"key_backup_active_version_none": "Brak",
|
||||||
"key_backup_inactive_warning": "Twoje klucze nie są przywracane na tej sesji.",
|
"key_backup_inactive_warning": "Twoje klucze nie są przywracane na tej sesji.",
|
||||||
@@ -800,6 +802,8 @@
|
|||||||
"secret_storage_ready": "gotowe",
|
"secret_storage_ready": "gotowe",
|
||||||
"secret_storage_status": "Sekretny magazyn:",
|
"secret_storage_status": "Sekretny magazyn:",
|
||||||
"self_signing_private_key_cached_status": "Samopodpisujący klucz prywatny:",
|
"self_signing_private_key_cached_status": "Samopodpisujący klucz prywatny:",
|
||||||
|
"session": "Sesja",
|
||||||
|
"session_fingerprint": "Odcisk palca (klucz sesji)",
|
||||||
"title": "Szyfrowanie end-to-end",
|
"title": "Szyfrowanie end-to-end",
|
||||||
"user_signing_private_key_cached_status": "Podpisany przez użytkownika klucz prywatny:"
|
"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": "Tryb niskiej przepustowości",
|
||||||
"low_bandwidth_mode_description": "Wymaga kompatybilnego serwera domowego.",
|
"low_bandwidth_mode_description": "Wymaga kompatybilnego serwera domowego.",
|
||||||
"main_timeline": "Główna oś czasu",
|
"main_timeline": "Główna oś czasu",
|
||||||
|
"manual_device_verification": "Ręczna weryfikacja urządzenia",
|
||||||
"no_receipt_found": "Nie znaleziono potwierdzenia",
|
"no_receipt_found": "Nie znaleziono potwierdzenia",
|
||||||
"notification_state": "Status powiadomień <strong>%(notificationState)s</strong>",
|
"notification_state": "Status powiadomień <strong>%(notificationState)s</strong>",
|
||||||
"notifications_debug": "Debug powiadomień",
|
"notifications_debug": "Debug powiadomień",
|
||||||
@@ -915,9 +920,11 @@
|
|||||||
"empty_room_was_name": "Pusty pokój (poprzednio %(oldName)s)",
|
"empty_room_was_name": "Pusty pokój (poprzednio %(oldName)s)",
|
||||||
"encryption": {
|
"encryption": {
|
||||||
"access_secret_storage_dialog": {
|
"access_secret_storage_dialog": {
|
||||||
|
"alternatives": "Możesz podać również klucz lub frazę bezpieczeństwa.",
|
||||||
"key_validation_text": {
|
"key_validation_text": {
|
||||||
"wrong_security_key": "Wprowadzony klucz przywracania nie jest poprawny."
|
"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",
|
"restoring": "Przywracanie kluczy z kopii zapasowej",
|
||||||
"security_key_title": "Klucz przywracania"
|
"security_key_title": "Klucz przywracania"
|
||||||
},
|
},
|
||||||
@@ -971,6 +978,8 @@
|
|||||||
"setup_secure_backup": {
|
"setup_secure_backup": {
|
||||||
"explainer": "Utwórz kopię zapasową kluczy przed wylogowaniem, aby ich nie utracić."
|
"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": {
|
"udd": {
|
||||||
"interactive_verification_button": "Zweryfikuj interaktywnie za pomocą emoji",
|
"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.",
|
"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_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_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ą.",
|
"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_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.",
|
"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ę.",
|
"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": "Włączając %(commaSeparatedMembers)s",
|
||||||
"face_pile_tooltip_shortcut_joined": "Włączając Ciebie, %(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",
|
"failed_reject_invite": "Nie udało się odrzucić zaproszenia",
|
||||||
"forget_room": "Zapomnij o tym pokoju",
|
"forget_room": "Zapomnij o tym pokoju",
|
||||||
"forget_space": "Zapomnij tą przestrzeń",
|
"forget_space": "Zapomnij tą przestrzeń",
|
||||||
@@ -2053,6 +2078,8 @@
|
|||||||
"read_topic": "Kliknij, aby przeczytać temat",
|
"read_topic": "Kliknij, aby przeczytać temat",
|
||||||
"rejecting": "Odrzucanie zaproszenia…",
|
"rejecting": "Odrzucanie zaproszenia…",
|
||||||
"rejoin_button": "Dołącz ponownie",
|
"rejoin_button": "Dołącz ponownie",
|
||||||
|
"room_content": "Zawartość pokoju",
|
||||||
|
"room_is_low_priority": "To jest pokój o niskim priorytecie",
|
||||||
"search": {
|
"search": {
|
||||||
"all_rooms_button": "Wyszukaj wszystkie pokoje",
|
"all_rooms_button": "Wyszukaj wszystkie pokoje",
|
||||||
"placeholder": "Szukaj wiadomości...",
|
"placeholder": "Szukaj wiadomości...",
|
||||||
@@ -2102,24 +2129,33 @@
|
|||||||
"add_space_label": "Dodaj przestrzeń",
|
"add_space_label": "Dodaj przestrzeń",
|
||||||
"breadcrumbs_empty": "Brak ostatnio odwiedzonych pokojów",
|
"breadcrumbs_empty": "Brak ostatnio odwiedzonych pokojów",
|
||||||
"breadcrumbs_label": "Ostatnio odwiedzane pokoje",
|
"breadcrumbs_label": "Ostatnio odwiedzane pokoje",
|
||||||
|
"collapse_filters": "Zwiń listę filtrów",
|
||||||
"empty": {
|
"empty": {
|
||||||
"no_chats": "Nie ma jeszcze czatów",
|
"no_chats": "Nie ma jeszcze czatów",
|
||||||
"no_chats_description": "Zacznij od wysłania wiadomości lub utworzenia pokoju",
|
"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_chats_description_no_room_rights": "Wyślij komuś wiadomość, aby rozpocząć.",
|
||||||
"no_favourites": "Nie masz jeszcze ulubionego czatu",
|
"no_favourites": "Nie masz jeszcze ulubionego czatu",
|
||||||
"no_favourites_description": "Dodaj czat do ulubionych w ustawieniach 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": "Nie prowadzisz jeszcze z nikim czatów prywatnych",
|
||||||
"no_people_description": "Wyczyść filtry, aby zobaczyć pozostałe czaty",
|
"no_people_description": "Wyczyść filtry, aby zobaczyć pozostałe czaty",
|
||||||
"no_rooms": "Nie jesteś jeszcze w żadnym pokoju",
|
"no_rooms": "Nie jesteś jeszcze w żadnym pokoju",
|
||||||
"no_rooms_description": "Wyczyść filtry, aby zobaczyć pozostałe czaty",
|
"no_rooms_description": "Wyczyść filtry, aby zobaczyć pozostałe czaty",
|
||||||
"no_unread": "Brawo! Nie masz żadnych nieprzeczytanych wiadomości",
|
"no_unread": "Brawo! Nie masz żadnych nieprzeczytanych wiadomości",
|
||||||
|
"show_activity": "Wyświetl całą aktywność",
|
||||||
"show_chats": "Pokaż wszystkie czaty"
|
"show_chats": "Pokaż wszystkie czaty"
|
||||||
},
|
},
|
||||||
|
"expand_filters": "Rozwiń listę filtrów",
|
||||||
"failed_add_tag": "Nie można dodać tagu %(tagName)s do pokoju",
|
"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_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",
|
"failed_set_dm_tag": "Nie udało się ustawić tagu wiadomości prywatnych",
|
||||||
"filters": {
|
"filters": {
|
||||||
"favourite": "Ulubione",
|
"favourite": "Ulubione",
|
||||||
|
"invites": "Zaproszenia",
|
||||||
|
"low_priority": "Niski priorytet",
|
||||||
|
"mentions": "Wzmianki",
|
||||||
"people": "Osoby",
|
"people": "Osoby",
|
||||||
"rooms": "Pokoje",
|
"rooms": "Pokoje",
|
||||||
"unread": "Nieprzeczytane"
|
"unread": "Nieprzeczytane"
|
||||||
@@ -2550,6 +2586,8 @@
|
|||||||
"session_key": "Klucz sesji:",
|
"session_key": "Klucz sesji:",
|
||||||
"title": "Zaawansowane"
|
"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": {
|
"delete_key_storage": {
|
||||||
"breadcrumb_page": "Usuń magazyn kluczy",
|
"breadcrumb_page": "Usuń magazyn kluczy",
|
||||||
"confirm": "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": "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)",
|
"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",
|
"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",
|
"jump_to_bottom_on_send": "Przejdź na dół osi czasu po wysłaniu wiadomości",
|
||||||
"key_backup": {
|
"key_backup": {
|
||||||
"setup_secure_backup": {
|
"setup_secure_backup": {
|
||||||
@@ -2746,6 +2787,7 @@
|
|||||||
"show_in_private": "W pokojach prywatnych",
|
"show_in_private": "W pokojach prywatnych",
|
||||||
"show_media": "Zawsze pokazuj"
|
"show_media": "Zawsze pokazuj"
|
||||||
},
|
},
|
||||||
|
"not_supported": "Twój serwer nie obsługuje tej funkcji.",
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"default_setting_description": "To ustawienie zastosuje się do wszystkich Twoich pokoi.",
|
"default_setting_description": "To ustawienie zastosuje się do wszystkich Twoich pokoi.",
|
||||||
"default_setting_section": "Chce otrzymywać powiadomienia (Domyślne ustawienie)",
|
"default_setting_section": "Chce otrzymywać powiadomienia (Domyślne ustawienie)",
|
||||||
@@ -2803,6 +2845,7 @@
|
|||||||
"voip": "Połączenia audio i wideo"
|
"voip": "Połączenia audio i wideo"
|
||||||
},
|
},
|
||||||
"preferences": {
|
"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).",
|
"Electron.enableHardwareAcceleration": "Włącz akcelerację sprzętową (uruchom ponownie %(appName)s, aby zastosować zmiany).",
|
||||||
"always_show_menu_bar": "Zawsze pokazuj pasek menu okna",
|
"always_show_menu_bar": "Zawsze pokazuj pasek menu okna",
|
||||||
"autocomplete_delay": "Opóźnienie autouzupełniania (ms)",
|
"autocomplete_delay": "Opóźnienie autouzupełniania (ms)",
|
||||||
@@ -2811,6 +2854,7 @@
|
|||||||
"composer_heading": "Kompozytor",
|
"composer_heading": "Kompozytor",
|
||||||
"default_timezone": "Ustawienie przeglądarki (%(timezone)s)",
|
"default_timezone": "Ustawienie przeglądarki (%(timezone)s)",
|
||||||
"dialog_title": "<strong>Ustawienia:</strong> Preferencje",
|
"dialog_title": "<strong>Ustawienia:</strong> Preferencje",
|
||||||
|
"enable_content_protection": "Włącz ochronę treści",
|
||||||
"enable_hardware_acceleration": "Włącz przyspieszenie sprzętowe",
|
"enable_hardware_acceleration": "Włącz przyspieszenie sprzętowe",
|
||||||
"enable_tray_icon": "Pokaż ikonę w zasobniku systemowym i zminimalizuj okno do niej zamiast zamknięcia",
|
"enable_tray_icon": "Pokaż ikonę w zasobniku systemowym i zminimalizuj okno do niej zamiast zamknięcia",
|
||||||
"keyboard_heading": "Skróty klawiszowe",
|
"keyboard_heading": "Skróty klawiszowe",
|
||||||
@@ -2974,6 +3018,7 @@
|
|||||||
"show_chat_effects": "Pokaż efekty czatu (animacje po odebraniu np. confetti)",
|
"show_chat_effects": "Pokaż efekty czatu (animacje po odebraniu np. confetti)",
|
||||||
"show_displayname_changes": "Pokaż zmiany wyświetlanej nazwy",
|
"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_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_nsfw_content": "Pokaż zawartość NSFW",
|
||||||
"show_read_receipts": "Pokaż potwierdzenia odczytania wysyłane przez innych użytkowników",
|
"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",
|
"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": "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.",
|
"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",
|
"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ę",
|
"me": "Wyświetla akcję",
|
||||||
"msg": "Wysyła wiadomość do wybranego użytkownika",
|
"msg": "Wysyła wiadomość do wybranego użytkownika",
|
||||||
"myavatar": "Zdjęcie profilowe zostanie zmienione we wszystkich pokojach",
|
"myavatar": "Zdjęcie profilowe zostanie zmienione we wszystkich pokojach",
|
||||||
@@ -3120,7 +3167,7 @@
|
|||||||
"upgraderoom": "Ulepsza pokój do nowej wersji",
|
"upgraderoom": "Ulepsza pokój do nowej wersji",
|
||||||
"upgraderoom_permission_error": "Nie posiadasz wymaganych uprawnień do użycia tego polecenia.",
|
"upgraderoom_permission_error": "Nie posiadasz wymaganych uprawnień do użycia tego polecenia.",
|
||||||
"usage": "Użycie",
|
"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",
|
"view": "Przegląda pokój z podanym adresem",
|
||||||
"whois": "Pokazuje informacje na temat użytkownika"
|
"whois": "Pokazuje informacje na temat użytkownika"
|
||||||
},
|
},
|
||||||
@@ -3340,7 +3387,6 @@
|
|||||||
"unable_to_decrypt": "Nie można rozszyfrować wiadomości"
|
"unable_to_decrypt": "Nie można rozszyfrować wiadomości"
|
||||||
},
|
},
|
||||||
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
||||||
"download_action_decrypting": "Rozszyfrowuję",
|
|
||||||
"download_action_downloading": "Pobieranie",
|
"download_action_downloading": "Pobieranie",
|
||||||
"download_failed": "Błąd pobierania",
|
"download_failed": "Błąd pobierania",
|
||||||
"download_failed_description": "Wystąpił błąd podczas pobierania tego pliku",
|
"download_failed_description": "Wystąpił błąd podczas pobierania tego pliku",
|
||||||
|
|||||||