Compare commits
30 Commits
toger5/dev
...
robin/noti
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fc9b53629 | ||
|
|
b2a1a6a0a4 | ||
|
|
81edfece6a | ||
|
|
ab26004c4c | ||
|
|
ffedca3954 | ||
|
|
f7ef948cf0 | ||
|
|
ba828b2194 | ||
|
|
16ef503174 | ||
|
|
7bfb9818f6 | ||
|
|
dcbba5ea9d | ||
|
|
6b40da5779 | ||
|
|
941835ccf2 | ||
|
|
4ec10a9b4d | ||
|
|
6a48183a35 | ||
|
|
62b080a50e | ||
|
|
0dc7fcc64a | ||
|
|
354867baa7 | ||
|
|
4c1e3c82e4 | ||
|
|
1e689ac098 | ||
|
|
16ab7ffbc7 | ||
|
|
b35e2a8c45 | ||
|
|
a07d5b82b3 | ||
|
|
ca1420e604 | ||
|
|
8e59ebb754 | ||
|
|
cc2ee5ea78 | ||
|
|
774e0e8f7b | ||
|
|
e0f5f48eef | ||
|
|
e7a772472e | ||
|
|
0a97cbaada | ||
|
|
8a879c7fca |
@@ -31,6 +31,9 @@ const config: StorybookConfig = {
|
||||
},
|
||||
// Needed for counterpart to work
|
||||
plugins: [nodePolyfills({ include: ["process", "util"] })],
|
||||
server: {
|
||||
allowedHosts: ["localhost", ".docker.internal"],
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# syntax=docker.io/docker/dockerfile:1.17-labs@sha256:9187104f31e3a002a8a6a3209ea1f937fb7486c093cbbde1e14b0fa0d7e4f1b5
|
||||
|
||||
# Builder
|
||||
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:125996cb2451482467fc2aa4d7653075894b08e9b7711bcd761044ca270a083e AS builder
|
||||
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:a80324457a2c8d09c83ff9edf2bdf71f378d3288de920e68a358bd3c484b8c4a AS builder
|
||||
|
||||
# Support custom branch of the js-sdk. This also helps us build images of element-web develop.
|
||||
ARG USE_CUSTOM_SDKS=false
|
||||
@@ -19,7 +19,7 @@ RUN /src/scripts/docker-package.sh
|
||||
RUN cp /src/config.sample.json /src/webapp/config.json
|
||||
|
||||
# App
|
||||
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:ef0100e39ffe377a42ad99e1f644b78097a84f1ac60a90eac3b888196b2eeb00
|
||||
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:86df552d36eb24c45d3f5becf6423bd056a3fd235d7085fe3d5ea28ba89a8232
|
||||
|
||||
# Need root user to install packages & manipulate the usr directory
|
||||
USER root
|
||||
|
||||
@@ -22,6 +22,8 @@ const config: Config = {
|
||||
setupFiles: ["jest-canvas-mock", "web-streams-polyfill/polyfill"],
|
||||
setupFilesAfterEnv: ["<rootDir>/test/setupTests.ts"],
|
||||
moduleNameMapper: {
|
||||
// Support CSS module
|
||||
"\\.(module.css)$": "identity-obj-proxy",
|
||||
"\\.(css|scss|pcss)$": "<rootDir>/__mocks__/cssMock.js",
|
||||
"\\.(gif|png|ttf|woff2)$": "<rootDir>/__mocks__/imageMock.js",
|
||||
"\\.svg$": "<rootDir>/__mocks__/svg.js",
|
||||
|
||||
12
package.json
@@ -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",
|
||||
|
||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.0 KiB |
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||
|
||||
const TAG = "develop@sha256:b38e55f06543f83f5a13f1d843489eb7aeaf7370a5c17a51897b462eeca315f5";
|
||||
const TAG = "develop@sha256:8c2d9a93dd209a79d3e5e50cd18addfe52d80bea0ffe48a5d3e15836032eeb9d";
|
||||
|
||||
/**
|
||||
* SynapseContainer which freezes the docker digest to stabilise tests,
|
||||
|
||||
@@ -53,8 +53,6 @@
|
||||
@import "./components/views/settings/shared/_SettingsSubsectionHeading.pcss";
|
||||
@import "./components/views/spaces/_QuickThemeSwitcher.pcss";
|
||||
@import "./components/views/typography/_Caption.pcss";
|
||||
@import "./components/views/utils/_Box.pcss";
|
||||
@import "./components/views/utils/_Flex.pcss";
|
||||
@import "./compound/_Icon.pcss";
|
||||
@import "./compound/_SuccessDialog.pcss";
|
||||
@import "./structures/_AutoHideScrollbar.pcss";
|
||||
|
||||
@@ -50,7 +50,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
color: var(--cpd-color-text-secondary);
|
||||
}
|
||||
|
||||
.mx_Flex {
|
||||
.mx_ErrorView_flexContainer {
|
||||
margin: 0 auto;
|
||||
max-width: max-content;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -32,7 +32,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
padding: 0 12px;
|
||||
color: var(--cpd-color-text-secondary);
|
||||
|
||||
.mx_Box {
|
||||
.mx_RoomSummaryCard_topic_box {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
@@ -108,11 +108,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
margin: 0;
|
||||
font-size: $font-20px;
|
||||
line-height: $font-25px;
|
||||
|
||||
/* E2E icon wrapper */
|
||||
.mx_Flex > span {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserInfo_profile_name {
|
||||
|
||||
@@ -12,10 +12,6 @@
|
||||
border-bottom: var(--cpd-border-width-1) solid var(--cpd-color-bg-subtle-primary);
|
||||
padding: 0 var(--cpd-space-3x);
|
||||
|
||||
svg {
|
||||
fill: var(--cpd-color-icon-secondary);
|
||||
}
|
||||
|
||||
.mx_RoomListSearch_search {
|
||||
/* The search button should take all the remaining space */
|
||||
flex: 1;
|
||||
@@ -23,6 +19,10 @@
|
||||
color: var(--cpd-color-text-secondary);
|
||||
min-width: 0;
|
||||
|
||||
svg {
|
||||
fill: var(--cpd-color-icon-secondary);
|
||||
}
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
|
||||
@@ -42,10 +42,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomListSearch_button:hover {
|
||||
svg {
|
||||
fill: var(--cpd-color-icon-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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")}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
import React, { type ComponentType } from "react";
|
||||
import { Text } from "@vector-im/compound-web";
|
||||
|
||||
import { Flex } from "../../utils/Flex";
|
||||
import { Flex } from "../../../shared-components/utils/Flex";
|
||||
|
||||
interface Props {
|
||||
Icon: ComponentType<React.SVGAttributes<SVGElement>>;
|
||||
|
||||
@@ -46,9 +46,9 @@ import RoomAvatar from "../avatars/RoomAvatar.tsx";
|
||||
import { E2EStatus } from "../../../utils/ShieldUtils.ts";
|
||||
import { type RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks.ts";
|
||||
import RoomName from "../elements/RoomName.tsx";
|
||||
import { Flex } from "../../utils/Flex.tsx";
|
||||
import { Flex } from "../../../shared-components/utils/Flex";
|
||||
import { Linkify, topicToHtml } from "../../../HtmlUtils.tsx";
|
||||
import { Box } from "../../utils/Box.tsx";
|
||||
import { Box } from "../../../shared-components/utils/Box";
|
||||
import { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement.tsx";
|
||||
import { useRoomSummaryCardViewModel } from "../../viewmodels/right_panel/RoomSummaryCardViewModel.tsx";
|
||||
import { useRoomTopicViewModel } from "../../viewmodels/right_panel/RoomSummaryCardTopicViewModel.tsx";
|
||||
@@ -80,7 +80,7 @@ const RoomTopic: React.FC<Pick<IProps, "room">> = ({ room }): JSX.Element | null
|
||||
gap="var(--cpd-space-2x)"
|
||||
className="mx_RoomSummaryCard_topic"
|
||||
>
|
||||
<Box flex="1">
|
||||
<Box flex="1" className="mx_RoomSummaryCard_topic_box">
|
||||
<Link kind="primary" onClick={vm.onEditClick}>
|
||||
<Text size="sm" weight="regular">
|
||||
{_t("right_panel|add_topic")}
|
||||
@@ -103,7 +103,7 @@ const RoomTopic: React.FC<Pick<IProps, "room">> = ({ room }): JSX.Element | null
|
||||
mx_RoomSummaryCard_topic_collapsed: !vm.expanded,
|
||||
})}
|
||||
>
|
||||
<Box flex="1" className="mx_RoomSummaryCard_topic_container">
|
||||
<Box flex="1" className="mx_RoomSummaryCard_topic_container mx_RoomSummaryCard_topic_box">
|
||||
<Text size="sm" weight="regular" onClick={vm.onTopicLinkClick}>
|
||||
{content}
|
||||
</Text>
|
||||
@@ -169,8 +169,8 @@ const RoomSummaryCardView: React.FC<IProps> = ({
|
||||
|
||||
<Flex as="section" justify="center" gap="var(--cpd-space-2x)" className="mx_RoomSummaryCard_badges">
|
||||
{!vm.isDirectMessage && vm.roomJoinRule === JoinRule.Public && (
|
||||
<Badge kind="grey">
|
||||
<PublicIcon width="1em" />
|
||||
<Badge kind="blue">
|
||||
<PublicIcon width="1em" color="var(--cpd-color-icon-info-primary)" />
|
||||
{_t("common|public_room")}
|
||||
</Badge>
|
||||
)}
|
||||
@@ -183,8 +183,8 @@ const RoomSummaryCardView: React.FC<IProps> = ({
|
||||
)}
|
||||
|
||||
{!vm.isRoomEncrypted && (
|
||||
<Badge kind="grey">
|
||||
<LockOffIcon width="1em" />
|
||||
<Badge kind="blue">
|
||||
<LockOffIcon width="1em" color="var(--cpd-color-icon-info-primary)" />
|
||||
{_t("common|unencrypted")}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -13,7 +13,7 @@ import VideoCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/vi
|
||||
import EmailIcon from "@vector-im/compound-design-tokens/assets/web/icons/email-solid";
|
||||
import { UnreadCounter, Unread } from "@vector-im/compound-web";
|
||||
|
||||
import { Flex } from "../../utils/Flex";
|
||||
import { Flex } from "../../../shared-components/utils/Flex";
|
||||
import { type RoomNotificationState } from "../../../stores/notifications/RoomNotificationState";
|
||||
import { useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
|
||||
|
||||
@@ -25,8 +25,8 @@ import { RightPanelPhases } from "../../../../stores/right-panel/RightPanelStore
|
||||
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext.tsx";
|
||||
import { useRoomMemberCount, useRoomMembers } from "../../../../hooks/useRoomMembers.ts";
|
||||
import { _t } from "../../../../languageHandler.tsx";
|
||||
import { Flex } from "../../../utils/Flex.tsx";
|
||||
import { Box } from "../../../utils/Box.tsx";
|
||||
import { Flex } from "../../../../shared-components/utils/Flex";
|
||||
import { Box } from "../../../../shared-components/utils/Box";
|
||||
import { getPlatformCallTypeProps, useRoomCall } from "../../../../hooks/room/useRoomCall.tsx";
|
||||
import { useRoomThreadNotifications } from "../../../../hooks/room/useRoomThreadNotifications.ts";
|
||||
import { useGlobalNotificationState } from "../../../../hooks/useGlobalNotificationState.ts";
|
||||
@@ -286,7 +286,8 @@ export default function RoomHeader({
|
||||
<PublicIcon
|
||||
width="16px"
|
||||
height="16px"
|
||||
className="mx_RoomHeader_icon text-secondary"
|
||||
className="mx_RoomHeader_icon"
|
||||
color="var(--cpd-color-icon-info-primary)"
|
||||
aria-label={_t("common|public_room")}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -21,7 +21,7 @@ import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check"
|
||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { Flex } from "../../../utils/Flex";
|
||||
import { Flex } from "../../../../shared-components/utils/Flex";
|
||||
import {
|
||||
type RoomListItemMenuViewState,
|
||||
useRoomListItemMenuViewModel,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { useRoomListItemViewModel } from "../../../viewmodels/roomlist/RoomListItemViewModel";
|
||||
import { Flex } from "../../../utils/Flex";
|
||||
import { Flex } from "../../../../shared-components/utils/Flex";
|
||||
import { RoomListItemMenuView } from "./RoomListItemMenuView";
|
||||
import { NotificationDecoration } from "../NotificationDecoration";
|
||||
import { RoomAvatarView } from "../../avatars/RoomAvatarView";
|
||||
@@ -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
|
||||
|
||||
@@ -12,7 +12,7 @@ import { UIComponent } from "../../../../settings/UIFeature";
|
||||
import { RoomListSearch } from "./RoomListSearch";
|
||||
import { RoomListHeaderView } from "./RoomListHeaderView";
|
||||
import { RoomListView } from "./RoomListView";
|
||||
import { Flex } from "../../../utils/Flex";
|
||||
import { Flex } from "../../../../shared-components/utils/Flex";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
|
||||
type RoomListPanelProps = {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ChatFilter, IconButton } from "@vector-im/compound-web";
|
||||
import ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down";
|
||||
|
||||
import type { RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel";
|
||||
import { Flex } from "../../../utils/Flex";
|
||||
import { Flex } from "../../../../shared-components/utils/Flex";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
|
||||
interface RoomListPrimaryFiltersProps {
|
||||
|
||||
@@ -20,7 +20,7 @@ import { MetaSpace } from "../../../../stores/spaces";
|
||||
import { Action } from "../../../../dispatcher/actions";
|
||||
import PosthogTrackers from "../../../../PosthogTrackers";
|
||||
import defaultDispatcher from "../../../../dispatcher/dispatcher";
|
||||
import { Flex } from "../../../utils/Flex";
|
||||
import { Flex } from "../../../../shared-components/utils/Flex";
|
||||
import { useTypedEventEmitterState } from "../../../../hooks/useEventEmitter";
|
||||
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../../../LegacyCallHandler";
|
||||
|
||||
@@ -61,7 +61,6 @@ export function RoomListSearch({ activeSpace }: RoomListSearchProps): JSX.Elemen
|
||||
</Button>
|
||||
{displayDialButton && (
|
||||
<Button
|
||||
className="mx_RoomListSearch_button"
|
||||
kind="secondary"
|
||||
size="sm"
|
||||
Icon={DialPadIcon}
|
||||
@@ -74,7 +73,6 @@ export function RoomListSearch({ activeSpace }: RoomListSearchProps): JSX.Elemen
|
||||
)}
|
||||
{displayExploreButton && (
|
||||
<Button
|
||||
className="mx_RoomListSearch_button"
|
||||
kind="secondary"
|
||||
size="sm"
|
||||
Icon={ExploreIcon}
|
||||
|
||||
@@ -19,7 +19,7 @@ import { isValid3pidInvite } from "../../../RoomInvite";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import BaseCard from "../right_panel/BaseCard";
|
||||
import { Flex } from "../../utils/Flex";
|
||||
import { Flex } from "../../../shared-components/utils/Flex";
|
||||
|
||||
interface IProps {
|
||||
event: MatrixEvent;
|
||||
|
||||
@@ -26,7 +26,7 @@ import AccessibleButton from "../elements/AccessibleButton";
|
||||
import LogoutDialog, { shouldShowLogoutDialog } from "../dialogs/LogoutDialog";
|
||||
import Modal from "../../../Modal";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import { Flex } from "../../utils/Flex";
|
||||
import { Flex } from "../../../shared-components/utils/Flex";
|
||||
|
||||
const SpinnerToast: React.FC<{ children?: ReactNode }> = ({ children }) => (
|
||||
<>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import React, { type JSX, type PropsWithChildren } from "react";
|
||||
|
||||
import { Flex } from "../../../utils/Flex";
|
||||
import { Flex } from "../../../../shared-components/utils/Flex";
|
||||
|
||||
/**
|
||||
* A component for emphasised text within an {@link EncryptionCard}
|
||||
|
||||
@@ -3376,7 +3376,6 @@
|
||||
"unable_to_decrypt": "Zprávu nelze dešifrovat"
|
||||
},
|
||||
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
||||
"download_action_decrypting": "Dešifrování",
|
||||
"download_action_downloading": "Stahování",
|
||||
"download_failed": "Stažení se nezdařilo",
|
||||
"download_failed_description": "Při stahování tohoto souboru došlo k chybě",
|
||||
|
||||
@@ -3341,7 +3341,6 @@
|
||||
"unable_to_decrypt": "Methu dadgryptio'r neges"
|
||||
},
|
||||
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
||||
"download_action_decrypting": "Dadgryptio",
|
||||
"download_action_downloading": "Yn llwytho i lawr",
|
||||
"download_failed": "Methodd y llwytho i lawr",
|
||||
"download_failed_description": "Bu gwall wrth lawrlwytho'r ffeil hon",
|
||||
|
||||
@@ -3334,7 +3334,6 @@
|
||||
"unable_to_decrypt": "Entschlüsselung der Nachricht nicht möglich"
|
||||
},
|
||||
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
||||
"download_action_decrypting": "Entschlüsseln",
|
||||
"download_action_downloading": "Herunterladen",
|
||||
"download_failed": "Herunterladen fehlgeschlagen",
|
||||
"download_failed_description": "Beim Herunterladen dieser Datei ist ein Fehler aufgetreten",
|
||||
|
||||
@@ -2688,7 +2688,6 @@
|
||||
},
|
||||
"creation_summary_dm": "Ο/η %(creator)s δημιούργησε αυτό το απευθείας μήνυμα.",
|
||||
"creation_summary_room": "Ο/η %(creator)s δημιούργησε και διαμόρφωσε το δωμάτιο.",
|
||||
"download_action_decrypting": "Αποκρυπτογράφηση",
|
||||
"download_action_downloading": "Γίνεται λήψη",
|
||||
"edits": {
|
||||
"tooltip_label": "Επεξεργάστηκε στις %(date)s. Κάντε κλικ για να δείτε τις τροποποιήσεις.",
|
||||
|
||||
@@ -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>",
|
||||
|
||||
@@ -2091,7 +2091,6 @@
|
||||
},
|
||||
"creation_summary_dm": "%(creator)s kreis ĉi tiun individuan ĉambron.",
|
||||
"creation_summary_room": "%(creator)s kreis kaj agordis la ĉambron.",
|
||||
"download_action_decrypting": "Malĉifrante",
|
||||
"edits": {
|
||||
"tooltip_label": "Redaktita je %(date)s. Klaku por vidi redaktojn.",
|
||||
"tooltip_sub": "Klaku por vidi redaktojn",
|
||||
|
||||
@@ -2672,7 +2672,6 @@
|
||||
},
|
||||
"creation_summary_dm": "%(creator)s creó este mensaje directo.",
|
||||
"creation_summary_room": "Sala creada y configurada por %(creator)s.",
|
||||
"download_action_decrypting": "Descifrando",
|
||||
"download_action_downloading": "Descargando",
|
||||
"edits": {
|
||||
"tooltip_label": "Última vez editado: %(date)s. Haz clic para ver los cambios.",
|
||||
|
||||
@@ -1450,6 +1450,7 @@
|
||||
"room_list_navigate_down": "Suundu jututubade loendis alla",
|
||||
"room_list_navigate_up": "Suundu jututubade loendis üles",
|
||||
"room_list_select_room": "Vali tubade loendist jututuba",
|
||||
"save": "Salvesta",
|
||||
"scroll_down_timeline": "Liigu ajajoonel alla",
|
||||
"scroll_up_timeline": "Liigu ajajoonel üles",
|
||||
"search": "Otsing (peab olema lubatud)",
|
||||
@@ -3370,7 +3371,6 @@
|
||||
"unable_to_decrypt": "Sõnumi dekrüptimine ei õnnestu"
|
||||
},
|
||||
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
||||
"download_action_decrypting": "Dekrüptin sisu",
|
||||
"download_action_downloading": "Laadin alla",
|
||||
"download_failed": "Allalaadimine ei õnnestunud",
|
||||
"download_failed_description": "Selle faili allalaadimisel tekkis viga",
|
||||
|
||||
@@ -708,7 +708,8 @@
|
||||
"twemoji_colr": "<colr>twemoji-colr</colr>-fontti © <author>Mozilla Foundation</author>, käytössä <terms>Apache 2.0</terms>:n ehtojen mukaisesti."
|
||||
},
|
||||
"decline_invitation_dialog": {
|
||||
"reason_description": "Kerro syy huoneen ilmoittamiseen."
|
||||
"reason_description": "Kerro syy huoneen ilmoittamiseen.",
|
||||
"title": "Kieltäydy kutsusta"
|
||||
},
|
||||
"desktop_default_device_name": "%(brand)sin työpöytäversio: %(platformName)s",
|
||||
"devtools": {
|
||||
@@ -1240,6 +1241,7 @@
|
||||
"room_list_navigate_down": "Liiku alas huoneluettelossa",
|
||||
"room_list_navigate_up": "Liiku ylös huoneluettelossa",
|
||||
"room_list_select_room": "Valitse huone huoneluettelosta",
|
||||
"save": "Tallenna",
|
||||
"scroll_down_timeline": "Vieritä alas aikajanalla",
|
||||
"scroll_up_timeline": "Vieritä ylös aikajanalla",
|
||||
"search": "Haku (pitää olla käytössä)",
|
||||
@@ -2356,6 +2358,7 @@
|
||||
"notifications": {
|
||||
"desktop_notification_message_preview": "Näytä viestin esikatselu työpöytäilmoituksessa",
|
||||
"dialog_title": "<strong>Asetukset:</strong> Ilmoitukset",
|
||||
"email_section": "Sähköpostin yhteenveto",
|
||||
"enable_audible_notifications_session": "Ota käyttöön ääni-ilmoitukset tälle istunnolle",
|
||||
"enable_desktop_notifications_session": "Ota käyttöön työpöytäilmoitukset tälle istunnolle",
|
||||
"enable_email_notifications": "Sähköposti-ilmoitukset osoitteeseen %(email)s",
|
||||
@@ -2548,6 +2551,7 @@
|
||||
"show_chat_effects": "Näytä keskustelutehosteet (animaatiot, kun saat esim. konfettia)",
|
||||
"show_displayname_changes": "Näytä näyttönimien muutokset",
|
||||
"show_join_leave": "Näytä liittymis- ja poistumisviestit (ei vaikutusta kutsuihin, poistamisiin ja porttikieltoihin)",
|
||||
"show_message_previews": "Näytä viestien esikatselut",
|
||||
"show_nsfw_content": "Näytä NSFW-sisältö",
|
||||
"show_read_receipts": "Näytä muiden käyttäjien lukukuittaukset",
|
||||
"show_redaction_placeholder": "Näytä paikanpitäjä poistetuille viesteille",
|
||||
@@ -2886,7 +2890,6 @@
|
||||
"unable_to_decrypt": "Viestin salausta ei voi purkaa"
|
||||
},
|
||||
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
||||
"download_action_decrypting": "Puretaan salausta",
|
||||
"download_action_downloading": "Ladataan",
|
||||
"download_failed": "Lataus epäonnistui",
|
||||
"download_failed_description": "Tiedostoa ladattaessa tapahtui virhe",
|
||||
|
||||
@@ -568,7 +568,7 @@
|
||||
"room_name": "Nom du salon",
|
||||
"rooms": "Salons",
|
||||
"save": "Enregistrer",
|
||||
"saved": "Sauvegardé",
|
||||
"saved": "Enregistré",
|
||||
"saving": "Enregistrement…",
|
||||
"secure_backup": "Sauvegarde sécurisée",
|
||||
"select_all": "Tout sélectionner",
|
||||
@@ -863,6 +863,7 @@
|
||||
"elementCallUrl": "Lien d'Element Call"
|
||||
},
|
||||
"settings_explorer": "Explorateur de paramètres",
|
||||
"show_empty_content_events": "Afficher les événements sans contenu",
|
||||
"show_hidden_events": "Afficher les évènements cachés dans le fil de discussion",
|
||||
"spaces": {
|
||||
"one": "<espace>",
|
||||
@@ -1450,6 +1451,7 @@
|
||||
"room_list_navigate_down": "Descendre dans la liste des salons",
|
||||
"room_list_navigate_up": "Remonter dans la liste des salons",
|
||||
"room_list_select_room": "Sélectionner un salon de la liste des salons",
|
||||
"save": "Enregistrer",
|
||||
"scroll_down_timeline": "Faire défiler le fil de discussion vers le bas",
|
||||
"scroll_up_timeline": "Faire défiler le fil de discussion vers le haut",
|
||||
"search": "Recherche (si activée)",
|
||||
@@ -2730,7 +2732,7 @@
|
||||
"set_phrase_again": "Retournez en arrière pour la redéfinir.",
|
||||
"settings_reminder": "Vous pouvez aussi configurer la sauvegarde sécurisée et gérer vos clés depuis les paramètres.",
|
||||
"title_confirm_phrase": "Confirmer la phrase de sécurité",
|
||||
"title_save_key": "Sauvegarder votre clé de récupération",
|
||||
"title_save_key": "Enregistrer votre clé de récupération",
|
||||
"title_set_phrase": "Définir une phrase de sécurité",
|
||||
"unable_to_setup": "Impossible de configurer le coffre secret",
|
||||
"use_different_passphrase": "Utiliser une phrase secrète différente ?",
|
||||
@@ -3369,7 +3371,6 @@
|
||||
"unable_to_decrypt": "Impossible de déchiffrer le message"
|
||||
},
|
||||
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
||||
"download_action_decrypting": "Déchiffrement",
|
||||
"download_action_downloading": "Téléchargement en cours",
|
||||
"download_failed": "Échec du téléchargement",
|
||||
"download_failed_description": "Une erreur s'est produite lors du téléchargement de ce fichier",
|
||||
|
||||
@@ -2435,7 +2435,6 @@
|
||||
},
|
||||
"creation_summary_dm": "%(creator)s creou esta MD.",
|
||||
"creation_summary_room": "%(creator)s creou e configurou a sala.",
|
||||
"download_action_decrypting": "Descifrando",
|
||||
"download_action_downloading": "Descargando",
|
||||
"edits": {
|
||||
"tooltip_label": "Editada o %(date)s. Preme para ver edicións.",
|
||||
|
||||
@@ -1985,7 +1985,6 @@
|
||||
},
|
||||
"creation_summary_dm": "%(creator)s יצר את DM הזה.",
|
||||
"creation_summary_room": "%(creator)s יצר/ה והגדיר/ה את החדר.",
|
||||
"download_action_decrypting": "מפענח",
|
||||
"download_action_downloading": "מוריד",
|
||||
"edits": {
|
||||
"tooltip_label": "נערך ב-%(date)s. לחץ לצפייה בעריכות.",
|
||||
|
||||
@@ -3344,7 +3344,6 @@
|
||||
"unable_to_decrypt": "Nem sikerült visszafejteni az üzenetet"
|
||||
},
|
||||
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
||||
"download_action_decrypting": "Visszafejtés",
|
||||
"download_action_downloading": "Letöltés",
|
||||
"download_failed": "Letöltés sikertelen",
|
||||
"download_failed_description": "Hiba történt a fájl letöltése közben",
|
||||
|
||||
@@ -3362,7 +3362,6 @@
|
||||
"unable_to_decrypt": "Tidak dapat mendekripsi pesan"
|
||||
},
|
||||
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
||||
"download_action_decrypting": "Mendekripsi",
|
||||
"download_action_downloading": "Mengunduh",
|
||||
"download_failed": "Pengunduhan gagal",
|
||||
"download_failed_description": "Terjadi kesalahan saat mengunduh berkas ini",
|
||||
|
||||
@@ -2360,7 +2360,6 @@
|
||||
},
|
||||
"creation_summary_dm": "%(creator)s bjó til oþessi beinu skilaboð.",
|
||||
"creation_summary_room": "%(creator)s bjó til og stillti spjallrásina.",
|
||||
"download_action_decrypting": "Afkóðun",
|
||||
"download_action_downloading": "Sæki",
|
||||
"edits": {
|
||||
"tooltip_label": "Breytt þann %(date)s. Smelltu hér til að skoða breytingar.",
|
||||
|
||||
@@ -2906,7 +2906,6 @@
|
||||
"creation_summary_dm": "%(creator)s ha creato questo MD.",
|
||||
"creation_summary_room": "%(creator)s ha creato e configurato la stanza.",
|
||||
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
||||
"download_action_decrypting": "Decifrazione",
|
||||
"download_action_downloading": "Scaricamento",
|
||||
"edits": {
|
||||
"tooltip_label": "Modificato alle %(date)s. Clicca per vedere le modifiche.",
|
||||
|
||||
@@ -2649,7 +2649,6 @@
|
||||
"creation_summary_dm": "%(creator)sがこのダイレクトメッセージを作成しました。",
|
||||
"creation_summary_room": "%(creator)sがルームを作成し設定しました。",
|
||||
"disambiguated_profile": "%(displayName)s(%(matrixId)s)",
|
||||
"download_action_decrypting": "復号化しています",
|
||||
"download_action_downloading": "ダウンロードしています",
|
||||
"edits": {
|
||||
"tooltip_label": "%(date)sに編集済。クリックすると変更履歴を表示。",
|
||||
|
||||
@@ -2262,7 +2262,6 @@
|
||||
"creation_summary_dm": "%(creator)sშექმნა ეს DM.",
|
||||
"creation_summary_room": "%(creator)sოთახის შექმნა და კონფიგურაცია.",
|
||||
"disambiguated_profile": "%(displayName)s(%(matrixId)s )",
|
||||
"download_action_decrypting": "გაშიფვრა",
|
||||
"download_action_downloading": "ჩამოტვირთვა",
|
||||
"edits": {
|
||||
"tooltip_label": "რედაქტირებულია%(date)s . დააწკაპუნეთ რედაქტირების სანახავად.",
|
||||
|
||||
@@ -2342,7 +2342,6 @@
|
||||
},
|
||||
"creation_summary_dm": "%(creator)s ສ້າງ DM ນີ້.",
|
||||
"creation_summary_room": "%(creator)s ສ້າງ ແລະ ກຳນົດຄ່າຫ້ອງ.",
|
||||
"download_action_decrypting": "ການຖອດລະຫັດ",
|
||||
"download_action_downloading": "ກຳລັງດາວໂຫຼດ",
|
||||
"edits": {
|
||||
"tooltip_label": "ແກ້ໄຂເມື່ອ %(date)s. ກົດເພື່ອເບິ່ງການແກ້ໄຂ.",
|
||||
|
||||
@@ -1827,7 +1827,6 @@
|
||||
},
|
||||
"creation_summary_dm": "%(creator)s sukūrė šį tiesioginio susirašymo kambarį.",
|
||||
"creation_summary_room": "%(creator)s sukūrė ir sukonfigūravo kambarį.",
|
||||
"download_action_decrypting": "Iššifruojama",
|
||||
"download_action_downloading": "Atsiunčiama",
|
||||
"edits": {
|
||||
"tooltip_label": "Keista %(date)s. Spustelėkite kad peržiūrėti pakeitimus.",
|
||||
|
||||
@@ -2836,7 +2836,6 @@
|
||||
"historical_event_unverified_device": "Ir jāapliecina šī ierīce, lai piekļūtu senākām ziņām."
|
||||
},
|
||||
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
||||
"download_action_decrypting": "Atšifrē",
|
||||
"download_action_downloading": "Lejupielādē",
|
||||
"edits": {
|
||||
"tooltip_label": "Labots %(date)s. Klikšķināt, lai skatītu labojumus.",
|
||||
|
||||
@@ -2889,7 +2889,6 @@
|
||||
"creation_summary_dm": "%(creator)snamorona ity DM ity.",
|
||||
"creation_summary_room": "%(creator)snamorona sy nanamboatra ny efitrano.",
|
||||
"disambiguated_profile": "%(displayName)s(%(matrixId)s )",
|
||||
"download_action_decrypting": "Decryptering",
|
||||
"download_action_downloading": "Misintona",
|
||||
"edits": {
|
||||
"tooltip_label": "Namboarina tamin'ny%(Rendez-vous . Kitiho raha hijery ny fanovana.",
|
||||
|
||||
@@ -1450,6 +1450,7 @@
|
||||
"room_list_navigate_down": "Naviger ned i romlisten",
|
||||
"room_list_navigate_up": "Naviger opp i romlisten",
|
||||
"room_list_select_room": "Velg rom fra romlisten",
|
||||
"save": "Lagre",
|
||||
"scroll_down_timeline": "Rull ned i tidslinjen",
|
||||
"scroll_up_timeline": "Rull opp i tidslinjen",
|
||||
"search": "Søk (må være aktivert)",
|
||||
@@ -3369,7 +3370,6 @@
|
||||
"unable_to_decrypt": "Kan ikke dekryptere meldingen"
|
||||
},
|
||||
"disambiguated_profile": "%(displayName)s(%(matrixId)s)",
|
||||
"download_action_decrypting": "Dekrypterer",
|
||||
"download_action_downloading": "Laster ned",
|
||||
"download_failed": "Nedlasting mislyktes",
|
||||
"download_failed_description": "Det oppstod en feil under nedlasting av denne filen",
|
||||
|
||||
@@ -2484,7 +2484,6 @@
|
||||
},
|
||||
"creation_summary_dm": "%(creator)s maakte deze directe chat.",
|
||||
"creation_summary_room": "Kamer gestart en ingesteld door %(creator)s.",
|
||||
"download_action_decrypting": "Ontsleutelen",
|
||||
"download_action_downloading": "Downloaden",
|
||||
"edits": {
|
||||
"tooltip_label": "Bewerkt op %(date)s. Klik om de bewerkingen te bekijken.",
|
||||
|
||||
@@ -159,6 +159,7 @@
|
||||
"view_message": "Wyświetl wiadomość",
|
||||
"view_source": "Wyświetl źródło",
|
||||
"yes": "Tak",
|
||||
"yes_dismiss": "Tak, odrzuć",
|
||||
"zoom_in": "Powiększ",
|
||||
"zoom_out": "Pomniejsz"
|
||||
},
|
||||
@@ -788,6 +789,7 @@
|
||||
"cross_signing_status": "Status weryfikacji krzyżowej:",
|
||||
"cross_signing_untrusted": "Twoje konto ma tożsamość weryfikacji krzyżowej w sekretnym magazynie, ale nie jest jeszcze zaufane przez tę sesję.",
|
||||
"crypto_not_available": "Moduł kryptograficzny nie jest dostępny",
|
||||
"device_id": "ID urządzenia",
|
||||
"key_backup_active_version": "Aktywna wersja kopii zapasowej:",
|
||||
"key_backup_active_version_none": "Brak",
|
||||
"key_backup_inactive_warning": "Twoje klucze nie są przywracane na tej sesji.",
|
||||
@@ -800,6 +802,8 @@
|
||||
"secret_storage_ready": "gotowe",
|
||||
"secret_storage_status": "Sekretny magazyn:",
|
||||
"self_signing_private_key_cached_status": "Samopodpisujący klucz prywatny:",
|
||||
"session": "Sesja",
|
||||
"session_fingerprint": "Odcisk palca (klucz sesji)",
|
||||
"title": "Szyfrowanie end-to-end",
|
||||
"user_signing_private_key_cached_status": "Podpisany przez użytkownika klucz prywatny:"
|
||||
},
|
||||
@@ -825,6 +829,7 @@
|
||||
"low_bandwidth_mode": "Tryb niskiej przepustowości",
|
||||
"low_bandwidth_mode_description": "Wymaga kompatybilnego serwera domowego.",
|
||||
"main_timeline": "Główna oś czasu",
|
||||
"manual_device_verification": "Ręczna weryfikacja urządzenia",
|
||||
"no_receipt_found": "Nie znaleziono potwierdzenia",
|
||||
"notification_state": "Status powiadomień <strong>%(notificationState)s</strong>",
|
||||
"notifications_debug": "Debug powiadomień",
|
||||
@@ -915,9 +920,11 @@
|
||||
"empty_room_was_name": "Pusty pokój (poprzednio %(oldName)s)",
|
||||
"encryption": {
|
||||
"access_secret_storage_dialog": {
|
||||
"alternatives": "Możesz podać również klucz lub frazę bezpieczeństwa.",
|
||||
"key_validation_text": {
|
||||
"wrong_security_key": "Wprowadzony klucz przywracania nie jest poprawny."
|
||||
},
|
||||
"privacy_warning": "Upewnij się, że nikt nie widzi tego ekranu!",
|
||||
"restoring": "Przywracanie kluczy z kopii zapasowej",
|
||||
"security_key_title": "Klucz przywracania"
|
||||
},
|
||||
@@ -971,6 +978,8 @@
|
||||
"setup_secure_backup": {
|
||||
"explainer": "Utwórz kopię zapasową kluczy przed wylogowaniem, aby ich nie utracić."
|
||||
},
|
||||
"turn_on_key_storage": "Włącz magazyn kluczy",
|
||||
"turn_on_key_storage_description": "Bezpiecznie przechowuj swoją tożsamość kryptograficzną i klucze wiadomości na serwerze. Umożliwi to przeglądanie historii wiadomości na każdym nowym urządzeniu.",
|
||||
"udd": {
|
||||
"interactive_verification_button": "Zweryfikuj interaktywnie za pomocą emoji",
|
||||
"other_ask_verify_text": "Poproś go/ją o zweryfikowanie tej sesji bądź zweryfikuj ją osobiście poniżej.",
|
||||
@@ -1005,6 +1014,21 @@
|
||||
"incoming_sas_dialog_waiting": "Oczekiwanie na potwierdzenie partnera…",
|
||||
"incoming_sas_user_dialog_text_1": "Zweryfikuj tego użytkownika, aby oznaczyć go jako zaufanego. Użytkownicy zaufani dodają większej pewności, gdy korzystasz z wiadomości szyfrowanych end-to-end.",
|
||||
"incoming_sas_user_dialog_text_2": "Weryfikacja tego użytkownika oznaczy Twoją i jego sesję jako zaufaną.",
|
||||
"manual": {
|
||||
"already_verified": "To urządzenie zostało już zweryfikowane.",
|
||||
"already_verified_and_wrong_fingerprint": "Podany odcinek palca nie pasuje, ale urządzenie zostało już zweryfikowane!",
|
||||
"device_id": "ID urządzenia",
|
||||
"failure_description": "Nie udało się zweryfikować '%(deviceId)s': %(error)s",
|
||||
"failure_title": "Weryfikacja nie powiodła się",
|
||||
"fingerprint": "Odcisk palca (klucz sesji)",
|
||||
"no_crypto": "Nie można zweryfikować urządzenia - krypto nie jest włączone",
|
||||
"no_device": "Nie można zweryfikować urządzenia - urządzenie '%(deviceId)s' nie zostało znalezione",
|
||||
"no_userid": "Nie można zweryfikować urządzenia - nie udało się znaleźć ID użytkownika",
|
||||
"success_description": "Urządzenie (%(deviceId)s) jest teraz podpisane krzyżowo",
|
||||
"success_title": "Zweryfikowano pomyślnie",
|
||||
"text": "Podaj ID i odcisk palca jednego ze swoich urządzeń, aby zweryfikować. UWAGA umożliwi to drugiemu urządzeniu wysyłanie i odbieranie wiadomości w Twoim imieniu. JEŚLI KTOŚ POWIEDZIAŁ CI, ŻEBYŚ COŚ TU WKLEIŁ, PRAWDOPODOBNIE JESTEŚ OSZUKIWANY!",
|
||||
"wrong_fingerprint": "Nie można zweryfikować urządzenia '%(deviceId)s' - podany odcisk palca '%(fingerprint)s' nie pasuje do odcisku palca na urządzeniu, '%(fprint)s'"
|
||||
},
|
||||
"no_key_or_device": "Wygląda na to, że nie masz klucza przywracania ani żadnych innych urządzeń, które mogłyby zweryfikować Twoją tożsamość. To urządzenie nie będzie mogło odczytać wcześniej zaszyfrowanych wiadomości. Aby zweryfikować swoją tożsamość na tym urządzeniu, musisz zresetować klucze weryfikacyjne.",
|
||||
"no_support_qr_emoji": "Urządzenie, które próbujesz zweryfikować nie wspiera skanowania kodu QR lub weryfikacji emoji, czyli tego co obsługuje %(brand)s. Spróbuj użyć innego klienta.",
|
||||
"other_party_cancelled": "Druga strona anulowała weryfikację.",
|
||||
@@ -1961,6 +1985,7 @@
|
||||
},
|
||||
"face_pile_tooltip_shortcut": "Włączając %(commaSeparatedMembers)s",
|
||||
"face_pile_tooltip_shortcut_joined": "Włączając Ciebie, %(commaSeparatedMembers)s",
|
||||
"failed_determine_user": "Nie można określić, którego użytkownika należy zignorować, ponieważ jego wydarzenie uległo zmianie.",
|
||||
"failed_reject_invite": "Nie udało się odrzucić zaproszenia",
|
||||
"forget_room": "Zapomnij o tym pokoju",
|
||||
"forget_space": "Zapomnij tą przestrzeń",
|
||||
@@ -2053,6 +2078,8 @@
|
||||
"read_topic": "Kliknij, aby przeczytać temat",
|
||||
"rejecting": "Odrzucanie zaproszenia…",
|
||||
"rejoin_button": "Dołącz ponownie",
|
||||
"room_content": "Zawartość pokoju",
|
||||
"room_is_low_priority": "To jest pokój o niskim priorytecie",
|
||||
"search": {
|
||||
"all_rooms_button": "Wyszukaj wszystkie pokoje",
|
||||
"placeholder": "Szukaj wiadomości...",
|
||||
@@ -2102,24 +2129,33 @@
|
||||
"add_space_label": "Dodaj przestrzeń",
|
||||
"breadcrumbs_empty": "Brak ostatnio odwiedzonych pokojów",
|
||||
"breadcrumbs_label": "Ostatnio odwiedzane pokoje",
|
||||
"collapse_filters": "Zwiń listę filtrów",
|
||||
"empty": {
|
||||
"no_chats": "Nie ma jeszcze czatów",
|
||||
"no_chats_description": "Zacznij od wysłania wiadomości lub utworzenia pokoju",
|
||||
"no_chats_description_no_room_rights": "Wyślij komuś wiadomość, aby rozpocząć.",
|
||||
"no_favourites": "Nie masz jeszcze ulubionego czatu",
|
||||
"no_favourites_description": "Dodaj czat do ulubionych w ustawieniach czatu",
|
||||
"no_invites": "Nie masz żadnych nieprzeczytanych zaproszeń",
|
||||
"no_lowpriority": "Nie masz pokoi o niskim priorytecie",
|
||||
"no_mentions": "Nie masz żadnych nieprzeczytanych wzmianek",
|
||||
"no_people": "Nie prowadzisz jeszcze z nikim czatów prywatnych",
|
||||
"no_people_description": "Wyczyść filtry, aby zobaczyć pozostałe czaty",
|
||||
"no_rooms": "Nie jesteś jeszcze w żadnym pokoju",
|
||||
"no_rooms_description": "Wyczyść filtry, aby zobaczyć pozostałe czaty",
|
||||
"no_unread": "Brawo! Nie masz żadnych nieprzeczytanych wiadomości",
|
||||
"show_activity": "Wyświetl całą aktywność",
|
||||
"show_chats": "Pokaż wszystkie czaty"
|
||||
},
|
||||
"expand_filters": "Rozwiń listę filtrów",
|
||||
"failed_add_tag": "Nie można dodać tagu %(tagName)s do pokoju",
|
||||
"failed_remove_tag": "Nie udało się usunąć tagu %(tagName)s z pokoju",
|
||||
"failed_set_dm_tag": "Nie udało się ustawić tagu wiadomości prywatnych",
|
||||
"filters": {
|
||||
"favourite": "Ulubione",
|
||||
"invites": "Zaproszenia",
|
||||
"low_priority": "Niski priorytet",
|
||||
"mentions": "Wzmianki",
|
||||
"people": "Osoby",
|
||||
"rooms": "Pokoje",
|
||||
"unread": "Nieprzeczytane"
|
||||
@@ -2550,6 +2586,8 @@
|
||||
"session_key": "Klucz sesji:",
|
||||
"title": "Zaawansowane"
|
||||
},
|
||||
"confirm_key_storage_off": "Czy na pewno pozostawić wyłączony magazyn kluczy?",
|
||||
"confirm_key_storage_off_description": "Jeśli wylogujesz się ze wszystkich swoich urządzeń, utracisz historię wiadomości i będziesz musiał ponownie zweryfikować wszystkie istniejące kontakty. <a>Dowiedz się więcej</a>",
|
||||
"delete_key_storage": {
|
||||
"breadcrumb_page": "Usuń magazyn kluczy",
|
||||
"confirm": "Usuń magazyn kluczy",
|
||||
@@ -2685,6 +2723,9 @@
|
||||
"inline_url_previews_room": "Włącz domyślny podgląd URL dla uczestników w tym pokoju",
|
||||
"inline_url_previews_room_account": "Włącz podgląd URL dla tego pokoju (dotyczy tylko Ciebie)",
|
||||
"insert_trailing_colon_mentions": "Wstawiaj dwukropek po wzmiance użytkownika na początku wiadomości",
|
||||
"invite_controls": {
|
||||
"default_label": "Zezwól użytkownikom na zapraszanie Cię do pokojów"
|
||||
},
|
||||
"jump_to_bottom_on_send": "Przejdź na dół osi czasu po wysłaniu wiadomości",
|
||||
"key_backup": {
|
||||
"setup_secure_backup": {
|
||||
@@ -2746,6 +2787,7 @@
|
||||
"show_in_private": "W pokojach prywatnych",
|
||||
"show_media": "Zawsze pokazuj"
|
||||
},
|
||||
"not_supported": "Twój serwer nie obsługuje tej funkcji.",
|
||||
"notifications": {
|
||||
"default_setting_description": "To ustawienie zastosuje się do wszystkich Twoich pokoi.",
|
||||
"default_setting_section": "Chce otrzymywać powiadomienia (Domyślne ustawienie)",
|
||||
@@ -2803,6 +2845,7 @@
|
||||
"voip": "Połączenia audio i wideo"
|
||||
},
|
||||
"preferences": {
|
||||
"Electron.enableContentProtection": "Zapobiegaj przechwytywaniu zawartości okna przez inne aplikacje",
|
||||
"Electron.enableHardwareAcceleration": "Włącz akcelerację sprzętową (uruchom ponownie %(appName)s, aby zastosować zmiany).",
|
||||
"always_show_menu_bar": "Zawsze pokazuj pasek menu okna",
|
||||
"autocomplete_delay": "Opóźnienie autouzupełniania (ms)",
|
||||
@@ -2811,6 +2854,7 @@
|
||||
"composer_heading": "Kompozytor",
|
||||
"default_timezone": "Ustawienie przeglądarki (%(timezone)s)",
|
||||
"dialog_title": "<strong>Ustawienia:</strong> Preferencje",
|
||||
"enable_content_protection": "Włącz ochronę treści",
|
||||
"enable_hardware_acceleration": "Włącz przyspieszenie sprzętowe",
|
||||
"enable_tray_icon": "Pokaż ikonę w zasobniku systemowym i zminimalizuj okno do niej zamiast zamknięcia",
|
||||
"keyboard_heading": "Skróty klawiszowe",
|
||||
@@ -2974,6 +3018,7 @@
|
||||
"show_chat_effects": "Pokaż efekty czatu (animacje po odebraniu np. confetti)",
|
||||
"show_displayname_changes": "Pokaż zmiany wyświetlanej nazwy",
|
||||
"show_join_leave": "Pokaż wiadomości dołączenia/opuszczenia pokoju (nie dotyczy wiadomości zaproszenia/wyrzucenia/banów)",
|
||||
"show_message_previews": "Pokaż podglądy wiadomości",
|
||||
"show_nsfw_content": "Pokaż zawartość NSFW",
|
||||
"show_read_receipts": "Pokaż potwierdzenia odczytania wysyłane przez innych użytkowników",
|
||||
"show_redaction_placeholder": "Pokaż symbol zastępczy dla usuniętych wiadomości",
|
||||
@@ -3080,6 +3125,8 @@
|
||||
"jumptodate": "Przeskocz do podanej daty w linii czasu",
|
||||
"jumptodate_invalid_input": "Nie jesteśmy w stanie rozpoznać podanej daty (%(inputDate)s). Spróbuj użyć formatu RRRR-MM-DD.",
|
||||
"lenny": "Dodaje ( ͡° ͜ʖ ͡°) na początku wiadomości tekstowej",
|
||||
"manual_device_verification_confirm_description": "Umożliwi to drugiemu urządzeniu wysyłanie i odbieranie wiadomości w Twoim imieniu. JEŚLI KTOŚ POWIEDZIAŁ CI, ŻEBYŚ COŚ TU WKLEIŁ, PRAWDOPODOBNIE JESTEŚ OSZUKIWANY! Czy na pewno chcesz zweryfikować drugie urządzenie?",
|
||||
"manual_device_verification_confirm_title": "Uwaga: ręczna weryfikacja urządzenia",
|
||||
"me": "Wyświetla akcję",
|
||||
"msg": "Wysyła wiadomość do wybranego użytkownika",
|
||||
"myavatar": "Zdjęcie profilowe zostanie zmienione we wszystkich pokojach",
|
||||
@@ -3120,7 +3167,7 @@
|
||||
"upgraderoom": "Ulepsza pokój do nowej wersji",
|
||||
"upgraderoom_permission_error": "Nie posiadasz wymaganych uprawnień do użycia tego polecenia.",
|
||||
"usage": "Użycie",
|
||||
"verify": "Weryfikuje użytkownika, sesję oraz klucz publiczny",
|
||||
"verify": "Zweryfikuj ręcznie jedno ze swoich urządzeń",
|
||||
"view": "Przegląda pokój z podanym adresem",
|
||||
"whois": "Pokazuje informacje na temat użytkownika"
|
||||
},
|
||||
@@ -3340,7 +3387,6 @@
|
||||
"unable_to_decrypt": "Nie można rozszyfrować wiadomości"
|
||||
},
|
||||
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
||||
"download_action_decrypting": "Rozszyfrowuję",
|
||||
"download_action_downloading": "Pobieranie",
|
||||
"download_failed": "Błąd pobierania",
|
||||
"download_failed_description": "Wystąpił błąd podczas pobierania tego pliku",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -3366,7 +3366,6 @@
|
||||
"unable_to_decrypt": "Не удалось расшифровать сообщение"
|
||||
},
|
||||
"disambiguated_profile": "%(displayName)s (%(matrixId)s)",
|
||||
"download_action_decrypting": "Расшифровка",
|
||||
"download_action_downloading": "Загрузка",
|
||||
"download_failed": "Загрузка не удалась",
|
||||
"download_failed_description": "Произошла ошибка при загрузке этого файла",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -3375,7 +3375,6 @@
|
||||
"unable_to_decrypt": "Не вдалося розшифрувати повідомлення"
|
||||
},
|
||||
"disambiguated_profile": "%(displayName)s(%(matrixId)s)",
|
||||
"download_action_decrypting": "Розшифрування",
|
||||
"download_action_downloading": "Завантаження",
|
||||
"download_failed": "Не вдалося завантажити",
|
||||
"download_failed_description": "Під час завантаження цього файлу сталася помилка",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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。点击以查看编辑历史。",
|
||||
|
||||
@@ -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。點擊以檢視編輯。",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
8
src/shared-components/utils/Box/index.ts
Normal 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";
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
8
src/shared-components/utils/Flex/index.ts
Normal 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";
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||