Compare commits

..

30 Commits

Author SHA1 Message Date
Robin
9fc9b53629 Add reaction capability missing from test
Element Call does in fact request this one.
2025-07-24 19:41:42 +02:00
Robin
b2a1a6a0a4 Allow Element Call to send call notifications
Currently Element Web is responsible for sending the call notification event, but this is planned to be changed soon. As of the upcoming Element Call 0.14.0 release, it will request the capability to send call notifications itself, and we should auto-approve this capability.
2025-07-24 19:33:26 +02:00
Florian Duros
81edfece6a fix: replace hardcoded string on qr code back button (#30401) 2025-07-24 15:39:26 +00:00
Florian Duros
ab26004c4c Change unencrypted and public pills to blue (#30399)
* feat: change unencrypted and public pill to blue

* test: update snapshots

* test(e2e): update screenshots
2025-07-24 14:52:59 +00:00
Richard van der Hoff
ffedca3954 Allow for unknown event shield reasons (#30397)
A forthcoming change to the js-sdk will add a new event shield reason. To avoid
a compile-time failure, add a `default` case to the code handling those
reasons.
2025-07-24 13:16:15 +00:00
David Baker
f7ef948cf0 Update playwright-common package (#30396)
So the shared components screenshot generator works
2025-07-24 10:45:15 +00:00
ElementRobot
ba828b2194 [create-pull-request] automated change (#30394)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-07-24 10:02:19 +00:00
Florian Duros
16ef503174 Storybook: add tooltip provider and sort stories (#30392)
* chore: add tooltip provider to storybook preview

* chore: order story alphabetically
2025-07-23 15:30:54 +00:00
Florian Duros
7bfb9818f6 Change color of public room icon (#30390)
* feat: change color of public room icon

* test: update room avatar snapshot

* test(e2E): update screenshots
2025-07-23 13:56:26 +00:00
David Baker
dcbba5ea9d Script for updating storybook screenshots (#30340)
* Script for updating storybook screenshots

Requires https://github.com/element-hq/element-modules/pull/43

* Prettier
2025-07-23 12:06:33 +00:00
ElementRobot
6b40da5779 [create-pull-request] automated change (#30384)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-07-23 11:24:19 +00:00
ElementRobot
941835ccf2 [create-pull-request] automated change (#30326)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-07-23 08:23:28 +00:00
renovate[bot]
4ec10a9b4d Update typescript-eslint monorepo to v8.37.0 (#30379)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-23 08:10:40 +00:00
renovate[bot]
6a48183a35 Update dependency @sentry/webpack-plugin to v4 (#30381)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 23:13:01 +00:00
renovate[bot]
62b080a50e Update all non-major dependencies (#30374)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 17:55:35 +00:00
renovate[bot]
0dc7fcc64a Update dependency @types/node to v18.19.120 (#30371)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 17:39:15 +00:00
renovate[bot]
354867baa7 Update playwright to v1.54.1 (#30378)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 17:01:34 +00:00
renovate[bot]
4c1e3c82e4 Update dependency testcontainers to v11.3.0 (#30377)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 16:33:02 +00:00
Florian Duros
1e689ac098 Move Flex & Box component into shared component folder (#30357)
* refactor: move Flex component in shared components

* refactor: update imports

* refactor: remove Flex pcss file

* fix: Flex component css override

* test: update snapshots

* fix: html export

* chore: add css module support to jest

* chore: keep old copyright

* refactor: change `mx_Flex` in `ErrorView` to `mx_ErrorView_flexContainer`

* test: update snapshots

* refactor: move Box component in shared components

* refactor: update import and css override

* test: update snapshots
2025-07-22 16:25:45 +00:00
renovate[bot]
16ab7ffbc7 Update Node.js to a803244 (#30370)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 16:24:07 +00:00
renovate[bot]
b35e2a8c45 Update dependency @stylistic/eslint-plugin to v5.2.0 (#30376)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 16:13:45 +00:00
renovate[bot]
a07d5b82b3 Update dependency @sentry/browser to v9.40.0 (#30375)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 16:13:35 +00:00
renovate[bot]
ca1420e604 Update nginxinc/nginx-unprivileged:alpine-slim Docker digest to 86df552 (#30369)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 16:12:35 +00:00
renovate[bot]
8e59ebb754 Update storybook monorepo to v9.0.17 (#30372)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 15:28:22 +00:00
Timo
cc2ee5ea78 Add toggle to hide empty state in devtools (#30352)
* Add toggle to hide empty state in devtools

* use translated string

* lint

* inverse logic(`hide`->`show`)

* move entry in i18n to correct position
2025-07-22 14:11:16 +00:00
Florian Duros
774e0e8f7b Fix color of icon button with outline (#30361)
* fix: room list header button color

* fix: room member list invite button

* test: update room list search snapshot

* test(e2e): update screenshots
2025-07-22 14:11:13 +00:00
dependabot[bot]
e0f5f48eef Bump form-data from 4.0.3 to 4.0.4 (#30360)
Bumps [form-data](https://github.com/form-data/form-data) from 4.0.3 to 4.0.4.
- [Release notes](https://github.com/form-data/form-data/releases)
- [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md)
- [Commits](https://github.com/form-data/form-data/compare/v4.0.3...v4.0.4)

---
updated-dependencies:
- dependency-name: form-data
  dependency-version: 4.0.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-22 12:28:53 +00:00
ElementRobot
e7a772472e [create-pull-request] automated change (#30341)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-07-22 08:51:21 +00:00
Marc
0a97cbaada MVVM userinfo: split header and verification components (#30214)
* feat: mvvm userinfo split header and verification

* test: add userinfoheader tests

* fix: userHeaderVerificationView verification method
2025-07-21 12:04:50 +00:00
R Midhun Suresh
8a879c7fca Message preview should show tooltip with the full message on hover (#30265)
* Add title attribute for message preview

So that the full message is shown in a tooltip on hover.

* Fix test

* Update src/components/views/rooms/RoomListPanel/RoomListItemView.tsx

Co-authored-by: Florian Duros <florianduros@element.io>

---------

Co-authored-by: Florian Duros <florianduros@element.io>
2025-07-21 11:58:54 +00:00
129 changed files with 2370 additions and 1629 deletions

View File

@@ -31,6 +31,9 @@ const config: StorybookConfig = {
},
// Needed for counterpart to work
plugins: [nodePolyfills({ include: ["process", "util"] })],
server: {
allowedHosts: ["localhost", ".docker.internal"],
},
});
},
};

View File

@@ -6,6 +6,7 @@ import "./preview.css";
import React, { useLayoutEffect } from "react";
import { FORCE_RE_RENDER } from "storybook/internal/core-events";
import { setLanguage } from "../src/shared-components/i18n";
import { TooltipProvider } from "@vector-im/compound-web";
export const globalTypes = {
theme: {
@@ -82,9 +83,24 @@ export const withLanguageProvider: Decorator = (Story, context) => {
);
};
const withTooltipProvider: Decorator = (Story) => {
return (
<TooltipProvider>
<Story />
</TooltipProvider>
);
};
const preview: Preview = {
tags: ["autodocs"],
decorators: [withThemeProvider, withLanguageProvider],
decorators: [withThemeProvider, withLanguageProvider, withTooltipProvider],
parameters: {
options: {
storySort: {
method: "alphabetical",
},
},
},
};
export default preview;

View File

@@ -1,7 +1,7 @@
# syntax=docker.io/docker/dockerfile:1.17-labs@sha256:9187104f31e3a002a8a6a3209ea1f937fb7486c093cbbde1e14b0fa0d7e4f1b5
# Builder
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:125996cb2451482467fc2aa4d7653075894b08e9b7711bcd761044ca270a083e AS builder
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:a80324457a2c8d09c83ff9edf2bdf71f378d3288de920e68a358bd3c484b8c4a AS builder
# Support custom branch of the js-sdk. This also helps us build images of element-web develop.
ARG USE_CUSTOM_SDKS=false
@@ -19,7 +19,7 @@ RUN /src/scripts/docker-package.sh
RUN cp /src/config.sample.json /src/webapp/config.json
# App
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:ef0100e39ffe377a42ad99e1f644b78097a84f1ac60a90eac3b888196b2eeb00
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:86df552d36eb24c45d3f5becf6423bd056a3fd235d7085fe3d5ea28ba89a8232
# Need root user to install packages & manipulate the usr directory
USER root

View File

@@ -22,6 +22,8 @@ const config: Config = {
setupFiles: ["jest-canvas-mock", "web-streams-polyfill/polyfill"],
setupFilesAfterEnv: ["<rootDir>/test/setupTests.ts"],
moduleNameMapper: {
// Support CSS module
"\\.(module.css)$": "identity-obj-proxy",
"\\.(css|scss|pcss)$": "<rootDir>/__mocks__/cssMock.js",
"\\.(gif|png|ttf|woff2)$": "<rootDir>/__mocks__/imageMock.js",
"\\.svg$": "<rootDir>/__mocks__/svg.js",

View File

@@ -69,11 +69,12 @@
"storybook": "storybook dev -p 6007",
"build-storybook": "storybook build",
"test:storybook": "test-storybook --url http://localhost:6007/",
"test:storybook:ci": "concurrently -k -s first -n \"SB,TEST\" \"yarn storybook\" \"wait-on tcp:6007 && yarn test-storybook --url http://localhost:6007/ --ci --maxWorkers=2\""
"test:storybook:ci": "concurrently -k -s first -n \"SB,TEST\" \"yarn storybook --no-open\" \"wait-on tcp:6007 && yarn test-storybook --url http://localhost:6007/ --ci --maxWorkers=2\"",
"test:storybook:update": "playwright-screenshots --entrypoint yarn --with-node-modules && playwright-screenshots --entrypoint /work/node_modules/.bin/test-storybook --with-node-modules --url http://host.docker.internal:6007/ --updateSnapshot"
},
"resolutions": {
"**/pretty-format/react-is": "19.1.0",
"@playwright/test": "1.53.2",
"@playwright/test": "1.54.1",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"oidc-client-ts": "3.3.0",
@@ -142,7 +143,7 @@
"opus-recorder": "^8.0.3",
"pako": "^2.0.3",
"png-chunks-extract": "^1.0.0",
"posthog-js": "1.256.2",
"posthog-js": "1.257.0",
"qrcode": "1.5.4",
"re-resizable": "6.11.2",
"react": "^19.0.0",
@@ -185,12 +186,12 @@
"@babel/runtime": "^7.12.5",
"@casualbot/jest-sonar-reporter": "2.2.7",
"@element-hq/element-call-embedded": "0.13.1",
"@element-hq/element-web-playwright-common": "^1.4.2",
"@element-hq/element-web-playwright-common": "^1.4.3",
"@peculiar/webcrypto": "^1.4.3",
"@playwright/test": "^1.50.1",
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
"@rrweb/types": "^2.0.0-alpha.18",
"@sentry/webpack-plugin": "^3.0.0",
"@sentry/webpack-plugin": "^4.0.0",
"@storybook/addon-designs": "^10.0.1",
"@storybook/addon-docs": "^9.0.12",
"@storybook/icons": "^1.4.0",
@@ -264,6 +265,7 @@
"file-loader": "^6.0.0",
"html-webpack-plugin": "^5.5.3",
"husky": "^9.0.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.6.2",
"jest-canvas-mock": "^2.5.2",
"jest-environment-jsdom": "^29.7.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
const TAG = "develop@sha256:b38e55f06543f83f5a13f1d843489eb7aeaf7370a5c17a51897b462eeca315f5";
const TAG = "develop@sha256:8c2d9a93dd209a79d3e5e50cd18addfe52d80bea0ffe48a5d3e15836032eeb9d";
/**
* SynapseContainer which freezes the docker digest to stabilise tests,

View File

@@ -53,8 +53,6 @@
@import "./components/views/settings/shared/_SettingsSubsectionHeading.pcss";
@import "./components/views/spaces/_QuickThemeSwitcher.pcss";
@import "./components/views/typography/_Caption.pcss";
@import "./components/views/utils/_Box.pcss";
@import "./components/views/utils/_Flex.pcss";
@import "./compound/_Icon.pcss";
@import "./compound/_SuccessDialog.pcss";
@import "./structures/_AutoHideScrollbar.pcss";

View File

@@ -50,7 +50,7 @@ Please see LICENSE files in the repository root for full details.
color: var(--cpd-color-text-secondary);
}
.mx_Flex {
.mx_ErrorView_flexContainer {
margin: 0 auto;
max-width: max-content;
flex-wrap: wrap;

View File

@@ -32,7 +32,7 @@ Please see LICENSE files in the repository root for full details.
padding: 0 12px;
color: var(--cpd-color-text-secondary);
.mx_Box {
.mx_RoomSummaryCard_topic_box {
width: 100%;
}

View File

@@ -108,11 +108,6 @@ Please see LICENSE files in the repository root for full details.
margin: 0;
font-size: $font-20px;
line-height: $font-25px;
/* E2E icon wrapper */
.mx_Flex > span {
display: inline-block;
}
}
.mx_UserInfo_profile_name {

View File

@@ -12,10 +12,6 @@
border-bottom: var(--cpd-border-width-1) solid var(--cpd-color-bg-subtle-primary);
padding: 0 var(--cpd-space-3x);
svg {
fill: var(--cpd-color-icon-secondary);
}
.mx_RoomListSearch_search {
/* The search button should take all the remaining space */
flex: 1;
@@ -23,6 +19,10 @@
color: var(--cpd-color-text-secondary);
min-width: 0;
svg {
fill: var(--cpd-color-icon-secondary);
}
span {
flex: 1;
@@ -42,10 +42,4 @@
}
}
}
.mx_RoomListSearch_button:hover {
svg {
fill: var(--cpd-color-icon-primary);
}
}
}

View File

@@ -10,7 +10,7 @@ import { Text, Heading, Button, Separator } from "@vector-im/compound-web";
import PopOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/pop-out";
import SdkConfig from "../../SdkConfig";
import { Flex } from "../../components/utils/Flex";
import { Flex } from "../../shared-components/utils/Flex";
import { _t } from "../../languageHandler";
import { Icon as AppleIcon } from "../../../res/themes/element/img/compound/apple.svg";
import { Icon as MicrosoftIcon } from "../../../res/themes/element/img/compound/microsoft.svg";
@@ -58,7 +58,7 @@ const MobileAppLinks: React.FC<{
googlePlayUrl?: string;
fdroidUrl?: string;
}> = ({ appleAppStoreUrl, googlePlayUrl, fdroidUrl }) => (
<Flex gap="var(--cpd-space-6x)">
<Flex gap="var(--cpd-space-6x)" className="mx_ErrorView_flexContainer">
{appleAppStoreUrl && (
<a href={appleAppStoreUrl} target="_blank" rel="noreferrer noopener">
<img height="64" src="themes/element/img/download/apple.svg" alt="Apple App Store" />
@@ -84,7 +84,7 @@ const DesktopAppLinks: React.FC<{
linuxUrl?: string;
}> = ({ macOsUrl, win64Url, win64ArmUrl, linuxUrl }) => {
return (
<Flex gap="var(--cpd-space-4x)">
<Flex gap="var(--cpd-space-4x)" className="mx_ErrorView_flexContainer">
{macOsUrl && (
<Button as="a" href={macOsUrl} kind="secondary" Icon={AppleIcon}>
{_t("incompatible_browser|macos")}
@@ -193,7 +193,7 @@ export const UnsupportedBrowserView: React.FC<{
)}
</Text>
<Flex gap="var(--cpd-space-4x)" className="mx_ErrorView_buttons">
<Flex gap="var(--cpd-space-4x)" className="mx_ErrorView_flexContainer mx_ErrorView_buttons">
<Button Icon={PopOutIcon} kind="secondary" size="sm">
{_t("incompatible_browser|learn_more")}
</Button>

View File

@@ -0,0 +1,87 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { type MatrixClient, type RoomMember, type User } from "matrix-js-sdk/src/matrix";
import { useContext } from "react";
import { type UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
import { type IDevice } from "../../../views/right_panel/UserInfo";
import { useAsyncMemo } from "../../../../hooks/useAsyncMemo";
import { verifyUser } from "../../../../verification";
export interface UserInfoVerificationSectionState {
/**
* variables used to check if we can verify the user and display the verify button
*/
canVerify: boolean;
hasCrossSigningKeys: boolean | undefined;
/**
* used to display correct badge value
*/
isUserVerified: boolean;
/**
* callback function when verifyUser button is clicked
*/
verifySelectedUser: () => Promise<void>;
}
const useHomeserverSupportsCrossSigning = (cli: MatrixClient): boolean => {
return useAsyncMemo<boolean>(
async () => {
return cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
},
[cli],
false,
);
};
const useHasCrossSigningKeys = (cli: MatrixClient, member: User, canVerify: boolean): boolean | undefined => {
return useAsyncMemo(async () => {
if (!canVerify) return undefined;
return cli.getCrypto()?.userHasCrossSigningKeys(member.userId, true);
}, [cli, member, canVerify]);
};
/**
* View model for the userInfoVerificationHeaderView
* @see {@link UserInfoVerificationSectionState} for more information about what this view model returns.
*/
export const useUserInfoVerificationViewModel = (
member: User | RoomMember,
devices: IDevice[],
): UserInfoVerificationSectionState => {
const cli = useContext(MatrixClientContext);
const homeserverSupportsCrossSigning = useHomeserverSupportsCrossSigning(cli);
const userTrust = useAsyncMemo<UserVerificationStatus | undefined>(
async () => cli.getCrypto()?.getUserVerificationStatus(member.userId),
[member.userId],
// the user verification status is not initialized
undefined,
);
const hasUserVerificationStatus = Boolean(userTrust);
const isUserVerified = Boolean(userTrust?.isVerified());
const isMe = member.userId === cli.getUserId();
const canVerify =
hasUserVerificationStatus &&
homeserverSupportsCrossSigning &&
!isUserVerified &&
!isMe &&
devices &&
devices.length > 0;
const hasCrossSigningKeys = useHasCrossSigningKeys(cli, member as User, canVerify);
const verifySelectedUser = (): Promise<void> => verifyUser(cli, member as User);
return {
canVerify,
hasCrossSigningKeys,
isUserVerified,
verifySelectedUser,
};
};

View File

@@ -0,0 +1,115 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { RoomMember, type User } from "matrix-js-sdk/src/matrix";
import { useCallback, useContext } from "react";
import { mediaFromMxc } from "../../../../customisations/Media";
import Modal from "../../../../Modal";
import ImageView from "../../../views/elements/ImageView";
import SdkConfig from "../../../../SdkConfig";
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
import { type Member } from "../../../views/right_panel/UserInfo";
import { useUserTimezone } from "../../../../hooks/useUserTimezone";
import UserIdentifierCustomisations from "../../../../customisations/UserIdentifier";
export interface PresenceInfo {
lastActiveAgo: number | undefined;
currentlyActive: boolean | undefined;
state: string | undefined;
}
export interface TimezoneInfo {
timezone: string;
friendly: string;
}
export interface UserInfoHeaderState {
/**
* callback function when selected user avatar is clicked in user info
*/
onMemberAvatarClick: () => void;
/**
* Object containing information about the precense of the selected user
*/
precenseInfo: PresenceInfo;
/**
* Boolean that show or hide the precense information
*/
showPresence: boolean;
/**
* Timezone object
*/
timezoneInfo: TimezoneInfo | null;
/**
* Displayed identifier for the selected user
*/
userIdentifier: string | null;
}
interface UserInfoHeaderViewModelProps {
member: Member;
roomId?: string;
}
/**
* View model for the userInfoHeaderView
* props
* @see {@link UserInfoHeaderState} for more information about what this view model returns.
*/
export function useUserfoHeaderViewModel({ member, roomId }: UserInfoHeaderViewModelProps): UserInfoHeaderState {
const cli = useContext(MatrixClientContext);
let showPresence = true;
const precenseInfo: PresenceInfo = {
lastActiveAgo: undefined,
currentlyActive: undefined,
state: undefined,
};
const enablePresenceByHsUrl = SdkConfig.get("enable_presence_by_hs_url");
const timezoneInfo = useUserTimezone(cli, member.userId);
const userIdentifier = UserIdentifierCustomisations.getDisplayUserIdentifier?.(member.userId, {
roomId,
withDisplayName: true,
});
const onMemberAvatarClick = useCallback(() => {
const avatarUrl = (member as RoomMember).getMxcAvatarUrl
? (member as RoomMember).getMxcAvatarUrl()
: (member as User).avatarUrl;
const httpUrl = mediaFromMxc(avatarUrl).srcHttp;
if (!httpUrl) return;
const params = {
src: httpUrl,
name: (member as RoomMember).name || (member as User).displayName,
};
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", undefined, true);
}, [member]);
if (member instanceof RoomMember && member.user) {
precenseInfo.state = member.user.presence;
precenseInfo.lastActiveAgo = member.user.lastActiveAgo;
precenseInfo.currentlyActive = member.user.currentlyActive;
}
if (enablePresenceByHsUrl && enablePresenceByHsUrl[cli.baseUrl] !== undefined) {
showPresence = enablePresenceByHsUrl[cli.baseUrl];
}
return {
onMemberAvatarClick,
showPresence,
precenseInfo,
timezoneInfo,
userIdentifier,
};
}

View File

@@ -290,7 +290,7 @@ export default class LoginWithQRFlow extends React.Component<Props> {
data-testid="back-button"
className="mx_LoginWithQR_BackButton"
onClick={this.handleClick(Click.Back)}
title="Back"
title={_t("action|back")}
>
<ChevronLeftIcon />
</AccessibleButton>

View File

@@ -135,7 +135,7 @@ function getAvatarDecoration(decoration: AvatarBadgeDecoration, presence: Presen
width="16px"
height="16px"
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")}
/>
);

View File

@@ -115,7 +115,7 @@ const RoomStateExplorerEventType: React.FC<IEventTypeProps> = ({ eventType, onBa
const [query, setQuery] = useState("");
const [event, setEvent] = useState<MatrixEvent | null>(null);
const [history, setHistory] = useState(false);
const [hideEmptyState, setHideEmptyState] = useState(false);
const [showEmptyState, setShowEmptyState] = useState(true);
const events = context.room.currentState.events.get(eventType)!;
@@ -152,12 +152,16 @@ const RoomStateExplorerEventType: React.FC<IEventTypeProps> = ({ eventType, onBa
<BaseTool onBack={onBack}>
<FilteredList query={query} onChange={setQuery}>
{Array.from(events.entries())
.filter(([_, ev]) => !hideEmptyState || Object.keys(ev.getContent()).length > 0)
.filter(([_, ev]) => showEmptyState || Object.keys(ev.getContent()).length > 0)
.map(([stateKey, ev]) => (
<StateEventButton key={stateKey} label={stateKey} onClick={() => setEvent(ev)} />
))}
</FilteredList>
<LabelledToggleSwitch label={_t("devtools|hide_empty_content_events")} onChange={setHideEmptyState} value={hideEmptyState} />
<LabelledToggleSwitch
label={_t("devtools|show_empty_content_events")}
onChange={setShowEmptyState}
value={showEmptyState}
/>
</BaseTool>
);
};

View File

@@ -14,7 +14,7 @@ import React, { type ChangeEvent, type FormEvent } from "react";
import { type SecretStorage } from "matrix-js-sdk/src/matrix";
import Field from "../../elements/Field";
import { Flex } from "../../../utils/Flex";
import { Flex } from "../../../../shared-components/utils/Flex";
import { _t } from "../../../../languageHandler";
import { EncryptionCard } from "../../settings/encryption/EncryptionCard";
import { EncryptionCardButtons } from "../../settings/encryption/EncryptionCardButtons";

View File

@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
import React, { type ComponentType } from "react";
import { Text } from "@vector-im/compound-web";
import { Flex } from "../../utils/Flex";
import { Flex } from "../../../shared-components/utils/Flex";
interface Props {
Icon: ComponentType<React.SVGAttributes<SVGElement>>;

View File

@@ -46,9 +46,9 @@ import RoomAvatar from "../avatars/RoomAvatar.tsx";
import { E2EStatus } from "../../../utils/ShieldUtils.ts";
import { type RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks.ts";
import RoomName from "../elements/RoomName.tsx";
import { Flex } from "../../utils/Flex.tsx";
import { Flex } from "../../../shared-components/utils/Flex";
import { Linkify, topicToHtml } from "../../../HtmlUtils.tsx";
import { Box } from "../../utils/Box.tsx";
import { Box } from "../../../shared-components/utils/Box";
import { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement.tsx";
import { useRoomSummaryCardViewModel } from "../../viewmodels/right_panel/RoomSummaryCardViewModel.tsx";
import { useRoomTopicViewModel } from "../../viewmodels/right_panel/RoomSummaryCardTopicViewModel.tsx";
@@ -80,7 +80,7 @@ const RoomTopic: React.FC<Pick<IProps, "room">> = ({ room }): JSX.Element | null
gap="var(--cpd-space-2x)"
className="mx_RoomSummaryCard_topic"
>
<Box flex="1">
<Box flex="1" className="mx_RoomSummaryCard_topic_box">
<Link kind="primary" onClick={vm.onEditClick}>
<Text size="sm" weight="regular">
{_t("right_panel|add_topic")}
@@ -103,7 +103,7 @@ const RoomTopic: React.FC<Pick<IProps, "room">> = ({ room }): JSX.Element | null
mx_RoomSummaryCard_topic_collapsed: !vm.expanded,
})}
>
<Box flex="1" className="mx_RoomSummaryCard_topic_container">
<Box flex="1" className="mx_RoomSummaryCard_topic_container mx_RoomSummaryCard_topic_box">
<Text size="sm" weight="regular" onClick={vm.onTopicLinkClick}>
{content}
</Text>
@@ -169,8 +169,8 @@ const RoomSummaryCardView: React.FC<IProps> = ({
<Flex as="section" justify="center" gap="var(--cpd-space-2x)" className="mx_RoomSummaryCard_badges">
{!vm.isDirectMessage && vm.roomJoinRule === JoinRule.Public && (
<Badge kind="grey">
<PublicIcon width="1em" />
<Badge kind="blue">
<PublicIcon width="1em" color="var(--cpd-color-icon-info-primary)" />
{_t("common|public_room")}
</Badge>
)}
@@ -183,8 +183,8 @@ const RoomSummaryCardView: React.FC<IProps> = ({
)}
{!vm.isRoomEncrypted && (
<Badge kind="grey">
<LockOffIcon width="1em" />
<Badge kind="blue">
<LockOffIcon width="1em" color="var(--cpd-color-icon-info-primary)" />
{_t("common|unencrypted")}
</Badge>
)}

View File

@@ -25,8 +25,7 @@ import {
import { KnownMembership } from "matrix-js-sdk/src/types";
import { type UserVerificationStatus, type VerificationRequest, CryptoEvent } from "matrix-js-sdk/src/crypto-api";
import { logger } from "matrix-js-sdk/src/logger";
import { Badge, Button, Heading, InlineSpinner, MenuItem, Text, Tooltip } from "@vector-im/compound-web";
import VerifiedIcon from "@vector-im/compound-design-tokens/assets/web/icons/verified";
import { MenuItem } from "@vector-im/compound-web";
import ChatIcon from "@vector-im/compound-design-tokens/assets/web/icons/chat";
import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check";
import ShareIcon from "@vector-im/compound-design-tokens/assets/web/icons/share";
@@ -40,41 +39,32 @@ import Modal from "../../../Modal";
import { _t, UserFriendlyError } from "../../../languageHandler";
import DMRoomMap from "../../../utils/DMRoomMap";
import { type ButtonEvent } from "../elements/AccessibleButton";
import SdkConfig from "../../../SdkConfig";
import MultiInviter from "../../../utils/MultiInviter";
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
import EncryptionPanel from "./EncryptionPanel";
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
import { verifyUser } from "../../../verification";
import { Action } from "../../../dispatcher/actions";
import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
import BaseCard from "./BaseCard";
import ImageView from "../elements/ImageView";
import Spinner from "../elements/Spinner";
import MemberAvatar from "../avatars/MemberAvatar";
import PresenceLabel from "../rooms/PresenceLabel";
import { ShareDialog } from "../dialogs/ShareDialog";
import ErrorDialog from "../dialogs/ErrorDialog";
import QuestionDialog from "../dialogs/QuestionDialog";
import { mediaFromMxc } from "../../../customisations/Media";
import { type ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature";
import { TimelineRenderingType } from "../../../contexts/RoomContext";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
import { type IRightPanelCardState } from "../../../stores/right-panel/RightPanelStoreIPanelState";
import UserIdentifierCustomisations from "../../../customisations/UserIdentifier";
import PosthogTrackers from "../../../PosthogTrackers";
import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { DirectoryMember, startDmOnFirstMessage } from "../../../utils/direct-messages";
import { SdkContextClass } from "../../../contexts/SDKContext";
import { Flex } from "../../utils/Flex";
import CopyableText from "../elements/CopyableText";
import { useUserTimezone } from "../../../hooks/useUserTimezone";
import { UserInfoAdminToolsContainer } from "./user_info/UserInfoAdminToolsContainer";
import { PowerLevelSection } from "./user_info/UserInfoPowerLevels";
import { UserInfoHeaderView } from "./user_info/UserInfoHeaderView";
export interface IDevice extends Device {
ambiguous?: boolean;
@@ -298,7 +288,7 @@ export const warnSelfDemote = async (isSpace: boolean): Promise<boolean> => {
return !!confirmed;
};
const Container: React.FC<{
export const Container: React.FC<{
children: ReactNode;
className?: string;
}> = ({ children, className }) => {
@@ -426,16 +416,6 @@ const useIsSynapseAdmin = (cli?: MatrixClient): boolean => {
return useAsyncMemo(async () => (cli ? cli.isSynapseAdministrator().catch(() => false) : false), [cli], false);
};
const useHomeserverSupportsCrossSigning = (cli: MatrixClient): boolean => {
return useAsyncMemo<boolean>(
async () => {
return cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
},
[cli],
false,
);
};
export interface IRoomPermissions {
modifyLevelMax: number;
canEdit: boolean;
@@ -567,80 +547,6 @@ export const useDevices = (userId: string): IDevice[] | undefined | null => {
return devices;
};
function useHasCrossSigningKeys(cli: MatrixClient, member: User, canVerify: boolean): boolean | undefined {
return useAsyncMemo(async () => {
if (!canVerify) return undefined;
return await cli.getCrypto()?.userHasCrossSigningKeys(member.userId, true);
}, [cli, member, canVerify]);
}
const VerificationSection: React.FC<{
member: User | RoomMember;
devices: IDevice[];
}> = ({ member, devices }) => {
const cli = useContext(MatrixClientContext);
let content;
const homeserverSupportsCrossSigning = useHomeserverSupportsCrossSigning(cli);
const userTrust = useAsyncMemo<UserVerificationStatus | undefined>(
async () => cli.getCrypto()?.getUserVerificationStatus(member.userId),
[member.userId],
// the user verification status is not initialized
undefined,
);
const hasUserVerificationStatus = Boolean(userTrust);
const isUserVerified = Boolean(userTrust?.isVerified());
const isMe = member.userId === cli.getUserId();
const canVerify =
hasUserVerificationStatus &&
homeserverSupportsCrossSigning &&
!isUserVerified &&
!isMe &&
devices &&
devices.length > 0;
const hasCrossSigningKeys = useHasCrossSigningKeys(cli, member as User, canVerify);
if (isUserVerified) {
content = (
<Badge kind="green" className="mx_UserInfo_verified_badge">
<VerifiedIcon className="mx_UserInfo_verified_icon" height="16px" width="16px" />
<Text size="sm" weight="medium" className="mx_UserInfo_verified_label">
{_t("common|verified")}
</Text>
</Badge>
);
} else if (hasCrossSigningKeys === undefined) {
// We are still fetching the cross-signing keys for the user, show spinner.
content = <InlineSpinner size={24} />;
} else if (canVerify && hasCrossSigningKeys) {
content = (
<div className="mx_UserInfo_container_verifyButton">
<Button
className="mx_UserInfo_verify_button"
kind="tertiary"
size="sm"
onClick={() => verifyUser(cli, member as User)}
>
{_t("user_info|verify_button")}
</Button>
</div>
);
} else {
content = (
<Text className="mx_UserInfo_verification_unavailable" size="sm">
({_t("user_info|verification_unavailable")})
</Text>
);
}
return (
<Flex justify="center" align="center" className="mx_UserInfo_verification">
{content}
</Flex>
);
};
const BasicUserInfo: React.FC<{
room: Room;
member: User | RoomMember;
@@ -761,114 +667,6 @@ const BasicUserInfo: React.FC<{
export type Member = User | RoomMember;
export const UserInfoHeader: React.FC<{
member: Member;
devices: IDevice[];
roomId?: string;
hideVerificationSection?: boolean;
}> = ({ member, devices, roomId, hideVerificationSection }) => {
const cli = useContext(MatrixClientContext);
const onMemberAvatarClick = useCallback(() => {
const avatarUrl = (member as RoomMember).getMxcAvatarUrl
? (member as RoomMember).getMxcAvatarUrl()
: (member as User).avatarUrl;
const httpUrl = mediaFromMxc(avatarUrl).srcHttp;
if (!httpUrl) return;
const params = {
src: httpUrl,
name: (member as RoomMember).name || (member as User).displayName,
};
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", undefined, true);
}, [member]);
const avatarUrl = (member as User).avatarUrl;
let presenceState: string | undefined;
let presenceLastActiveAgo: number | undefined;
let presenceCurrentlyActive: boolean | undefined;
if (member instanceof RoomMember && member.user) {
presenceState = member.user.presence;
presenceLastActiveAgo = member.user.lastActiveAgo;
presenceCurrentlyActive = member.user.currentlyActive;
}
const enablePresenceByHsUrl = SdkConfig.get("enable_presence_by_hs_url");
let showPresence = true;
if (enablePresenceByHsUrl && enablePresenceByHsUrl[cli.baseUrl] !== undefined) {
showPresence = enablePresenceByHsUrl[cli.baseUrl];
}
let presenceLabel: JSX.Element | undefined;
if (showPresence) {
presenceLabel = (
<PresenceLabel
activeAgo={presenceLastActiveAgo}
currentlyActive={presenceCurrentlyActive}
presenceState={presenceState}
className="mx_UserInfo_profileStatus"
coloured
/>
);
}
const timezoneInfo = useUserTimezone(cli, member.userId);
const userIdentifier = UserIdentifierCustomisations.getDisplayUserIdentifier?.(member.userId, {
roomId,
withDisplayName: true,
});
const displayName = (member as RoomMember).rawDisplayName;
return (
<React.Fragment>
<div className="mx_UserInfo_avatar">
<div className="mx_UserInfo_avatar_transition">
<div className="mx_UserInfo_avatar_transition_child">
<MemberAvatar
key={member.userId} // to instantly blank the avatar when UserInfo changes members
member={member as RoomMember}
size="120px"
resizeMethod="scale"
fallbackUserId={member.userId}
onClick={onMemberAvatarClick}
urls={avatarUrl ? [avatarUrl] : undefined}
/>
</div>
</div>
</div>
<Container className="mx_UserInfo_header">
<Flex direction="column" align="center" className="mx_UserInfo_profile">
<Heading size="sm" weight="semibold" as="h1" dir="auto">
<Flex className="mx_UserInfo_profile_name" direction="row-reverse" align="center">
{displayName}
</Flex>
</Heading>
{presenceLabel}
{timezoneInfo && (
<Tooltip label={timezoneInfo?.timezone ?? ""}>
<Flex align="center" className="mx_UserInfo_timezone">
<Text size="sm" weight="regular">
{timezoneInfo?.friendly ?? ""}
</Text>
</Flex>
</Tooltip>
)}
<Text size="sm" weight="semibold" className="mx_UserInfo_profile_mxid">
<CopyableText getTextToCopy={() => userIdentifier} border={false}>
{userIdentifier}
</CopyableText>
</Text>
</Flex>
{!hideVerificationSection && <VerificationSection member={member} devices={devices} />}
</Container>
</React.Fragment>
);
};
interface IProps {
user: Member;
room?: Room;
@@ -927,7 +725,7 @@ const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPha
const header = (
<>
<UserInfoHeader
<UserInfoHeaderView
hideVerificationSection={phase === RightPanelPhases.EncryptionPanel}
member={member}
devices={devices}

View File

@@ -0,0 +1,63 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { type User, type RoomMember } from "matrix-js-sdk/src/matrix";
import { Text, Button, InlineSpinner, Badge } from "@vector-im/compound-web";
import { VerifiedIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { useUserInfoVerificationViewModel } from "../../../viewmodels/right_panel/user_info/UserInfoHeaderVerificationViewModel";
import { type IDevice } from "../UserInfo";
import { Flex } from "../../../../shared-components/utils/Flex";
import { _t } from "../../../../languageHandler";
export const UserInfoHeaderVerificationView: React.FC<{
member: User | RoomMember;
devices: IDevice[];
}> = ({ member, devices }) => {
let content;
const vm = useUserInfoVerificationViewModel(member, devices);
if (vm.isUserVerified) {
content = (
<Badge kind="green" className="mx_UserInfo_verified_badge">
<VerifiedIcon className="mx_UserInfo_verified_icon" height="16px" width="16px" />
<Text size="sm" weight="medium" className="mx_UserInfo_verified_label">
{_t("common|verified")}
</Text>
</Badge>
);
} else if (vm.hasCrossSigningKeys === undefined) {
// We are still fetching the cross-signing keys for the user, show spinner.
content = <InlineSpinner size={24} />;
} else if (vm.canVerify && vm.hasCrossSigningKeys) {
content = (
<div className="mx_UserInfo_container_verifyButton">
<Button
className="mx_UserInfo_verify_button"
kind="tertiary"
size="sm"
onClick={() => vm.verifySelectedUser()}
>
{_t("user_info|verify_button")}
</Button>
</div>
);
} else {
content = (
<Text className="mx_UserInfo_verification_unavailable" size="sm">
({_t("user_info|verification_unavailable")})
</Text>
);
}
return (
<Flex justify="center" align="center" className="mx_UserInfo_verification">
{content}
</Flex>
);
};

View File

@@ -0,0 +1,96 @@
/*
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, { type JSX } from "react";
import { type User, type RoomMember } from "matrix-js-sdk/src/matrix";
import { Heading, Tooltip, Text } from "@vector-im/compound-web";
import { useUserfoHeaderViewModel } from "../../../viewmodels/right_panel/user_info/UserInfoHeaderViewModel";
import MemberAvatar from "../../avatars/MemberAvatar";
import { Container, type Member, type IDevice } from "../UserInfo";
import { Flex } from "../../../../shared-components/utils/Flex";
import PresenceLabel from "../../rooms/PresenceLabel";
import CopyableText from "../../elements/CopyableText";
import { UserInfoHeaderVerificationView } from "./UserInfoHeaderVerificationView";
export interface UserInfoHeaderViewProps {
member: Member;
roomId?: string;
devices: IDevice[];
hideVerificationSection: boolean;
}
export const UserInfoHeaderView: React.FC<UserInfoHeaderViewProps> = ({
member,
devices,
roomId,
hideVerificationSection,
}) => {
const vm = useUserfoHeaderViewModel({ member, roomId });
const avatarUrl = (member as User).avatarUrl;
const displayName = (member as RoomMember).rawDisplayName;
let presenceLabel: JSX.Element | undefined;
if (vm.showPresence) {
presenceLabel = (
<PresenceLabel
activeAgo={vm.precenseInfo.lastActiveAgo}
currentlyActive={vm.precenseInfo.currentlyActive}
presenceState={vm.precenseInfo.state}
className="mx_UserInfo_profileStatus"
coloured
/>
);
}
return (
<React.Fragment>
<div className="mx_UserInfo_avatar">
<div className="mx_UserInfo_avatar_transition">
<div className="mx_UserInfo_avatar_transition_child">
<MemberAvatar
key={member.userId} // to instantly blank the avatar when UserInfo changes members
member={member as RoomMember}
size="120px"
resizeMethod="scale"
fallbackUserId={member.userId}
onClick={vm.onMemberAvatarClick}
urls={avatarUrl ? [avatarUrl] : undefined}
/>
</div>
</div>
</div>
<Container className="mx_UserInfo_header">
<Flex direction="column" align="center" className="mx_UserInfo_profile">
<Heading size="sm" weight="semibold" as="h1" dir="auto">
<Flex className="mx_UserInfo_profile_name" direction="row-reverse" align="center">
{displayName}
</Flex>
</Heading>
{presenceLabel}
{vm.timezoneInfo && (
<Tooltip label={vm.timezoneInfo?.timezone ?? ""}>
<Flex align="center" className="mx_UserInfo_timezone">
<Text size="sm" weight="regular">
{vm.timezoneInfo?.friendly ?? ""}
</Text>
</Flex>
</Tooltip>
)}
<Text size="sm" weight="semibold" className="mx_UserInfo_profile_mxid">
<CopyableText getTextToCopy={() => vm.userIdentifier} border={false}>
{vm.userIdentifier}
</CopyableText>
</Text>
</Flex>
{!hideVerificationSection && <UserInfoHeaderVerificationView member={member} devices={devices} />}
</Container>
</React.Fragment>
);
};

View File

@@ -729,11 +729,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
if (this.state.shieldColour !== EventShieldColour.NONE) {
let shieldReasonMessage: string;
switch (this.state.shieldReason) {
case null:
case EventShieldReason.UNKNOWN:
shieldReasonMessage = _t("error|unknown");
break;
case EventShieldReason.UNVERIFIED_IDENTITY:
shieldReasonMessage = _t("encryption|event_shield_reason_unverified_identity");
break;
@@ -761,6 +756,10 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
case EventShieldReason.VERIFICATION_VIOLATION:
shieldReasonMessage = _t("timeline|decryption_failure|sender_identity_previously_verified");
break;
default:
shieldReasonMessage = _t("error|unknown");
break;
}
if (this.state.shieldColour === EventShieldColour.GREY) {

View File

@@ -7,10 +7,10 @@ Please see LICENSE files in the repository root for full details.
import { Search, Text, Button, Tooltip, InlineSpinner } from "@vector-im/compound-web";
import React from "react";
import InviteIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-add-solid";
import InviteIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-add";
import { UserAddIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { Flex } from "../../../utils/Flex";
import { Flex } from "../../../../shared-components/utils/Flex";
import { type MemberListViewState } from "../../../viewmodels/memberlist/MemberListViewModel";
import { _t } from "../../../../languageHandler";
@@ -45,7 +45,7 @@ const InviteButton: React.FC<Props> = ({ vm }) => {
<OptionalTooltip canInvite={vm.canInvite}>
<Button
className="mx_MemberListHeaderView_invite_small"
kind="primary"
kind="secondary"
onClick={vm.onInviteButtonClick}
size="sm"
iconOnly={true}

View File

@@ -10,7 +10,7 @@ import React, { type JSX } 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 {
type MemberWithSeparator,
SEPARATOR,

View File

@@ -9,7 +9,7 @@ import React, { type JSX } from "react";
import EmailIcon from "@vector-im/compound-design-tokens/assets/web/icons/email-solid";
import UserAddIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-add-solid";
import { Flex } from "../../../../../utils/Flex";
import { Flex } from "../../../../../../shared-components/utils/Flex";
interface Props {
isThreePid: boolean;

View File

@@ -13,7 +13,7 @@ import VideoCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/vi
import EmailIcon from "@vector-im/compound-design-tokens/assets/web/icons/email-solid";
import { UnreadCounter, Unread } from "@vector-im/compound-web";
import { Flex } from "../../utils/Flex";
import { Flex } from "../../../shared-components/utils/Flex";
import { type RoomNotificationState } from "../../../stores/notifications/RoomNotificationState";
import { useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";

View File

@@ -25,8 +25,8 @@ import { RightPanelPhases } from "../../../../stores/right-panel/RightPanelStore
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext.tsx";
import { useRoomMemberCount, useRoomMembers } from "../../../../hooks/useRoomMembers.ts";
import { _t } from "../../../../languageHandler.tsx";
import { Flex } from "../../../utils/Flex.tsx";
import { Box } from "../../../utils/Box.tsx";
import { Flex } from "../../../../shared-components/utils/Flex";
import { Box } from "../../../../shared-components/utils/Box";
import { getPlatformCallTypeProps, useRoomCall } from "../../../../hooks/room/useRoomCall.tsx";
import { useRoomThreadNotifications } from "../../../../hooks/room/useRoomThreadNotifications.ts";
import { useGlobalNotificationState } from "../../../../hooks/useGlobalNotificationState.ts";
@@ -286,7 +286,8 @@ export default function RoomHeader({
<PublicIcon
width="16px"
height="16px"
className="mx_RoomHeader_icon text-secondary"
className="mx_RoomHeader_icon"
color="var(--cpd-color-icon-info-primary)"
aria-label={_t("common|public_room")}
/>
</Tooltip>

View File

@@ -11,7 +11,7 @@ import UserAddIcon from "@vector-im/compound-design-tokens/assets/web/icons/user
import RoomIcon from "@vector-im/compound-design-tokens/assets/web/icons/room";
import type { RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel";
import { Flex } from "../../../utils/Flex";
import { Flex } from "../../../../shared-components/utils/Flex";
import { _t } from "../../../../languageHandler";
import { FilterKey } from "../../../../stores/room-list-v3/skip-list/filters";
import { type PrimaryFilter } from "../../../viewmodels/roomlist/useFilteredRooms";

View File

@@ -16,7 +16,7 @@ import SettingsIcon from "@vector-im/compound-design-tokens/assets/web/icons/set
import VideoCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/video-call";
import { _t } from "../../../../languageHandler";
import { Flex } from "../../../utils/Flex";
import { Flex } from "../../../../shared-components/utils/Flex";
import {
type RoomListHeaderViewState,
useRoomListHeaderViewModel,

View File

@@ -21,7 +21,7 @@ import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check"
import { type Room } from "matrix-js-sdk/src/matrix";
import { _t } from "../../../../languageHandler";
import { Flex } from "../../../utils/Flex";
import { Flex } from "../../../../shared-components/utils/Flex";
import {
type RoomListItemMenuViewState,
useRoomListItemMenuViewModel,

View File

@@ -10,7 +10,7 @@ import { type Room } from "matrix-js-sdk/src/matrix";
import classNames from "classnames";
import { useRoomListItemViewModel } from "../../../viewmodels/roomlist/RoomListItemViewModel";
import { Flex } from "../../../utils/Flex";
import { Flex } from "../../../../shared-components/utils/Flex";
import { RoomListItemMenuView } from "./RoomListItemMenuView";
import { NotificationDecoration } from "../NotificationDecoration";
import { RoomAvatarView } from "../../avatars/RoomAvatarView";
@@ -94,7 +94,11 @@ export const RoomListItemView = memo(function RoomListItemView({
<div className="mx_RoomListItemView_roomName" title={vm.name}>
{vm.name}
</div>
<div className="mx_RoomListItemView_messagePreview">{vm.messagePreview}</div>
{vm.messagePreview && (
<div className="mx_RoomListItemView_messagePreview" title={vm.messagePreview}>
{vm.messagePreview}
</div>
)}
</div>
{showHoverMenu ? (
<RoomListItemMenuView

View File

@@ -12,7 +12,7 @@ import { UIComponent } from "../../../../settings/UIFeature";
import { RoomListSearch } from "./RoomListSearch";
import { RoomListHeaderView } from "./RoomListHeaderView";
import { RoomListView } from "./RoomListView";
import { Flex } from "../../../utils/Flex";
import { Flex } from "../../../../shared-components/utils/Flex";
import { _t } from "../../../../languageHandler";
type RoomListPanelProps = {

View File

@@ -10,7 +10,7 @@ import { ChatFilter, IconButton } from "@vector-im/compound-web";
import ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down";
import type { RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel";
import { Flex } from "../../../utils/Flex";
import { Flex } from "../../../../shared-components/utils/Flex";
import { _t } from "../../../../languageHandler";
interface RoomListPrimaryFiltersProps {

View File

@@ -20,7 +20,7 @@ import { MetaSpace } from "../../../../stores/spaces";
import { Action } from "../../../../dispatcher/actions";
import PosthogTrackers from "../../../../PosthogTrackers";
import defaultDispatcher from "../../../../dispatcher/dispatcher";
import { Flex } from "../../../utils/Flex";
import { Flex } from "../../../../shared-components/utils/Flex";
import { useTypedEventEmitterState } from "../../../../hooks/useEventEmitter";
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../../../LegacyCallHandler";
@@ -61,7 +61,6 @@ export function RoomListSearch({ activeSpace }: RoomListSearchProps): JSX.Elemen
</Button>
{displayDialButton && (
<Button
className="mx_RoomListSearch_button"
kind="secondary"
size="sm"
Icon={DialPadIcon}
@@ -74,7 +73,6 @@ export function RoomListSearch({ activeSpace }: RoomListSearchProps): JSX.Elemen
)}
{displayExploreButton && (
<Button
className="mx_RoomListSearch_button"
kind="secondary"
size="sm"
Icon={ExploreIcon}

View File

@@ -19,7 +19,7 @@ import { isValid3pidInvite } from "../../../RoomInvite";
import { Action } from "../../../dispatcher/actions";
import ErrorDialog from "../dialogs/ErrorDialog";
import BaseCard from "../right_panel/BaseCard";
import { Flex } from "../../utils/Flex";
import { Flex } from "../../../shared-components/utils/Flex";
interface IProps {
event: MatrixEvent;

View File

@@ -26,7 +26,7 @@ import AccessibleButton from "../elements/AccessibleButton";
import LogoutDialog, { shouldShowLogoutDialog } from "../dialogs/LogoutDialog";
import Modal from "../../../Modal";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { Flex } from "../../utils/Flex";
import { Flex } from "../../../shared-components/utils/Flex";
const SpinnerToast: React.FC<{ children?: ReactNode }> = ({ children }) => (
<>

View File

@@ -7,7 +7,7 @@
import React, { type JSX, type PropsWithChildren } from "react";
import { Flex } from "../../../utils/Flex";
import { Flex } from "../../../../shared-components/utils/Flex";
/**
* A component for emphasised text within an {@link EncryptionCard}

View File

@@ -3376,7 +3376,6 @@
"unable_to_decrypt": "Zprávu nelze dešifrovat"
},
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Dešifrování",
"download_action_downloading": "Stahování",
"download_failed": "Stažení se nezdařilo",
"download_failed_description": "Při stahování tohoto souboru došlo k chybě",

View File

@@ -3341,7 +3341,6 @@
"unable_to_decrypt": "Methu dadgryptio'r neges"
},
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Dadgryptio",
"download_action_downloading": "Yn llwytho i lawr",
"download_failed": "Methodd y llwytho i lawr",
"download_failed_description": "Bu gwall wrth lawrlwytho'r ffeil hon",

View File

@@ -3334,7 +3334,6 @@
"unable_to_decrypt": "Entschlüsselung der Nachricht nicht möglich"
},
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Entschlüsseln",
"download_action_downloading": "Herunterladen",
"download_failed": "Herunterladen fehlgeschlagen",
"download_failed_description": "Beim Herunterladen dieser Datei ist ein Fehler aufgetreten",

View File

@@ -2688,7 +2688,6 @@
},
"creation_summary_dm": "Ο/η %(creator)s δημιούργησε αυτό το απευθείας μήνυμα.",
"creation_summary_room": "Ο/η %(creator)s δημιούργησε και διαμόρφωσε το δωμάτιο.",
"download_action_decrypting": "Αποκρυπτογράφηση",
"download_action_downloading": "Γίνεται λήψη",
"edits": {
"tooltip_label": "Επεξεργάστηκε στις %(date)s. Κάντε κλικ για να δείτε τις τροποποιήσεις.",

View File

@@ -819,7 +819,6 @@
"failed_to_load": "Failed to load.",
"failed_to_save": "Failed to save settings.",
"failed_to_send": "Failed to send event!",
"hide_empty_content_events": "Hide events with empty content",
"id": "ID: ",
"invalid_json": "Doesn't look like valid JSON.",
"level": "Level",
@@ -864,6 +863,7 @@
"elementCallUrl": "Element Call URL"
},
"settings_explorer": "Settings explorer",
"show_empty_content_events": "Show events with empty content",
"show_hidden_events": "Show hidden events in timeline",
"spaces": {
"one": "<space>",

View File

@@ -2091,7 +2091,6 @@
},
"creation_summary_dm": "%(creator)s kreis ĉi tiun individuan ĉambron.",
"creation_summary_room": "%(creator)s kreis kaj agordis la ĉambron.",
"download_action_decrypting": "Malĉifrante",
"edits": {
"tooltip_label": "Redaktita je %(date)s. Klaku por vidi redaktojn.",
"tooltip_sub": "Klaku por vidi redaktojn",

View File

@@ -2672,7 +2672,6 @@
},
"creation_summary_dm": "%(creator)s creó este mensaje directo.",
"creation_summary_room": "Sala creada y configurada por %(creator)s.",
"download_action_decrypting": "Descifrando",
"download_action_downloading": "Descargando",
"edits": {
"tooltip_label": "Última vez editado: %(date)s. Haz clic para ver los cambios.",

View File

@@ -1450,6 +1450,7 @@
"room_list_navigate_down": "Suundu jututubade loendis alla",
"room_list_navigate_up": "Suundu jututubade loendis üles",
"room_list_select_room": "Vali tubade loendist jututuba",
"save": "Salvesta",
"scroll_down_timeline": "Liigu ajajoonel alla",
"scroll_up_timeline": "Liigu ajajoonel üles",
"search": "Otsing (peab olema lubatud)",
@@ -3370,7 +3371,6 @@
"unable_to_decrypt": "Sõnumi dekrüptimine ei õnnestu"
},
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Dekrüptin sisu",
"download_action_downloading": "Laadin alla",
"download_failed": "Allalaadimine ei õnnestunud",
"download_failed_description": "Selle faili allalaadimisel tekkis viga",

View File

@@ -708,7 +708,8 @@
"twemoji_colr": "<colr>twemoji-colr</colr>-fontti © <author>Mozilla Foundation</author>, käytössä <terms>Apache 2.0</terms>:n ehtojen mukaisesti."
},
"decline_invitation_dialog": {
"reason_description": "Kerro syy huoneen ilmoittamiseen."
"reason_description": "Kerro syy huoneen ilmoittamiseen.",
"title": "Kieltäydy kutsusta"
},
"desktop_default_device_name": "%(brand)sin työpöytäversio: %(platformName)s",
"devtools": {
@@ -1240,6 +1241,7 @@
"room_list_navigate_down": "Liiku alas huoneluettelossa",
"room_list_navigate_up": "Liiku ylös huoneluettelossa",
"room_list_select_room": "Valitse huone huoneluettelosta",
"save": "Tallenna",
"scroll_down_timeline": "Vieritä alas aikajanalla",
"scroll_up_timeline": "Vieritä ylös aikajanalla",
"search": "Haku (pitää olla käytössä)",
@@ -2356,6 +2358,7 @@
"notifications": {
"desktop_notification_message_preview": "Näytä viestin esikatselu työpöytäilmoituksessa",
"dialog_title": "<strong>Asetukset:</strong> Ilmoitukset",
"email_section": "Sähköpostin yhteenveto",
"enable_audible_notifications_session": "Ota käyttöön ääni-ilmoitukset tälle istunnolle",
"enable_desktop_notifications_session": "Ota käyttöön työpöytäilmoitukset tälle istunnolle",
"enable_email_notifications": "Sähköposti-ilmoitukset osoitteeseen %(email)s",
@@ -2548,6 +2551,7 @@
"show_chat_effects": "Näytä keskustelutehosteet (animaatiot, kun saat esim. konfettia)",
"show_displayname_changes": "Näytä näyttönimien muutokset",
"show_join_leave": "Näytä liittymis- ja poistumisviestit (ei vaikutusta kutsuihin, poistamisiin ja porttikieltoihin)",
"show_message_previews": "Näytä viestien esikatselut",
"show_nsfw_content": "Näytä NSFW-sisältö",
"show_read_receipts": "Näytä muiden käyttäjien lukukuittaukset",
"show_redaction_placeholder": "Näytä paikanpitäjä poistetuille viesteille",
@@ -2886,7 +2890,6 @@
"unable_to_decrypt": "Viestin salausta ei voi purkaa"
},
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Puretaan salausta",
"download_action_downloading": "Ladataan",
"download_failed": "Lataus epäonnistui",
"download_failed_description": "Tiedostoa ladattaessa tapahtui virhe",

View File

@@ -568,7 +568,7 @@
"room_name": "Nom du salon",
"rooms": "Salons",
"save": "Enregistrer",
"saved": "Sauvegardé",
"saved": "Enregistré",
"saving": "Enregistrement…",
"secure_backup": "Sauvegarde sécurisée",
"select_all": "Tout sélectionner",
@@ -863,6 +863,7 @@
"elementCallUrl": "Lien d'Element Call"
},
"settings_explorer": "Explorateur de paramètres",
"show_empty_content_events": "Afficher les événements sans contenu",
"show_hidden_events": "Afficher les évènements cachés dans le fil de discussion",
"spaces": {
"one": "<espace>",
@@ -1450,6 +1451,7 @@
"room_list_navigate_down": "Descendre dans la liste des salons",
"room_list_navigate_up": "Remonter dans la liste des salons",
"room_list_select_room": "Sélectionner un salon de la liste des salons",
"save": "Enregistrer",
"scroll_down_timeline": "Faire défiler le fil de discussion vers le bas",
"scroll_up_timeline": "Faire défiler le fil de discussion vers le haut",
"search": "Recherche (si activée)",
@@ -2730,7 +2732,7 @@
"set_phrase_again": "Retournez en arrière pour la redéfinir.",
"settings_reminder": "Vous pouvez aussi configurer la sauvegarde sécurisée et gérer vos clés depuis les paramètres.",
"title_confirm_phrase": "Confirmer la phrase de sécurité",
"title_save_key": "Sauvegarder votre clé de récupération",
"title_save_key": "Enregistrer votre clé de récupération",
"title_set_phrase": "Définir une phrase de sécurité",
"unable_to_setup": "Impossible de configurer le coffre secret",
"use_different_passphrase": "Utiliser une phrase secrète différente ?",
@@ -3369,7 +3371,6 @@
"unable_to_decrypt": "Impossible de déchiffrer le message"
},
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Déchiffrement",
"download_action_downloading": "Téléchargement en cours",
"download_failed": "Échec du téléchargement",
"download_failed_description": "Une erreur s'est produite lors du téléchargement de ce fichier",

View File

@@ -2435,7 +2435,6 @@
},
"creation_summary_dm": "%(creator)s creou esta MD.",
"creation_summary_room": "%(creator)s creou e configurou a sala.",
"download_action_decrypting": "Descifrando",
"download_action_downloading": "Descargando",
"edits": {
"tooltip_label": "Editada o %(date)s. Preme para ver edicións.",

View File

@@ -1985,7 +1985,6 @@
},
"creation_summary_dm": "%(creator)s יצר את DM הזה.",
"creation_summary_room": "%(creator)s יצר/ה והגדיר/ה את החדר.",
"download_action_decrypting": "מפענח",
"download_action_downloading": "מוריד",
"edits": {
"tooltip_label": "נערך ב-%(date)s. לחץ לצפייה בעריכות.",

View File

@@ -3344,7 +3344,6 @@
"unable_to_decrypt": "Nem sikerült visszafejteni az üzenetet"
},
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Visszafejtés",
"download_action_downloading": "Letöltés",
"download_failed": "Letöltés sikertelen",
"download_failed_description": "Hiba történt a fájl letöltése közben",

View File

@@ -3362,7 +3362,6 @@
"unable_to_decrypt": "Tidak dapat mendekripsi pesan"
},
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Mendekripsi",
"download_action_downloading": "Mengunduh",
"download_failed": "Pengunduhan gagal",
"download_failed_description": "Terjadi kesalahan saat mengunduh berkas ini",

View File

@@ -2360,7 +2360,6 @@
},
"creation_summary_dm": "%(creator)s bjó til oþessi beinu skilaboð.",
"creation_summary_room": "%(creator)s bjó til og stillti spjallrásina.",
"download_action_decrypting": "Afkóðun",
"download_action_downloading": "Sæki",
"edits": {
"tooltip_label": "Breytt þann %(date)s. Smelltu hér til að skoða breytingar.",

View File

@@ -2906,7 +2906,6 @@
"creation_summary_dm": "%(creator)s ha creato questo MD.",
"creation_summary_room": "%(creator)s ha creato e configurato la stanza.",
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Decifrazione",
"download_action_downloading": "Scaricamento",
"edits": {
"tooltip_label": "Modificato alle %(date)s. Clicca per vedere le modifiche.",

View File

@@ -2649,7 +2649,6 @@
"creation_summary_dm": "%(creator)sがこのダイレクトメッセージを作成しました。",
"creation_summary_room": "%(creator)sがルームを作成し設定しました。",
"disambiguated_profile": "%(displayName)s%(matrixId)s",
"download_action_decrypting": "復号化しています",
"download_action_downloading": "ダウンロードしています",
"edits": {
"tooltip_label": "%(date)sに編集済。クリックすると変更履歴を表示。",

View File

@@ -2262,7 +2262,6 @@
"creation_summary_dm": "%(creator)sშექმნა ეს DM.",
"creation_summary_room": "%(creator)sოთახის შექმნა და კონფიგურაცია.",
"disambiguated_profile": "%(displayName)s(%(matrixId)s )",
"download_action_decrypting": "გაშიფვრა",
"download_action_downloading": "ჩამოტვირთვა",
"edits": {
"tooltip_label": "რედაქტირებულია%(date)s . დააწკაპუნეთ რედაქტირების სანახავად.",

View File

@@ -2342,7 +2342,6 @@
},
"creation_summary_dm": "%(creator)s ສ້າງ DM ນີ້.",
"creation_summary_room": "%(creator)s ສ້າງ ແລະ ກຳນົດຄ່າຫ້ອງ.",
"download_action_decrypting": "ການຖອດລະຫັດ",
"download_action_downloading": "ກຳລັງດາວໂຫຼດ",
"edits": {
"tooltip_label": "ແກ້ໄຂເມື່ອ %(date)s. ກົດເພື່ອເບິ່ງການແກ້ໄຂ.",

View File

@@ -1827,7 +1827,6 @@
},
"creation_summary_dm": "%(creator)s sukūrė šį tiesioginio susirašymo kambarį.",
"creation_summary_room": "%(creator)s sukūrė ir sukonfigūravo kambarį.",
"download_action_decrypting": "Iššifruojama",
"download_action_downloading": "Atsiunčiama",
"edits": {
"tooltip_label": "Keista %(date)s. Spustelėkite kad peržiūrėti pakeitimus.",

View File

@@ -2836,7 +2836,6 @@
"historical_event_unverified_device": "Ir jāapliecina šī ierīce, lai piekļūtu senākām ziņām."
},
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Atšifrē",
"download_action_downloading": "Lejupielādē",
"edits": {
"tooltip_label": "Labots %(date)s. Klikšķināt, lai skatītu labojumus.",

View File

@@ -2889,7 +2889,6 @@
"creation_summary_dm": "%(creator)snamorona ity DM ity.",
"creation_summary_room": "%(creator)snamorona sy nanamboatra ny efitrano.",
"disambiguated_profile": "%(displayName)s(%(matrixId)s )",
"download_action_decrypting": "Decryptering",
"download_action_downloading": "Misintona",
"edits": {
"tooltip_label": "Namboarina tamin'ny%(Rendez-vous . Kitiho raha hijery ny fanovana.",

View File

@@ -1450,6 +1450,7 @@
"room_list_navigate_down": "Naviger ned i romlisten",
"room_list_navigate_up": "Naviger opp i romlisten",
"room_list_select_room": "Velg rom fra romlisten",
"save": "Lagre",
"scroll_down_timeline": "Rull ned i tidslinjen",
"scroll_up_timeline": "Rull opp i tidslinjen",
"search": "Søk (må være aktivert)",
@@ -3369,7 +3370,6 @@
"unable_to_decrypt": "Kan ikke dekryptere meldingen"
},
"disambiguated_profile": "%(displayName)s(%(matrixId)s)",
"download_action_decrypting": "Dekrypterer",
"download_action_downloading": "Laster ned",
"download_failed": "Nedlasting mislyktes",
"download_failed_description": "Det oppstod en feil under nedlasting av denne filen",

View File

@@ -2484,7 +2484,6 @@
},
"creation_summary_dm": "%(creator)s maakte deze directe chat.",
"creation_summary_room": "Kamer gestart en ingesteld door %(creator)s.",
"download_action_decrypting": "Ontsleutelen",
"download_action_downloading": "Downloaden",
"edits": {
"tooltip_label": "Bewerkt op %(date)s. Klik om de bewerkingen te bekijken.",

View File

@@ -159,6 +159,7 @@
"view_message": "Wyświetl wiadomość",
"view_source": "Wyświetl źródło",
"yes": "Tak",
"yes_dismiss": "Tak, odrzuć",
"zoom_in": "Powiększ",
"zoom_out": "Pomniejsz"
},
@@ -788,6 +789,7 @@
"cross_signing_status": "Status weryfikacji krzyżowej:",
"cross_signing_untrusted": "Twoje konto ma tożsamość weryfikacji krzyżowej w sekretnym magazynie, ale nie jest jeszcze zaufane przez tę sesję.",
"crypto_not_available": "Moduł kryptograficzny nie jest dostępny",
"device_id": "ID urządzenia",
"key_backup_active_version": "Aktywna wersja kopii zapasowej:",
"key_backup_active_version_none": "Brak",
"key_backup_inactive_warning": "Twoje klucze nie są przywracane na tej sesji.",
@@ -800,6 +802,8 @@
"secret_storage_ready": "gotowe",
"secret_storage_status": "Sekretny magazyn:",
"self_signing_private_key_cached_status": "Samopodpisujący klucz prywatny:",
"session": "Sesja",
"session_fingerprint": "Odcisk palca (klucz sesji)",
"title": "Szyfrowanie end-to-end",
"user_signing_private_key_cached_status": "Podpisany przez użytkownika klucz prywatny:"
},
@@ -825,6 +829,7 @@
"low_bandwidth_mode": "Tryb niskiej przepustowości",
"low_bandwidth_mode_description": "Wymaga kompatybilnego serwera domowego.",
"main_timeline": "Główna oś czasu",
"manual_device_verification": "Ręczna weryfikacja urządzenia",
"no_receipt_found": "Nie znaleziono potwierdzenia",
"notification_state": "Status powiadomień <strong>%(notificationState)s</strong>",
"notifications_debug": "Debug powiadomień",
@@ -915,9 +920,11 @@
"empty_room_was_name": "Pusty pokój (poprzednio %(oldName)s)",
"encryption": {
"access_secret_storage_dialog": {
"alternatives": "Możesz podać również klucz lub frazę bezpieczeństwa.",
"key_validation_text": {
"wrong_security_key": "Wprowadzony klucz przywracania nie jest poprawny."
},
"privacy_warning": "Upewnij się, że nikt nie widzi tego ekranu!",
"restoring": "Przywracanie kluczy z kopii zapasowej",
"security_key_title": "Klucz przywracania"
},
@@ -971,6 +978,8 @@
"setup_secure_backup": {
"explainer": "Utwórz kopię zapasową kluczy przed wylogowaniem, aby ich nie utracić."
},
"turn_on_key_storage": "Włącz magazyn kluczy",
"turn_on_key_storage_description": "Bezpiecznie przechowuj swoją tożsamość kryptograficzną i klucze wiadomości na serwerze. Umożliwi to przeglądanie historii wiadomości na każdym nowym urządzeniu.",
"udd": {
"interactive_verification_button": "Zweryfikuj interaktywnie za pomocą emoji",
"other_ask_verify_text": "Poproś go/ją o zweryfikowanie tej sesji bądź zweryfikuj ją osobiście poniżej.",
@@ -1005,6 +1014,21 @@
"incoming_sas_dialog_waiting": "Oczekiwanie na potwierdzenie partnera…",
"incoming_sas_user_dialog_text_1": "Zweryfikuj tego użytkownika, aby oznaczyć go jako zaufanego. Użytkownicy zaufani dodają większej pewności, gdy korzystasz z wiadomości szyfrowanych end-to-end.",
"incoming_sas_user_dialog_text_2": "Weryfikacja tego użytkownika oznaczy Twoją i jego sesję jako zaufaną.",
"manual": {
"already_verified": "To urządzenie zostało już zweryfikowane.",
"already_verified_and_wrong_fingerprint": "Podany odcinek palca nie pasuje, ale urządzenie zostało już zweryfikowane!",
"device_id": "ID urządzenia",
"failure_description": "Nie udało się zweryfikować '%(deviceId)s': %(error)s",
"failure_title": "Weryfikacja nie powiodła się",
"fingerprint": "Odcisk palca (klucz sesji)",
"no_crypto": "Nie można zweryfikować urządzenia - krypto nie jest włączone",
"no_device": "Nie można zweryfikować urządzenia - urządzenie '%(deviceId)s' nie zostało znalezione",
"no_userid": "Nie można zweryfikować urządzenia - nie udało się znaleźć ID użytkownika",
"success_description": "Urządzenie (%(deviceId)s) jest teraz podpisane krzyżowo",
"success_title": "Zweryfikowano pomyślnie",
"text": "Podaj ID i odcisk palca jednego ze swoich urządzeń, aby zweryfikować. UWAGA umożliwi to drugiemu urządzeniu wysyłanie i odbieranie wiadomości w Twoim imieniu. JEŚLI KTOŚ POWIEDZIAŁ CI, ŻEBYŚ COŚ TU WKLEIŁ, PRAWDOPODOBNIE JESTEŚ OSZUKIWANY!",
"wrong_fingerprint": "Nie można zweryfikować urządzenia '%(deviceId)s' - podany odcisk palca '%(fingerprint)s' nie pasuje do odcisku palca na urządzeniu, '%(fprint)s'"
},
"no_key_or_device": "Wygląda na to, że nie masz klucza przywracania ani żadnych innych urządzeń, które mogłyby zweryfikować Twoją tożsamość. To urządzenie nie będzie mogło odczytać wcześniej zaszyfrowanych wiadomości. Aby zweryfikować swoją tożsamość na tym urządzeniu, musisz zresetować klucze weryfikacyjne.",
"no_support_qr_emoji": "Urządzenie, które próbujesz zweryfikować nie wspiera skanowania kodu QR lub weryfikacji emoji, czyli tego co obsługuje %(brand)s. Spróbuj użyć innego klienta.",
"other_party_cancelled": "Druga strona anulowała weryfikację.",
@@ -1961,6 +1985,7 @@
},
"face_pile_tooltip_shortcut": "Włączając %(commaSeparatedMembers)s",
"face_pile_tooltip_shortcut_joined": "Włączając Ciebie, %(commaSeparatedMembers)s",
"failed_determine_user": "Nie można określić, którego użytkownika należy zignorować, ponieważ jego wydarzenie uległo zmianie.",
"failed_reject_invite": "Nie udało się odrzucić zaproszenia",
"forget_room": "Zapomnij o tym pokoju",
"forget_space": "Zapomnij tą przestrzeń",
@@ -2053,6 +2078,8 @@
"read_topic": "Kliknij, aby przeczytać temat",
"rejecting": "Odrzucanie zaproszenia…",
"rejoin_button": "Dołącz ponownie",
"room_content": "Zawartość pokoju",
"room_is_low_priority": "To jest pokój o niskim priorytecie",
"search": {
"all_rooms_button": "Wyszukaj wszystkie pokoje",
"placeholder": "Szukaj wiadomości...",
@@ -2102,24 +2129,33 @@
"add_space_label": "Dodaj przestrzeń",
"breadcrumbs_empty": "Brak ostatnio odwiedzonych pokojów",
"breadcrumbs_label": "Ostatnio odwiedzane pokoje",
"collapse_filters": "Zwiń listę filtrów",
"empty": {
"no_chats": "Nie ma jeszcze czatów",
"no_chats_description": "Zacznij od wysłania wiadomości lub utworzenia pokoju",
"no_chats_description_no_room_rights": "Wyślij komuś wiadomość, aby rozpocząć.",
"no_favourites": "Nie masz jeszcze ulubionego czatu",
"no_favourites_description": "Dodaj czat do ulubionych w ustawieniach czatu",
"no_invites": "Nie masz żadnych nieprzeczytanych zaproszeń",
"no_lowpriority": "Nie masz pokoi o niskim priorytecie",
"no_mentions": "Nie masz żadnych nieprzeczytanych wzmianek",
"no_people": "Nie prowadzisz jeszcze z nikim czatów prywatnych",
"no_people_description": "Wyczyść filtry, aby zobaczyć pozostałe czaty",
"no_rooms": "Nie jesteś jeszcze w żadnym pokoju",
"no_rooms_description": "Wyczyść filtry, aby zobaczyć pozostałe czaty",
"no_unread": "Brawo! Nie masz żadnych nieprzeczytanych wiadomości",
"show_activity": "Wyświetl całą aktywność",
"show_chats": "Pokaż wszystkie czaty"
},
"expand_filters": "Rozwiń listę filtrów",
"failed_add_tag": "Nie można dodać tagu %(tagName)s do pokoju",
"failed_remove_tag": "Nie udało się usunąć tagu %(tagName)s z pokoju",
"failed_set_dm_tag": "Nie udało się ustawić tagu wiadomości prywatnych",
"filters": {
"favourite": "Ulubione",
"invites": "Zaproszenia",
"low_priority": "Niski priorytet",
"mentions": "Wzmianki",
"people": "Osoby",
"rooms": "Pokoje",
"unread": "Nieprzeczytane"
@@ -2550,6 +2586,8 @@
"session_key": "Klucz sesji:",
"title": "Zaawansowane"
},
"confirm_key_storage_off": "Czy na pewno pozostawić wyłączony magazyn kluczy?",
"confirm_key_storage_off_description": "Jeśli wylogujesz się ze wszystkich swoich urządzeń, utracisz historię wiadomości i będziesz musiał ponownie zweryfikować wszystkie istniejące kontakty. <a>Dowiedz się więcej</a>",
"delete_key_storage": {
"breadcrumb_page": "Usuń magazyn kluczy",
"confirm": "Usuń magazyn kluczy",
@@ -2685,6 +2723,9 @@
"inline_url_previews_room": "Włącz domyślny podgląd URL dla uczestników w tym pokoju",
"inline_url_previews_room_account": "Włącz podgląd URL dla tego pokoju (dotyczy tylko Ciebie)",
"insert_trailing_colon_mentions": "Wstawiaj dwukropek po wzmiance użytkownika na początku wiadomości",
"invite_controls": {
"default_label": "Zezwól użytkownikom na zapraszanie Cię do pokojów"
},
"jump_to_bottom_on_send": "Przejdź na dół osi czasu po wysłaniu wiadomości",
"key_backup": {
"setup_secure_backup": {
@@ -2746,6 +2787,7 @@
"show_in_private": "W pokojach prywatnych",
"show_media": "Zawsze pokazuj"
},
"not_supported": "Twój serwer nie obsługuje tej funkcji.",
"notifications": {
"default_setting_description": "To ustawienie zastosuje się do wszystkich Twoich pokoi.",
"default_setting_section": "Chce otrzymywać powiadomienia (Domyślne ustawienie)",
@@ -2803,6 +2845,7 @@
"voip": "Połączenia audio i wideo"
},
"preferences": {
"Electron.enableContentProtection": "Zapobiegaj przechwytywaniu zawartości okna przez inne aplikacje",
"Electron.enableHardwareAcceleration": "Włącz akcelerację sprzętową (uruchom ponownie %(appName)s, aby zastosować zmiany).",
"always_show_menu_bar": "Zawsze pokazuj pasek menu okna",
"autocomplete_delay": "Opóźnienie autouzupełniania (ms)",
@@ -2811,6 +2854,7 @@
"composer_heading": "Kompozytor",
"default_timezone": "Ustawienie przeglądarki (%(timezone)s)",
"dialog_title": "<strong>Ustawienia:</strong> Preferencje",
"enable_content_protection": "Włącz ochronę treści",
"enable_hardware_acceleration": "Włącz przyspieszenie sprzętowe",
"enable_tray_icon": "Pokaż ikonę w zasobniku systemowym i zminimalizuj okno do niej zamiast zamknięcia",
"keyboard_heading": "Skróty klawiszowe",
@@ -2974,6 +3018,7 @@
"show_chat_effects": "Pokaż efekty czatu (animacje po odebraniu np. confetti)",
"show_displayname_changes": "Pokaż zmiany wyświetlanej nazwy",
"show_join_leave": "Pokaż wiadomości dołączenia/opuszczenia pokoju (nie dotyczy wiadomości zaproszenia/wyrzucenia/banów)",
"show_message_previews": "Pokaż podglądy wiadomości",
"show_nsfw_content": "Pokaż zawartość NSFW",
"show_read_receipts": "Pokaż potwierdzenia odczytania wysyłane przez innych użytkowników",
"show_redaction_placeholder": "Pokaż symbol zastępczy dla usuniętych wiadomości",
@@ -3080,6 +3125,8 @@
"jumptodate": "Przeskocz do podanej daty w linii czasu",
"jumptodate_invalid_input": "Nie jesteśmy w stanie rozpoznać podanej daty (%(inputDate)s). Spróbuj użyć formatu RRRR-MM-DD.",
"lenny": "Dodaje ( ͡° ͜ʖ ͡°) na początku wiadomości tekstowej",
"manual_device_verification_confirm_description": "Umożliwi to drugiemu urządzeniu wysyłanie i odbieranie wiadomości w Twoim imieniu. JEŚLI KTOŚ POWIEDZIAŁ CI, ŻEBYŚ COŚ TU WKLEIŁ, PRAWDOPODOBNIE JESTEŚ OSZUKIWANY! Czy na pewno chcesz zweryfikować drugie urządzenie?",
"manual_device_verification_confirm_title": "Uwaga: ręczna weryfikacja urządzenia",
"me": "Wyświetla akcję",
"msg": "Wysyła wiadomość do wybranego użytkownika",
"myavatar": "Zdjęcie profilowe zostanie zmienione we wszystkich pokojach",
@@ -3120,7 +3167,7 @@
"upgraderoom": "Ulepsza pokój do nowej wersji",
"upgraderoom_permission_error": "Nie posiadasz wymaganych uprawnień do użycia tego polecenia.",
"usage": "Użycie",
"verify": "Weryfikuje użytkownika, sesję oraz klucz publiczny",
"verify": "Zweryfikuj ręcznie jedno ze swoich urządzeń",
"view": "Przegląda pokój z podanym adresem",
"whois": "Pokazuje informacje na temat użytkownika"
},
@@ -3340,7 +3387,6 @@
"unable_to_decrypt": "Nie można rozszyfrować wiadomości"
},
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Rozszyfrowuję",
"download_action_downloading": "Pobieranie",
"download_failed": "Błąd pobierania",
"download_failed_description": "Wystąpił błąd podczas pobierania tego pliku",

View File

@@ -3267,7 +3267,6 @@
"unable_to_decrypt": "Não é possível desencriptar a mensagem"
},
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Descriptografando",
"download_action_downloading": "A transferir…",
"download_failed": "A transferência falhou",
"download_failed_description": "Ocorreu um erro ao transferir este ficheiro",

View File

@@ -1450,6 +1450,7 @@
"room_list_navigate_down": "Navegar para baixo na lista de salas",
"room_list_navigate_up": "Navegar para cima na lista de salas",
"room_list_select_room": "Selecionar sala da lista de salas",
"save": "Salvar",
"scroll_down_timeline": "Rola para baixo no histórico",
"scroll_up_timeline": "Rola para cima no histórico",
"search": "Pesquisar (deve estar ativado)",
@@ -3369,7 +3370,6 @@
"unable_to_decrypt": "Não foi possível descriptografar a mensagem"
},
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Decriptando",
"download_action_downloading": "Baixando",
"download_failed": "Falha no download",
"download_failed_description": "Ocorreu um erro ao baixar esse arquivo",

View File

@@ -3366,7 +3366,6 @@
"unable_to_decrypt": "Не удалось расшифровать сообщение"
},
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Расшифровка",
"download_action_downloading": "Загрузка",
"download_failed": "Загрузка не удалась",
"download_failed_description": "Произошла ошибка при загрузке этого файла",

View File

@@ -1466,6 +1466,7 @@
"room_list_navigate_down": "Prejsť v zozname miestností smerom nadol",
"room_list_navigate_up": "Prejsť v zozname miestností smerom hore",
"room_list_select_room": "Vybrať miestnosť zo zoznamu miestností",
"save": "Uložiť",
"scroll_down_timeline": "Posun dole na časovej osi",
"scroll_up_timeline": "Posun hore na časovej osi",
"search": "Vyhľadávanie (musí byť povolené)",
@@ -3412,7 +3413,6 @@
"unable_to_decrypt": "Správu sa nepodarilo dešifrovať"
},
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Dešifrovanie",
"download_action_downloading": "Preberanie",
"download_failed": "Sťahovanie zlyhalo",
"download_failed_description": "Pri sťahovaní tohto súboru sa vyskytla chyba",

View File

@@ -2711,7 +2711,6 @@
},
"creation_summary_dm": "%(creator)s krijoi këtë DM.",
"creation_summary_room": "%(creator)s krijoi dhe formësoi dhomën.",
"download_action_decrypting": "Po shfshehtëzohet",
"download_action_downloading": "Shkarkim",
"edits": {
"tooltip_label": "Përpunuar më %(date)s. Klikoni që të shihni përpunimet.",

View File

@@ -785,6 +785,7 @@
"cross_signing_status": "Korssigneringsstatus:",
"cross_signing_untrusted": "Ditt konto har en korssigneringsidentitet i hemlig lagring, men den är ännu inte betrodd av den här sessionen.",
"crypto_not_available": "Kryptografisk modul är inte tillgänglig",
"device_id": "Enhets-ID",
"key_backup_active_version": "Aktiv säkerhetskopiaversion:",
"key_backup_active_version_none": "Ingen",
"key_backup_inactive_warning": "Dina nycklar säkerhetskopieras inte från den här sessionen.",
@@ -797,6 +798,8 @@
"secret_storage_ready": "klar",
"secret_storage_status": "Hemlig lagring:",
"self_signing_private_key_cached_status": "Självsignerande privat nyckel:",
"session": "Session",
"session_fingerprint": "Fingeravtryck (sessionsnyckel)",
"title": "Totalsträckskryptering",
"user_signing_private_key_cached_status": "Privat nyckel för användarsignering:"
},
@@ -822,6 +825,7 @@
"low_bandwidth_mode": "Lågt bandbreddsläge",
"low_bandwidth_mode_description": "Kräver kompatibel hemserver.",
"main_timeline": "Huvudtidslinje",
"manual_device_verification": "Manuell enhetsverifiering",
"no_receipt_found": "Inga kvitton hittade",
"notification_state": "Aviseringsstatus är <strong>%(notificationState)s</strong>",
"notifications_debug": "Aviseringsfelsökning",
@@ -2682,6 +2686,9 @@
"inline_url_previews_room": "Aktivera URL-förhandsgranskning som standard för deltagare i detta rum",
"inline_url_previews_room_account": "Aktivera URL-förhandsgranskning för detta rum (påverkar bara dig)",
"insert_trailing_colon_mentions": "Infoga kolon efter användaromnämnande på början av ett meddelande",
"invite_controls": {
"default_label": "Tillåt användare att bjuda in dig till rum"
},
"jump_to_bottom_on_send": "Hoppa till botten av tidslinjen när du skickar ett meddelande",
"key_backup": {
"setup_secure_backup": {
@@ -2743,6 +2750,7 @@
"show_in_private": "I privata rum",
"show_media": "Visa alltid"
},
"not_supported": "Din server implementerar inte den här funktionen.",
"notifications": {
"default_setting_description": "Denna inställning kommer att tillämpas som standard för alla dina rum.",
"default_setting_section": "Jag vill bli meddelad för (Standardinställning)",
@@ -3339,7 +3347,6 @@
"unable_to_decrypt": "Det gick inte att dekryptera meddelandet"
},
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Avkrypterar",
"download_action_downloading": "Laddar ner",
"download_failed": "Nedladdning misslyckades",
"download_failed_description": "Ett fel uppstod när den här filen laddades ned",

View File

@@ -3237,7 +3237,6 @@
"unable_to_decrypt": "Mesaj şifresi çözülemedi"
},
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
"download_action_decrypting": "Şifre çözülüyor",
"download_action_downloading": "İndiriliyor",
"download_failed": "İndirme başarısız oldu",
"download_failed_description": "Bu dosya indirilirken bir hata oluştu",

View File

@@ -3375,7 +3375,6 @@
"unable_to_decrypt": "Не вдалося розшифрувати повідомлення"
},
"disambiguated_profile": "%(displayName)s(%(matrixId)s)",
"download_action_decrypting": "Розшифрування",
"download_action_downloading": "Завантаження",
"download_failed": "Не вдалося завантажити",
"download_failed_description": "Під час завантаження цього файлу сталася помилка",

View File

@@ -2623,7 +2623,6 @@
},
"creation_summary_dm": "%(creator)s đã tạo DM này.",
"creation_summary_room": "%(creator)s đã tạo và định cấu hình phòng.",
"download_action_decrypting": "Đang giải mã",
"download_action_downloading": "Đang tải xuống",
"edits": {
"tooltip_label": "Đã chỉnh sửa vào %(date)s. Bấm để xem các chỉnh sửa.",

View File

@@ -2589,7 +2589,6 @@
},
"creation_summary_dm": "%(creator)s 创建了此私聊。",
"creation_summary_room": "%(creator)s 创建并配置了此房间。",
"download_action_decrypting": "解密中",
"download_action_downloading": "下载中",
"edits": {
"tooltip_label": "编辑于 %(date)s。点击以查看编辑历史。",

View File

@@ -2833,7 +2833,6 @@
},
"creation_summary_dm": "%(creator)s 建立了此私人訊息。",
"creation_summary_room": "%(creator)s 建立並設定了聊天室。",
"download_action_decrypting": "正在解密",
"download_action_downloading": "正在下載",
"edits": {
"tooltip_label": "編輯於 %(date)s。點擊以檢視編輯。",

View File

@@ -6,14 +6,14 @@ 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.
*/
.mx_Box--flex {
.box-flex {
flex: var(--mx-box-flex, unset);
}
.mx_Box--shrink {
.box-shrink {
flex-shrink: var(--mx-box-shrink, unset);
}
.mx_Box--grow {
.box-grow {
flex-grow: var(--mx-box-grow, unset);
}

View File

@@ -9,7 +9,9 @@ Please see LICENSE files in the repository root for full details.
import classNames from "classnames";
import React, { type JSX, useMemo } from "react";
type FlexProps = {
import styles from "./Box.module.css";
type BoxProps = {
/**
* The type of the HTML element
* @default div
@@ -51,7 +53,7 @@ export function Box({
className,
children,
...props
}: React.PropsWithChildren<FlexProps>): JSX.Element {
}: React.PropsWithChildren<BoxProps>): JSX.Element {
const style = useMemo(() => {
const style: Record<string, any> = {};
if (flex) style["--mx-box-flex"] = flex;
@@ -64,10 +66,10 @@ export function Box({
as,
{
...props,
className: classNames("mx_Box", className, {
"mx_Box--flex": !!flex,
"mx_Box--shrink": !!shrink,
"mx_Box--grow": !!grow,
className: classNames(className, {
[styles["box-flex"]]: !!flex,
[styles["box-shrink"]]: !!shrink,
[styles["box-grow"]]: !!grow,
}),
style,
},

View File

@@ -0,0 +1,8 @@
/*
* 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.
*/
export { Box } from "./Box";

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
.mx_Flex {
.flex {
display: var(--mx-flex-display, unset);
flex-direction: var(--mx-flex-direction, unset);
align-items: var(--mx-flex-align, unset);

View File

@@ -9,6 +9,8 @@ Please see LICENSE files in the repository root for full details.
import classNames from "classnames";
import React, { type JSX, type ComponentProps, type JSXElementConstructor, useMemo } from "react";
import styles from "./Flex.module.css";
type FlexProps<T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>> = {
/**
* The type of the HTML element
@@ -82,5 +84,5 @@ export function Flex<T extends keyof JSX.IntrinsicElements | JSXElementConstruct
[align, direction, display, gap, justify, wrap],
);
return React.createElement(as, { ...props, className: classNames("mx_Flex", className), style }, children);
return React.createElement(as, { ...props, className: classNames(styles.flex, className), style }, children);
}

View File

@@ -0,0 +1,8 @@
/*
* 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.
*/
export { Flex } from "./Flex";

View File

@@ -168,6 +168,7 @@ export class StopGapWidgetDriver extends WidgetDriver {
WidgetEventCapability.forStateEvent(EventDirection.Receive, EventType.RoomCreate).raw,
);
const sendRoomEvents = [EventType.CallNotify, EventType.RTCNotification];
const sendRecvRoomEvents = [
"io.element.call.encryption_keys",
"org.matrix.rageshake_request",
@@ -175,10 +176,10 @@ export class StopGapWidgetDriver extends WidgetDriver {
EventType.RoomRedaction,
"io.element.call.reaction",
];
for (const eventType of sendRecvRoomEvents) {
for (const eventType of [...sendRoomEvents, ...sendRecvRoomEvents])
this.allowedCapabilities.add(WidgetEventCapability.forRoomEvent(EventDirection.Send, eventType).raw);
for (const eventType of sendRecvRoomEvents)
this.allowedCapabilities.add(WidgetEventCapability.forRoomEvent(EventDirection.Receive, eventType).raw);
}
const sendRecvToDevice = [
EventType.CallInvite,

View File

@@ -161,7 +161,7 @@ export default class HTMLExporter extends Exporter {
<div class="mx_MatrixChat_wrapper" aria-hidden="false">
<div class="mx_MatrixChat">
<main class="mx_RoomView">
<div class="mx_Flex mx_RoomHeader light-panel">
<div class="mx_RoomHeader light-panel">
${roomAvatar}
<div class="mx_RoomHeader_infoWrapper">
<div

View File

@@ -131,11 +131,11 @@ a.mx_reply_anchor:hover {
}
.mx_RoomHeader {
--mx-flex-display: flex;
--mx-flex-direction: row;
--mx-flex-align: center;
--mx-flex-justify: start;
--mx-flex-gap: var(--cpd-space-3x);
display: flex;
flex-direction: row;
align-items: center;
justify-content: start;
gap: var(--cpd-space-3x);
}
.mx_ReplyChain_Export {

View File

@@ -98,7 +98,7 @@ exports[`<UnsupportedBrowserView /> should match snapshot 1`] = `
</span>
</p>
<div
class="mx_Flex mx_ErrorView_buttons"
class="flex mx_ErrorView_flexContainer mx_ErrorView_buttons"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x); --mx-flex-wrap: nowrap;"
>
<button
@@ -139,7 +139,7 @@ exports[`<UnsupportedBrowserView /> should match snapshot 1`] = `
Use Element Desktop instead
</h2>
<div
class="mx_Flex"
class="flex mx_ErrorView_flexContainer"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-4x); --mx-flex-wrap: nowrap;"
>
<a
@@ -209,7 +209,7 @@ exports[`<UnsupportedBrowserView /> should match snapshot 1`] = `
Or use our mobile app
</h2>
<div
class="mx_Flex"
class="flex mx_ErrorView_flexContainer"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-6x); --mx-flex-wrap: nowrap;"
>
<a

View File

@@ -52,7 +52,7 @@ exports[`FilePanel renders empty state 1`] = `
class="mx_RoomView_empty"
>
<div
class="mx_Flex mx_EmptyState"
class="flex mx_EmptyState"
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-4x); --mx-flex-wrap: nowrap;"
>
<svg

View File

@@ -6,7 +6,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
class="mx_RoomView mx_RoomView--local"
>
<header
class="mx_Flex mx_RoomHeader light-panel"
class="flex mx_RoomHeader light-panel"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
>
<button
@@ -28,7 +28,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
tabindex="0"
>
<div
class="mx_Box mx_RoomHeader_info mx_Box--flex"
class="mx_RoomHeader_info box-flex"
style="--mx-box-flex: 1;"
>
<div
@@ -225,7 +225,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
class="mx_RoomView mx_RoomView--local"
>
<header
class="mx_Flex mx_RoomHeader light-panel"
class="flex mx_RoomHeader light-panel"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
>
<button
@@ -247,7 +247,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
tabindex="0"
>
<div
class="mx_Box mx_RoomHeader_info mx_Box--flex"
class="mx_RoomHeader_info box-flex"
style="--mx-box-flex: 1;"
>
<div
@@ -530,7 +530,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
class="mx_RoomView mx_RoomView--local"
>
<header
class="mx_Flex mx_RoomHeader light-panel"
class="flex mx_RoomHeader light-panel"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
>
<button
@@ -552,7 +552,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
tabindex="0"
>
<div
class="mx_Box mx_RoomHeader_info mx_Box--flex"
class="mx_RoomHeader_info box-flex"
style="--mx-box-flex: 1;"
>
<div
@@ -912,7 +912,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
class="mx_RoomView mx_RoomView--local"
>
<header
class="mx_Flex mx_RoomHeader light-panel"
class="flex mx_RoomHeader light-panel"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
>
<button
@@ -934,7 +934,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
tabindex="0"
>
<div
class="mx_Box mx_RoomHeader_info mx_Box--flex"
class="mx_RoomHeader_info box-flex"
style="--mx-box-flex: 1;"
>
<div
@@ -1375,7 +1375,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
data-layout="group"
>
<header
class="mx_Flex mx_RoomHeader light-panel"
class="flex mx_RoomHeader light-panel"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
>
<button
@@ -1397,7 +1397,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
tabindex="0"
>
<div
class="mx_Box mx_RoomHeader_info mx_Box--flex"
class="mx_RoomHeader_info box-flex"
style="--mx-box-flex: 1;"
>
<div
@@ -1587,7 +1587,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
data-layout="group"
>
<header
class="mx_Flex mx_RoomHeader light-panel"
class="flex mx_RoomHeader light-panel"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
>
<button
@@ -1609,7 +1609,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
tabindex="0"
>
<div
class="mx_Box mx_RoomHeader_info mx_Box--flex"
class="mx_RoomHeader_info box-flex"
style="--mx-box-flex: 1;"
>
<div
@@ -1976,7 +1976,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
data-layout="group"
>
<header
class="mx_Flex mx_RoomHeader light-panel"
class="flex mx_RoomHeader light-panel"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x); --mx-flex-wrap: nowrap;"
>
<button
@@ -1998,7 +1998,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
tabindex="0"
>
<div
class="mx_Box mx_RoomHeader_info mx_Box--flex"
class="mx_RoomHeader_info box-flex"
style="--mx-box-flex: 1;"
>
<div

View File

@@ -0,0 +1,194 @@
/*
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 { Device, type MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix";
import { type Mocked } from "jest-mock";
import { UserVerificationStatus, type CryptoApi } from "matrix-js-sdk/src/crypto-api";
import { renderHook, waitFor } from "jest-matrix-react";
import { createTestClient, withClientContextRenderOptions } from "../../../../../test-utils";
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
import { useUserInfoVerificationViewModel } from "../../../../../../src/components/viewmodels/right_panel/user_info/UserInfoHeaderVerificationViewModel";
describe("useUserInfoVerificationHeaderViewModel", () => {
const defaultRoomId = "!fkfk";
const defaultUserId = "@user:example.com";
const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
const defaultProps = {
devices: [] as Device[],
member: defaultMember,
};
let mockClient: MatrixClient;
let mockCrypto: Mocked<CryptoApi>;
beforeEach(() => {
mockCrypto = {
bootstrapSecretStorage: jest.fn(),
bootstrapCrossSigning: jest.fn(),
getCrossSigningKeyId: jest.fn(),
getVerificationRequestsToDeviceInProgress: jest.fn().mockReturnValue([]),
getUserDeviceInfo: jest.fn(),
getDeviceVerificationStatus: jest.fn(),
getUserVerificationStatus: jest.fn(),
isDehydrationSupported: jest.fn().mockResolvedValue(false),
startDehydration: jest.fn(),
getKeyBackupInfo: jest.fn().mockResolvedValue(null),
userHasCrossSigningKeys: jest.fn().mockResolvedValue(false),
} as unknown as Mocked<CryptoApi>;
mockClient = createTestClient();
jest.spyOn(mockClient, "doesServerSupportUnstableFeature").mockResolvedValue(true);
jest.spyOn(mockClient.secretStorage, "hasKey").mockResolvedValue(true);
jest.spyOn(mockClient, "getCrypto").mockReturnValue(mockCrypto);
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient);
});
afterEach(() => {
jest.restoreAllMocks();
});
const renderUserInfoHeaderVerificationHook = (props = defaultProps) => {
return renderHook(
() => useUserInfoVerificationViewModel(props.member, props.devices),
withClientContextRenderOptions(mockClient),
);
};
it("should be able to verify user", async () => {
const notMeId = "@notMe";
const notMetMember = new RoomMember(defaultRoomId, notMeId);
const device1 = new Device({
deviceId: "d1",
userId: notMeId,
displayName: "my device",
algorithms: [],
keys: new Map(),
});
// mock the user as not verified
jest.spyOn(mockCrypto, "getUserVerificationStatus").mockResolvedValue(
new UserVerificationStatus(false, false, false),
);
jest.spyOn(mockClient, "getUserId").mockReturnValue(defaultMember.userId);
// the selected user is not the default user, so he can make user verification
const { result } = renderUserInfoHeaderVerificationHook({ member: notMetMember, devices: [device1] });
await waitFor(() => {
const canVerify = result.current.canVerify;
expect(canVerify).toBeTruthy();
});
});
it("should not be able to verify user if user is not me", async () => {
const device1 = new Device({
deviceId: "d1",
userId: defaultMember.userId,
displayName: "my device",
algorithms: [],
keys: new Map(),
});
// mock the user as not verified
jest.spyOn(mockCrypto, "getUserVerificationStatus").mockResolvedValue(
new UserVerificationStatus(false, false, false),
);
jest.spyOn(mockClient, "getUserId").mockReturnValue(defaultMember.userId);
const { result } = renderUserInfoHeaderVerificationHook({ member: defaultMember, devices: [device1] });
await waitFor(() => {
const canVerify = result.current.canVerify;
expect(canVerify).toBeFalsy();
// if we cant verify the user the hasCrossSigningKeys value should also be undefined
expect(result.current.hasCrossSigningKeys).toBeUndefined();
});
});
it("should not be able to verify user if im already verified", async () => {
const notMeId = "@notMe";
const notMetMember = new RoomMember(defaultRoomId, notMeId);
const device1 = new Device({
deviceId: "d1",
userId: notMeId,
displayName: "my device",
algorithms: [],
keys: new Map(),
});
// mock the user as already verified
jest.spyOn(mockCrypto, "getUserVerificationStatus").mockResolvedValue(
new UserVerificationStatus(true, true, false),
);
jest.spyOn(mockClient, "getUserId").mockReturnValue(defaultMember.userId);
// the selected user is not the default user, so he can make user verification
const { result } = renderUserInfoHeaderVerificationHook({ member: notMetMember, devices: [device1] });
await waitFor(() => {
const canVerify = result.current.canVerify;
expect(canVerify).toBeFalsy();
// if we cant verify the user the hasCrossSigningKeys value should also be undefined
expect(result.current.hasCrossSigningKeys).toBeUndefined();
});
});
it("should not be able to verify user there is no devices", async () => {
const notMeId = "@notMe";
const notMetMember = new RoomMember(defaultRoomId, notMeId);
// mock the user as not verified
jest.spyOn(mockCrypto, "getUserVerificationStatus").mockResolvedValue(
new UserVerificationStatus(false, false, false),
);
jest.spyOn(mockClient, "getUserId").mockReturnValue(defaultMember.userId);
// the selected user is not the default user, so he can make user verification
const { result } = renderUserInfoHeaderVerificationHook({ member: notMetMember, devices: [] });
await waitFor(() => {
const canVerify = result.current.canVerify;
expect(canVerify).toBeFalsy();
// if we cant verify the user the hasCrossSigningKeys value should also be undefined
expect(result.current.hasCrossSigningKeys).toBeUndefined();
});
});
it("should get correct hasCrossSigningKeys values", async () => {
const notMeId = "@notMe";
const notMetMember = new RoomMember(defaultRoomId, notMeId);
const device1 = new Device({
deviceId: "d1",
userId: notMeId,
displayName: "my device",
algorithms: [],
keys: new Map(),
});
// mock the user as not verified
jest.spyOn(mockCrypto, "getUserVerificationStatus").mockResolvedValue(
new UserVerificationStatus(false, false, false),
);
jest.spyOn(mockClient, "getUserId").mockReturnValue(defaultMember.userId);
jest.spyOn(mockCrypto, "userHasCrossSigningKeys").mockResolvedValue(true);
const { result } = renderUserInfoHeaderVerificationHook({ member: notMetMember, devices: [device1] });
await waitFor(() => {
const hasCrossSigningKeys = result.current.hasCrossSigningKeys;
expect(hasCrossSigningKeys).toBeTruthy();
});
});
});

View File

@@ -0,0 +1,179 @@
/*
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 { type MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix";
import { mocked, type Mocked } from "jest-mock";
import { type CryptoApi } from "matrix-js-sdk/src/crypto-api";
import { renderHook } from "jest-matrix-react";
import { withClientContextRenderOptions } from "../../../../../test-utils";
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
import { useUserfoHeaderViewModel } from "../../../../../../src/components/viewmodels/right_panel/user_info/UserInfoHeaderViewModel";
import * as UseTimezone from "../../../../../../src/hooks/useUserTimezone";
import SdkConfig from "../../../../../../src/SdkConfig";
import Modal from "../../../../../../src/Modal";
import ImageView from "../../../../../../src/components/views/elements/ImageView";
import * as Media from "../../../../../../src/customisations/Media";
import { type IConfigOptions } from "../../../../../../src/IConfigOptions";
jest.mock("../../../../../../src/customisations/UserIdentifier", () => {
return {
getDisplayUserIdentifier: jest.fn().mockReturnValue("customUserIdentifier"),
};
});
describe("useUserInfoHeaderViewModel", () => {
const defaultRoomId = "!fkfk";
const defaultUserId = "@user:example.com";
const defaultMember = new RoomMember(defaultRoomId, defaultUserId);
const defaultProps = {
member: defaultMember,
roomId: defaultRoomId,
};
let mockClient: Mocked<MatrixClient>;
let mockCrypto: Mocked<CryptoApi>;
const mockAvatarUrl = "mock-avatar-url";
const oldGet = SdkConfig.get;
beforeEach(() => {
mockCrypto = mocked({
getDeviceVerificationStatus: jest.fn(),
getUserDeviceInfo: jest.fn(),
userHasCrossSigningKeys: jest.fn().mockResolvedValue(false),
getUserVerificationStatus: jest.fn(),
isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(false),
} as unknown as CryptoApi);
mockClient = mocked({
getUser: jest.fn(),
isGuest: jest.fn().mockReturnValue(false),
isUserIgnored: jest.fn(),
getIgnoredUsers: jest.fn(),
setIgnoredUsers: jest.fn(),
getUserId: jest.fn(),
getSafeUserId: jest.fn(),
getDomain: jest.fn(),
on: jest.fn(),
off: jest.fn(),
isSynapseAdministrator: jest.fn().mockResolvedValue(false),
doesServerSupportUnstableFeature: jest.fn().mockReturnValue(false),
doesServerSupportExtendedProfiles: jest.fn().mockResolvedValue(false),
getExtendedProfileProperty: jest.fn().mockRejectedValue(new Error("Not supported")),
mxcUrlToHttp: jest.fn().mockReturnValue(mockAvatarUrl),
removeListener: jest.fn(),
currentState: {
on: jest.fn(),
},
getRoom: jest.fn(),
credentials: {},
setPowerLevel: jest.fn(),
getCrypto: jest.fn().mockReturnValue(mockCrypto),
baseUrl: "homeserver.url",
} as unknown as MatrixClient);
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient);
});
afterEach(() => {
jest.restoreAllMocks();
});
const renderUserInfoHeaderViewModelHook = (props = defaultProps) => {
return renderHook(() => useUserfoHeaderViewModel(props), withClientContextRenderOptions(mockClient));
};
it("should give user timezone info", () => {
const defaultTZ = { timezone: "FR", friendly: "fr" };
jest.spyOn(UseTimezone, "useUserTimezone").mockReturnValue(defaultTZ);
const { result } = renderUserInfoHeaderViewModelHook();
const timezone = result.current.timezoneInfo;
expect(UseTimezone.useUserTimezone).toHaveBeenCalledWith(mockClient, defaultMember.userId);
expect(timezone).toEqual(defaultTZ);
});
it("should give correct showPresence value based on enablePresenceByHsUrl", () => {
jest.spyOn(SdkConfig, "get").mockImplementation((key: string) => {
if (key === "enable_presence_by_hs_url") {
return {
[mockClient.baseUrl]: false,
};
}
return oldGet(key as keyof IConfigOptions);
});
const { result } = renderUserInfoHeaderViewModelHook();
const showPresence = result.current.showPresence;
expect(showPresence).toBeFalsy();
});
it("should have default value true for showPresence", () => {
jest.spyOn(SdkConfig, "get").mockImplementation(() => false);
const { result } = renderUserInfoHeaderViewModelHook();
const showPresence = result.current.showPresence;
expect(showPresence).toBeTruthy();
});
it("should open image dialog when avatar is clicked", () => {
const props = Object.assign({}, defaultProps);
const spyModale = jest.spyOn(Modal, "createDialog");
const spyMedia = jest.spyOn(Media, "mediaFromMxc");
jest.spyOn(props.member, "getMxcAvatarUrl").mockReturnValue(mockAvatarUrl);
const { result } = renderUserInfoHeaderViewModelHook(props);
result.current.onMemberAvatarClick();
expect(spyModale).toHaveBeenCalledWith(
ImageView,
{
src: mockAvatarUrl,
name: defaultMember.name,
},
"mx_Dialog_lightbox",
undefined,
true,
);
expect(spyMedia).toHaveBeenCalledWith(mockAvatarUrl);
});
it("should not open image dialog when avatar url is null", () => {
const props = Object.assign({}, defaultProps);
const spyModale = jest.spyOn(Modal, "createDialog");
jest.spyOn(props.member, "getMxcAvatarUrl").mockReturnValue(mockAvatarUrl);
jest.spyOn(Media, "mediaFromMxc").mockReturnValue({
srcHttp: null,
isEncrypted: false,
srcMxc: "",
thumbnailMxc: undefined,
hasThumbnail: false,
thumbnailHttp: null,
getThumbnailHttp: function (width: number, height: number, mode?: "scale" | "crop"): string | null {
throw new Error("Function not implemented.");
},
getThumbnailOfSourceHttp: function (width: number, height: number, mode?: "scale" | "crop"): string | null {
throw new Error("Function not implemented.");
},
getSquareThumbnailHttp: function (dim: number): string | null {
throw new Error("Function not implemented.");
},
downloadSource: function (): Promise<Response> {
throw new Error("Function not implemented.");
},
});
const { result } = renderUserInfoHeaderViewModelHook(props);
result.current.onMemberAvatarClick();
expect(spyModale).not.toHaveBeenCalled();
});
});

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