From eb2e61e9cfa8d54760dddc82b7e8f98a58d5df8a Mon Sep 17 00:00:00 2001 From: Kerry Date: Tue, 9 Aug 2022 10:14:30 +0200 Subject: [PATCH 01/64] Device manager: generic settings subsection component (PSG-636) (#9147) * add feature_new_device_manager labs flag * add generic settings tab container * settingstab section styles * add session manager tab to user settings * add sessions tab case to UserSettingDialog test * fussy import ordering * remove posthog tracking * i18n * add generic settings subsection component --- res/css/_components.pcss | 1 + .../settings/shared/_SettingsSubsection.pcss | 36 +++++++++ res/css/views/settings/tabs/_SettingsTab.pcss | 2 +- .../settings/shared/SettingsSubsection.tsx | 37 +++++++++ .../settings/tabs/user/SessionManagerTab.tsx | 9 ++- src/i18n/strings/en_EN.json | 1 + .../shared/SettingsSubsection-test.tsx | 45 +++++++++++ .../SettingsSubsection-test.tsx.snap | 81 +++++++++++++++++++ 8 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 res/css/components/views/settings/shared/_SettingsSubsection.pcss create mode 100644 src/components/views/settings/shared/SettingsSubsection.tsx create mode 100644 test/components/views/settings/shared/SettingsSubsection-test.tsx create mode 100644 test/components/views/settings/shared/__snapshots__/SettingsSubsection-test.tsx.snap diff --git a/res/css/_components.pcss b/res/css/_components.pcss index fe23a1c3882..d4d7ecc3163 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -28,6 +28,7 @@ @import "./components/views/messages/_MBeaconBody.pcss"; @import "./components/views/messages/shared/_MediaProcessingError.pcss"; @import "./components/views/settings/devices/_DeviceTile.pcss"; +@import "./components/views/settings/shared/_SettingsSubsection.pcss"; @import "./components/views/spaces/_QuickThemeSwitcher.pcss"; @import "./structures/_AutoHideScrollbar.pcss"; @import "./structures/_BackdropPanel.pcss"; diff --git a/res/css/components/views/settings/shared/_SettingsSubsection.pcss b/res/css/components/views/settings/shared/_SettingsSubsection.pcss new file mode 100644 index 00000000000..9eb51696bab --- /dev/null +++ b/res/css/components/views/settings/shared/_SettingsSubsection.pcss @@ -0,0 +1,36 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_SettingsSubsection { + width: 100%; + box-sizing: border-box; +} + +.mx_SettingsSubsection_heading { + padding-bottom: $spacing-8; +} + +.mx_SettingsSubsection_description { + width: 100%; + box-sizing: inherit; + line-height: $font-24px; + margin-bottom: $spacing-32; + color: $secondary-content; +} + +.mx_SettingsSubsection_content { + width: 100%; +} diff --git a/res/css/views/settings/tabs/_SettingsTab.pcss b/res/css/views/settings/tabs/_SettingsTab.pcss index 8b4d17d8e97..544b5c623b2 100644 --- a/res/css/views/settings/tabs/_SettingsTab.pcss +++ b/res/css/views/settings/tabs/_SettingsTab.pcss @@ -103,5 +103,5 @@ limitations under the License. grid-template-columns: 1fr; grid-gap: $spacing-32; - padding: 0 $spacing-16; + padding: $spacing-16 0; } diff --git a/src/components/views/settings/shared/SettingsSubsection.tsx b/src/components/views/settings/shared/SettingsSubsection.tsx new file mode 100644 index 00000000000..5dcdc9dad6f --- /dev/null +++ b/src/components/views/settings/shared/SettingsSubsection.tsx @@ -0,0 +1,37 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; + +import Heading from "../../typography/Heading"; + +export interface SettingsSubsectionProps { + heading: string; + description?: string | React.ReactNode; + children?: React.ReactNode; +} + +const SettingsSubsection: React.FC = ({ heading, description, children }) => ( +
+ { heading } + { !!description &&
{ description }
} +
+ { children } +
+
+); + +export default SettingsSubsection; diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index afa663392ba..17c09aeb7a3 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -17,10 +17,17 @@ limitations under the License. import React from 'react'; import { _t } from "../../../../../languageHandler"; +import SettingsSubsection from '../../shared/SettingsSubsection'; import SettingsTab from '../SettingsTab'; const SessionManagerTab: React.FC = () => { - return ; + return + + ; }; export default SessionManagerTab; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9716fc98139..e601003ecb4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1563,6 +1563,7 @@ "Where you're signed in": "Where you're signed in", "Manage your signed-in devices below. A device's name is visible to people you communicate with.": "Manage your signed-in devices below. A device's name is visible to people you communicate with.", "Sessions": "Sessions", + "Current session": "Current session", "Sidebar": "Sidebar", "Spaces to show": "Spaces to show", "Spaces are ways to group rooms and people. Alongside the spaces you're in, you can use some pre-built ones too.": "Spaces are ways to group rooms and people. Alongside the spaces you're in, you can use some pre-built ones too.", diff --git a/test/components/views/settings/shared/SettingsSubsection-test.tsx b/test/components/views/settings/shared/SettingsSubsection-test.tsx new file mode 100644 index 00000000000..acc2e6db957 --- /dev/null +++ b/test/components/views/settings/shared/SettingsSubsection-test.tsx @@ -0,0 +1,45 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import { render } from '@testing-library/react'; + +import SettingsSubsection from '../../../../../src/components/views/settings/shared/SettingsSubsection'; + +describe('', () => { + const defaultProps = { + heading: 'Test', + children:
test settings content
, + }; + const getComponent = (props = {}): React.ReactElement => + (); + + it('renders without description', () => { + const { container } = render(getComponent()); + expect(container).toMatchSnapshot(); + }); + + it('renders with plain text description', () => { + const { container } = render(getComponent({ description: 'This describes the subsection' })); + expect(container).toMatchSnapshot(); + }); + + it('renders with react element description', () => { + const description =

This describes the section link

; + const { container } = render(getComponent({ description })); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/test/components/views/settings/shared/__snapshots__/SettingsSubsection-test.tsx.snap b/test/components/views/settings/shared/__snapshots__/SettingsSubsection-test.tsx.snap new file mode 100644 index 00000000000..10309d2f676 --- /dev/null +++ b/test/components/views/settings/shared/__snapshots__/SettingsSubsection-test.tsx.snap @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders with plain text description 1`] = ` +
+
+

+ Test +

+
+ This describes the subsection +
+
+
+ test settings content +
+
+
+
+`; + +exports[` renders with react element description 1`] = ` +
+
+

+ Test +

+
+

+ This describes the section + + link + +

+
+
+
+ test settings content +
+
+
+
+`; + +exports[` renders without description 1`] = ` +
+
+

+ Test +

+
+
+ test settings content +
+
+
+
+`; From 5fbeb20df864721e2acbb7bff56ff234abf9e3fb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 9 Aug 2022 12:55:49 +0100 Subject: [PATCH 02/64] Some small tidying up (#9149) * Remove stale comment * Fix typing * Install katex type definitions --- package.json | 1 + src/components/structures/MatrixChat.tsx | 2 +- src/linkify-matrix.ts | 4 ++-- yarn.lock | 5 +++++ 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 1973a2f5b58..933ccfd7ada 100644 --- a/package.json +++ b/package.json @@ -150,6 +150,7 @@ "@types/flux": "^3.1.9", "@types/fs-extra": "^9.0.13", "@types/jest": "^26.0.20", + "@types/katex": "^0.14.0", "@types/lodash": "^4.14.168", "@types/modernizr": "^3.5.3", "@types/node": "^14.14.22", diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 41be62e1e69..8c173a36301 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -157,7 +157,7 @@ interface IScreen { params?: QueryDict; } -interface IProps { // TODO type things better +interface IProps { config: IConfigOptions; serverConfig?: ValidatedServerConfig; onNewScreen: (screen: string, replaceLast: boolean) => void; diff --git a/src/linkify-matrix.ts b/src/linkify-matrix.ts index b626756f7c6..896784cb454 100644 --- a/src/linkify-matrix.ts +++ b/src/linkify-matrix.ts @@ -130,8 +130,8 @@ function onAliasClick(event: MouseEvent, roomAlias: string) { }); } -const escapeRegExp = function(string): string { - return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +const escapeRegExp = function(s: string): string { + return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }; // Recognise URLs from both our local and official Element deployments. diff --git a/yarn.lock b/yarn.lock index de8172c7287..f2f623b7edf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2094,6 +2094,11 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/katex@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.14.0.tgz#b84c0afc3218069a5ad64fe2a95321881021b5fe" + integrity sha512-+2FW2CcT0K3P+JMR8YG846bmDwplKUTsWgT2ENwdQ1UdVfRk3GQrh6Mi4sTopy30gI8Uau5CEqHTDZ6YvWIUPA== + "@types/lodash@^4.14.168": version "4.14.182" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2" From 5a9c2e530a9c0ff59742aef0ad3a571c4944336d Mon Sep 17 00:00:00 2001 From: Kerry Date: Tue, 9 Aug 2022 15:07:25 +0200 Subject: [PATCH 03/64] Device manager - selectable device tile wrapper (PSG-637) (#9153) * add selectabledevicetile wrapper * set pointer cursor * line up own device icon with new checkboxes --- res/css/_components.pcss | 1 + .../devices/_SelectableDeviceTile.pcss | 28 ++++++ res/css/views/settings/_DevicesPanel.pcss | 6 +- .../views/elements/StyledCheckbox.tsx | 8 +- .../views/settings/DevicesPanelEntry.tsx | 26 +++--- .../views/settings/devices/DeviceTile.tsx | 9 +- .../settings/devices/SelectableDeviceTile.tsx | 42 +++++++++ .../LabelledCheckbox-test.tsx.snap | 2 - .../devices/SelectableDeviceTile-test.tsx | 85 +++++++++++++++++++ .../SelectableDeviceTile-test.tsx.snap | 71 ++++++++++++++++ 10 files changed, 255 insertions(+), 23 deletions(-) create mode 100644 res/css/components/views/settings/devices/_SelectableDeviceTile.pcss create mode 100644 src/components/views/settings/devices/SelectableDeviceTile.tsx create mode 100644 test/components/views/settings/devices/SelectableDeviceTile-test.tsx create mode 100644 test/components/views/settings/devices/__snapshots__/SelectableDeviceTile-test.tsx.snap diff --git a/res/css/_components.pcss b/res/css/_components.pcss index d4d7ecc3163..d6445f01435 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -28,6 +28,7 @@ @import "./components/views/messages/_MBeaconBody.pcss"; @import "./components/views/messages/shared/_MediaProcessingError.pcss"; @import "./components/views/settings/devices/_DeviceTile.pcss"; +@import "./components/views/settings/devices/_SelectableDeviceTile.pcss"; @import "./components/views/settings/shared/_SettingsSubsection.pcss"; @import "./components/views/spaces/_QuickThemeSwitcher.pcss"; @import "./structures/_AutoHideScrollbar.pcss"; diff --git a/res/css/components/views/settings/devices/_SelectableDeviceTile.pcss b/res/css/components/views/settings/devices/_SelectableDeviceTile.pcss new file mode 100644 index 00000000000..5d6a497e02c --- /dev/null +++ b/res/css/components/views/settings/devices/_SelectableDeviceTile.pcss @@ -0,0 +1,28 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_SelectableDeviceTile { + display: flex; + flex-direction: row; + align-items: center; + width: 100%; + cursor: pointer; +} + +.mx_SelectableDeviceTile_checkbox { + flex: 0 0; + margin-right: $spacing-16; +} diff --git a/res/css/views/settings/_DevicesPanel.pcss b/res/css/views/settings/_DevicesPanel.pcss index 9cbdb6a2a1b..8581225cee8 100644 --- a/res/css/views/settings/_DevicesPanel.pcss +++ b/res/css/views/settings/_DevicesPanel.pcss @@ -56,10 +56,12 @@ limitations under the License. align-items: flex-start; margin-block: 10px; min-height: 35px; + padding: 0 $spacing-8; } -.mx_DevicesPanel_icon, .mx_DevicesPanel_checkbox { - margin-left: 9px; +.mx_DevicesPanel_icon { + margin-left: 0px; + margin-right: $spacing-16; margin-top: 2px; } diff --git a/src/components/views/elements/StyledCheckbox.tsx b/src/components/views/elements/StyledCheckbox.tsx index 333fbb8adbc..35422366822 100644 --- a/src/components/views/elements/StyledCheckbox.tsx +++ b/src/components/views/elements/StyledCheckbox.tsx @@ -70,9 +70,11 @@ export default class StyledCheckbox extends React.PureComponent
-
- { this.props.children } -
+ { !!this.props.children && +
+ { this.props.children } +
+ } ; } diff --git a/src/components/views/settings/DevicesPanelEntry.tsx b/src/components/views/settings/DevicesPanelEntry.tsx index 5a5330fd3ee..b0301214b9b 100644 --- a/src/components/views/settings/DevicesPanelEntry.tsx +++ b/src/components/views/settings/DevicesPanelEntry.tsx @@ -20,7 +20,6 @@ import { logger } from "matrix-js-sdk/src/logger"; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; -import StyledCheckbox, { CheckboxStyle } from '../elements/StyledCheckbox'; import AccessibleButton from "../elements/AccessibleButton"; import Field from "../elements/Field"; import Modal from "../../../Modal"; @@ -28,6 +27,7 @@ import SetupEncryptionDialog from '../dialogs/security/SetupEncryptionDialog'; import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog'; import LogoutDialog from '../dialogs/LogoutDialog'; import DeviceTile from './devices/DeviceTile'; +import SelectableDeviceTile from './devices/SelectableDeviceTile'; interface IProps { device: IMyDevice; @@ -133,14 +133,6 @@ export default class DevicesPanelEntry extends React.Component { ; } - const left = this.props.isOwnDevice ? -
- -
: -
- -
; - const buttons = this.state.renaming ?
{ ; - return ( -
- { left } + if (this.props.isOwnDevice) { + return
+
+ +
{ buttons } +
; + } + + return ( +
+ + { buttons } +
); } diff --git a/src/components/views/settings/devices/DeviceTile.tsx b/src/components/views/settings/devices/DeviceTile.tsx index 03d952fbb1e..33f9fc40a85 100644 --- a/src/components/views/settings/devices/DeviceTile.tsx +++ b/src/components/views/settings/devices/DeviceTile.tsx @@ -23,15 +23,16 @@ import TooltipTarget from "../../elements/TooltipTarget"; import { Alignment } from "../../elements/Tooltip"; import Heading from "../../typography/Heading"; -interface Props { +export interface DeviceTileProps { device: IMyDevice; children?: React.ReactNode; + onClick?: () => void; } const DeviceTileName: React.FC<{ device: IMyDevice }> = ({ device }) => { if (device.display_name) { return @@ -59,7 +60,7 @@ const DeviceMetadata: React.FC<{ value: string, id: string }> = ({ value, id }) value ? { value } : null ); -const DeviceTile: React.FC = ({ device, children }) => { +const DeviceTile: React.FC = ({ device, children, onClick }) => { const lastActivity = device.last_seen_ts && `${_t('Last activity')} ${formatLastActivity(device.last_seen_ts)}`; const metadata = [ { id: 'lastActivity', value: lastActivity }, @@ -67,7 +68,7 @@ const DeviceTile: React.FC = ({ device, children }) => { ]; return
-
+
{ metadata.map(({ id, value }, index) => diff --git a/src/components/views/settings/devices/SelectableDeviceTile.tsx b/src/components/views/settings/devices/SelectableDeviceTile.tsx new file mode 100644 index 00000000000..e232e5ff50a --- /dev/null +++ b/src/components/views/settings/devices/SelectableDeviceTile.tsx @@ -0,0 +1,42 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; + +import StyledCheckbox, { CheckboxStyle } from '../../elements/StyledCheckbox'; +import DeviceTile, { DeviceTileProps } from './DeviceTile'; + +interface Props extends DeviceTileProps { + isSelected: boolean; + onClick: () => void; +} + +const SelectableDeviceTile: React.FC = ({ children, device, isSelected, onClick }) => { + return
+ + + { children } + +
; +}; + +export default SelectableDeviceTile; diff --git a/test/components/views/elements/__snapshots__/LabelledCheckbox-test.tsx.snap b/test/components/views/elements/__snapshots__/LabelledCheckbox-test.tsx.snap index 34cdbe59be9..286c69a8d02 100644 --- a/test/components/views/elements/__snapshots__/LabelledCheckbox-test.tsx.snap +++ b/test/components/views/elements/__snapshots__/LabelledCheckbox-test.tsx.snap @@ -34,7 +34,6 @@ exports[` should render with byline of "this is a byline" 1` className="mx_Checkbox_checkmark" />
-
@@ -90,7 +89,6 @@ exports[` should render with byline of null 1`] = ` className="mx_Checkbox_checkmark" />
-
diff --git a/test/components/views/settings/devices/SelectableDeviceTile-test.tsx b/test/components/views/settings/devices/SelectableDeviceTile-test.tsx new file mode 100644 index 00000000000..77dad3e1383 --- /dev/null +++ b/test/components/views/settings/devices/SelectableDeviceTile-test.tsx @@ -0,0 +1,85 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { fireEvent, render } from '@testing-library/react'; +import React from 'react'; +import { act } from 'react-dom/test-utils'; + +import SelectableDeviceTile from '../../../../../src/components/views/settings/devices/SelectableDeviceTile'; + +describe('', () => { + const device = { + display_name: 'My Device', + device_id: 'my-device', + last_seen_ip: '123.456.789', + }; + const defaultProps = { + onClick: jest.fn(), + device, + children:
test
, + isSelected: false, + }; + const getComponent = (props = {}) => + (); + + it('renders unselected device tile with checkbox', () => { + const { container } = render(getComponent()); + expect(container).toMatchSnapshot(); + }); + + it('renders selected tile', () => { + const { container } = render(getComponent({ isSelected: true })); + expect(container.querySelector(`#device-tile-checkbox-${device.device_id}`)).toMatchSnapshot(); + }); + + it('calls onClick on checkbox click', () => { + const onClick = jest.fn(); + const { container } = render(getComponent({ onClick })); + + act(() => { + fireEvent.click(container.querySelector(`#device-tile-checkbox-${device.device_id}`)); + }); + + expect(onClick).toHaveBeenCalled(); + }); + + it('calls onClick on device tile info click', () => { + const onClick = jest.fn(); + const { getByText } = render(getComponent({ onClick })); + + act(() => { + fireEvent.click(getByText(device.display_name)); + }); + + expect(onClick).toHaveBeenCalled(); + }); + + it('does not call onClick when clicking device tiles actions', () => { + const onClick = jest.fn(); + const onDeviceActionClick = jest.fn(); + const children = ; + const { getByTestId } = render(getComponent({ onClick, children })); + + act(() => { + fireEvent.click(getByTestId('device-action-button')); + }); + + // action click handler called + expect(onDeviceActionClick).toHaveBeenCalled(); + // main click handler not called + expect(onClick).not.toHaveBeenCalled(); + }); +}); diff --git a/test/components/views/settings/devices/__snapshots__/SelectableDeviceTile-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/SelectableDeviceTile-test.tsx.snap new file mode 100644 index 00000000000..09b81870a1f --- /dev/null +++ b/test/components/views/settings/devices/__snapshots__/SelectableDeviceTile-test.tsx.snap @@ -0,0 +1,71 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders selected tile 1`] = ` + +`; + +exports[` renders unselected device tile with checkbox 1`] = ` +
+
+ + +
+`; From 147ec49ff57e9f9c7736798e43e865527daf0f30 Mon Sep 17 00:00:00 2001 From: kegsay Date: Tue, 9 Aug 2022 15:22:32 +0100 Subject: [PATCH 04/64] cypress: log stdout/stderr when docker exec fails (#9154) Otherwise you cannot debug anything with errors like: ``` > Command failed: docker exec 134c9a0afd7dadd0b82ce69b4d72d3d6d8ca1b211540d4390a88357b68fa03b9 pg_isready -U postgres ``` Now we include the stdout/err prior to logging this. --- cypress/plugins/docker/index.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/cypress/plugins/docker/index.ts b/cypress/plugins/docker/index.ts index 2f3c6464083..98f8c2584db 100644 --- a/cypress/plugins/docker/index.ts +++ b/cypress/plugins/docker/index.ts @@ -61,9 +61,14 @@ export function dockerExec(args: { childProcess.execFile("docker", [ "exec", args.containerId, ...args.params, - ], { encoding: 'utf8' }, err => { - if (err) reject(err); - else resolve(); + ], { encoding: 'utf8' }, (err, stdout, stderr) => { + if (err) { + console.log(stdout); + console.log(stderr); + reject(err); + return; + } + resolve(); }); }); } From 48ae16b5a55091ee71645fc2def6dc179e28af79 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 9 Aug 2022 15:37:13 +0100 Subject: [PATCH 05/64] Fix pillification sometimes doubling up (#9152) * Fix pillification sometimes doubling up * Remove redundant assignment * Add unit tests around pillification * Kill ts-ignore --- src/utils/pillify.tsx | 4 +- test/utils/pillify-test.tsx | 93 +++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 test/utils/pillify-test.tsx diff --git a/src/utils/pillify.tsx b/src/utils/pillify.tsx index 87b95007e32..b7a1b4e5583 100644 --- a/src/utils/pillify.tsx +++ b/src/utils/pillify.tsx @@ -44,8 +44,8 @@ export function pillifyLinks(nodes: ArrayLike, mxEvent: MatrixEvent, pi while (node) { let pillified = false; - if (node.tagName === "PRE" || node.tagName === "CODE") { - // Skip code blocks + if (node.tagName === "PRE" || node.tagName === "CODE" || pills.includes(node)) { + // Skip code blocks and existing pills node = node.nextSibling as Element; continue; } else if (node.tagName === "A" && node.getAttribute("href")) { diff --git a/test/utils/pillify-test.tsx b/test/utils/pillify-test.tsx new file mode 100644 index 00000000000..1ceff1cb848 --- /dev/null +++ b/test/utils/pillify-test.tsx @@ -0,0 +1,93 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import { render } from "@testing-library/react"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { ConditionKind, EventType, PushRuleActionName, Room, TweakName } from "matrix-js-sdk/src/matrix"; + +import { pillifyLinks } from "../../src/utils/pillify"; +import { stubClient } from "../test-utils"; +import { MatrixClientPeg } from "../../src/MatrixClientPeg"; +import DMRoomMap from "../../src/utils/DMRoomMap"; + +describe("pillify", () => { + const roomId = "!room:id"; + const event = new MatrixEvent({ + room_id: roomId, + type: EventType.RoomMessage, + content: { + body: "@room", + }, + }); + + beforeEach(() => { + stubClient(); + const cli = MatrixClientPeg.get(); + (cli.getRoom as jest.Mock).mockReturnValue(new Room(roomId, cli, cli.getUserId())); + cli.pushRules.global = { + override: [ + { + rule_id: ".m.rule.roomnotif", + default: true, + enabled: true, + conditions: [{ + kind: ConditionKind.EventMatch, + key: "content.body", + pattern: "@room", + }], + actions: [ + PushRuleActionName.Notify, + { + set_tweak: TweakName.Highlight, + value: true, + }, + ], + }, + ], + }; + + DMRoomMap.makeShared(); + }); + + it("should do nothing for empty element", () => { + const { container } = render(
); + const originalHtml = container.outerHTML; + const containers: Element[] = []; + pillifyLinks([container], event, containers); + expect(containers).toHaveLength(0); + expect(container.outerHTML).toEqual(originalHtml); + }); + + it("should pillify @room", () => { + const { container } = render(
@room
); + const containers: Element[] = []; + pillifyLinks([container], event, containers); + expect(containers).toHaveLength(1); + expect(container.querySelector(".mx_Pill.mx_AtRoomPill").textContent).toBe("!@room"); + }); + + it("should not double up pillification on repeated calls", () => { + const { container } = render(
@room
); + const containers: Element[] = []; + pillifyLinks([container], event, containers); + pillifyLinks([container], event, containers); + pillifyLinks([container], event, containers); + pillifyLinks([container], event, containers); + expect(containers).toHaveLength(1); + expect(container.querySelector(".mx_Pill.mx_AtRoomPill").textContent).toBe("!@room"); + }); +}); From e63072e21fa7001aa9d8ba2162ce7c51967b791c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 9 Aug 2022 15:37:55 +0100 Subject: [PATCH 06/64] Fixes around URL tooltips and in-app matrix.to link handling (#9139) * Add regression test for tooltipify exposing raw HTML * Handle m.to links involving children better * Comments * Fix mistaken assertion --- .../pills-click-in-app.spec.ts | 5 +++-- src/components/views/messages/TextualBody.tsx | 16 ++++++++++----- src/utils/tooltipify.tsx | 20 ++++++++----------- test/utils/tooltipify-test.tsx | 15 ++++++++++++++ 4 files changed, 37 insertions(+), 19 deletions(-) diff --git a/cypress/e2e/regression-tests/pills-click-in-app.spec.ts b/cypress/e2e/regression-tests/pills-click-in-app.spec.ts index 33c86cbf3ac..cebbc86ed8b 100644 --- a/cypress/e2e/regression-tests/pills-click-in-app.spec.ts +++ b/cypress/e2e/regression-tests/pills-click-in-app.spec.ts @@ -59,8 +59,9 @@ describe("Pills", () => { // find the pill in the timeline and click it cy.get(".mx_EventTile_body .mx_Pill").click(); + const localUrl = `/#/room/#${targetLocalpart}:`; // verify we landed at a sane place - cy.url().should("contain", `/#/room/#${targetLocalpart}:`); + cy.url().should("contain", localUrl); cy.wait(250); // let the room list settle @@ -69,7 +70,7 @@ describe("Pills", () => { cy.get(".mx_EventTile_body .mx_Pill .mx_Pill_linkText") .should("have.css", "pointer-events", "none") .click({ force: true }); // force is to ensure we bypass pointer-events - cy.url().should("contain", `https://matrix.to/#/#${targetLocalpart}:`); + cy.url().should("contain", localUrl); }); }); }); diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 459f3845580..23ba901acdf 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -432,11 +432,17 @@ export default class TextualBody extends React.Component { * to start with (e.g. pills, links in the content). */ private onBodyLinkClick = (e: MouseEvent): void => { - const target = e.target as Element; - if (target.nodeName !== "A" || target.classList.contains(linkifyOpts.className)) return; - const { href } = target as HTMLLinkElement; - const localHref = tryTransformPermalinkToLocalHref(href); - if (localHref !== href) { + let target = e.target as HTMLLinkElement; + // links processed by linkifyjs have their own handler so don't handle those here + if (target.classList.contains(linkifyOpts.className)) return; + if (target.nodeName !== "A") { + // Jump to parent as the `` may contain children, e.g. an anchor wrapping an inline code section + target = target.closest("a"); + } + if (!target) return; + + const localHref = tryTransformPermalinkToLocalHref(target.href); + if (localHref !== target.href) { // it could be converted to a localHref -> therefore handle locally e.preventDefault(); window.location.hash = localHref; diff --git a/src/utils/tooltipify.tsx b/src/utils/tooltipify.tsx index 3f7042e1575..afdcf29609b 100644 --- a/src/utils/tooltipify.tsx +++ b/src/utils/tooltipify.tsx @@ -39,9 +39,7 @@ export function tooltipifyLinks(rootNodes: ArrayLike, ignoredNodes: Ele let node = rootNodes[0]; while (node) { - let tooltipified = false; - - if (ignoredNodes.indexOf(node) >= 0) { + if (ignoredNodes.includes(node) || containers.includes(node)) { node = node.nextSibling as Element; continue; } @@ -49,20 +47,18 @@ export function tooltipifyLinks(rootNodes: ArrayLike, ignoredNodes: Ele if (node.tagName === "A" && node.getAttribute("href") && node.getAttribute("href") !== node.textContent.trim() ) { - const container = document.createElement("span"); const href = node.getAttribute("href"); + // The node's innerHTML was already sanitized before being rendered in the first place, here we are just + // wrapping the link with the LinkWithTooltip component, keeping the same children. Ideally we'd do this + // without the superfluous span but this is not something React trivially supports at this time. const tooltip = - { node.innerHTML } + ; - ReactDOM.render(tooltip, container); - node.replaceChildren(container); - containers.push(container); - tooltipified = true; - } - - if (node.childNodes?.length && !tooltipified) { + ReactDOM.render(tooltip, node); + containers.push(node); + } else if (node.childNodes?.length) { tooltipifyLinks(node.childNodes as NodeListOf, ignoredNodes, containers); } diff --git a/test/utils/tooltipify-test.tsx b/test/utils/tooltipify-test.tsx index 8e3784e7fdf..1cad2a0ea25 100644 --- a/test/utils/tooltipify-test.tsx +++ b/test/utils/tooltipify-test.tsx @@ -57,4 +57,19 @@ describe('tooltipify', () => { expect(containers).toHaveLength(0); expect(root.outerHTML).toEqual(originalHtml); }); + + it("does not re-wrap if called multiple times", () => { + const component = mount(); + const root = component.getDOMNode(); + const containers: Element[] = []; + tooltipifyLinks([root], [], containers); + tooltipifyLinks([root], [], containers); + tooltipifyLinks([root], [], containers); + tooltipifyLinks([root], [], containers); + expect(containers).toHaveLength(1); + const anchor = root.querySelector("a"); + expect(anchor?.getAttribute("href")).toEqual("/foo"); + const tooltip = anchor.querySelector(".mx_TextWithTooltip_target"); + expect(tooltip).toBeDefined(); + }); }); From 736d8dfec7300b5d77a94faaf8d42e33f8bcb087 Mon Sep 17 00:00:00 2001 From: Element Translate Bot Date: Tue, 9 Aug 2022 17:43:13 +0200 Subject: [PATCH 07/64] Translations update from Weblate (#9159) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (German) Currently translated at 97.4% (3346 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (German) Currently translated at 97.4% (3347 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (German) Currently translated at 97.4% (3347 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (German) Currently translated at 97.4% (3348 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (German) Currently translated at 97.4% (3348 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (German) Currently translated at 97.4% (3349 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (German) Currently translated at 97.4% (3349 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (German) Currently translated at 98.0% (3367 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (German) Currently translated at 98.0% (3367 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (German) Currently translated at 98.1% (3371 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (German) Currently translated at 98.1% (3371 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (German) Currently translated at 98.1% (3372 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (German) Currently translated at 98.1% (3372 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (German) Currently translated at 98.1% (3373 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (German) Currently translated at 98.1% (3373 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (German) Currently translated at 98.3% (3377 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (German) Currently translated at 98.3% (3377 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (Spanish) Currently translated at 100.0% (3435 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/es/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (3435 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3435 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Italian) Currently translated at 100.0% (3435 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/it/ * Translated using Weblate (Slovak) Currently translated at 100.0% (3435 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sk/ * Translated using Weblate (Galician) Currently translated at 100.0% (3435 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/gl/ * Translated using Weblate (French) Currently translated at 100.0% (3435 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/ * Translated using Weblate (Dutch) Currently translated at 100.0% (3435 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/nl/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3435 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Korean) Currently translated at 35.5% (1220 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ko/ * Translated using Weblate (Korean) Currently translated at 35.5% (1220 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ko/ * Translated using Weblate (Czech) Currently translated at 100.0% (3435 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (3435 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/id/ * Translated using Weblate (Estonian) Currently translated at 99.9% (3432 of 3435 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3436 of 3436 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Czech) Currently translated at 100.0% (3436 of 3436 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ * Translated using Weblate (Slovak) Currently translated at 100.0% (3436 of 3436 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sk/ * Translated using Weblate (Estonian) Currently translated at 99.9% (3433 of 3436 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3436 of 3436 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Persian) Currently translated at 72.7% (2499 of 3436 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fa/ * Translated using Weblate (Persian) Currently translated at 72.7% (2499 of 3436 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fa/ * Translated using Weblate (French) Currently translated at 100.0% (3436 of 3436 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (3436 of 3436 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/hu/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (3436 of 3436 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ * Translated using Weblate (Galician) Currently translated at 100.0% (3436 of 3436 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/gl/ * Translated using Weblate (French) Currently translated at 100.0% (3436 of 3436 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/ * Translated using Weblate (Chinese (Simplified)) Currently translated at 88.3% (3036 of 3436 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hans/ * Translated using Weblate (Italian) Currently translated at 100.0% (3436 of 3436 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/it/ * Translated using Weblate (Swedish) Currently translated at 99.4% (3416 of 3436 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sv/ * Translated using Weblate (Vietnamese) Currently translated at 87.7% (3015 of 3437 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/vi/ * Translated using Weblate (Estonian) Currently translated at 99.9% (3434 of 3437 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ * Translated using Weblate (Vietnamese) Currently translated at 89.3% (3072 of 3437 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/vi/ * Translated using Weblate (Czech) Currently translated at 100.0% (3437 of 3437 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (3438 of 3438 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/hu/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 80.2% (2758 of 3438 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/pt_BR/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (3438 of 3438 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3438 of 3438 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Italian) Currently translated at 100.0% (3438 of 3438 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/it/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (3438 of 3438 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/id/ * Translated using Weblate (Czech) Currently translated at 100.0% (3438 of 3438 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ * Translated using Weblate (Slovak) Currently translated at 100.0% (3438 of 3438 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sk/ * Translated using Weblate (Estonian) Currently translated at 99.9% (3435 of 3438 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ * Translated using Weblate (French) Currently translated at 100.0% (3439 of 3439 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (3439 of 3439 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3439 of 3439 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Italian) Currently translated at 100.0% (3439 of 3439 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/it/ * Translated using Weblate (Czech) Currently translated at 100.0% (3439 of 3439 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ * Translated using Weblate (Slovak) Currently translated at 100.0% (3439 of 3439 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sk/ * Translated using Weblate (Galician) Currently translated at 100.0% (3439 of 3439 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/gl/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (3439 of 3439 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/id/ * Translated using Weblate (Chinese (Simplified)) Currently translated at 89.3% (3072 of 3439 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hans/ * Translated using Weblate (Chinese (Simplified)) Currently translated at 89.3% (3072 of 3439 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hans/ * Translated using Weblate (Chinese (Simplified)) Currently translated at 89.4% (3076 of 3439 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hans/ * Translated using Weblate (Swedish) Currently translated at 100.0% (3439 of 3439 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sv/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (3439 of 3439 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ * Translated using Weblate (Chinese (Simplified)) Currently translated at 92.2% (3172 of 3439 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hans/ * Translated using Weblate (Chinese (Simplified)) Currently translated at 92.5% (3183 of 3439 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hans/ * Translated using Weblate (Chinese (Simplified)) Currently translated at 95.0% (3268 of 3439 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hans/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (3439 of 3439 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ * Translated using Weblate (Dutch) Currently translated at 100.0% (3439 of 3439 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/nl/ * Translated using Weblate (Chinese (Simplified)) Currently translated at 96.5% (3322 of 3439 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hans/ * Translated using Weblate (Polish) Currently translated at 61.1% (2103 of 3439 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/pl/ * Translated using Weblate (Spanish) Currently translated at 100.0% (3439 of 3439 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/es/ * Translated using Weblate (Chinese (Simplified)) Currently translated at 96.5% (3323 of 3440 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hans/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (3440 of 3440 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3440 of 3440 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (3440 of 3440 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/id/ * Translated using Weblate (Czech) Currently translated at 100.0% (3440 of 3440 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ * Translated using Weblate (Slovak) Currently translated at 100.0% (3440 of 3440 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sk/ * Translated using Weblate (Vietnamese) Currently translated at 89.7% (3086 of 3440 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/vi/ * Translated using Weblate (Estonian) Currently translated at 99.9% (3437 of 3440 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ * Translated using Weblate (French) Currently translated at 100.0% (3440 of 3440 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/ * Translated using Weblate (Albanian) Currently translated at 99.6% (3427 of 3440 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sq/ * Translated using Weblate (French) Currently translated at 99.9% (3469 of 3470 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/ * Translated using Weblate (Russian) Currently translated at 96.9% (3365 of 3470 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ru/ * Translated using Weblate (Chinese (Simplified)) Currently translated at 95.9% (3331 of 3470 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hans/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (3470 of 3470 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3470 of 3470 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Czech) Currently translated at 100.0% (3470 of 3470 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ * Translated using Weblate (Slovak) Currently translated at 100.0% (3470 of 3470 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sk/ * Translated using Weblate (Icelandic) Currently translated at 87.7% (3044 of 3470 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/is/ * Translated using Weblate (Estonian) Currently translated at 99.9% (3467 of 3470 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ * Translated using Weblate (Russian) Currently translated at 97.0% (3367 of 3471 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ru/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3471 of 3471 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Italian) Currently translated at 100.0% (3471 of 3471 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/it/ * Translated using Weblate (Czech) Currently translated at 100.0% (3471 of 3471 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ * Translated using Weblate (Russian) Currently translated at 97.0% (3371 of 3473 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ru/ * Translated using Weblate (Chinese (Simplified)) Currently translated at 95.9% (3332 of 3473 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hans/ * Translated using Weblate (Russian) Currently translated at 97.3% (3380 of 3473 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ru/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (3473 of 3473 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3473 of 3473 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Italian) Currently translated at 100.0% (3473 of 3473 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/it/ * Translated using Weblate (Galician) Currently translated at 100.0% (3473 of 3473 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/gl/ * Translated using Weblate (French) Currently translated at 99.9% (3472 of 3473 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/ * Translated using Weblate (German) Currently translated at 96.9% (3376 of 3482 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (French) Currently translated at 99.9% (3481 of 3482 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/ * Translated using Weblate (Hungarian) Currently translated at 98.6% (3436 of 3482 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/hu/ * Translated using Weblate (Russian) Currently translated at 97.0% (3380 of 3482 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ru/ * Translated using Weblate (Chinese (Simplified)) Currently translated at 95.8% (3339 of 3482 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hans/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (3482 of 3482 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (3482 of 3482 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3482 of 3482 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Italian) Currently translated at 100.0% (3482 of 3482 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/it/ * Translated using Weblate (Estonian) Currently translated at 99.9% (3479 of 3482 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ * Translated using Weblate (Chinese (Simplified)) Currently translated at 96.4% (3358 of 3483 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hans/ * Translated using Weblate (French) Currently translated at 99.9% (3482 of 3483 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/ * Translated using Weblate (Dutch) Currently translated at 100.0% (3483 of 3483 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/nl/ * Translated using Weblate (Russian) Currently translated at 97.0% (3380 of 3483 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ru/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (3483 of 3483 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3483 of 3483 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Korean) Currently translated at 35.1% (1223 of 3483 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ko/ * Translated using Weblate (Galician) Currently translated at 100.0% (3483 of 3483 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/gl/ * Translated using Weblate (Korean) Currently translated at 38.0% (1325 of 3483 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ko/ * Translated using Weblate (Italian) Currently translated at 100.0% (3483 of 3483 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/it/ * Translated using Weblate (French) Currently translated at 100.0% (3486 of 3486 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/ * Translated using Weblate (French) Currently translated at 100.0% (3486 of 3486 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/ * Translated using Weblate (Russian) Currently translated at 96.9% (3380 of 3486 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ru/ * Translated using Weblate (Chinese (Simplified)) Currently translated at 96.3% (3360 of 3486 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hans/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (3486 of 3486 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3486 of 3486 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (3486 of 3486 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/id/ * Translated using Weblate (Slovak) Currently translated at 100.0% (3486 of 3486 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sk/ * Translated using Weblate (Galician) Currently translated at 100.0% (3486 of 3486 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/gl/ * Translated using Weblate (Estonian) Currently translated at 99.8% (3482 of 3486 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ * Translated using Weblate (Slovak) Currently translated at 100.0% (3485 of 3485 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sk/ * Translated using Weblate (German) Currently translated at 96.8% (3374 of 3485 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (German) Currently translated at 96.8% (3375 of 3485 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (German) Currently translated at 96.8% (3375 of 3485 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3485 of 3485 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Czech) Currently translated at 99.5% (3470 of 3485 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ * Translated using Weblate (German) Currently translated at 96.9% (3380 of 3485 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (German) Currently translated at 97.0% (3382 of 3485 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3487 of 3487 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (3487 of 3487 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/id/ * Translated using Weblate (Slovak) Currently translated at 100.0% (3487 of 3487 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sk/ * Translated using Weblate (Estonian) Currently translated at 99.8% (3483 of 3487 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (3487 of 3487 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ * Translated using Weblate (German) Currently translated at 97.0% (3385 of 3488 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (French) Currently translated at 100.0% (3488 of 3488 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/ * Translated using Weblate (Hungarian) Currently translated at 99.3% (3467 of 3488 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/hu/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3488 of 3488 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (3488 of 3488 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/id/ * Translated using Weblate (Estonian) Currently translated at 99.8% (3484 of 3488 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ * Translated using Weblate (German) Currently translated at 97.2% (3393 of 3488 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (German) Currently translated at 97.2% (3393 of 3488 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (3488 of 3488 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/hu/ * Translated using Weblate (Italian) Currently translated at 100.0% (3488 of 3488 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/it/ Co-authored-by: libexus Co-authored-by: jejo86 Co-authored-by: iaiz Co-authored-by: Jeff Huang Co-authored-by: Ihor Hordiichuk Co-authored-by: random Co-authored-by: Jozef Gaal Co-authored-by: Xose M Co-authored-by: Weblate Co-authored-by: Glandos Co-authored-by: Johan Smits Co-authored-by: revblue Co-authored-by: Ryo Co-authored-by: waclaw66 Co-authored-by: Linerly Co-authored-by: Priit Jรตerรผรผt Co-authored-by: nafi3h Co-authored-by: Hivaa Co-authored-by: Szimszon Co-authored-by: c-cal Co-authored-by: Percy Co-authored-by: LinAGKar Co-authored-by: Dinh Quang Tuyen Co-authored-by: lvre <7uu3qrbvm@relay.firefox.com> Co-authored-by: c1bebff3 Co-authored-by: phardyle Co-authored-by: phardyle Co-authored-by: krzmaciek Co-authored-by: Besnik Bleta Co-authored-by: Nui Harime Co-authored-by: Trendyne Co-authored-by: JokerGermany Co-authored-by: notramo Co-authored-by: Figurant16 Co-authored-by: Vri Co-authored-by: Michael Weimann --- src/i18n/strings/cs.json | 4 +- src/i18n/strings/de_DE.json | 29 ++++++-- src/i18n/strings/et.json | 22 +++++- src/i18n/strings/fr.json | 23 ++++++- src/i18n/strings/gl.json | 16 ++++- src/i18n/strings/hu.json | 84 ++++++++++++++++++----- src/i18n/strings/id.json | 53 ++++++++++++++- src/i18n/strings/it.json | 20 +++++- src/i18n/strings/ko.json | 123 +++++++++++++++++++++++++++++++--- src/i18n/strings/nl.json | 47 ++++++++++++- src/i18n/strings/ru.json | 66 +++++++++--------- src/i18n/strings/sk.json | 22 +++++- src/i18n/strings/uk.json | 20 +++++- src/i18n/strings/zh_Hans.json | 71 ++++++++++++++------ src/i18n/strings/zh_Hant.json | 23 ++++++- 15 files changed, 527 insertions(+), 96 deletions(-) diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index ef39579c83a..e068de9d5c1 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -3478,5 +3478,7 @@ "Itโ€™s what youโ€™re here for, so lets get to it": "Kvลฏli tomu jste tady, tak se do toho pusลฅte", "Find and invite your friends": "Najdฤ›te a pozvฤ›te svรฉ pล™รกtele", "You made it!": "Zvlรกdli jste to!", - "Help": "Nรกpovฤ›da" + "Help": "Nรกpovฤ›da", + "iOS": "iOS", + "Android": "Android" } diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index cf72b7ed8d6..4cb248cb20f 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -412,7 +412,7 @@ "Key request sent.": "Schlรผsselanfrage gesendet.", "Submit debug logs": "Fehlerbericht abschicken", "Code": "Code", - "Opens the Developer Tools dialog": "Entwickler-Werkzeuge รถffnen", + "Opens the Developer Tools dialog": "ร–ffnet die Entwicklerwerkzeuge", "You don't currently have any stickerpacks enabled": "Keine Stickerpakete aktiviert", "Stickerpack": "Stickerpaket", "Fetching third party location failed": "Das Abrufen des Drittanbieterstandorts ist fehlgeschlagen", @@ -812,7 +812,7 @@ "The user must be unbanned before they can be invited.": "Verbannte Nutzer kรถnnen nicht eingeladen werden.", "Show read receipts sent by other users": "Lesebestรคtigungen anzeigen", "Scissors": "Schere", - "Upgrade to your own domain": "Upgrade zu deiner eigenen Domain", + "Upgrade to your own domain": "Zu deiner eigenen Domain aufwerten", "Accept all %(invitedRooms)s invites": "Akzeptiere alle %(invitedRooms)s Einladungen", "Change room avatar": "Raumbild รคndern", "Change room name": "Raumname รคndern", @@ -918,7 +918,7 @@ "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Diese Handlung erfordert es, auf den Standardidentitรคtsserver zuzugreifen, um eine E-Mail-Adresse oder Telefonnummer zu validieren, aber der Server hat keine Nutzungsbedingungen.", "Only continue if you trust the owner of the server.": "Fahre nur fort, wenn du den Betreibern des Servers vertraust.", "Trust": "Vertrauen", - "Custom (%(level)s)": "Selbstdefiniert (%(level)s)", + "Custom (%(level)s)": "Benutzerdefiniert (%(level)s)", "Sends a message as plain text, without interpreting it as markdown": "Verschickt eine Nachricht in Rohtext, ohne sie als Markdown darzustellen", "Use an identity server to invite by email. Manage in Settings.": "Mit einem Identitรคtsserver kannst du รผber E-Mail Einladungen zu verschicken. Verwalte ihn in den Einstellungen.", "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", @@ -1855,7 +1855,7 @@ "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Nachrichten hier sind Ende-zu-Ende-verschlรผsselt. Verifiziere %(displayName)s im deren Profil - klicke auf deren Avatar.", "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Nachrichten in diesem Raum sind Ende-zu-Ende-verschlรผsselt. Wenn Personen beitreten, kannst du sie in ihrem Profil verifizieren, indem du auf deren Avatar klickst.", "Comment": "Kommentar", - "Please view existing bugs on Github first. No match? Start a new one.": "Bitte wirf einen Blick auf existierende Programmfehler auf Github. Keinen gefunden? Erstelle einen neuen.", + "Please view existing bugs on Github first. No match? Start a new one.": "Bitte wirf einen Blick auf existierende Programmfehler auf Github. Keinen passenden gefunden? Erstelle einen neuen.", "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.": "PRO TIPP: Wenn du einen Programmfehler meldest, fรผge bitte Debug-Logs hinzu um uns zu helfen das Problem zu finden.", "Invite by email": "Via Email einladen", "Start a conversation with someone using their name, email address or username (like ).": "Beginne eine Konversation mit jemanden unter Benutzung des Namens, der Email-Adresse oder der Matrix-ID (wie ).", @@ -3390,5 +3390,24 @@ "Failed to set direct message tag": "Fehler beim Setzen der Nachrichtenmarkierung", "Resent!": "Verschickt!", "Did not receive it? Resend it": "Nicht angekommen? Erneut senden", - "Unread email icon": "Ungelesene E-Mail Symbol" + "Unread email icon": "Ungelesene E-Mail Symbol", + "Video rooms are always-on VoIP channels embedded within a room in %(brand)s.": "Videorรคume sind dauerhaft aktive VoIP-Kanรคle, die in einem Raum in %(brand)s eingebettet sind.", + "A new way to chat over voice and video in %(brand)s.": "Eine neue Art in %(brand)s รผber Audio und Video zu kommunizieren.", + "Coworkers and teams": "Kollegen und Gruppen", + "Friends and family": "Freunde und Familie", + "iOS": "iOS", + "Android": "Android", + "You can't disable this later. The room will be encrypted but the embedded call will not.": "Dies kann spรคter nicht deaktiviert werden. Der Raum wird verschlรผsselt sein, nicht aber der eingebettete Anruf.", + "You need to have the right permissions in order to share locations in this room.": "Du brauchst du richtigen Berechtigungen, um deinen Live-Standort in diesem Raum zu teilen.", + "Who will you chat to the most?": "Mit wem wirst du am meisten chatten?", + "We're creating a room with %(names)s": "Wir erstellen einen Raum mit %(names)s", + "Messages in this chat will be end-to-end encrypted.": "Nachrichten in dieser Konversation werden Ende-zu-Ende verschlรผsselt.", + "Send your first message to invite to chat": "Schreibe die erste Nachricht, um zur Konversation einzuladen", + "Your server doesn't support disabling sending read receipts.": "Dein Server unterstรผtzt das deaktivieren von Lesebestรคtigungen nicht.", + "Send read receipts": "Sende Lesebestรคtigungen", + "Share your activity and status with others.": "Teile anderen deine Aktivitรคt und deinen Status mit.", + "Presence": "Anwesenheit", + "Deactivating your account is a permanent action โ€” be careful!": "Die Deaktivierung deines Kontos ist unwiderruflich - sei vorsichtig!", + "Favourite Messages (under active development)": "Favorisierte Nachrichten (in aktiver Entwicklung)", + "Use new session manager (under active development)": "Benutze neue Sitzungsverwaltung (in aktiver Entwicklung)" } diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index b96a1e1451e..f98bc3a873f 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -3474,5 +3474,25 @@ "Get stuff done by finding your teammates": "Saa tรถรถd tehtud รผheskoos oma kaasteelistega", "Find and invite your co-workers": "Leia kolleege ja saada neile kutse", "Find friends": "Leia sรตpru", - "Find and invite your friends": "Leia sรตpru ja saada neile kutse" + "Find and invite your friends": "Leia sรตpru ja saada neile kutse", + "Google Play and the Google Play logo are trademarks of Google LLC.": "Google Play ja Google Play logo on Google LLC kaubamรคrgid.", + "App Storeยฎ and the Apple logoยฎ are trademarks of Apple Inc.": "App Storeยฎ ja Apple logoยฎ on Apple Inc kaubamรคrgid.", + "Get it on F-Droid": "Laadi alla F-Droid'ist", + "Get it on Google Play": "Laadi alla Google Play'st", + "Android": "Android", + "Download on the App Store": "Laadi alla App Store'st", + "iOS": "iOS", + "Download %(brand)s Desktop": "Laadi alla %(brand)s tรถรถlaua rakendusena", + "Download %(brand)s": "Laadi alla %(brand)s", + "Weโ€™d appreciate any feedback on how youโ€™re finding Element.": "Meile meeldiks kui sa saadad meile oma arvamuse Element'i kohta.", + "How are you finding Element so far?": "Mis mulje sulle Element seni on jรคtnud?", + "Help": "Abiteave", + "Your server doesn't support disabling sending read receipts.": "Sinu koduserver ei vรตimalda lugemisteatiste keelamist.", + "Share your activity and status with others.": "Jaga teistega oma olekut ja tegevusi.", + "Presence": "Olek vรตrgus", + "Send read receipts": "Saada lugemisteatiseid", + "Last activity": "Viimased tegevused", + "Sessions": "Sessionid", + "Use new session manager (under active development)": "Uus sessioonihaldur (aktiivselt arendamisel)", + "Current session": "Praegune sessioon" } diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index c384660c359..f48c7468d26 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -3454,7 +3454,7 @@ "Only %(count)s steps to go|other": "Plus que %(count)s รฉtapes", "Welcome to %(brand)s": "Bienvenue sur %(brand)s", "Find your people": "Trouvez vos contacts", - "Keep ownership and control of community discussion.\nScale to support millions, with powerful moderation and interoperability.": "Gardez le contrรดle sur la discussion de votre communautรฉ.\nPrend en charge des millions de messages, avec une interopรฉrabilitรฉ et une modรฉration efficaces.", + "Keep ownership and control of community discussion.\nScale to support millions, with powerful moderation and interoperability.": "Gardez le contrรดle sur la discussion de votre communautรฉ.\nPrend en charge des millions de messages, avec une interopรฉrabilitรฉ et une modรฉration efficace.", "Find your co-workers": "Trouver vos collรจgues", "Secure messaging for work": "Messagerie sรฉcurisรฉe pour le travail", "Start your first chat": "Dรฉmarrer votre premiรจre conversation", @@ -3479,5 +3479,24 @@ "You made it!": "Vous avez rรฉussi !", "Help": "Aide", "Weโ€™d appreciate any feedback on how youโ€™re finding Element.": "Nous apprรฉcierions toutes vos remarques sur Element.", - "How are you finding Element so far?": "Comment trouvez-vous Element jusque-lร ย ?" + "How are you finding Element so far?": "Comment trouvez-vous Element jusque-lร ย ?", + "Google Play and the Google Play logo are trademarks of Google LLC.": "Google Play et le logo Google Play sont des marques dรฉposรฉes de Google LLC.", + "App Storeยฎ and the Apple logoยฎ are trademarks of Apple Inc.": "App Storeยฎ et le logo Appleยฎ sont des marques dรฉposรฉes de Apple Inc.", + "Get it on F-Droid": "Rรฉcupรฉrez-le sur F-Droid", + "Get it on Google Play": "Rรฉcupรฉrez-le sur Google Play", + "Android": "Android", + "Download on the App Store": "Tรฉlรฉcharger sur lโ€™App Store", + "iOS": "iOS", + "Download %(brand)s Desktop": "Tรฉlรฉcharger %(brand)s Desktop", + "Download %(brand)s": "Tรฉlรฉcharger %(brand)s", + "We're creating a room with %(names)s": "Nous crรฉons un salon avec %(names)s", + "Community ownership": "Propriรฉtรฉ de la communautรฉ", + "Your server doesn't support disabling sending read receipts.": "Votre serveur ne supporte pas la dรฉsactivation de lโ€™envoi des accusรฉs de rรฉception.", + "Share your activity and status with others.": "Partager votre activitรฉ et votre statut avec les autres.", + "Presence": "Prรฉsence", + "Send read receipts": "Envoyer les accusรฉs de rรฉception", + "Last activity": "Derniรจre activitรฉ", + "Current session": "Cette session", + "Sessions": "Sessions", + "Use new session manager (under active development)": "Utiliser un nouveau gestionnaire de session (en cours de dรฉveloppement)" } diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 08d11fe7a53..1ceacdc5735 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -3480,5 +3480,19 @@ "Find friends": "Atopar amizades", "Itโ€™s what youโ€™re here for, so lets get to it": "ร‰ a razรณn de que estรฉs aquรญ, asi que imos", "Find and invite your friends": "Atopa e convida รกs tรบas amizades", - "You made it!": "Conseguรญchelo!" + "You made it!": "Conseguรญchelo!", + "We're creating a room with %(names)s": "Estamos creando unha sala con %(names)s", + "Google Play and the Google Play logo are trademarks of Google LLC.": "Google Play e o logo de Google Play son marcas de Google LLC.", + "App Storeยฎ and the Apple logoยฎ are trademarks of Apple Inc.": "App Storeยฎ e o Apple logoยฎ son marcas de Apple Inc.", + "Get it on F-Droid": "Descargar desde F-Droid", + "Get it on Google Play": "Descargar desde Google Play", + "Android": "Android", + "Download on the App Store": "Descargar na App Store", + "iOS": "iOS", + "Download %(brand)s Desktop": "Descargar %(brand)s Desktop", + "Download %(brand)s": "Descargar %(brand)s", + "Your server doesn't support disabling sending read receipts.": "O teu servidor non ten soporte para desactivar o envรญo de resgardos de lectura.", + "Share your activity and status with others.": "Comparte a tรบa actividade e estado con outras persoas.", + "Presence": "Presenza", + "Send read receipts": "Enviar resgardos de lectura" } diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 4c7f0129f57..dec3bbc71d8 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -3418,31 +3418,85 @@ "Video rooms are always-on VoIP channels embedded within a room in %(brand)s.": "Videรณ szobรกk รกllandรณ VoIP csatornรกk a szobรกkba รกgyazva itt: %(brand)s.", "A new way to chat over voice and video in %(brand)s.": "รšj lehetล‘sรฉg hanggal รฉs videรณval csevegni itt: %(brand)s.", "Video rooms": "Videรณ szobรกk", - "Stop and close": "Megรกllรญt รฉs kilรฉp", + "Stop and close": "Befejezรฉs รฉs kilรฉpรฉs", "You can't disable this later. The room will be encrypted but the embedded call will not.": "Ezt kรฉsล‘bb nem lehet kikapcsolni. A szoba titkosรญtva lesz de a hรญvรกsok nem.", - "Online community members": "Online tagok a kรถzรถssรฉgekbล‘l", - "Coworkers and teams": "Munkatรกrsak รฉs csapatok", + "Online community members": "Online kรถzรถssรฉgek tagjai", + "Coworkers and teams": "Munkatรกrsak รฉs csoportok", "Friends and family": "Barรกtok รฉs csalรกd", "We'll help you get connected.": "Segรญtรผnk a kapcsolatteremtรฉsben.", "Who will you chat to the most?": "Kivel beszรฉlget a legtรถbbet?", - "You're in": "ร–n belรฉpett", - "You need to have the right permissions in order to share locations in this room.": "A helymegosztรกshoz ebben a szobรกban megfelelล‘ jogosultsรกgokra van szรผksรฉge.", - "You don't have permission to share locations": "Nincs jogosultsรกga a helymegosztรกshoz", - "Join the room to participate": "Belรฉpรฉs a szobรกba a rรฉszvรฉtelhez", + "You're in": "Itt vagy:", + "You need to have the right permissions in order to share locations in this room.": "A helymegosztรกshoz ebben a szobรกban megfelelล‘ jogosultsรกgokra van szรผksรฉged.", + "You don't have permission to share locations": "Nincs jogosultsรกgod a helymegosztรกshoz", + "Join the room to participate": "Csatlakozz a szobรกhoz, hogy rรฉszt vehess", "Favourite Messages (under active development)": "Kedvenc รผzenetek (aktรญv fejlesztรฉs alatt)", "Reset bearing to north": "ร‰szaki irรกnyba รกllรญtรกs", - "Mapbox logo": "Tรฉrkรฉp logรณ", + "Mapbox logo": "Mapbox logรณ", "Location not available": "Fรถldrajzi helyzet nem meghatรกrozhatรณ", "Find my location": "Jelenlegi helyzetem megkeresรฉse", "Exit fullscreen": "Kilรฉpรฉs a teljes kรฉpernyล‘bล‘l", "Enter fullscreen": "Teljes kรฉpernyล‘re vรกltรกs", "Map feedback": "Tรฉrkรฉp visszajelzรฉs", "Toggle attribution": "Tulajdonsรกgok รกtkapcsolรกsa", - "In %(spaceName)s and %(count)s other spaces.|one": "Itt: %(spaceName)s รฉs %(count)s mรกsik tรฉr.", - "In %(spaceName)s and %(count)s other spaces.|zero": "Tรฉren: %(spaceName)s.", - "In %(spaceName)s and %(count)s other spaces.|other": "Itt: %(spaceName)s รฉs %(count)s mรกsik tรฉr.", - "In spaces %(space1Name)s and %(space2Name)s.": "A tรฉren: %(space1Name)s รฉs %(space2Name)s.", - "Developer command: Discards the current outbound group session and sets up new Olm sessions": "Fejlesztล‘i parancs: Eldobja a jelenlegi kimenล‘ csoport kapcsolatot รฉs รบj Olm kapcsolatot hoz lรฉtre", - "Send your first message to invite to chat": "Az elsล‘ รผzeneteddel hรญvd meg ide ล‘t: ", - "Messages in this chat will be end-to-end encrypted.": "Az รผzenetek a beszรฉlgetรฉsben vรฉgponttรณl vรฉgpontig titkosรญtottak." + "In %(spaceName)s and %(count)s other spaces.|one": "Itt: %(spaceName)s รฉs %(count)s mรกsik tรฉrben.", + "In %(spaceName)s and %(count)s other spaces.|zero": "Ebben a tรฉrben: %(spaceName)s.", + "In %(spaceName)s and %(count)s other spaces.|other": "Itt: %(spaceName)s รฉs %(count)s mรกsik tรฉrben.", + "In spaces %(space1Name)s and %(space2Name)s.": "Ezekben a terekben: %(space1Name)s รฉs %(space2Name)s.", + "Developer command: Discards the current outbound group session and sets up new Olm sessions": "Fejlesztล‘i parancs: Eldobja a jelenlegi kimenล‘ csoport kapcsolatot รฉs รบj Olm munkamenetet hoz lรฉtre", + "Send your first message to invite to chat": "Kรผldj egy รผzenetet ahhoz, hogy meghรญvd felhasznรกlรณt", + "Messages in this chat will be end-to-end encrypted.": "Az รผzenetek ebben a beszรฉlgetรฉsben vรฉgponti titkosรญtรกssal vannak vรฉdve.", + "You did it!": "Kรฉsz!", + "Only %(count)s steps to go|one": "Mรฉg %(count)s lรฉpรฉs", + "Only %(count)s steps to go|other": "Mรฉg %(count)s lรฉpรฉs", + "Welcome to %(brand)s": "รœdvรถzlรถm itt: %(brand)s", + "Find your people": "Talรกlja meg az embereket", + "Keep ownership and control of community discussion.\nScale to support millions, with powerful moderation and interoperability.": "Tartsa meg a beszรฉlgetรฉs feletti irรกnyรญtรกst.\nMilliรณk tรกmogatรกsa erล‘s moderรกciรณs kรฉpessรฉgekkel รฉs egyรผttmลฑkรถdรฉsi lehetล‘sรฉgekkel.", + "Community ownership": "Kรถzรถssรฉg tulajdonjoga", + "Find your co-workers": "Talรกlja meg a munkatรกrsait", + "Secure messaging for work": "Biztonsรกgos รผzenetkรผldรฉs munkรกhoz", + "Start your first chat": "Az elsล‘ beszรฉlgetรฉs elkezdรฉse", + "With free end-to-end encrypted messaging, and unlimited voice and video calls, %(brand)s is a great way to stay in touch.": "Ingyenes vรฉgpontok kรถzรถtti titkosรญtott รผzenetkรผldรฉs รฉs korlรกtlan hang รฉs videรณ hรญvรกs, %(brand)s hasznรกlata jรณ lehetล‘sรฉg a kapcsolattartรกshoz.", + "Secure messaging for friends and family": "Biztonsรกgos รผzenetkรผldรฉs barรกtokkal, csalรกddal", + "Weโ€™d appreciate any feedback on how youโ€™re finding Element.": "Minden visszajelzรฉsnek รถrรผlรผnk azzal kapcsolatban, hogy milyennek talรกlja Elementet.", + "How are you finding Element so far?": "Eddig milyennek talรกlja Elementet?", + "Enable notifications": "ร‰rtesรญtรฉsek engedรฉlyezรฉse", + "Donโ€™t miss a reply or important message": "Ne maradjon le vรกlaszrรณl vagy fontos รผzenetrล‘l", + "Turn on notifications": "ร‰rtesรญtรฉsek bekapcsolรกsa", + "Your profile": "Profil", + "Make sure people know itโ€™s really you": "Biztosรญtsa a tรถbbieket arrรณl, hogy ร–n valรณjรกban ร–n", + "Set up your profile": "Profil beรกllรญtรกsa", + "Download apps": "Alkalmazรกsok letรถltรฉse", + "Donโ€™t miss a thing by taking Element with you": "Ne maradjon le semmirล‘l vigye magรกval Elementet", + "Download Element": "Element letรถltรฉse", + "Find and invite your community members": "Kรถzรถssรฉg tagjรกnak megkeresรฉse รฉs meghรญvรกsa", + "Find people": "Emberek megkeresรฉse", + "Get stuff done by finding your teammates": "Fejezzen be dolgokat csoporttรกrs megtalรกlรกsรกval", + "Find and invite your co-workers": "Munkatรกrs keresรฉse รฉs meghรญvรกsa", + "Find friends": "Barรกtok keresรฉse", + "Itโ€™s what youโ€™re here for, so lets get to it": "Kezdjรผk amiรฉrt itt van", + "Find and invite your friends": "Keresse meg รฉs hรญvja meg barรกtait", + "You made it!": "Elkรฉszรผlt!", + "Use new session manager (under active development)": "รšj munkamenet kezelล‘ hasznรกlata (aktรญv fejlesztรฉs alatt)", + "Send read receipts": "Olvasรกs visszajelzรฉs kรผldรฉse", + "We're creating a room with %(names)s": "Szobรกt kรฉszรญtรผnk: %(names)s", + "Google Play and the Google Play logo are trademarks of Google LLC.": "A Google Play รฉs a Google Play logรณ a Google LLC vรฉdjegye.", + "App Storeยฎ and the Apple logoยฎ are trademarks of Apple Inc.": "Az App Storeยฎ รฉs az Apple logoยฎ az Apple Inc. vรฉdjegyei.", + "Get it on F-Droid": "Letรถltรฉs az F-Droidrรณl", + "Get it on Google Play": "Letรถltรฉs a Google Play-bล‘l", + "Android": "Android", + "Download on the App Store": "Letรถltรฉs az App Store-bรณl", + "iOS": "iOS", + "Download %(brand)s Desktop": "Asztali %(brand)s letรถltรฉse", + "Download %(brand)s": "%(brand)s eltรถltรฉse", + "Choose a locale": "Vรกlasszon nyelvet", + "Help": "Segรญtsรฉg", + "Saved Items": "Mentett elemek", + "Last activity": "Utolsรณ tevรฉkenysรฉg", + "Current session": "Jelenlegi munkamenet", + "Sessions": "Munkamenetek", + "Your server doesn't support disabling sending read receipts.": "A matrix szervere nem tรกmogatja az olvasรกs visszajelzรฉsek elkรผldรฉsรฉnek tiltรกsรกt.", + "Share your activity and status with others.": "Ossza meg a tevรฉkenysรฉgรฉt รฉs รกllapotรกt mรกsokkal.", + "Presence": "รllapot", + "Spell check": "Helyesรญrรกs ellenล‘rzรฉs", + "Complete these to get the most out of %(brand)s": "Ezen lรฉpรฉsek befejezรฉsรฉvel hozhatod ki a legtรถbbet %(brand)s alkalmazรกsbรณl" } diff --git a/src/i18n/strings/id.json b/src/i18n/strings/id.json index 0794a0b34d7..e4761685980 100644 --- a/src/i18n/strings/id.json +++ b/src/i18n/strings/id.json @@ -3447,5 +3447,56 @@ "Favourite Messages (under active development)": "Pesan Favorit (dalam pengembangan aktif)", "Saved Items": "Item yang Tersimpan", "Choose a locale": "Pilih locale", - "Spell check": "Pemeriksa ejaan" + "Spell check": "Pemeriksa ejaan", + "Download %(brand)s": "Unduh %(brand)s", + "Complete these to get the most out of %(brand)s": "Selesaikan untuk mendapatkan hasil yang maksimal dari %(brand)s", + "Welcome to %(brand)s": "Selamat datang di %(brand)s", + "We're creating a room with %(names)s": "Kami sedang membuat sebuah ruangan dengan %(names)s", + "Google Play and the Google Play logo are trademarks of Google LLC.": "Google Play dan logo Google Play adalah merek dagang dari Google LLC.", + "App Storeยฎ and the Apple logoยฎ are trademarks of Apple Inc.": "App Storeยฎ dan logo Appleยฎ adalah merek dagang dari Apple Inc.", + "Get it on F-Droid": "Dapatkan di F-Droid", + "Get it on Google Play": "Dapatkan di Google Play", + "Android": "Android", + "Download on the App Store": "Unduh di App Store", + "iOS": "iOS", + "Download %(brand)s Desktop": "Unduh %(brand)s Desktop", + "Help": "Bantuan", + "Your server doesn't support disabling sending read receipts.": "Server Anda tidak mendukung penonaktifkan pengiriman laporan dibaca.", + "Share your activity and status with others.": "Bagikan aktivitas dan status Anda dengan orang lain.", + "Presence": "Presensi", + "You did it!": "Anda berhasil!", + "Only %(count)s steps to go|one": "Hanya %(count)s langkah lagi untuk dilalui", + "Only %(count)s steps to go|other": "Hanya %(count)s langkah lagi untuk dilalui", + "Find your people": "Temukan orang-orang Anda", + "Keep ownership and control of community discussion.\nScale to support millions, with powerful moderation and interoperability.": "Tetap miliki kemilikan dan kendali atas diskusi komunitas.\nBesar untuk mendukung jutaan anggota, dengan moderasi dan interoperabilitas berdaya.", + "Community ownership": "Kemilikan komunitas", + "Find your co-workers": "Temukan rekan kerja Anda", + "Secure messaging for work": "Perpesanan aman untuk berkerja", + "Start your first chat": "Mulai obrolan pertama Anda", + "With free end-to-end encrypted messaging, and unlimited voice and video calls, %(brand)s is a great way to stay in touch.": "Dengan perpesanan terenkripsi ujung-ke-ujung gratis, dan panggilan suara & video tidak terbatas, %(brand)s adalah cara yang baik untuk tetap terhubung.", + "Secure messaging for friends and family": "Perpesanan aman untuk teman dan keluarga", + "Weโ€™d appreciate any feedback on how youโ€™re finding Element.": "Kami akan menghargai masukan apa pun tentang bagaimana Anda menemukan Element.", + "How are you finding Element so far?": "Bagaimana Anda menemukan Element sejauh ini?", + "Enable notifications": "Nyalakan notifikasi", + "Donโ€™t miss a reply or important message": "Jangan lewatkan sebuah balasan atau pesan yang penting", + "Turn on notifications": "Nyalakan notifikasi", + "Your profile": "Profil Anda", + "Make sure people know itโ€™s really you": "Pastikan orang-orang tahu bahwa itu memang Anda", + "Set up your profile": "Siapkan profil Anda", + "Download apps": "Unduh aplikasi", + "Donโ€™t miss a thing by taking Element with you": "Jangan lewatkan apa pun dengan membawa Element dengan Anda", + "Download Element": "Unduh Element", + "Find and invite your community members": "Temukan dan undang anggota komunitas Anda", + "Find people": "Temukan orang-orang", + "Get stuff done by finding your teammates": "Selesaikan hal-hal dengan menemukan rekan setim Anda", + "Find and invite your co-workers": "Temukan dan undang rekan kerja Anda", + "Find friends": "Temukan teman-teman", + "Itโ€™s what youโ€™re here for, so lets get to it": "Untuk itulah Anda di sini, jadi mari kita lakukan", + "Find and invite your friends": "Temukan dan undang teman Anda", + "You made it!": "Anda berhasil!", + "Send read receipts": "Kirim laporan dibaca", + "Last activity": "Aktivitas terakhir", + "Sessions": "Sesi", + "Use new session manager (under active development)": "Gunakan pengelola sesi baru (dalam pengembangan aktif)", + "Current session": "Sesi saat ini" } diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 079415783d0..5ffaf3df34c 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -3480,5 +3480,23 @@ "Find and invite your friends": "Trova e invita i tuoi amici", "You made it!": "Ce l'hai fatta!", "Weโ€™d appreciate any feedback on how youโ€™re finding Element.": "Ci piacerebbe avere una tua opinione riguardo Element.", - "How are you finding Element so far?": "Come ti sta sembrando Element?" + "How are you finding Element so far?": "Come ti sta sembrando Element?", + "Google Play and the Google Play logo are trademarks of Google LLC.": "Google Play e il logo Google Play sono marchi registrati di Google LLC.", + "App Storeยฎ and the Apple logoยฎ are trademarks of Apple Inc.": "App Storeยฎ e il logo Appleยฎ sono marchi registrati di Apple Inc.", + "Get it on F-Droid": "Ottienilo su F-Droid", + "Get it on Google Play": "Ottienilo su Google Play", + "Android": "Android", + "Download on the App Store": "Scarica dall'App Store", + "iOS": "iOS", + "Download %(brand)s Desktop": "Scarica %(brand)s Desktop", + "Download %(brand)s": "Scarica %(brand)s", + "We're creating a room with %(names)s": "Stiamo creando una stanza con %(names)s", + "Last activity": "Ultima attivitร ", + "Current session": "Sessione attuale", + "Sessions": "Sessioni", + "Your server doesn't support disabling sending read receipts.": "Il tuo server non supporta la disattivazione delle conferme di lettura.", + "Share your activity and status with others.": "Condividi la tua attivitร  e lo stato con gli altri.", + "Presence": "Presenza", + "Use new session manager (under active development)": "Usa il nuovo gestore di sessioni (in sviluppo attivo)", + "Send read receipts": "Invia le conferme di lettura" } diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index 5056fb30b13..042036debb2 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -429,17 +429,17 @@ "Jump to read receipt": "์ฝ์€ ๊ธฐ๋ก์œผ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ", "Share room": "๋ฐฉ ๊ณต์œ ํ•˜๊ธฐ", "Members only (since they joined)": "๊ตฌ์„ฑ์›๋งŒ(๊ตฌ์„ฑ์›๋“ค์ด ์ฐธ์—ฌํ•œ ์‹œ์ ๋ถ€ํ„ฐ)", - "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s์ด ์ฐธ๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค", - "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)s์ด %(count)s๋ฒˆ ์ฐธ๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค", - "%(oneUser)sjoined %(count)s times|other": "%(oneUser)s๋‹˜์ด %(count)s๋ฒˆ ์ฐธ๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค", - "%(oneUser)sjoined %(count)s times|one": "%(oneUser)s๋‹˜์ด ์ฐธ๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค", + "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s์ด ์ฐธ์—ฌํ–ˆ์Šต๋‹ˆ๋‹ค", + "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)s์ด %(count)s๋ฒˆ ์ฐธ์—ฌํ–ˆ์Šต๋‹ˆ๋‹ค", + "%(oneUser)sjoined %(count)s times|other": "%(oneUser)s๋‹˜์ด %(count)s๋ฒˆ ์ฐธ์—ฌํ–ˆ์Šต๋‹ˆ๋‹ค", + "%(oneUser)sjoined %(count)s times|one": "%(oneUser)s๋‹˜์ด ์ฐธ์—ฌํ–ˆ์Šต๋‹ˆ๋‹ค", "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)s์ด %(count)s๋ฒˆ ์ฐธ๊ฐ€ํ•˜๋‹ค๊ฐ€ ๋– ๋‚ฌ์Šต๋‹ˆ๋‹ค", "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)s์ด ์ฐธ๊ฐ€ํ•˜๋‹ค๊ฐ€ ๋– ๋‚ฌ์Šต๋‹ˆ๋‹ค", "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s๋‹˜์ด %(count)s๋ฒˆ ์ฐธ๊ฐ€ํ•˜๋‹ค๊ฐ€ ๋– ๋‚ฌ์Šต๋‹ˆ๋‹ค", "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)s๋‹˜์ด ์ฐธ๊ฐ€ํ•˜๋‹ค๊ฐ€ ๋– ๋‚ฌ์Šต๋‹ˆ๋‹ค", - "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)s์ด ๋– ๋‚˜๊ณ  ๋‹ค์‹œ ์ฐธ๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค", - "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s๋‹˜์ด %(count)s๋ฒˆ ๋– ๋‚˜๊ณ  ๋‹ค์‹œ ์ฐธ๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค", - "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)s๋‹˜์ด ๋– ๋‚˜๊ณ  ๋‹ค์‹œ ์ฐธ๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค", + "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)s์ด ๋– ๋‚˜๊ณ  ๋‹ค์‹œ ์ฐธ์—ฌํ–ˆ์Šต๋‹ˆ๋‹ค", + "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s๋‹˜์ด %(count)s๋ฒˆ ๋– ๋‚˜๊ณ  ๋‹ค์‹œ ์ฐธ์—ฌํ–ˆ์Šต๋‹ˆ๋‹ค", + "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)s๋‹˜์ด ๋– ๋‚˜๊ณ  ๋‹ค์‹œ ์ฐธ์—ฌํ–ˆ์Šต๋‹ˆ๋‹ค", "%(severalUsers)sleft %(count)s times|other": "%(severalUsers)s์ด %(count)s๋ฒˆ ๋– ๋‚ฌ์Šต๋‹ˆ๋‹ค", "%(severalUsers)sleft %(count)s times|one": "%(severalUsers)s์ด ๋– ๋‚ฌ์Šต๋‹ˆ๋‹ค", "%(oneUser)sleft %(count)s times|other": "%(oneUser)s๋‹˜์ด %(count)s๋ฒˆ ๋– ๋‚ฌ์Šต๋‹ˆ๋‹ค", @@ -511,7 +511,7 @@ "This room is a continuation of another conversation.": "์ด ๋ฐฉ์€ ๋‹ค๋ฅธ ๋Œ€ํ™”๋ฐฉ์˜ ์—ฐ์žฅ์„ ์ž…๋‹ˆ๋‹ค.", "Click here to see older messages.": "์—ฌ๊ธธ ๋ˆŒ๋Ÿฌ ์˜ค๋ž˜๋œ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด์„ธ์š”.", "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", - "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)s์ด %(count)s๋ฒˆ ๋– ๋‚˜๊ณ  ๋‹ค์‹œ ์ฐธ๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค", + "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)s์ด %(count)s๋ฒˆ ๋– ๋‚˜๊ณ  ๋‹ค์‹œ ์ฐธ์—ฌํ–ˆ์Šต๋‹ˆ๋‹ค", "In reply to ": "๊ด€๋ จ ๋Œ€ํ™” ", "Updating %(brand)s": "%(brand)s ์—…๋ฐ์ดํŠธ ์ค‘", "Upgrade this room to version %(version)s": "์ด ๋ฐฉ์„ %(version)s ๋ฒ„์ „์œผ๋กœ ์—…๊ทธ๋ ˆ์ด๋“œ", @@ -1219,5 +1219,110 @@ "Pin to sidebar": "์‚ฌ์ด๋“œ๋ฐ” ๊ณ ์ •", "Developer tools": "๊ฐœ๋ฐœ์ž ๋„๊ตฌ", "All settings": "์ „์ฒด ์„ค์ •", - "Use Single Sign On to continue": "SSO๋กœ ๊ณ„์†ํ•˜๊ธฐ" + "Use Single Sign On to continue": "SSO๋กœ ๊ณ„์†ํ•˜๊ธฐ", + "Join public room": "๊ณต๊ฐœ ๋ฐฉ ์ฐธ๊ฐ€ํ•˜๊ธฐ", + "New room": "์ƒˆ๋กœ์šด ๋ฐฉ ๋งŒ๋“ค๊ธฐ", + "Start new chat": "์ƒˆ๋กœ์šด ๋Œ€ํ™” ์‹œ์ž‘ํ•˜๊ธฐ", + "Room options": "๋ฐฉ ์˜ต์…˜", + "Mentions & Keywords": "๋ฉ˜์…˜ ๋ฐ ํ‚ค์›Œ๋“œ", + "Mentions & keywords": "๋ฉ˜์…˜ ๋ฐ ํ‚ค์›Œ๋“œ", + "Use default": "๊ธฐ๋ณธ ์„ค์ • ์‚ฌ์šฉ", + "Results not as expected? Please give feedback.": "์˜ˆ์ƒํ•œ ๊ฒฐ๊ณผ๊ฐ€ ์•„๋‹Œ๊ฐ€์š”? ํ”ผ๋“œ๋ฐฑ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค.", + "Get notified only with mentions and keywords as set up in your settings": "์„ค์ •์—์„œ ์ง€์ •ํ•œ ๋ฉ˜์…˜๊ณผ ํ‚ค์›Œ๋“œ์ธ ๊ฒฝ์šฐ์—๋งŒ ์•Œ๋ฆผ์„ ๋ฐ›์Šต๋‹ˆ๋‹ค", + "Get notifications as set up in your settings": "์„ค์ •์—์„œ ์ง€์ •ํ•œ ์•Œ๋ฆผ๋งŒ ๋ฐ›์Šต๋‹ˆ๋‹ค", + "Get notified for every message": "๋ชจ๋“  ๋ฉ”์„ธ์ง€ ์•Œ๋ฆผ์„ ๋ฐ›์Šต๋‹ˆ๋‹ค", + "You won't get any notifications": "์–ด๋–ค ์•Œ๋žŒ๋„ ๋ฐ›์ง€ ์•Š์Šต๋‹ˆ๋‹ค", + "Public": "๊ณต๊ฐœ", + "Space members": "์ŠคํŽ˜์ด์Šค ๋ฉค๋ฒ„ ๋ชฉ๋ก", + "Private room (invite only)": "๋น„๊ณต๊ฐœ ๋ฐฉ (์ดˆ๋Œ€ ํ•„์š”)", + "Private (invite only)": "๋น„๊ณต๊ฐœ (์ดˆ๋Œ€ ํ•„์š”)", + "Never send encrypted messages to unverified sessions in this room from this session": "์ด ์ฑ„ํŒ…๋ฐฉ์˜ ํ˜„์žฌ ์„ธ์…˜์—์„œ ํ™•์ธ๋˜์ง€ ์•Š์€ ์„ธ์…˜์œผ๋กœ ์•”ํ˜ธํ™”๋œ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ด์ง€ ์•Š์Œ", + "Decide who can view and join %(spaceName)s.": "๋ˆ„๊ฐ€ %(spaceName)s๋ฅผ ๋ณด๊ฑฐ๋‚˜ ์ฐธ์—ฌํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.", + "Accessibility": "์ ‘๊ทผ์„ฑ", + "Access": "์ ‘๊ทผ", + "Recommended for public spaces.": "๊ณต๊ฐœ ์ŠคํŽ˜์ด์Šค์— ๊ถŒ์žฅ ํ•ฉ๋‹ˆ๋‹ค.", + "Allow people to preview your space before they join.": "์ŠคํŽ˜์ด์Šค์— ์ฐธ์—ฌํ•˜๊ธฐ ์ „์— ๋ฏธ๋ฆฌ๋ณผ ์ˆ˜ ์žˆ๋„๋ก ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.", + "Preview Space": "์ŠคํŽ˜์ด์Šค ๋ฏธ๋ฆฌ๋ณด๊ธฐ", + "Message Previews": "๋ฉ”์„ธ์ง€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ", + "Anyone can find and join.": "๋ˆ„๊ตฌ๋‚˜ ์ฐพ๊ณ  ์ฐธ์—ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", + "Anyone in can find and join. You can select other spaces too.": "์— ์†Œ์†๋œ ๋ˆ„๊ตฌ๋‚˜ ์ฐพ๊ณ  ์ฐธ์—ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ์ŠคํŽ˜์ด์Šค๋„ ์„ ํƒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.", + "Only invited people can join.": "์ดˆ๋Œ€ํ•œ ๊ฒฝ์šฐ์—๋งŒ ์ฐธ์—ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", + "Visibility": "๊ฐ€์‹œ์„ฑ", + "Explore Public Rooms": "๊ณต๊ฐœ ๋ฐฉ ์‚ดํŽด๋ณด๊ธฐ", + "Manage & explore rooms": "๊ด€๋ฆฌ ๋ฐ ๋ฐฉ ๋ชฉ๋ก ๋ณด๊ธฐ", + "Space home": "์ŠคํŽ˜์ด์Šค ํ™ˆ", + "Clear": "์ง€์šฐ๊ธฐ", + "Clear notifications": "์•Œ๋ฆผ ์ง€์šฐ๊ธฐ", + "Search for": "๊ฒ€์ƒ‰ ๊ธฐ์ค€", + "Search for rooms or people": "๋ฐฉ ๋˜๋Š” ์‚ฌ๋žŒ ๊ฒ€์ƒ‰", + "Search for rooms": "๋ฐฉ ๊ฒ€์ƒ‰", + "Search for spaces": "์ŠคํŽ˜์ด์Šค ๊ฒ€์ƒ‰", + "Recently Direct Messaged": "์ตœ๊ทผ ๋‹ค์ด๋ ‰ํŠธ ๋ฉ”์„ธ์ง€", + "No recently visited rooms": "์ตœ๊ทผ์— ๋ฐฉ๋ฌธํ•˜์ง€ ์•Š์€ ๋ฐฉ ๋ชฉ๋ก", + "Recently visited rooms": "์ตœ๊ทผ ๋ฐฉ๋ฌธํ•œ ๋ฐฉ ๋ชฉ๋ก", + "Recently viewed": "์ตœ๊ทผ์— ํ™•์ธํ•œ", + "%(senderName)s removed their display name (%(oldDisplayName)s)": "%(senderName)s๋‹˜์ด ํ‘œ์‹œ์ด๋ฆ„์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค (%(oldDisplayName)s)", + "%(senderName)s changed their profile picture": "%(senderName)s๋‹˜์ด ํ”„๋กœํ•„ ์‚ฌ์ง„์„ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค", + "%(senderName)s invited %(targetName)s": "%(senderName)s๋‹˜์ด %(targetName)s๋‹˜์„ ์ดˆ๋Œ€ํ–ˆ์Šต๋‹ˆ๋‹ค", + "%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s๋‹˜์ด ๋ฐฉ ์ด๋ฆ„์„ %(oldRoomName)s์—์„œ %(newRoomName)s(์œผ)๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.", + "%(oldDisplayName)s changed their display name to %(displayName)s": "%(oldDisplayName)s๋‹˜์ด ํ‘œ์‹œ์ด๋ฆ„์„ %(displayName)s(์œผ)๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค", + "%(targetName)s left the room": "%(targetName)s๋‹˜์ด ๋ฐฉ์„ ๋– ๋‚ฌ์Šต๋‹ˆ๋‹ค", + "%(targetName)s joined the room": "%(targetName)s๋‹˜์ด ๋ฐฉ์— ์ฐธ์—ฌํ–ˆ์Šต๋‹ˆ๋‹ค", + "%(senderName)s set a profile picture": "%(senderName)s๋‹˜์ด ํ”„๋กœํ•„ ์‚ฌ์ง„์„ ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค", + "Scroll to most recent messages": "๊ฐ€์žฅ ์ตœ๊ทผ ๋ฉ”์„ธ์ง€๋กœ ์Šคํฌ๋กค", + "Room Info": "๋ฐฉ ์ •๋ณด", + "If you can't find the room you're looking for, ask for an invite or create a new room.": "๋งŒ์•ฝ ์ฐพ๊ณ  ์žˆ๋Š” ๋ฐฉ์ด ์—†๋‹ค๋ฉด, ์ดˆ๋Œ€๋ฅผ ์š”์ฒญํ•˜๊ฑฐ๋‚˜ ์ƒˆ๋กœ์šด ๋ฐฉ์„ ๋งŒ๋“œ์„ธ์š”.", + "If you can't find the room you're looking for, ask for an invite or create a new room.": "๋งŒ์•ฝ ์ฐพ๊ณ  ์žˆ๋Š” ๋ฐฉ์ด ์—†๋‹ค๋ฉด, ์ดˆ๋Œ€๋ฅผ ์š”์ฒญํ•˜๊ฑฐ๋‚˜ ์ƒˆ๋กœ์šด ๋ฐฉ์„ ๋งŒ๋“œ์„ธ์š”.", + "Unable to copy a link to the room to the clipboard.": "๋ฐฉ ๋งํฌ๋ฅผ ํด๋ฆฝ๋ณด๋“œ์— ๋ณต์‚ฌํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", + "Unable to copy room link": "๋ฐฉ ๋งํฌ๋ฅผ ๋ณต์‚ฌํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค", + "Copy room link": "๋ฐฉ ๋งํฌ ๋ณต์‚ฌ", + "Share your public space": "๋‹น์‹ ์˜ ๊ณต๊ฐœ ์ŠคํŽ˜์ด์Šค ๊ณต์œ ํ•˜๊ธฐ", + "Your public space": "๋‹น์‹ ์˜ ๊ณต๊ฐœ ์ŠคํŽ˜์ด์Šค", + "Public space": "๊ณต๊ฐœ ์ŠคํŽ˜์ด์Šค", + "Private space (invite only)": "๋น„๊ณต๊ฐœ ์ŠคํŽ˜์ด์Šค (์ดˆ๋Œ€ ํ•„์š”)", + "Your private space": "๋‹น์‹ ์˜ ๋น„๊ณต๊ฐœ ์ŠคํŽ˜์ด์Šค", + "Private space": "๋น„๊ณต๊ฐœ ์ŠคํŽ˜์ด์Šค", + "Rooms and spaces": "๋ฐฉ ๋ฐ ์ŠคํŽ˜์ด์Šค ๋ชฉ๋ก", + "Sidebar": "์‚ฌ์ด๋“œ๋ฐ”", + "Keyboard": "ํ‚ค๋ณด๋“œ (๋‹จ์ถ•ํ‚ค)", + "Send feedback": "ํ”ผ๋“œ๋ฐฑ ๋ณด๋‚ด๊ธฐ", + "Feedback sent": "ํ”ผ๋“œ๋ฐฑ ๋ณด๋‚ด๊ธฐ", + "Feedback": "ํ”ผ๋“œ๋ฐฑ", + "Show all rooms in Home": "๋ชจ๋“  ๋ฐฉ์„ ํ™ˆ์—์„œ ๋ณด๊ธฐ", + "Leave all rooms": "๋ชจ๋“  ๋ฐฉ์—์„œ ๋– ๋‚˜๊ธฐ", + "Show all rooms": "๋ชจ๋“  ๋ฐฉ ๋ชฉ๋ก ๋ณด๊ธฐ", + "All rooms": "๋ชจ๋“  ๋ฐฉ ๋ชฉ๋ก", + "Expand": "ํŽผ์น˜๊ธฐ", + "Create a new space": "์ƒˆ๋กœ์šด ์ŠคํŽ˜์ด์Šค ๋งŒ๋“ค๊ธฐ", + "Create a space": "์ŠคํŽ˜์ด์Šค ๋งŒ๋“ค๊ธฐ", + "Export Chat": "๋Œ€ํ™” ๋‚ด๋ณด๋‚ด๊ธฐ", + "Export chat": "๋Œ€ํ™” ๋‚ด๋ณด๋‚ด๊ธฐ", + "Room settings": "๋ฐฉ ์„ค์ •", + "Hide Widgets": "์œ„์ ฏ ์ˆจ๊ธฐ๊ธฐ", + "Widgets": "์œ„์ ฏ", + "Nothing pinned, yet": "์•„์ง ๊ณ ์ •๋œ ๊ฒƒ์ด ์—†์Šต๋‹ˆ๋‹ค", + "Pinned messages": "๊ณ ์ •๋œ ๋ฉ”์„ธ์ง€", + "Pinned": "๊ณ ์ •๋จ", + "Files": "ํŒŒ์ผ ๋ชฉ๋ก", + "Poll": "ํˆฌํ‘œ", + "Send voice message": "์Œ์„ฑ ๋ฉ”์„ธ์ง€ ๋ณด๋‚ด๊ธฐ", + "Voice Message": "์Œ์„ฑ ๋ฉ”์„ธ์ง€", + "Sticker": "์Šคํ‹ฐ์ปค", + "View source": "์†Œ์Šค ๋ณด๊ธฐ", + "Report a bug": "๋ฒ„๊ทธ ๋ณด๊ณ ", + "Report": "๋ณด๊ณ ", + "Forward message": "์ „๋‹ฌ ๋ฉ”์„ธ์ง€", + "Forward": "์ „๋‹ฌ", + "about a day ago": "์•ฝ 1์ผ ์ „", + "%(num)s days ago": "%(num)s์ผ ์ „", + "about an hour ago": "์•ฝ 1 ์‹œ๊ฐ„ ์ „", + "%(num)s minutes ago": "%(num)s๋ถ„ ์ „", + "Or send invite link": "๋˜๋Š” ์ดˆ๋Œ€ ๋งํฌ ๋ณด๋‚ด๊ธฐ", + "Suggestions": "์ถ”์ฒœ ๋ชฉ๋ก", + "Recent Conversations": "์ตœ๊ทผ ๋Œ€ํ™” ๋ชฉ๋ก", + "Start a conversation with someone using their name or username (like ).": "์ด๋ฆ„์ด๋‚˜ ์‚ฌ์šฉ์ž๋ช…( ํ˜•์‹)์„ ์‚ฌ์šฉํ•˜๋Š” ์‚ฌ๋žŒ๋“ค๊ณผ ๋Œ€ํ™”๋ฅผ ์‹œ์ž‘ํ•˜์„ธ์š”.", + "Direct Messages": "๋‹ค์ด๋ ‰ํŠธ ๋ฉ”์„ธ์ง€", + "Explore public rooms": "๊ณต๊ฐœ ๋ฐฉ ๋ชฉ๋ก ์‚ดํŽด๋ณด๊ธฐ", + "Show %(count)s more|other": "%(count)s๊ฐœ ๋” ๋ณด๊ธฐ", + "People": "์‚ฌ๋žŒ๋“ค" } diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index e85cf626e2f..ac3b344ef5d 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -3445,5 +3445,50 @@ "Messages in this chat will be end-to-end encrypted.": "Berichten in deze chat worden eind-tot-eind versleuteld.", "Saved Items": "Opgeslagen items", "Send your first message to invite to chat": "Stuur uw eerste bericht om uit te nodigen om te chatten", - "Favourite Messages (under active development)": "Favoriete berichten (in actieve ontwikkeling)" + "Favourite Messages (under active development)": "Favoriete berichten (in actieve ontwikkeling)", + "We're creating a room with %(names)s": "We maken een kamer aan met %(names)s", + "Google Play and the Google Play logo are trademarks of Google LLC.": "Google Play en het Google Play-logo zijn handelsmerken van Google LLC.", + "App Storeยฎ and the Apple logoยฎ are trademarks of Apple Inc.": "App Storeยฎ en het Apple logoยฎ zijn handelsmerken van Apple Inc.", + "Get it on F-Droid": "Download het op F-Droid", + "Get it on Google Play": "Verkrijg het via Google Play", + "Android": "Android", + "Download on the App Store": "Te downloaden in de App Store", + "iOS": "iOS", + "Download %(brand)s Desktop": "%(brand)s Desktop downloaden", + "Download %(brand)s": "%(brand)s downloaden", + "Choose a locale": "Kies een landinstelling", + "Help": "Help", + "Spell check": "Spellingscontrole", + "Complete these to get the most out of %(brand)s": "Voltooi deze om het meeste uit %(brand)s te halen", + "You did it!": "Het is u gelukt!", + "Only %(count)s steps to go|one": "Nog maar %(count)s stap te gaan", + "Only %(count)s steps to go|other": "Nog maar %(count)s stappen te gaan", + "Welcome to %(brand)s": "Welkom bij %(brand)s", + "Find your people": "Vind uw mensen", + "Keep ownership and control of community discussion.\nScale to support millions, with powerful moderation and interoperability.": "Houd het eigendom en de controle over de discussie in de gemeenschap.\nSchaal om miljoenen te ondersteunen, met krachtige beheersbaarheid en interoperabiliteit.", + "Community ownership": "Gemeenschapseigendom", + "Find your co-workers": "Vind uw collega's", + "Secure messaging for work": "Veilig berichten versturen voor werk", + "Start your first chat": "Start uw eerste chat", + "With free end-to-end encrypted messaging, and unlimited voice and video calls, %(brand)s is a great way to stay in touch.": "Met gratis eind-tot-eind versleutelde berichten en onbeperkte spraak- en video-oproepen, is %(brand)s een geweldige manier om in contact te blijven.", + "Secure messaging for friends and family": "Veilig berichten versturen voor vrienden en familie", + "Weโ€™d appreciate any feedback on how youโ€™re finding Element.": "We stellen het op prijs als u feedback geeft over hoe u Element vindt.", + "How are you finding Element so far?": "Hoe vind u Element tot nu toe?", + "Enable notifications": "Meldingen inschakelen", + "Donโ€™t miss a reply or important message": "Mis geen antwoord of belangrijk bericht", + "Turn on notifications": "Meldingen aanzetten", + "Your profile": "Uw profiel", + "Make sure people know itโ€™s really you": "Zorg ervoor dat mensen weten dat u het echt bent", + "Set up your profile": "Stel uw profiel in", + "Download apps": "Apps downloaden", + "Donโ€™t miss a thing by taking Element with you": "Mis niets door Element mee te nemen", + "Download Element": "Element downloaden", + "Find and invite your community members": "Vind en nodig uw communityleden uit", + "Find people": "Zoek mensen", + "Get stuff done by finding your teammates": "Krijg dingen gedaan door uw teamgenoten te vinden", + "Find and invite your co-workers": "Vind en nodig uw collega's uit", + "Find friends": "Zoek vrienden", + "Itโ€™s what youโ€™re here for, so lets get to it": "Daar bent u voor, dus laten we beginnen", + "Find and invite your friends": "Zoek uw vrienden en nodig ze uit", + "You made it!": "Het is u gelukt!" } diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 781b22ea5a4..7c2d4ffb17b 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -328,22 +328,22 @@ "Members only (since they joined)": "ะขะพะปัŒะบะพ ัƒั‡ะฐัั‚ะฝะธะบะธ (ั ะผะพะผะตะฝั‚ะฐ ะธั… ะฒั…ะพะดะฐ)", "A text message has been sent to %(msisdn)s": "ะขะตะบัั‚ะพะฒะพะต ัะพะพะฑั‰ะตะฝะธะต ะพั‚ะฟั€ะฐะฒะปะตะฝะพ ะฝะฐ %(msisdn)s", "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", - "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)sะฒะพัˆะปะธ %(count)s ั€ะฐะท(ะฐ)", - "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)sะฒะพัˆะปะธ", - "%(oneUser)sjoined %(count)s times|other": "%(oneUser)sะฒะพัˆั‘ะป(-ะปะฐ) %(count)s ั€ะฐะท(ะฐ)", - "%(oneUser)sjoined %(count)s times|one": "%(oneUser)sะฒะพัˆั‘ะป(-ะปะฐ)", - "%(severalUsers)sleft %(count)s times|other": "%(severalUsers)sะฒั‹ัˆะปะธ %(count)s ั€ะฐะท(ะฐ)", - "%(severalUsers)sleft %(count)s times|one": "%(severalUsers)sะฒั‹ัˆะปะธ", - "%(oneUser)sleft %(count)s times|other": "%(oneUser)sะฒั‹ัˆะตะป(-ะปะฐ) %(count)s ั€ะฐะท(ะฐ)", - "%(oneUser)sleft %(count)s times|one": "%(oneUser)sะฒั‹ัˆะตะป(-ะปะฐ)", - "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)sะฒะพัˆะปะธ ะธ ะฒั‹ัˆะปะธ %(count)s ั€ะฐะท(ะฐ)", - "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)sะฒะพัˆะปะธ ะธ ะฒั‹ัˆะปะธ", - "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)sะฒะพัˆั‘ะป(-ะปะฐ) ะธ ะฒั‹ัˆะตะป(-ะปะฐ) %(count)s ั€ะฐะท(ะฐ)", - "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)sะฒะพัˆั‘ะป(-ะปะฐ) ะธ ะฒั‹ัˆะตะป(-ะปะฐ)", - "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)sะฒั‹ัˆะปะธ ะธ ัะฝะพะฒะฐ ะฒะพัˆะปะธ %(count)s ั€ะฐะท(ะฐ)", - "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)sะฒั‹ัˆะปะธ ะธ ัะฝะพะฒะฐ ะฒะพัˆะปะธ", - "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)sะฒั‹ัˆะตะป(-ะปะฐ) ะธ ัะฝะพะฒะฐ ะฒะพัˆั‘ะป(-ะปะฐ) %(count)s ั€ะฐะท(ะฐ)", - "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)sะฒั‹ัˆะตะป(-ะปะฐ) ะธ ัะฝะพะฒะฐ ะฒะพัˆั‘ะป(-ะปะฐ)", + "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)s ะฟั€ะธัะพะตะดะธะฝะธะปะธััŒ %(count)s ั€ะฐะท(ะฐ)", + "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s ะฟั€ะธัะพะตะดะธะฝะธะปะธััŒ", + "%(oneUser)sjoined %(count)s times|other": "%(oneUser)s ะฟั€ะธัะพะตะดะธะฝะธะปัั(ะปะฐััŒ) %(count)s ั€ะฐะท(ะฐ)", + "%(oneUser)sjoined %(count)s times|one": "%(oneUser)s ะฟั€ะธัะพะตะดะธะฝะธะปัั(ะปะฐััŒ)", + "%(severalUsers)sleft %(count)s times|other": "%(severalUsers)s ะฟะพะบะธะฝัƒะปะธ %(count)s ั€ะฐะท(ะฐ)", + "%(severalUsers)sleft %(count)s times|one": "%(severalUsers)s ะฟะพะบะธะฝัƒะปะธ", + "%(oneUser)sleft %(count)s times|other": "%(oneUser)s ะฟะพะบะธะฝัƒะป(ะฐ) %(count)s ั€ะฐะท(ะฐ)", + "%(oneUser)sleft %(count)s times|one": "%(oneUser)s ะฟะพะบะธะฝัƒะป(ะฐ)", + "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)s ะฟั€ะธัะพะตะดะธะฝะธะปะธััŒ ะธ ะฟะพะบะธะฝัƒะปะธ %(count)s ั€ะฐะท(ะฐ)", + "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)s ะฟั€ะธัะพะตะดะธะฝะธะปะธััŒ ะธ ะฟะพะบะธะฝัƒะปะธ", + "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)s ะฟั€ะธัะพะตะดะธะฝะธะปัั(ะปะฐััŒ) ะธ ะฟะพะบะธะฝัƒะป(ะฐ) %(count)s ั€ะฐะท(ะฐ)", + "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)s ะฟั€ะธัะพะตะดะธะฝะธะปัั(ะปะฐััŒ) ะธ ะฟะพะบะธะฝัƒะป(ะฐ)", + "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)s ะฟะพะบะธะฝัƒะปะธ ะธ ัะฝะพะฒะฐ ะฟั€ะธัะพะตะดะธะฝะธะปะธััŒ %(count)s ั€ะฐะท(ะฐ)", + "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)s ะฟะพะบะธะฝัƒะปะธ ะธ ัะฝะพะฒะฐ ะฟั€ะธัะพะตะดะธะฝะธะปะธััŒ", + "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)s ะฟะพะบะธะฝัƒะป(ะฐ) ะธ ัะฝะพะฒะฐ ะฟั€ะธัะพะตะดะธะฝะธะปัั(ะปะฐััŒ) %(count)s ั€ะฐะท(ะฐ)", + "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)s ะฟะพะบะธะฝัƒะป(ะฐ) ะธ ัะฝะพะฒะฐ ะฟั€ะธัะพะตะดะธะฝะธะปัั(ะปะฐััŒ)", "were invited %(count)s times|other": "ะฟั€ะธะณะปะฐัˆะตะฝั‹ %(count)s ั€ะฐะท(ะฐ)", "were invited %(count)s times|one": "ะฟั€ะธะณะปะฐัˆะตะฝั‹", "was invited %(count)s times|other": "ะฟั€ะธะณะปะฐัˆะตะฝ(ะฐ) %(count)s ั€ะฐะท(ะฐ)", @@ -526,10 +526,10 @@ "Put a link back to the old room at the start of the new room so people can see old messages": "ะ ะฐะทะผะตัั‚ะธะผ ััั‹ะปะบัƒ ะฝะฐ ัั‚ะฐั€ัƒัŽ ะบะพะผะฝะฐั‚ัƒ, ั‡ั‚ะพะฑั‹ ะปัŽะดะธ ะผะพะณะปะธ ะฒะธะดะตั‚ัŒ ัั‚ะฐั€ั‹ะต ัะพะพะฑั‰ะตะฝะธั", "Please contact your service administrator to continue using this service.": "ะŸะพะถะฐะปัƒะนัั‚ะฐ, ะพะฑั€ะฐั‚ะธั‚ะตััŒ ะบ ะฒะฐัˆะตะผัƒ ะฐะดะผะธะฝะธัั‚ั€ะฐั‚ะพั€ัƒ, ั‡ั‚ะพะฑั‹ ะฟั€ะพะดะพะปะถะธั‚ัŒ ะธัะฟะพะปัŒะทะพะฒะฐั‚ัŒ ัั‚ะพั‚ ัะตั€ะฒะธั.", "Unable to load! Check your network connectivity and try again.": "ะะต ัƒะดะฐะปะพััŒ ะทะฐะณั€ัƒะทะธั‚ัŒ! ะŸั€ะพะฒะตั€ัŒั‚ะต ะฟะพะดะบะปัŽั‡ะตะฝะธะต ะบ ัะตั‚ะธ ะธ ะฟะพะฟั€ะพะฑัƒะนั‚ะต ัะฝะพะฒะฐ.", - "Upgrades a room to a new version": "ะœะพะดะตั€ะฝะธะทะธั€ัƒะตั‚ ะบะพะผะฝะฐั‚ัƒ ะดะพ ะฝะพะฒะพะน ะฒะตั€ัะธะธ", + "Upgrades a room to a new version": "ะžะฑะฝะพะฒะปัะตั‚ ะบะพะผะฝะฐั‚ัƒ ะดะพ ะฝะพะฒะพะน ะฒะตั€ัะธะธ", "Sets the room name": "ะฃัั‚ะฐะฝะฐะฒะปะธะฒะฐะตั‚ ะฝะฐะทะฒะฐะฝะธะต ะบะพะผะฝะฐั‚ั‹", "Forces the current outbound group session in an encrypted room to be discarded": "ะŸั€ะธะฝัƒะดะธั‚ะตะปัŒะฝะพ ะพั‚ะฑั€ะฐัั‹ะฒะฐะตั‚ ั‚ะตะบัƒั‰ัƒัŽ ะณั€ัƒะฟะฟะพะฒัƒัŽ ัะตััะธัŽ ะดะปั ะพั‚ะฟั€ะฐะฒะบะธ ัะพะพะฑั‰ะตะฝะธะน ะฒ ะทะฐัˆะธั„ั€ะพะฒะฐะฝะฝัƒัŽ ะบะพะผะฝะฐั‚ัƒ", - "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s ะผะพะดะตั€ะฝะธะทะธั€ะพะฒะฐะป ัั‚ัƒ ะบะพะผะฝะฐั‚ัƒ.", + "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s ะพะฑะฝะพะฒะธะป(ะฐ) ัั‚ัƒ ะบะพะผะฝะฐั‚ัƒ.", "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s ัƒัั‚ะฐะฝะพะฒะธะป(ะฐ) %(address)s ะฒ ะบะฐั‡ะตัั‚ะฒะต ะณะปะฐะฒะฝะพะณะพ ะฐะดั€ะตัะฐ ะบะพะผะฝะฐั‚ั‹.", "%(senderName)s removed the main address for this room.": "%(senderName)s ัƒะดะฐะปะธะป ะณะปะฐะฒะฝั‹ะน ะฐะดั€ะตั ะบะพะผะฝะฐั‚ั‹.", "%(displayName)s is typing โ€ฆ": "%(displayName)s ะฟะตั‡ะฐั‚ะฐะตั‚โ€ฆ", @@ -1472,7 +1472,7 @@ "Ok": "ะฅะพั€ะพัˆะพ", "New login. Was this you?": "ะะพะฒั‹ะน ะฒั…ะพะด ะฒ ะฒะฐัˆัƒ ัƒั‡ั‘ั‚ะฝัƒัŽ ะทะฐะฟะธััŒ. ะญั‚ะพ ะฑั‹ะปะธ ะ’ั‹?", "You joined the call": "ะ’ั‹ ะฟั€ะธัะพะตะดะธะฝะธะปะธััŒ ะบ ะทะฒะพะฝะบัƒ", - "%(senderName)s joined the call": "%(senderName)s ะฟั€ะธัะพะตะดะธะฝะธะปัั(-ะฐััŒ) ะบ ะทะฒะพะฝะบัƒ", + "%(senderName)s joined the call": "%(senderName)s ะฟั€ะธัะพะตะดะธะฝะธะปัั(ะปะฐััŒ) ะบ ะทะฒะพะฝะบัƒ", "Call in progress": "ะ—ะฒะพะฝะพะบ ะฒ ะฟั€ะพั†ะตััะต", "Call ended": "ะ—ะฒะพะฝะพะบ ะทะฐะฒะตั€ัˆั‘ะฝ", "You started a call": "ะ’ั‹ ะฝะฐั‡ะฐะปะธ ะทะฒะพะฝะพะบ", @@ -1594,8 +1594,8 @@ "Confirm by comparing the following with the User Settings in your other session:": "ะŸะพะดั‚ะฒะตั€ะดะธั‚ะต, ัั€ะฐะฒะฝะธะฒ ัะปะตะดัƒัŽั‰ะธะต ะฟะฐั€ะฐะผะตั‚ั€ั‹ ั ะฝะฐัั‚ั€ะพะนะบะฐะผะธ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปั ะฒ ะดั€ัƒะณะพะน ะฒะฐัˆะตะน ัะตััะธะธ:", "Confirm this user's session by comparing the following with their User Settings:": "ะŸะพะดั‚ะฒะตั€ะดะธั‚ะต ัะตััะธัŽ ัั‚ะพะณะพ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปั, ัั€ะฐะฒะฝะธะฒ ัะปะตะดัƒัŽั‰ะธะต ะฟะฐั€ะฐะผะตั‚ั€ั‹ ั ะตะณะพ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปัŒัะบะธะผะธ ะฝะฐัั‚ั€ะพะนะบะฐะผะธ:", "If they don't match, the security of your communication may be compromised.": "ะ•ัะปะธ ะพะฝะธ ะฝะต ัะพะฒะฟะฐะดะฐัŽั‚, ะฑะตะทะพะฟะฐัะฝะพัั‚ัŒ ะฒะฐัˆะตะณะพ ะพะฑั‰ะตะฝะธั ะผะพะถะตั‚ ะฑั‹ั‚ัŒ ะฟะพัั‚ะฐะฒะปะตะฝะฐ ะฟะพะด ัƒะณั€ะพะทัƒ.", - "Upgrade private room": "ะœะพะดะตั€ะฝะธะทะธั€ะพะฒะฐั‚ัŒ ะฟั€ะธะฒะฐั‚ะฝัƒัŽ ะบะพะผะฝะฐั‚ัƒ", - "Upgrade public room": "ะœะพะดะตั€ะฝะธะทะธั€ะพะฒะฐั‚ัŒ ะฟัƒะฑะปะธั‡ะฝัƒัŽ ะบะพะผะฝะฐั‚ัƒ", + "Upgrade private room": "ะžะฑะฝะพะฒะธั‚ัŒ ะฟั€ะธะฒะฐั‚ะฝัƒัŽ ะบะพะผะฝะฐั‚ัƒ", + "Upgrade public room": "ะžะฑะฝะพะฒะธั‚ัŒ ะฟัƒะฑะปะธั‡ะฝัƒัŽ ะบะพะผะฝะฐั‚ัƒ", "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "ะœะพะดะตั€ะฝะธะทะฐั†ะธั ะบะพะผะฝะฐั‚ั‹ - ัั‚ะพ ั€ะฐััˆะธั€ะตะฝะฝะพะต ะดะตะนัั‚ะฒะธะต, ะบะพั‚ะพั€ะพะต ะพะฑั‹ั‡ะฝะพ ั€ะตะบะพะผะตะฝะดัƒะตั‚ัั, ะบะพะณะดะฐ ะบะพะผะฝะฐั‚ะฐ ะฝะตัั‚ะฐะฑะธะปัŒะฝะฐ ะธะท-ะทะฐ ะพัˆะธะฑะพะบ, ะพั‚ััƒั‚ัั‚ะฒัƒัŽั‰ะธั… ั„ัƒะฝะบั†ะธะน ะธะปะธ ัƒัะทะฒะธะผะพัั‚ะตะน ะฑะตะทะพะฟะฐัะฝะพัั‚ะธ.", "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.": "ะžะฑั‹ั‡ะฝะพ ัั‚ะพ ะฒะปะธัะตั‚ ั‚ะพะปัŒะบะพ ะฝะฐ ั‚ะพ, ะบะฐะบ ะบะพะผะฝะฐั‚ะฐ ะพะฑั€ะฐะฑะฐั‚ั‹ะฒะฐะตั‚ัั ะฝะฐ ัะตั€ะฒะตั€ะต. ะ•ัะปะธ ัƒ ะฒะฐั ะฒะพะทะฝะธะบะปะธ ะฟั€ะพะฑะปะตะผั‹ ั ะฒะฐัˆะธะผ %(brand)s, ะฟะพะถะฐะปัƒะนัั‚ะฐ, ัะพะพะฑั‰ะธั‚ะต ะพะฑ ะพัˆะธะฑะบะต.", "You'll upgrade this room from to .": "ะ’ั‹ ะผะพะดะตั€ะฝะธะทะธั€ัƒะตั‚ะต ัั‚ัƒ ะบะพะผะฝะฐั‚ัƒ ั ะดะพ .", @@ -1792,7 +1792,7 @@ "Add a topic to help people know what it is about.": "ะ”ะพะฑะฐะฒัŒั‚ะต ั‚ะตะผัƒ, ั‡ั‚ะพะฑั‹ ะปัŽะดะธ ะทะฝะฐะปะธ, ะพ ั‡ั‘ะผ ะบะพะผะฝะฐั‚ะฐ.", "Topic: %(topic)s ": "ะขะตะผะฐ: %(topic)s ", "Topic: %(topic)s (edit)": "ะขะตะผะฐ: %(topic)s (ะธะทะผะตะฝะธั‚ัŒ)", - "This is the beginning of your direct message history with .": "ะญั‚ะพ ะฝะฐั‡ะฐะปะพ ะฒะฐัˆะตะน ะธัั‚ะพั€ะธะธ ะฟั€ัะผั‹ั… ัะพะพะฑั‰ะตะฝะธะน ั .", + "This is the beginning of your direct message history with .": "ะญั‚ะพ ะฝะฐั‡ะฐะปะพ ะฒะฐัˆะตะน ะฟะตั€ะตะฟะธัะบะธ ั .", "Only the two of you are in this conversation, unless either of you invites anyone to join.": "ะ’ ัั‚ะพะผ ั€ะฐะทะณะพะฒะพั€ะต ั‚ะพะปัŒะบะพ ะฒั‹ ะดะฒะพะต, ะตัะปะธ ั‚ะพะปัŒะบะพ ะบั‚ะพ-ะฝะธะฑัƒะดัŒ ะธะท ะฒะฐั ะฝะต ะฟั€ะธะณะปะฐัะธั‚ ะบะพะณะพ-ะฝะธะฑัƒะดัŒ ะฟั€ะธัะพะตะดะธะฝะธั‚ัŒัั.", "Takes the call in the current room off hold": "ะŸั€ะตะบั€ะฐั‚ะธั‚ัŒ ัƒะดะตั€ะถะฐะฝะธะต ะฒั‹ะทะพะฒะฐ ะฒ ั‚ะตะบัƒั‰ะตะน ะบะพะผะฝะฐั‚ะต", "Places the call in the current room on hold": "ะŸะตั€ะตะฒะตัั‚ะธ ะฒั‹ะทะพะฒ ะฒ ั‚ะตะบัƒั‰ะตะน ะบะพะผะฝะฐั‚ะต ะฝะฐ ัƒะดะตั€ะถะฐะฝะธะต", @@ -2342,7 +2342,7 @@ "A private space to organise your rooms": "ะŸั€ะธะฒะฐั‚ะฝะพะต ะฟั€ะพัั‚ั€ะฐะฝัั‚ะฒะพ ะดะปั ะพั€ะณะฐะฝะธะทะฐั†ะธะธ ะฒะฐัˆะธั… ะบะพะผะฝะฐั‚", "Just me": "ะขะพะปัŒะบะพ ั", "Make sure the right people have access to %(name)s": "ะฃะฑะตะดะธั‚ะตััŒ, ั‡ั‚ะพ ะฟั€ะฐะฒะธะปัŒะฝั‹ะต ะปัŽะดะธ ะธะผะตัŽั‚ ะดะพัั‚ัƒะฟ ะบ %(name)s", - "Who are you working with?": "ะก ะบะตะผ ั‚ั‹ ั€ะฐะฑะพั‚ะฐะตัˆัŒ?", + "Who are you working with?": "ะก ะบะตะผ ะฒั‹ ั€ะฐะฑะพั‚ะฐะตั‚ะต?", "Go to my first room": "ะŸะตั€ะตะนั‚ะธ ะฒ ะผะพัŽ ะฟะตั€ะฒัƒัŽ ะบะพะผะฝะฐั‚ัƒ", "It's just you at the moment, it will be even better with others.": "ะกะตะนั‡ะฐั ะทะดะตััŒ ั‚ะพะปัŒะบะพ ั‚ั‹, ั ะดั€ัƒะณะธะผะธ ะฑัƒะดะตั‚ ะตั‰ั‘ ะปัƒั‡ัˆะต.", "Share %(name)s": "ะŸะพะดะตะปะธั‚ัŒัั %(name)s", @@ -2513,7 +2513,7 @@ "Zoom in": "ะฃะฒะตะปะธั‡ะธั‚ัŒ", "Zoom out": "ะฃะผะตะฝัŒัˆะธั‚ัŒ", "%(count)s people you know have already joined|one": "%(count)s ั‡ะตะปะพะฒะตะบ, ะบะพั‚ะพั€ะพะณะพ ะฒั‹ ะทะฝะฐะตั‚ะต, ัƒะถะต ะฟั€ะธัะพะตะดะธะฝะธะปัั", - "%(count)s people you know have already joined|other": "%(count)s ั‡ะตะปะพะฒะตะบ, ะบะพั‚ะพั€ั‹ั… ะฒั‹ ะทะฝะฐะตั‚ะต, ัƒะถะต ะฟั€ะธัะพะตะดะธะฝะธะปะธััŒ", + "%(count)s people you know have already joined|other": "%(count)s ั‡ะตะปะพะฒะตะบ(ะฐ), ะบะพั‚ะพั€ั‹ั… ะฒั‹ ะทะฝะฐะตั‚ะต, ัƒะถะต ะฟั€ะธัะพะตะดะธะฝะธะปะธััŒ", "Including %(commaSeparatedMembers)s": "ะ’ะบะปัŽั‡ะฐั %(commaSeparatedMembers)s", "View all %(count)s members|one": "ะŸะพัะผะพั‚ั€ะตั‚ัŒ 1 ัƒั‡ะฐัั‚ะฝะธะบะฐ", "View all %(count)s members|other": "ะŸั€ะพัะผะพั‚ั€ะตั‚ัŒ ะฒัะตั… %(count)s ัƒั‡ะฐัั‚ะฝะธะบะพะฒ", @@ -2613,7 +2613,7 @@ "Give feedback.": "ะžัั‚ะฐะฒะธั‚ัŒ ะพั‚ะทั‹ะฒ.", "Thank you for trying Spaces. Your feedback will help inform the next versions.": "ะกะฟะฐัะธะฑะพ, ั‡ั‚ะพ ะฟะพะฟั€ะพะฑะพะฒะฐะปะธ ะฟั€ะพัั‚ั€ะฐะฝัั‚ะฒะฐ. ะ’ะฐัˆะธ ะพั‚ะทั‹ะฒั‹ ะฟะพะผะพะณัƒั‚ ะฟั€ะธ ั€ะฐะทั€ะฐะฑะพั‚ะบะต ัะปะตะดัƒัŽั‰ะธั… ะฒะตั€ัะธะน.", "Spaces feedback": "ะžั‚ะทั‹ะฒ ะพ ะฟั€ะพัั‚ั€ะฐะฝัั‚ะฒะฐั…", - "Spaces are a new feature.": "ะŸั€ะพัั‚ั€ะฐะฝัั‚ะฒะฐ - ัั‚ะพ ะฝะพะฒะฐั ั„ัƒะฝะบั†ะธั.", + "Spaces are a new feature.": "ะŸั€ะพัั‚ั€ะฐะฝัั‚ะฒะฐ โ€” ัั‚ะพ ะฝะพะฒะฐั ั„ัƒะฝะบั†ะธั.", "Please enter a name for the space": "ะŸะพะถะฐะปัƒะนัั‚ะฐ, ะฒะฒะตะดะธั‚ะต ะฝะฐะทะฒะฐะฝะธะต ะฟั€ะพัั‚ั€ะฐะฝัั‚ะฒะฐ", "Your camera is still enabled": "ะ’ะฐัˆะฐ ะบะฐะผะตั€ะฐ ะฒัั‘ ะตั‰ั‘ ะฒะบะปัŽั‡ะตะฝะฐ", "Your camera is turned off": "ะ’ะฐัˆะฐ ะบะฐะผะตั€ะฐ ะฒั‹ะบะปัŽั‡ะตะฝะฐ", @@ -2650,7 +2650,7 @@ "%(targetName)s left the room": "%(targetName)s ะฟะพะบะธะฝัƒะป(ะฐ) ะบะพะผะฝะฐั‚ัƒ", "%(targetName)s left the room: %(reason)s": "%(targetName)s ะฟะพะบะธะฝัƒะป(ะฐ) ะบะพะผะฝะฐั‚ัƒ: %(reason)s", "%(targetName)s rejected the invitation": "%(targetName)s ะพั‚ะบะปะพะฝะธะป(ะฐ) ะฟั€ะธะณะปะฐัˆะตะฝะธะต", - "%(targetName)s joined the room": "%(targetName)s ะฒะพัˆั‘ะป(-ะปะฐ) ะฒ ะบะพะผะฝะฐั‚ัƒ", + "%(targetName)s joined the room": "%(targetName)s ะฟั€ะธัะพะตะดะธะฝะธะปัั(ะปะฐััŒ) ะบ ะบะพะผะฝะฐั‚ะต", "%(senderName)s made no change": "%(senderName)s ะฝะต ัะดะตะปะฐะป(ะฐ) ะธะทะผะตะฝะตะฝะธะน", "%(senderName)s set a profile picture": "%(senderName)s ัƒัั‚ะฐะฝะพะฒะธะป(ะฐ) ะฐะฒะฐั‚ะฐั€", "%(senderName)s changed their profile picture": "%(senderName)s ะธะทะผะตะฝะธะป(ะฐ) ะฐะฒะฐั‚ะฐั€", @@ -3206,7 +3206,7 @@ "Can I use text chat alongside the video call?": "ะœะพะถะฝะพ ะปะธ ะธัะฟะพะปัŒะทะพะฒะฐั‚ัŒ ั‚ะตะบัั‚ะพะฒั‹ะน ั‡ะฐั‚ ะพะดะฝะพะฒั€ะตะผะตะฝะฝะพ ั ะฒะธะดะตะพะทะฒะพะฝะบะพะผ?", "Use the โ€œ+โ€ button in the room section of the left panel.": "ะ˜ัะฟะพะปัŒะทัƒะนั‚ะต ะบะฝะพะฟะบัƒ \"+\" ะฒ ั€ะฐะทะดะตะปะต ะบะพะผะฝะฐั‚ ะฝะฐ ะปะตะฒะพะน ะฟะฐะฝะตะปะธ.", "How can I create a video room?": "ะšะฐะบ ัะพะทะดะฐั‚ัŒ ะฒะธะดะตะพะบะพะผะฝะฐั‚ัƒ?", - "A new way to chat over voice and video in %(brand)s.": "ะะพะฒั‹ะน ัะฟะพัะพะฑ ะณะพะปะพัะพะฒะพะณะพ ะธ ะฒะธะดะตะพ ะพะฑั‰ะตะฝะธั ะฒ %(brand)s.", + "A new way to chat over voice and video in %(brand)s.": "ะะพะฒั‹ะน ัะฟะพัะพะฑ ะณะพะปะพัะพะฒะพะณะพ ะธ ะฒะธะดะตะพะพะฑั‰ะตะฝะธั ะฒ %(brand)s.", "Video rooms": "ะ’ะธะดะตะพะบะพะผะฝะฐั‚ั‹", "Connection lost": "ะกะพะตะดะธะฝะตะฝะธะต ะฟะพั‚ะตั€ัะฝะพ", "The person who invited you has already left, or their server is offline.": "ะŸั€ะธะณะปะฐัะธะฒัˆะธะน ะฒะฐั ั‡ะตะปะพะฒะตะบ ัƒะถะต ัƒัˆั‘ะป, ะธะปะธ ะตะณะพ ัะตั€ะฒะตั€ ะฝะต ะฟะพะดะบะปัŽั‡ั‘ะฝ ะบ ัะตั‚ะธ.", @@ -3247,8 +3247,8 @@ "Video devices": "ะ’ะธะดะตะพัƒัั‚ั€ะพะนัั‚ะฒะฐ", "Mute microphone": "ะžั‚ะบะปัŽั‡ะธั‚ัŒ ะผะธะบั€ะพั„ะพะฝ", "Audio devices": "ะัƒะดะธะพัƒัั‚ั€ะพะนัั‚ะฒะฐ", - "%(count)s people joined|one": "%(count)s ะฟั€ะธัะพะตะดะธะฝะธะฒัˆะธั…ัั", - "%(count)s people joined|other": "%(count)s ะฟั€ะธัะพะตะดะธะฝะธะฒัˆะธั…ัั", + "%(count)s people joined|one": "%(count)s ั‡ะตะปะพะฒะตะบ ะฟั€ะธัะพะตะดะธะฝะธะปัั", + "%(count)s people joined|other": "%(count)s ั‡ะตะปะพะฒะตะบ(ะฐ) ะฟั€ะธัะพะตะดะธะฝะธะปะธััŒ", "sends hearts": "ะพั‚ะฟั€ะฐะฒะปัะตั‚ ัะตั€ะดะตั‡ะบะธ", "Enable hardware acceleration": "ะ’ะบะปัŽั‡ะธั‚ัŒ ะฐะฟะฟะฐั€ะฐั‚ะฝะพะต ัƒัะบะพั€ะตะฝะธะต", "Remove from space": "ะ˜ัะบะปัŽั‡ะธั‚ัŒ ะธะท ะฟั€ะพัั‚ั€ะฐะฝัั‚ะฒะฐ", @@ -3264,7 +3264,7 @@ "Signing out your devices will delete the message encryption keys stored on them, making encrypted chat history unreadable.": "ะŸั€ะธ ะฒั‹ั…ะพะดะต ะธะท ัƒัั‚ั€ะพะนัั‚ะฒ ัƒะดะฐะปััŽั‚ัั ั…ั€ะฐะฝัั‰ะธะตัั ะฝะฐ ะฝะธั… ะบะปัŽั‡ะธ ัˆะธั„ั€ะพะฒะฐะฝะธั ัะพะพะฑั‰ะตะฝะธะน, ั‡ั‚ะพ ัะดะตะปะฐะตั‚ ะทะฐัˆะธั„ั€ะพะฒะฐะฝะฝัƒัŽ ะธัั‚ะพั€ะธัŽ ั‡ะฐั‚ะพะฒ ะฝะตั‡ะธั‚ะฐะตะผะพะน.", "Resetting your password on this homeserver will cause all of your devices to be signed out. This will delete the message encryption keys stored on them, making encrypted chat history unreadable.": "ะกะฑั€ะพั ะฟะฐั€ะพะปั ะฝะฐ ัั‚ะพะผ ะดะพะผะฐัˆะฝะตะผ ัะตั€ะฒะตั€ะต ะฟั€ะธะฒะตะดะตั‚ ะบ ั‚ะพะผัƒ, ั‡ั‚ะพ ะฒัะต ะฒะฐัˆะธ ัƒัั‚ั€ะพะนัั‚ะฒะฐ ะฑัƒะดัƒั‚ ะพั‚ะบะปัŽั‡ะตะฝั‹. ะญั‚ะพ ะฟั€ะธะฒะตะดะตั‚ ะบ ัƒะดะฐะปะตะฝะธัŽ ั…ั€ะฐะฝัั‰ะธั…ัั ะฝะฐ ะฝะธั… ะบะปัŽั‡ะตะน ัˆะธั„ั€ะพะฒะฐะฝะธั ัะพะพะฑั‰ะตะฝะธะน, ั‡ั‚ะพ ัะดะตะปะฐะตั‚ ะทะฐัˆะธั„ั€ะพะฒะฐะฝะฝัƒัŽ ะธัั‚ะพั€ะธัŽ ั‡ะฐั‚ะฐ ะฝะตั‡ะธั‚ะฐะตะผะพะน.", "Event ID: %(eventId)s": "ID ัะพะฑั‹ั‚ะธั: %(eventId)s", - "Threads are a beta feature": "ะ’ะตั‚ะบะธ - ะฑะตั‚ะฐ-ั„ัƒะฝะบั†ะธั", + "Threads are a beta feature": "ะžะฑััƒะถะดะตะฝะธั โ€” ะฑะตั‚ะฐ-ั„ัƒะฝะบั†ะธั", "Your message wasn't sent because this homeserver has been blocked by its administrator. Please contact your service administrator to continue using the service.": "ะ’ะฐัˆะต ัะพะพะฑั‰ะตะฝะธะต ะฝะต ะพั‚ะฟั€ะฐะฒะปะตะฝะพ, ะฟะพัะบะพะปัŒะบัƒ ะดะพะผะฐัˆะฝะธะน ัะตั€ะฒะตั€ ะทะฐะฑะปะพะบะธั€ะพะฒะฐะฝ ะตะณะพ ะฐะดะผะธะฝะธัั‚ั€ะฐั‚ะพั€ะพะผ. ะžะฑั€ะฐั‚ะธั‚ะตััŒ ะบ ะฐะดะผะธะฝะธัั‚ั€ะฐั‚ะพั€ัƒ ัะปัƒะถะฑั‹, ั‡ั‚ะพะฑั‹ ะฟั€ะพะดะพะปะถะธั‚ัŒ ะตั‘ ะธัะฟะพะปัŒะทะพะฒะฐะฝะธะต.", "Resent!": "ะžั‚ะฟั€ะฐะฒะปะตะฝะพ ะฟะพะฒั‚ะพั€ะฝะพ!", "Did not receive it? Resend it": "ะะต ะฟะพะปัƒั‡ะธะปะธ? ะžั‚ะฟั€ะฐะฒะธั‚ัŒ ะตะณะพ ะฟะพะฒั‚ะพั€ะฝะพ", @@ -3372,8 +3372,8 @@ "%(brand)s was denied permission to fetch your location. Please allow location access in your browser settings.": "%(brand)s ะฝะต ะฟะพะปัƒั‡ะธะป ะดะพัั‚ัƒะฟะฐ ะบ ะฒะฐัˆะตะผัƒ ะผะตัั‚ะพะฝะฐั…ะพะถะดะตะฝะธัŽ. ะ ะฐะทั€ะตัˆะธั‚ะต ะดะพัั‚ัƒะฟ ะบ ะผะตัั‚ะพะฟะพะปะพะถะตะฝะธัŽ ะฒ ะฝะฐัั‚ั€ะพะนะบะฐั… ะฑั€ะฐัƒะทะตั€ะฐ.", "Share for %(duration)s": "ะŸะพะดะตะปะธั‚ัŒัั ะฝะฐ %(duration)s", "Live location sharing": "ะžั‚ะฟั€ะฐะฒะบะฐ ะผะตัั‚ะพะฝะฐั…ะพะถะดะตะฝะธั ะฒ ั€ะตะฐะปัŒะฝะพะผ ะฒั€ะตะผะตะฝะธ", - "Beta feature. Click to learn more.": "ะ‘ะตั‚ะฐั„ัƒะฝะบั†ะธั. ะะฐะถะผะธั‚ะต, ั‡ั‚ะพะฑั‹ ัƒะทะฝะฐั‚ัŒ ะฑะพะปัŒัˆะต.", - "Beta feature": "ะ‘ะตั‚ะฐั„ัƒะฝะบั†ะธั", + "Beta feature. Click to learn more.": "ะ‘ะตั‚ะฐ-ั„ัƒะฝะบั†ะธั. ะะฐะถะผะธั‚ะต, ั‡ั‚ะพะฑั‹ ัƒะทะฝะฐั‚ัŒ ะฑะพะปัŒัˆะต.", + "Beta feature": "ะ‘ะตั‚ะฐ-ั„ัƒะฝะบั†ะธั", "Ban from room": "ะ—ะฐะฑะปะพะบะธั€ะพะฒะฐั‚ัŒ ะฒ ะบะพะผะฝะฐั‚ะต", "Unban from room": "ะ ะฐะทะฑะปะพะบะธั€ะพะฒะฐั‚ัŒ ะฒ ะบะพะผะฝะฐั‚ะต", "Ban from space": "ะ—ะฐะฑะปะพะบะธั€ะพะฒะฐั‚ัŒ ะฒ ะฟั€ะพัั‚ั€ะฐะฝัั‚ะฒะต", @@ -3403,7 +3403,7 @@ "If you've submitted a bug via GitHub, debug logs can help us track down the problem. ": "ะ•ัะปะธ ะฒั‹ ะพั‚ะฟั€ะฐะฒะธะปะธ ะพัˆะธะฑะบัƒ ั‡ะตั€ะตะท GitHub, ะถัƒั€ะฝะฐะปั‹ ะพั‚ะปะฐะดะบะธ ะผะพะณัƒั‚ ะฟะพะผะพั‡ัŒ ะฝะฐะผ ะพั‚ัะปะตะดะธั‚ัŒ ะฟั€ะพะฑะปะตะผัƒ. ", "Right-click message context menu": "ะŸั€ะฐะฒะฐั ะบะฝะพะฟะบะฐ ะผั‹ัˆะธ โ€“ ะบะพะฝั‚ะตะบัั‚ะฝะพะต ะผะตะฝัŽ ัะพะพะฑั‰ะตะฝะธั", "Show HTML representation of room topics": "ะŸะพะบะฐะทะฐั‚ัŒ HTML-ะฟั€ะตะดัั‚ะฐะฒะปะตะฝะธะต ั‚ะตะผ ะบะพะผะฝะฐั‚ั‹", - "Video rooms are always-on VoIP channels embedded within a room in %(brand)s.": "ะ’ะธะดะตะพะบะพะผะฝะฐั‚ั‹ - ัั‚ะพ ะฟะพัั‚ะพัะฝะฝั‹ะต VoIP-ะบะฐะฝะฐะปั‹, ะฒัั‚ั€ะพะตะฝะฝั‹ะต ะฒ ะบะพะผะฝะฐั‚ัƒ ะฒ %(brand)s.", + "Video rooms are always-on VoIP channels embedded within a room in %(brand)s.": "ะ’ะธะดะตะพะบะพะผะฝะฐั‚ั‹ โ€” ัั‚ะพ ะฟะพัั‚ะพัะฝะฝั‹ะต VoIP-ะบะฐะฝะฐะปั‹, ะฒัั‚ั€ะพะตะฝะฝั‹ะต ะฒ ะบะพะผะฝะฐั‚ัƒ ะฒ %(brand)s.", "You're trying to access a community link (%(groupId)s).
Communities are no longer supported and have been replaced by spaces.Learn more about spaces here.": "ะ’ั‹ ะฟั‹ั‚ะฐะตั‚ะตััŒ ะฟะพะปัƒั‡ะธั‚ัŒ ะดะพัั‚ัƒะฟ ะบ ััั‹ะปะบะต ะฝะฐ ัะพะพะฑั‰ะตัั‚ะฒะพ (%(groupId)s).
ะกะพะพะฑั‰ะตัั‚ะฒะฐ ะฑะพะปัŒัˆะต ะฝะต ะฟะพะดะดะตั€ะถะธะฒะฐัŽั‚ัั ะธ ะธั… ะทะฐะผะตะฝะธะปะธ ะฟั€ะพัั‚ั€ะฐะฝัั‚ะฒะฐะผะธ.ะฃะทะฝะฐะนั‚ะต ะฑะพะปัŒัˆะต ะพ ะฟั€ะพัั‚ั€ะฐะฝัั‚ะฒะฐั… ะทะดะตััŒ.", "Enable live location sharing": "ะ’ะบะปัŽั‡ะธั‚ัŒ ั„ัƒะฝะบั†ะธัŽ \"ะŸะพะดะตะปะธั‚ัŒัั ั‚ั€ะฐะฝัะปัั†ะธะตะน ะผะตัั‚ะพะฟะพะปะพะถะตะฝะธั\"", "Live location ended": "ะขั€ะฐะฝัะปัั†ะธั ะผะตัั‚ะพะฟะพะปะพะถะตะฝะธั ะทะฐะฒะตั€ัˆะตะฝะฐ", diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index 5304644a25d..05285a15234 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -3477,5 +3477,25 @@ "Find friends": "Nรกjsลฅ priateฤพov", "Itโ€™s what youโ€™re here for, so lets get to it": "Kvรดli tomu ste tu, tak sa do toho pustite", "Find and invite your friends": "Nรกjdite a pozvite svojich priateฤพov", - "You made it!": "Zvlรกdli ste to!" + "You made it!": "Zvlรกdli ste to!", + "We're creating a room with %(names)s": "Vytvรกrame miestnosลฅ s %(names)s", + "Google Play and the Google Play logo are trademarks of Google LLC.": "Google Play a logo Google Play sรบ ochrannรฉ znรกmky spoloฤnosti Google LLC.", + "App Storeยฎ and the Apple logoยฎ are trademarks of Apple Inc.": "App Storeยฎ a logo Appleยฎ sรบ ochrannรฉ znรกmky spoloฤnosti Apple Inc.", + "Get it on F-Droid": "Zรญskajte ho v sluลพbe F-Droid", + "Get it on Google Play": "Zรญskajte ho v sluลพbe Google Play", + "Android": "Android", + "Download on the App Store": "Stiahnuลฅ v obchode App Store", + "iOS": "iOS", + "Download %(brand)s Desktop": "Stiahnuลฅ %(brand)s Desktop", + "Download %(brand)s": "Stiahnuลฅ %(brand)s", + "Help": "Pomocnรญk", + "Your server doesn't support disabling sending read receipts.": "Vรกลก server nepodporuje vypnutie odosielania potvrdenรญ o preฤรญtanรญ.", + "Share your activity and status with others.": "Zdieฤพajte svoju aktivitu a stav s ostatnรฝmi.", + "Presence": "Prรญtomnosลฅ", + "Weโ€™d appreciate any feedback on how youโ€™re finding Element.": "Budeme vฤaฤnรญ za akรบkoฤพvek spรคtnรบ vรคzbu o tom, ako sa vรกm Element osvedฤil.", + "How are you finding Element so far?": "Ako sa vรกm zatiaฤพ pรกฤi Element?", + "Send read receipts": "Odosielaลฅ potvrdenia o preฤรญtanรญ", + "Last activity": "Poslednรก aktivita", + "Sessions": "Relรกcie", + "Use new session manager (under active development)": "Pouลพiลฅ novรฉho sprรกvcu relรกciรญ (v ลกtรกdiu aktรญvneho vรฝvoja)" } diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json index dd087f8506e..d86168051bc 100644 --- a/src/i18n/strings/uk.json +++ b/src/i18n/strings/uk.json @@ -3480,5 +3480,23 @@ "You made it!": "ะ’ะธ ั†ะต ะทั€ะพะฑะธะปะธ!", "Help": "ะ”ะพะฒั–ะดะบะฐ", "Weโ€™d appreciate any feedback on how youโ€™re finding Element.": "ะœะธ ะฑัƒะดะตะผะพ ะฒะดัั‡ะฝั– ะทะฐ ะฒะฐัˆ ะฒั–ะดะณัƒะบ ะฟั€ะพ Element.", - "How are you finding Element so far?": "ะฏะบ ะฒะฐะผ Element?" + "How are you finding Element so far?": "ะฏะบ ะฒะฐะผ Element?", + "Google Play and the Google Play logo are trademarks of Google LLC.": "Google Play ั– ะปะพะณะพั‚ะธะฟ Google Play ั” ั‚ะพะฒะฐั€ะฝะธะผะธ ะทะฝะฐะบะฐะผะธ Google LLC.", + "App Storeยฎ and the Apple logoยฎ are trademarks of Apple Inc.": "App Storeยฎ ั– ะปะพะณะพั‚ะธะฟ Appleยฎ ั” ั‚ะพะฒะฐั€ะฝะธะผะธ ะทะฝะฐะบะฐะผะธ Apple Inc.", + "Get it on F-Droid": "ะžั‚ั€ะธะผะฐั‚ะธ ะท F-Droid", + "Get it on Google Play": "ะžั‚ั€ะธะผะฐั‚ะธ ะท Google Play", + "Android": "Android", + "Download on the App Store": "ะ—ะฐะฒะฐะฝั‚ะฐะถะธั‚ะธ ะท App Store", + "iOS": "iOS", + "Download %(brand)s Desktop": "ะ—ะฐะฒะฐะฝั‚ะฐะถะธั‚ะธ %(brand)s ะดะปั ะบะพะผะฟสผัŽั‚ะตั€ะฐ", + "Download %(brand)s": "ะ—ะฐะฒะฐะฝั‚ะฐะถะธั‚ะธ %(brand)s", + "We're creating a room with %(names)s": "ะœะธ ัั‚ะฒะพั€ัŽั”ะผะพ ะบั–ะผะฝะฐั‚ัƒ ะท %(names)s", + "Your server doesn't support disabling sending read receipts.": "ะ’ะฐัˆ ัะตั€ะฒะตั€ ะฝะต ะฟั–ะดั‚ั€ะธะผัƒั” ะฒะธะผะบะฝะตะฝะฝั ะฝะฐะดัะธะปะฐะฝะฝั ัะฟะพะฒั–ั‰ะตะฝัŒ ะฟั€ะพ ะฟั€ะพั‡ะธั‚ะฐะฝะฝั.", + "Share your activity and status with others.": "ะ”ั–ะปั–ั‚ัŒัั ัะฒะพั”ัŽ ะฐะบั‚ะธะฒะฝั–ัั‚ัŽ ั‚ะฐ ัั‚ะฐะฝะพะผ ะท ั–ะฝัˆะธะผะธ.", + "Presence": "ะŸั€ะธััƒั‚ะฝั–ัั‚ัŒ", + "Send read receipts": "ะะฐะดัะธะปะฐั‚ะธ ะฟั–ะดั‚ะฒะตั€ะดะถะตะฝะฝั ะฟั€ะพั‡ะธั‚ะฐะฝะฝั", + "Last activity": "ะžัั‚ะฐะฝะฝั ะฐะบั‚ะธะฒะฝั–ัั‚ัŒ", + "Sessions": "ะกะตะฐะฝัะธ", + "Use new session manager (under active development)": "ะ’ะธะบะพั€ะธัั‚ะพะฒัƒะฒะฐั‚ะธ ะฝะพะฒะธะน ะผะตะฝะตะดะถะตั€ ัะตะฐะฝัั–ะฒ (ะฒ ะฐะบั‚ะธะฒะฝั–ะน ั€ะพะทั€ะพะฑั†ั–)", + "Current session": "ะŸะพั‚ะพั‡ะฝะธะน ัะตะฐะฝั" } diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json index ddc03caf37a..50f04c4800f 100644 --- a/src/i18n/strings/zh_Hans.json +++ b/src/i18n/strings/zh_Hans.json @@ -493,7 +493,7 @@ "Muted Users": "่ขซ็ฆ่จ€็š„็”จๆˆท", "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "ๅœจๅŠ ๅฏ†็š„ๆˆฟ้—ดไธญ๏ผŒๆฏ”ๅฆ‚่ฟ™ไธช๏ผŒ้ป˜่ฎค็ฆ็”จURL้ข„่งˆ๏ผŒไปฅ็กฎไฟไธปๆœๅŠกๅ™จ๏ผˆ็”Ÿๆˆ้ข„่งˆ็š„ๅœฐๆ–น๏ผ‰ๆ— ๆณ•่Žท็Ÿฅไฝ ๅœจๆญคๆˆฟ้—ดไธญ็œ‹ๅˆฐ็š„้“พๆŽฅ็š„ๆœ‰ๅ…ณ็š„ไฟกๆฏใ€‚", "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "ๅฝ“ๆœ‰ไบบๅ‘้€ไธ€ๆกๅธฆๆœ‰้“พๆŽฅ็š„ๆถˆๆฏๅŽ๏ผŒๅฏๆ˜พ็คบ้“พๆŽฅ็š„้ข„่งˆ๏ผŒ้“พๆŽฅ้ข„่งˆๅฏๅŒ…ๅซๆญค้“พๆŽฅ็š„็ฝ‘้กตๆ ‡้ข˜ใ€ๆ่ฟฐไปฅๅŠๅ›พ็‰‡ใ€‚", - "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "ไฝ ็กฎๅฎš่ฆ็งป้™ค๏ผˆๅˆ ้™ค๏ผ‰ๆญคไบ‹ไปถๅ—๏ผŸๆณจๆ„๏ผŒๅฆ‚ๆžœๅˆ ้™คไบ†ๆˆฟ้—ดๅ็งฐๆˆ–่ฏ้ข˜็š„ไฟฎๆ”นไบ‹ไปถ๏ผŒๅฐฑไผšๆ’ค้”€ๆญคๆ›ดๆ”นใ€‚", + "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "ไฝ ็กฎๅฎš่ฆ็งป้™ค๏ผˆๅˆ ้™ค๏ผ‰ๆญคไบ‹ไปถๅ—๏ผŸๆณจๆ„๏ผŒๅฆ‚ๆžœๅˆ ้™คๆˆฟ้—ดๅ็งฐๆˆ–่ฏ้ข˜็š„ๆ›ดๆ”น๏ผŒๆ›ดๆ”นไผš่ขซๆ’ค้”€ใ€‚", "Clear Storage and Sign Out": "ๆธ…้™คๆ•ฐๆฎๅนถ้€€ๅ‡บ็™ปๅฝ•", "Send Logs": "ๅ‘้€ๆ—ฅๅฟ—", "Refresh": "ๅˆทๆ–ฐ", @@ -756,7 +756,7 @@ "Unable to load commit detail: %(msg)s": "ๆ— ๆณ•ๅŠ ่ฝฝๆไบค่ฏฆๆƒ…๏ผš%(msg)s", "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "ไธบ้ฟๅ…ไธขๅคฑ่Šๅคฉ่ฎฐๅฝ•๏ผŒไฝ ๅฟ…้กปๅœจ็™ปๅ‡บๅ‰ๅฏผๅ‡บๆˆฟ้—ดๅฏ†้’ฅใ€‚ไฝ ้œ€่ฆๅˆ‡ๆข่‡ณๆ–ฐ็‰ˆ %(brand)s ๆ–นๅฏ็ปง็ปญๆ‰ง่กŒๆญคๆ“ไฝœ", "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "้ชŒ่ฏๆญค็”จๆˆทๅนถๅฐ†ๅ…ถๆ ‡่ฎฐไธบๅทฒไฟกไปปใ€‚ๅœจๆ”ถๅ‘็ซฏๅˆฐ็ซฏๅŠ ๅฏ†ๆถˆๆฏๆ—ถ๏ผŒไฟกไปป็”จๆˆทๅฏ่ฎฉไฝ ๆ›ดๅŠ ๆ”พๅฟƒใ€‚", - "Waiting for partner to confirm...": "็ญ‰ๅพ…ๅฏนๆ–น็กฎ่ฎคไธญ...", + "Waiting for partner to confirm...": "็ญ‰ๅพ…ๅฏนๆ–น็กฎ่ฎคไธญโ€ฆโ€ฆ", "Incoming Verification Request": "ๆ”ถๅˆฐ้ชŒ่ฏ่ฏทๆฑ‚", "You've previously used %(brand)s on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, %(brand)s needs to resync your account.": "ไฝ ไน‹ๅ‰ๅœจ %(host)s ไธŠๅผ€ๅฏไบ† %(brand)s ็š„ๆˆๅ‘˜ๅˆ—่กจๅปถ่ฟŸๅŠ ่ฝฝ่ฎพ็ฝฎใ€‚็›ฎๅ‰็‰ˆๆœฌไธญๅปถ่ฟŸๅŠ ่ฝฝๅŠŸ่ƒฝๅทฒ่ขซๅœ็”จใ€‚ๅ› ไธบๆœฌๅœฐ็ผ“ๅญ˜ๅœจ่ฟ™ไธคไธช่ฎพ็ฝฎ้กนไธŠไธ็›ธๅฎน๏ผŒ%(brand)s ้œ€่ฆ้‡ๆ–ฐๅŒๆญฅไฝ ็š„่ดฆๆˆทใ€‚", "%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "้€š่ฟ‡ไป…ๅœจ้œ€่ฆๆ—ถๅŠ ่ฝฝๅ…ถไป–็”จๆˆท็š„ไฟกๆฏ๏ผŒ%(brand)s ็Žฐๅœจไฝฟ็”จ็š„ๅ†…ๅญ˜ๅ‡ๅฐ‘ๅˆฐไบ†ๅŽŸๆฅ็š„ไธ‰ๅˆ†ไน‹ไธ€่‡ณไบ”ๅˆ†ไน‹ไธ€ใ€‚ ่ฏท็ญ‰ๅพ…ไธŽๆœๅŠกๅ™จ้‡ๆ–ฐๅŒๆญฅ๏ผ", @@ -794,7 +794,7 @@ "Create account": "ๅˆ›ๅปบ่ดฆๆˆท", "Registration has been disabled on this homeserver.": "ๆญคไธปๆœๅŠกๅ™จๅทฒ็ฆๆญขๆณจๅ†Œใ€‚", "Unable to query for supported registration methods.": "ๆ— ๆณ•ๆŸฅ่ฏขๆ”ฏๆŒ็š„ๆณจๅ†Œๆ–นๆณ•ใ€‚", - "Keep going...": "่ฏท็ปง็ปญ...", + "Keep going...": "่ฏท็ปง็ปญโ€ฆโ€ฆ", "For maximum security, this should be different from your account password.": "ไธบ็กฎไฟๆœ€ๅคง็š„ๅฎ‰ๅ…จๆ€ง๏ผŒๅฎƒๅบ”่ฏฅไธŽไฝ ็š„่ดฆๆˆทๅฏ†็ ไธๅŒใ€‚", "That matches!": "ๅŒน้…ๆˆๅŠŸ๏ผ", "That doesn't match.": "ไธๅŒน้…ใ€‚", @@ -835,8 +835,8 @@ "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "ๆˆฟ้—ดๅŠ ๅฏ†ไธ€็ปๅฏ็”จ๏ผŒไพฟๆ— ๆณ•็ฆ็”จใ€‚ๅœจๅŠ ๅฏ†ๆˆฟ้—ดไธญ๏ผŒๅ‘้€็š„ๆถˆๆฏๆ— ๆณ•่ขซๆœๅŠกๅ™จ็œ‹ๅˆฐ๏ผŒๅช่ƒฝ่ขซๆˆฟ้—ด็š„ๅ‚ไธŽ่€…็œ‹ๅˆฐใ€‚ๅฏ็”จๅŠ ๅฏ†ๅฏ่ƒฝไผšไฝฟ่ฎธๅคšๆœบๅ™จไบบๅ’ŒๆกฅๆŽฅๆ— ๆณ•ๆญฃๅธธ่ฟไฝœใ€‚ ่ฏฆ็ป†ไบ†่งฃๅŠ ๅฏ†ใ€‚", "Power level": "ๆƒๅŠ›็บงๅˆซ", "Warning: Upgrading a room will not automatically migrate room members to the new version of the room. We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "่ญฆๅ‘Š๏ผšๅ‡็บงๆˆฟ้—ด ไธไผš่‡ชๅŠจๅฐ†ๆˆฟ้—ดๆˆๅ‘˜่ฝฌ็งปๅˆฐๆ–ฐ็‰ˆๆˆฟ้—ดไธญใ€‚ ๆˆ‘ไปฌๅฐ†ไผšๅœจๆ—ง็‰ˆๆˆฟ้—ดไธญๅ‘ๅธƒไธ€ไธชๆ–ฐ็‰ˆๆˆฟ้—ด็š„้“พๆŽฅโ€”โ€”ๆˆฟ้—ดๆˆๅ‘˜ๅฟ…้กป็‚นๅ‡ปๆญค้“พๆŽฅไปฅๅŠ ๅ…ฅๆ–ฐๆˆฟ้—ดใ€‚", - "Adds a custom widget by URL to the room": "้€š่ฟ‡้“พๆŽฅไธบๆˆฟ้—ดๆทปๅŠ ่‡ชๅฎšไน‰ๆŒ‚ไปถ", - "Please supply a https:// or http:// widget URL": "่ฏทๆไพ›ไธ€ไธช https:// ๆˆ– http:// ๅฝขๅผ็š„ๆ’ไปถ", + "Adds a custom widget by URL to the room": "้€š่ฟ‡URLๆทปๅŠ ่‡ชๅฎšไน‰ๆŒ‚ไปถๅˆฐๆˆฟ้—ด", + "Please supply a https:// or http:// widget URL": "่ฏทๆไพ›ไธ€ไธช https:// ๆˆ– http:// ๆŒ‚ไปถURL", "You cannot modify widgets in this room.": "ไฝ ๆ— ๆณ•ไฟฎๆ”นๆญคๆˆฟ้—ด็š„ๆ’ไปถใ€‚", "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s ๆ’ค้”€ไบ†ๅฏน %(targetDisplayName)s ๅŠ ๅ…ฅๆˆฟ้—ด็š„้‚€่ฏทใ€‚", "Upgrade this room to the recommended room version": "ๅ‡็บงๆญคๆˆฟ้—ด่‡ณๆŽจ่็‰ˆๆœฌ", @@ -883,7 +883,7 @@ "Use your account or create a new one to continue.": "ไฝฟ็”จๅทฒๆœ‰่ดฆๆˆทๆˆ–ๅˆ›ๅปบไธ€ไธชๆ–ฐ่ดฆๆˆทใ€‚", "Create Account": "ๅˆ›ๅปบ่ดฆๆˆท", "Sign In": "็™ปๅฝ•", - "Custom (%(level)s)": "่ฎฟๅฎข(%(level)s)", + "Custom (%(level)s)": "่‡ชๅฎšไน‰๏ผˆ%(level)s๏ผ‰", "Messages": "ไฟกๆฏ", "Actions": "ๅŠจไฝœ", "Sends a message as plain text, without interpreting it as markdown": "ไปฅ็บฏๆ–‡ๆœฌๅฝขๅผๅ‘้€ๆถˆๆฏ๏ผŒไธๅฐ†ๅ…ถไฝœไธบ markdown ๅค„็†", @@ -899,7 +899,7 @@ "Use an identity server to invite by email. Manage in Settings.": "ไฝฟ็”จ่บซไปฝๆœๅŠกๅ™จไปฅ้€š่ฟ‡็”ตๅญ้‚ฎไปถ้‚€่ฏทๅ…ถไป–็”จๆˆทใ€‚ๅœจ่ฎพ็ฝฎไธญ่ฟ›่กŒ็ฎก็†ใ€‚", "Unbans user with given ID": "ๆŒ‰็…ง ID ่งฃๅฐ็”จๆˆท", "Could not find user in room": "ๆˆฟ้—ดไธญๆ— ็”จๆˆท", - "Please supply a widget URL or embed code": "่ฏทๆไพ›ไธ€ไธชๆ’ไปถๆˆ–ๅตŒๅ…ฅไปฃ็ ", + "Please supply a widget URL or embed code": "่ฏทๆไพ›ไธ€ไธชๆŒ‚ไปถURLๆˆ–ๅตŒๅ…ฅไปฃ็ ", "Verifies a user, session, and pubkey tuple": "้ชŒ่ฏ็”จๆˆทใ€ไผš่ฏๅ’Œๅ…ฌ้’ฅๅ…ƒ็ป„", "Session already verified!": "ไผš่ฏๅทฒ้ชŒ่ฏ๏ผ", "WARNING: Session already verified, but keys do NOT MATCH!": "่ญฆๅ‘Š๏ผšไผš่ฏๅทฒ้ชŒ่ฏ๏ผŒไฝ†ๅฏ†้’ฅไธๅŒน้…๏ผ", @@ -946,7 +946,7 @@ "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s(%(userId)s)็™ปๅฝ•ๅˆฐๆœช้ชŒ่ฏ็š„ๆ–ฐไผš่ฏ๏ผš", "Ask this user to verify their session, or manually verify it below.": "่ฆๆฑ‚ๆญค็”จๆˆท้ชŒ่ฏๅ…ถไผš่ฏ๏ผŒๆˆ–ๅœจไธ‹้ขๆ‰‹ๅŠจ่ฟ›่กŒ้ชŒ่ฏใ€‚", "Not Trusted": "ไธๅฏไฟกไปป", - "Manually Verify by Text": "ๆ‰‹ๅŠจ้ชŒ่ฏๆ–‡ๅญ—", + "Manually Verify by Text": "็”จๆ–‡ๆœฌๆ‰‹ๅŠจ้ชŒ่ฏ", "Interactively verify by Emoji": "้€š่ฟ‡่กจๆƒ…็ฌฆๅท่ฟ›่กŒไบคไบ’ๅผ้ชŒ่ฏ", "Done": "ๅฎŒๆˆ", "Cannot reach homeserver": "ๆ— ๆณ•่ฟžๆŽฅๅˆฐไธปๆœๅŠกๅ™จ", @@ -996,7 +996,7 @@ "Call ended": "้€š่ฏ็ป“ๆŸ", "You started a call": "ไฝ ๅผ€ๅง‹ไบ†้€š่ฏ", "%(senderName)s started a call": "%(senderName)sๅผ€ๅง‹ไบ†้€š่ฏ", - "Waiting for answer": "็ญ‰ๅพ…ๆŽฅๅฌ", + "Waiting for answer": "ๆญฃๅœจ็ญ‰ๅพ…ๆŽฅๅฌ", "%(senderName)s is calling": "%(senderName)sๆญฃๅœจ้€š่ฏ", "Support adding custom themes": "ๆ”ฏๆŒๆทปๅŠ ่‡ชๅฎšไน‰ไธป้ข˜", "Font size": "ๅญ—ไฝ“ๅคงๅฐ", @@ -1017,8 +1017,8 @@ "My Ban List": "ๆˆ‘็š„ๅฐ็ฆๅˆ—่กจ", "This is your list of users/servers you have blocked - don't leave the room!": "่ฟ™ๆ˜ฏไฝ ๅฑ่”ฝ็š„็”จๆˆท/ๆœๅŠกๅ™จ็š„ๅˆ—่กจโ€”โ€”ไธ่ฆ็ฆปๅผ€ๆญคๆˆฟ้—ด๏ผ", "Unknown caller": "ๆœช็Ÿฅๆฅ็”ตไบบ", - "Waiting for %(displayName)s to verifyโ€ฆ": "็ญ‰ๅพ… %(displayName)s ่ฟ›่กŒ้ชŒ่ฏโ€ฆ", - "Cancellingโ€ฆ": "ๆญฃๅœจๅ–ๆถˆโ€ฆ", + "Waiting for %(displayName)s to verifyโ€ฆ": "ๆญฃๅœจ็ญ‰ๅพ…%(displayName)s่ฟ›่กŒ้ชŒ่ฏโ€ฆโ€ฆ", + "Cancellingโ€ฆ": "ๆญฃๅœจๅ–ๆถˆโ€ฆโ€ฆ", "They match": "ๅฎƒไปฌๅŒน้…", "They don't match": "ๅฎƒไปฌไธๅŒน้…", "To be secure, do this in person or use a trusted way to communicate.": "ไธบไบ†ๅฎ‰ๅ…จ๏ผŒ่ฏทๅฝ“้ขๅฎŒๆˆๆˆ–ไฝฟ็”จไฟกไปป็š„ๆ–นๆณ•ไบคๆตใ€‚", @@ -1096,7 +1096,7 @@ "Custom font size can only be between %(min)s pt and %(max)s pt": "่‡ชๅฎšไน‰ๅญ—ไฝ“ๅคงๅฐๅช่ƒฝไป‹ไบŽ %(min)s pt ๅ’Œ %(max)s pt ไน‹้—ด", "Error downloading theme information.": "ไธ‹่ฝฝไธป้ข˜ไฟกๆฏๆ—ถๅ‘็”Ÿ้”™่ฏฏใ€‚", "Theme added!": "ไธป้ข˜ๅทฒๆทปๅŠ ๏ผ", - "Custom theme URL": "่‡ชๅฎšไน‰ไธป้ข˜้“พๆŽฅ", + "Custom theme URL": "่‡ชๅฎšไน‰ไธป้ข˜URL", "Add theme": "ๆทปๅŠ ไธป้ข˜", "Message layout": "ไฟกๆฏๅธƒๅฑ€", "Modern": "็Žฐไปฃ", @@ -1145,7 +1145,7 @@ "Sounds": "ๅฃฐ้Ÿณ", "Notification sound": "้€š็Ÿฅๅฃฐ้Ÿณ", "Reset": "้‡็ฝฎ", - "Set a new custom sound": "ไฝฟ็”จๆ–ฐ็š„่‡ชๅฎšไน‰ๅฃฐ้Ÿณ", + "Set a new custom sound": "่ฎพ็ฝฎๆ–ฐ็š„่‡ชๅฎšไน‰ๅฃฐ้Ÿณ", "Browse": "ๆต่งˆ", "Upgrade the room": "ๆ›ดๆ–ฐๆˆฟ้—ด", "Enable room encryption": "ๅฏ็”จๆˆฟ้—ดๅŠ ๅฏ†", @@ -1223,7 +1223,7 @@ "Room %(name)s": "ๆˆฟ้—ด %(name)s", "No recently visited rooms": "ๆฒกๆœ‰ๆœ€่ฟ‘่ฎฟ้—ฎ่ฟ‡็š„ๆˆฟ้—ด", "People": "่”็ณปไบบ", - "Joining room โ€ฆ": "ๆญฃๅœจๅŠ ๅ…ฅๆˆฟ้—ดโ€ฆ", + "Joining room โ€ฆ": "ๆญฃๅœจๅŠ ๅ…ฅๆˆฟ้—ดโ€ฆโ€ฆ", "Loading โ€ฆ": "ๆญฃๅœจๅŠ ่ฝฝโ€ฆโ€ฆ", "Rejecting invite โ€ฆ": "ๆญฃๅœจๆ‹’็ป้‚€่ฏทโ€ฆโ€ฆ", "Join the conversation with an account": "ไฝฟ็”จไธ€ไธช่ดฆๆˆทๅŠ ๅ…ฅๅฏน่ฏ", @@ -1294,7 +1294,7 @@ "New published address (e.g. #alias:server)": "ๆ–ฐ็š„ๅ‘ๅธƒ็š„ๅœฐๅ€๏ผˆไพ‹ๅฆ‚ #alias:server๏ผ‰", "Local Addresses": "ๆœฌๅœฐๅœฐๅ€", "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "ไธบๆญคๆˆฟ้—ด่ฎพ็ฝฎๅœฐๅ€ไปฅไพฟ็”จๆˆท้€š่ฟ‡ไฝ ็š„ไธปๆœๅŠกๅ™จ๏ผˆ%(localDomain)s๏ผ‰ๆ‰พๅˆฐๆญคๆˆฟ้—ด", - "Waiting for %(displayName)s to acceptโ€ฆ": "็ญ‰ๅพ…%(displayName)sๆŽฅๅ—โ€ฆโ€ฆ", + "Waiting for %(displayName)s to acceptโ€ฆ": "ๆญฃๅœจ็ญ‰ๅพ…%(displayName)sๆŽฅๅ—โ€ฆโ€ฆ", "Acceptingโ€ฆ": "ๆญฃๅœจๆŽฅๅ—โ€ฆโ€ฆ", "Start Verification": "ๅผ€ๅง‹้ชŒ่ฏ", "Messages in this room are end-to-end encrypted.": "ๆญคๆˆฟ้—ดๅ†…็š„ๆถˆๆฏๆ˜ฏ็ซฏๅฏน็ซฏๅŠ ๅฏ†็š„ใ€‚", @@ -2198,7 +2198,7 @@ "Decrypted event source": "่งฃๅฏ†็š„ไบ‹ไปถๆบ็ ", "Original event source": "ๅŽŸๅง‹ไบ‹ไปถๆบ็ ", "Invite by username": "ๆŒ‰็…ง็”จๆˆทๅ้‚€่ฏท", - "Inviting...": "ๆญฃๅœจ้‚€่ฏทโ€ฆ", + "Inviting...": "ๆญฃๅœจ้‚€่ฏทโ€ฆโ€ฆ", "Welcome to ": "ๆฌข่ฟŽๆฅๅˆฐ ", "Share %(name)s": "ๅˆ†ไบซ %(name)s", "Add a topic to help people know what it is about.": "ๆทปๅŠ ่ฏ้ข˜๏ผŒ่ฎฉๅคงๅฎถ็Ÿฅ้“่ฟ™้‡Œๆ˜ฏ่ฎจ่ฎบไป€ไนˆ็š„ใ€‚", @@ -2306,7 +2306,7 @@ "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.": "่ฟ™้€šๅธธไป…ๅฝฑๅ“ๆœๅŠกๅ™จๅฆ‚ไฝ•ๅค„็†ๆˆฟ้—ดใ€‚ๅฆ‚ๆžœไฝ ็š„ %(brand)s ้‡ๅˆฐ้—ฎ้ข˜๏ผŒ่ฏทๅ›žๆŠฅ้”™่ฏฏใ€‚", "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.": "่ฏทๆณจๆ„๏ผŒๅฆ‚ๆžœไฝ ไธๆทปๅŠ ็”ตๅญ้‚ฎ็ฎฑๅนถไธ”ๅฟ˜่ฎฐๅฏ†็ ๏ผŒไฝ ๅฐ†ๆฐธ่ฟœๅคฑๅŽปๅฏนไฝ ่ดฆๆˆท็š„่ฎฟ้—ฎๆƒใ€‚", "Continuing without email": "ไธไฝฟ็”จ็”ตๅญ้‚ฎ็ฎฑๅนถ็ปง็ปญ", - "Data on this screen is shared with %(widgetDomain)s": "ๅœจๆญค็”ป้ขไธŠ็š„่ต„ๆ–™ไผšไธŽ %(widgetDomain)s ๅˆ†ไบซ", + "Data on this screen is shared with %(widgetDomain)s": "ๆญคๅฑๅน•ไธŠ็š„ๆ•ฐๆฎไธŽ%(widgetDomain)sๅˆ†ไบซ", "Consult first": "ๅ…ˆ่ฏข้—ฎ", "Invited people will be able to read old messages.": "่ขซ้‚€่ฏท็š„ไบบๅฐ†่ƒฝๅคŸ้˜…่ฏป่ฟ‡ๅŽป็š„ๆถˆๆฏใ€‚", "Invite someone using their name, username (like ) or share this room.": "ไฝฟ็”จๆŸไบบ็š„ๅๅญ—ใ€็”จๆˆทๅ๏ผˆๅฆ‚ ๏ผ‰ๆˆ–ๅˆ†ไบซๆญคๆˆฟ้—ดๆฅ้‚€่ฏทไป–ไปฌใ€‚", @@ -2498,7 +2498,7 @@ "Collapse": "ๆŠ˜ๅ ", "Expand": "ๅฑ•ๅผ€", "Recommended for public spaces.": "ๅปบ่ฎฎ็”จไบŽๅ…ฌๅผ€็ฉบ้—ดใ€‚", - "Allow people to preview your space before they join.": "ๅ…่ฎธๅœจๅŠ ๅ…ฅๅ‰้ข„่งˆไฝ ็š„็ฉบ้—ดใ€‚", + "Allow people to preview your space before they join.": "ๅ…่ฎธไบบไปฌๅœจๅŠ ๅ…ฅๅ‰้ข„่งˆไฝ ็š„็ฉบ้—ดใ€‚", "Preview Space": "้ข„่งˆ็ฉบ้—ด", "Decide who can view and join %(spaceName)s.": "ๅ†ณๅฎš่ฐๅฏไปฅๆŸฅ็œ‹ๅ’ŒๅŠ ๅ…ฅ %(spaceName)sใ€‚", "Visibility": "ๅฏ่งๆ€ง", @@ -2528,7 +2528,7 @@ "%(senderName)s removed their profile picture": "%(senderName)s ๅทฒ็งป้™คไป–ไปฌ็š„่ต„ๆ–™ๅ›พ็‰‡", "%(senderName)s removed their display name (%(oldDisplayName)s)": "%(senderName)s ๅทฒๅฐ†ไป–ไปฌ็š„ๆ˜ต็งฐ็งป้™ค๏ผˆ%(oldDisplayName)s๏ผ‰", "%(senderName)s set their display name to %(displayName)s": "%(senderName)s ๅทฒๅฐ†ไป–ไปฌ็š„ๆ˜ต็งฐ่ฎพ็ฝฎไธบ %(displayName)s", - "%(oldDisplayName)s changed their display name to %(displayName)s": "%(oldDisplayName)s ๅทฒๅฐ†ไป–ไปฌ็š„ๆ˜ต็งฐๆ›ดๆ”นไธบ %(displayName)s", + "%(oldDisplayName)s changed their display name to %(displayName)s": "%(oldDisplayName)sๅฐ†ๅ…ถๆ˜พ็คบๅ็งฐๆ”นไธบ%(displayName)s", "%(senderName)s banned %(targetName)s": "%(senderName)s ๅทฒๅฐ็ฆ %(targetName)s", "%(senderName)s banned %(targetName)s: %(reason)s": "%(senderName)s ๅทฒๅฐ็ฆ %(targetName)s๏ผš %(reason)s", "%(senderName)s invited %(targetName)s": "%(senderName)s ๅทฒ้‚€่ฏท %(targetName)s", @@ -2876,7 +2876,7 @@ "Files": "ๆ–‡ไปถ", "You won't get any notifications": "ไฝ ไธไผšๆ”ถๅˆฐไปปไฝ•้€š็Ÿฅ", "Get notified only with mentions and keywords as set up in your settings": "ๅฆ‚่ฎพ็ฝฎไธญ่ฎพๅฎš็š„้‚ฃๆ ทไป…้€š็ŸฅๆๅŠๅ’Œๅ…ณ้”ฎ่ฏ", - "@mentions & keywords": "@ๆๅŠ & ๅ…ณ้”ฎ่ฏ", + "@mentions & keywords": "@ๆๅŠๅ’Œๅ…ณ้”ฎ่ฏ", "Get notified for every message": "่Žทๅพ—ๆฏๆกๆถˆๆฏ็š„้€š็Ÿฅ", "Get notifications as set up in your settings": "ๅฆ‚่ฎพ็ฝฎไธญ่ฎพๅฎš็š„้‚ฃๆ ท่Žทๅ–้€š็Ÿฅ", "sends rainfall": "ๅ‘้€้™้›จ", @@ -3339,5 +3339,34 @@ "Set up your profile": "่ฎพ็ฝฎไฝ ็š„็”จๆˆท่ต„ๆ–™", "Download apps": "ไธ‹่ฝฝๅบ”็”จ", "Download Element": "ไธ‹่ฝฝElement", - "Help": "ๅธฎๅŠฉ" + "Help": "ๅธฎๅŠฉ", + "Results are only revealed when you end the poll": "็ป“ๆžœไป…ๅœจไฝ ็ป“ๆŸๆŠ•็ฅจๅŽๅฑ•็คบ", + "Voters see results as soon as they have voted": "ๆŠ•็ฅจ่€…ไธ€ๆŠ•ๅฎŒ็ฅจๅฐฑ่ƒฝ็œ‹ๅˆฐ็ป“ๆžœ", + "Closed poll": "ๅฐ้—ญๅผๆŠ•็ฅจ", + "Open poll": "ๅผ€ๆ”พๅผๆŠ•็ฅจ", + "Poll type": "ๆŠ•็ฅจ็ฑปๅž‹", + "App Storeยฎ and the Apple logoยฎ are trademarks of Apple Inc.": "App Storeยฎๅ’ŒApple logoยฎๆ˜ฏApple Inc.็š„ๅ•†ๆ ‡", + "Google Play and the Google Play logo are trademarks of Google LLC.": "Google PlayๅŠๅ…ถlogoๆ˜ฏGoogle LLC็š„ๅ•†ๆ ‡ใ€‚", + "Community ownership": "็คพ็พคๆ‰€ๆœ‰ๆƒ", + "With free end-to-end encrypted messaging, and unlimited voice and video calls, %(brand)s is a great way to stay in touch.": "%(brand)sๆไพ›ๅ…่ดน็š„็ซฏๅˆฐ็ซฏๅŠ ๅฏ†ๆถˆๆฏไผ ้€’ไปฅๅŠๆ— ้™ๅˆถ็š„่ฏญ้Ÿณๅ’Œ่ง†้ข‘้€š่ฏ๏ผŒๆ˜ฏไฟๆŒ่”็ณป็š„็ปไฝณๆ–นๅผใ€‚", + "Weโ€™d appreciate any feedback on how youโ€™re finding Element.": "ๅฏนไบŽๆ‚จๅฆ‚ไฝ•ๆ‰พๅˆฐElement็š„ไปปไฝ•ๅ้ฆˆ๏ผŒๆˆ‘ไปฌๅฐ†ไธ่ƒœๆ„Ÿๆฟ€ใ€‚", + "Donโ€™t miss a reply or important message": "ไธ่ฆ้”™่ฟ‡ๅ›žๅคๆˆ–้‡่ฆๆถˆๆฏ", + "Make sure people know itโ€™s really you": "็กฎไฟไบบไปฌ็Ÿฅ้“่ฟ™็œŸ็š„ๆ˜ฏไฝ ", + "Donโ€™t miss a thing by taking Element with you": "้š่บซๆบๅธฆElement๏ผŒไธ่ฆ้”™่ฟ‡ไปปไฝ•ไบ‹", + "Find and invite your community members": "ๅ‘็Žฐๅนถ้‚€่ฏทไฝ ็š„็คพ็พคๆˆๅ‘˜", + "Find people": "ๆ‰พไบบ", + "Find and invite your co-workers": "ๅ‘็Žฐๅนถ้‚€่ฏทไฝ ็š„ๅŒไบ‹", + "Find friends": "ๅ‘็Žฐๆœ‹ๅ‹", + "Find and invite your friends": "ๅ‘็Žฐๅนถ้‚€่ฏทไฝ ็š„ๆœ‹ๅ‹", + "Find your people": "ๅฏปๆ‰พไฝ ็š„ไบบ", + "Welcome to %(brand)s": "ๆฌข่ฟŽๆฅๅˆฐ%(brand)s", + "Only %(count)s steps to go|other": "ไป…้œ€%(count)sๆญฅ", + "Only %(count)s steps to go|one": "ไป…้œ€%(count)sๆญฅ", + "How are you finding Element so far?": "ไฝ ๆ˜ฏๅฆ‚ไฝ•ๅ‘็ŽฐElement็š„๏ผŸ", + "Download %(brand)s": "ไธ‹่ฝฝ%(brand)s", + "Download %(brand)s Desktop": "ไธ‹่ฝฝ%(brand)sๆกŒ้ข็‰ˆ", + "Download on the App Store": "ๅœจApp Storeไธ‹่ฝฝ", + "Send read receipts": "ๅ‘้€ๅทฒ่ฏปๅ›žๆ‰ง", + "Share your activity and status with others.": "ไธŽๅˆซไบบๅˆ†ไบซไฝ ็š„ๆดปๅŠจๅ’Œ็Šถๆ€ใ€‚", + "Your server doesn't support disabling sending read receipts.": "ไฝ ็š„ๆœๅŠกๅ™จไธๆ”ฏๆŒ็ฆ็”จๅ‘้€ๅทฒ่ฏปๅ›žๆ‰งใ€‚" } diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 06dac88d787..e17feb2b9a9 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -264,7 +264,7 @@ "Reject all %(invitedRooms)s invites": "ๆ‹’็ต•ๆ‰€ๆœ‰ %(invitedRooms)s ้‚€่ซ‹", "Failed to invite": "้‚€่ซ‹ๅคฑๆ•—", "Confirm Removal": "็ขบ่ช็งป้™ค", - "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "ๆ‚จ็ขบๅฎšๆ‚จๆƒณ่ฆ็งป้™ค๏ผˆๅˆช้™ค๏ผ‰ๆญคๆดปๅ‹•ๅ—Ž๏ผŸๆณจๆ„่‹ฅๆ‚จๅˆช้™คๆˆฟ้–“ๅ็จฑๆˆ–ไธป้กŒ่ฎŠๆ›ด๏ผŒ้‚„ๆ˜ฏๅฏไปฅๅพฉๅŽŸ่ฎŠๆ›ดใ€‚", + "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "ๆ‚จ็ขบๅฎšๆ‚จๆƒณ่ฆ็งป้™ค๏ผˆๅˆช้™ค๏ผ‰ๆญคๆดปๅ‹•ๅ—Ž๏ผŸๆณจๆ„๏ผŒ่‹ฅๆ‚จๅˆช้™คๆˆฟ้–“ๅ็จฑๆˆ–ไธป้กŒ็š„่ฎŠๆ›ด๏ผŒ่ฎŠๆ›ดๆœƒ่ขซๅพฉๅŽŸใ€‚", "Unknown error": "ๆœช็Ÿฅ็š„้Œฏ่ชค", "Incorrect password": "ไธๆญฃ็ขบ็š„ๅฏ†็ขผ", "Unable to restore session": "็„กๆณ•ๅพฉๅŽŸๅทฅไฝœ้šŽๆฎต", @@ -3129,7 +3129,7 @@ "Results are only revealed when you end the poll": "็ตๆžœๅƒ…ๅœจๆ‚จ็ตๆŸๆŠ•็ฅจๆ™‚้กฏ็คบ", "Voters see results as soon as they have voted": "ๆŠ•็ฅจ่€…ๅฏไปฅๅœจๆŠ•็ฅจๅพŒ็ซ‹ๅˆป็œ‹ๅˆฐ็ตๆžœ", "Open poll": "้–‹ๆ”พๆŠ•็ฅจ", - "Closed poll": "ๅทฒ้—œ้–‰็š„ๆŠ•็ฅจ", + "Closed poll": "ๅฐ้—ญๅผๆŠ•็ฅจ", "Poll type": "ๆŠ•็ฅจ้กžๅž‹", "Results will be visible when the poll is ended": "็ตๆžœๅฐ‡ๅœจๆŠ•็ฅจ็ตๆŸๆ™‚ๅฏ่ฆ‹", "Open user settings": "้–‹ๅ•Ÿไฝฟ็”จ่€…่จญๅฎš", @@ -3480,5 +3480,22 @@ "You made it!": "ๆ‚จๅšๅˆฐไบ†๏ผ", "Help": "่ชชๆ˜Ž", "Weโ€™d appreciate any feedback on how youโ€™re finding Element.": "ๅฐๆ–ผๆ‚จๅฆ‚ไฝ•ๆ‰พๅˆฐ Element ็š„ไปปไฝ•ๅ›ž้ฅ‹๏ผŒๆˆ‘ๅ€‘ๅฐ‡ไธๅ‹ๆ„Ÿๆฟ€ใ€‚", - "How are you finding Element so far?": "ๆ‚จๆ˜ฏๅฆ‚ไฝ•ๆ‰พๅˆฐ Element ็š„๏ผŸ" + "How are you finding Element so far?": "ๆ‚จๆ˜ฏๅฆ‚ไฝ•ๆ‰พๅˆฐ Element ็š„๏ผŸ", + "Google Play and the Google Play logo are trademarks of Google LLC.": "Google Play ่ˆ‡ Google Play logo ๆ˜ฏ Google ๅ…ฌๅธ็š„ๅ•†ๆจ™ใ€‚", + "App Storeยฎ and the Apple logoยฎ are trademarks of Apple Inc.": "App Storeยฎ ่ˆ‡ Apple logoยฎ ๆ˜ฏ่˜‹ๆžœๅ…ฌๅธ็š„ๅ•†ๆจ™ใ€‚", + "Get it on F-Droid": "ๅœจ F-Droid ไธŠๅ–ๅพ—", + "Get it on Google Play": "ๅœจ Google Play ไธŠๅ–ๅพ—", + "Android": "Android", + "Download on the App Store": "ๅœจ App Store ไธŠไธ‹่ผ‰", + "iOS": "iOS", + "Download %(brand)s Desktop": "ไธ‹่ผ‰ %(brand)s ๆกŒ้ข็‰ˆ", + "Download %(brand)s": "ไธ‹่ผ‰ %(brand)s", + "We're creating a room with %(names)s": "ๆˆ‘ๅ€‘ๆญฃๅœจๅปบ็ซ‹ไธ€ๅ€‹ๅŒ…ๅซ %(names)s ็š„่Šๅคฉๅฎค", + "Your server doesn't support disabling sending read receipts.": "ๆ‚จ็š„ไผบๆœๅ™จไธๆ”ฏๆดๅœ็”จๅ‚ณ้€่ฎ€ๅ–ๅ›žๆขใ€‚", + "Share your activity and status with others.": "่ˆ‡ไป–ไบบๅˆ†ไบซๆ‚จ็š„ๆดปๅ‹•่ˆ‡็‹€ๆ…‹ใ€‚", + "Presence": "ๅœจๅ ด", + "Send read receipts": "ๅ‚ณ้€่ฎ€ๅ–ๅ›žๆข", + "Last activity": "ไธŠๆฌกๆดปๅ‹•", + "Sessions": "ๅทฅไฝœ้šŽๆฎต", + "Use new session manager (under active development)": "ไฝฟ็”จๆ–ฐ็š„ๅทฅไฝœ้šŽๆฎต็ฎก็†็จ‹ๅผ๏ผˆๆญฃๅœจ็ฉๆฅต้–‹็™ผไธญ๏ผ‰" } From 2cae2be90964f2779f0259335d8e0596be2e0ee5 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 9 Aug 2022 17:11:26 +0100 Subject: [PATCH 08/64] Upgrade matrix-js-sdk to 19.3.0-rc.1 --- package.json | 2 +- yarn.lock | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 933ccfd7ada..1bf41275ee4 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "maplibre-gl": "^1.15.2", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "^0.0.1-beta.7", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "19.3.0-rc.1", "matrix-widget-api": "^0.1.0-beta.18", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index f2f623b7edf..37037a01489 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6776,9 +6776,10 @@ matrix-events-sdk@^0.0.1-beta.7: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1-beta.7.tgz#5ffe45eba1f67cc8d7c2377736c728b322524934" integrity sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": - version "19.2.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/cf33569a2187628dd7954ac771995cce3e804af4" +matrix-js-sdk@19.3.0-rc.1: + version "19.3.0-rc.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-19.3.0-rc.1.tgz#dda9f140dca076a77c5c00315b53bf2d417e5946" + integrity sha512-sK5bdn0AClJD+Cgned/rHcqMNRy1HEDuU/4SiRFoO+ROhBnNTsN3L7nOosS318WTCCpTwu2OdJCcMSJ9BBridw== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From 8cf9b357f8d1eb69242c9290df465cd8c2d494c1 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 9 Aug 2022 17:14:58 +0100 Subject: [PATCH 09/64] Prepare changelog for v3.52.0-rc.1 --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c27429be7f3..cec9cf24ebf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ +Changes in [3.52.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.52.0-rc.1) (2022-08-09) +=============================================================================================================== + +## โœจ Features + * Device manager generic settings subsection component ([\#9147](https://github.com/matrix-org/matrix-react-sdk/pull/9147)). + * Migrate the hidden read receipts flag to new "send read receipts" option ([\#9141](https://github.com/matrix-org/matrix-react-sdk/pull/9141)). + * Live location sharing - share location at most every 5 seconds ([\#9148](https://github.com/matrix-org/matrix-react-sdk/pull/9148)). + * Increase max length of voice messages to 15m ([\#9133](https://github.com/matrix-org/matrix-react-sdk/pull/9133)). Fixes vector-im/element-web#18620. + * Move pin drop out of labs ([\#9135](https://github.com/matrix-org/matrix-react-sdk/pull/9135)). + * Device manager - New device tile info design ([\#9122](https://github.com/matrix-org/matrix-react-sdk/pull/9122)). + * Start DM on first message ([\#8612](https://github.com/matrix-org/matrix-react-sdk/pull/8612)). Fixes vector-im/element-web#14736. + * Remove "Add Space" button from RoomListHeader when user cannot create spaces ([\#9129](https://github.com/matrix-org/matrix-react-sdk/pull/9129)). + * The Welcome Home Screen: Dedicated Download Apps Dialog ([\#9120](https://github.com/matrix-org/matrix-react-sdk/pull/9120)). Fixes vector-im/element-web#22921. + * The Welcome Home Screen: "Submit Feedback" pane ([\#9090](https://github.com/matrix-org/matrix-react-sdk/pull/9090)). Fixes vector-im/element-web#22918. + * New User Onboarding Task List ([\#9083](https://github.com/matrix-org/matrix-react-sdk/pull/9083)). Fixes vector-im/element-web#22919. + * Add support for disabling spell checking ([\#8604](https://github.com/matrix-org/matrix-react-sdk/pull/8604)). Fixes vector-im/element-web#21901. + * Live location share - leave maximised map open when beacons expire ([\#9098](https://github.com/matrix-org/matrix-react-sdk/pull/9098)). + +## ๐Ÿ› Bug Fixes + * Fix pillification sometimes doubling up ([\#9152](https://github.com/matrix-org/matrix-react-sdk/pull/9152)). Fixes vector-im/element-web#23036. + * Use stable reference for active tab in tabbedView ([\#9145](https://github.com/matrix-org/matrix-react-sdk/pull/9145)). + * Fix composer padding ([\#9137](https://github.com/matrix-org/matrix-react-sdk/pull/9137)). Fixes vector-im/element-web#22992. + * Fix highlights not being applied to plaintext messages ([\#9126](https://github.com/matrix-org/matrix-react-sdk/pull/9126)). Fixes vector-im/element-web#22787. + * Fix dismissing edit composer when change was undone ([\#9109](https://github.com/matrix-org/matrix-react-sdk/pull/9109)). Fixes vector-im/element-web#22932. + * 1-to-1 DM rooms with bots now act like DM rooms instead of multi-user-rooms before ([\#9124](https://github.com/matrix-org/matrix-react-sdk/pull/9124)). Fixes vector-im/element-web#22894. + * Apply inline start padding to selected lines on modern layout only ([\#9006](https://github.com/matrix-org/matrix-react-sdk/pull/9006)). Fixes vector-im/element-web#22768. Contributed by @luixxiul. + * Peek into world-readable rooms from spotlight ([\#9115](https://github.com/matrix-org/matrix-react-sdk/pull/9115)). Fixes vector-im/element-web#22862. + * Use default styling on nested numbered lists due to MD being sensitive ([\#9110](https://github.com/matrix-org/matrix-react-sdk/pull/9110)). Fixes vector-im/element-web#22935. + * Fix replying using chat effect commands ([\#9101](https://github.com/matrix-org/matrix-react-sdk/pull/9101)). Fixes vector-im/element-web#22824. + Changes in [3.51.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.51.0) (2022-08-02) ===================================================================================================== From dcc12a142dff6796597fffea33496e4b130bcc09 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 9 Aug 2022 17:14:59 +0100 Subject: [PATCH 10/64] v3.52.0-rc.1 --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 1bf41275ee4..cf88aa102c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.51.0", + "version": "3.52.0-rc.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -23,7 +23,7 @@ "package.json", ".stylelintrc.js" ], - "main": "./src/index.ts", + "main": "./lib/index.ts", "matrix_src_main": "./src/index.ts", "matrix_lib_main": "./lib/index.ts", "matrix_lib_typings": "./lib/index.d.ts", @@ -252,5 +252,6 @@ "jestSonar": { "reportPath": "coverage", "sonar56x": true - } + }, + "typings": "./lib/index.d.ts" } From 394e181854947f0be1d9e5b8f83a1edf3040da4d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 9 Aug 2022 20:46:59 +0100 Subject: [PATCH 11/64] Define interface for RLS to ease wiring in Sliding Sync (#9150) * Define iface for RLS * Iterate interface --- src/@types/global.d.ts | 4 +- src/components/views/rooms/RoomSublist.tsx | 2 +- src/stores/room-list/Interface.ts | 107 ++++++++++++++++++ src/stores/room-list/RoomListStore.ts | 15 +-- test/components/views/rooms/RoomList-test.tsx | 4 +- 5 files changed, 118 insertions(+), 14 deletions(-) create mode 100644 src/stores/room-list/Interface.ts diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 7c595640fd0..00758371112 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -23,7 +23,7 @@ import ContentMessages from "../ContentMessages"; import { IMatrixClientPeg } from "../MatrixClientPeg"; import ToastStore from "../stores/ToastStore"; import DeviceListener from "../DeviceListener"; -import { RoomListStoreClass } from "../stores/room-list/RoomListStore"; +import { RoomListStore } from "../stores/room-list/Interface"; import { PlatformPeg } from "../PlatformPeg"; import RoomListLayoutStore from "../stores/room-list/RoomListLayoutStore"; import { IntegrationManagers } from "../integrations/IntegrationManagers"; @@ -79,7 +79,7 @@ declare global { mxContentMessages: ContentMessages; mxToastStore: ToastStore; mxDeviceListener: DeviceListener; - mxRoomListStore: RoomListStoreClass; + mxRoomListStore: RoomListStore; mxRoomListLayoutStore: RoomListLayoutStore; mxPlatformPeg: PlatformPeg; mxIntegrationManagers: typeof IntegrationManagers; diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index 677b63bcf95..bd09ecb4a01 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -372,7 +372,7 @@ export default class RoomSublist extends React.Component { }; private onTagSortChanged = async (sort: SortAlgorithm) => { - await RoomListStore.instance.setTagSorting(this.props.tagId, sort); + RoomListStore.instance.setTagSorting(this.props.tagId, sort); this.forceUpdate(); }; diff --git a/src/stores/room-list/Interface.ts b/src/stores/room-list/Interface.ts new file mode 100644 index 00000000000..ab538709896 --- /dev/null +++ b/src/stores/room-list/Interface.ts @@ -0,0 +1,107 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import type { Room } from "matrix-js-sdk/src/models/room"; +import type { EventEmitter } from "events"; +import { ITagMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models"; +import { RoomUpdateCause, TagID } from "./models"; +import { IFilterCondition } from "./filters/IFilterCondition"; + +export enum RoomListStoreEvent { + // The event/channel which is called when the room lists have been changed. + ListsUpdate = "lists_update", +} + +export interface RoomListStore extends EventEmitter { + /** + * Gets an ordered set of rooms for the all known tags. + * @returns {ITagMap} The cached list of rooms, ordered, + * for each tag. May be empty, but never null/undefined. + */ + get orderedLists(): ITagMap; + + /** + * Set the sort algorithm for the specified tag. + * @param tagId the tag to set the algorithm for + * @param sort the sort algorithm to set to + */ + setTagSorting(tagId: TagID, sort: SortAlgorithm): void; + + /** + * Get the sort algorithm for the specified tag. + * @param tagId tag to get the sort algorithm for + * @returns the sort algorithm + */ + getTagSorting(tagId: TagID): SortAlgorithm; + + /** + * Set the list algorithm for the specified tag. + * @param tagId the tag to set the algorithm for + * @param order the list algorithm to set to + */ + setListOrder(tagId: TagID, order: ListAlgorithm): void; + + /** + * Get the list algorithm for the specified tag. + * @param tagId tag to get the list algorithm for + * @returns the list algorithm + */ + getListOrder(tagId: TagID): ListAlgorithm; + + /** + * Regenerates the room whole room list, discarding any previous results. + * + * Note: This is only exposed externally for the tests. Do not call this from within + * the app. + * @param params.trigger Set to false to prevent a list update from being sent. Should only + * be used if the calling code will manually trigger the update. + */ + regenerateAllLists(params: { trigger: boolean }): void; + + /** + * Adds a filter condition to the room list store. Filters may be applied async, + * and thus might not cause an update to the store immediately. + * @param {IFilterCondition} filter The filter condition to add. + */ + addFilter(filter: IFilterCondition): Promise; + + /** + * Removes a filter condition from the room list store. If the filter was + * not previously added to the room list store, this will no-op. The effects + * of removing a filter may be applied async and therefore might not cause + * an update right away. + * @param {IFilterCondition} filter The filter condition to remove. + */ + removeFilter(filter: IFilterCondition): void; + + /** + * Gets the tags for a room identified by the store. The returned set + * should never be empty, and will contain DefaultTagID.Untagged if + * the store is not aware of any tags. + * @param room The room to get the tags for. + * @returns The tags for the room. + */ + getTagsForRoom(room: Room): TagID[]; + + /** + * Manually update a room with a given cause. This should only be used if the + * room list store would otherwise be incapable of doing the update itself. Note + * that this may race with the room list's regular operation. + * @param {Room} room The room to update. + * @param {RoomUpdateCause} cause The cause to update for. + */ + manualRoomUpdate(room: Room, cause: RoomUpdateCause): Promise; +} diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index c15567afdc8..9083943ed9f 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -37,18 +37,15 @@ import { RoomNotificationStateStore } from "../notifications/RoomNotificationSta import { VisibilityProvider } from "./filters/VisibilityProvider"; import { SpaceWatcher } from "./SpaceWatcher"; import { IRoomTimelineActionPayload } from "../../actions/MatrixActionCreators"; +import { RoomListStore as Interface, RoomListStoreEvent } from "./Interface"; interface IState { // state is tracked in underlying classes } -/** - * The event/channel which is called when the room lists have been changed. Raised - * with one argument: the instance of the store. - */ -export const LISTS_UPDATE_EVENT = "lists_update"; +export const LISTS_UPDATE_EVENT = RoomListStoreEvent.ListsUpdate; -export class RoomListStoreClass extends AsyncStoreWithClient { +export class RoomListStoreClass extends AsyncStoreWithClient implements Interface { /** * Set to true if you're running tests on the store. Should not be touched in * any other environment. @@ -365,7 +362,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { this.algorithm.updatesInhibited = false; } - public async setTagSorting(tagId: TagID, sort: SortAlgorithm) { + public setTagSorting(tagId: TagID, sort: SortAlgorithm) { this.setAndPersistTagSorting(tagId, sort); this.updateFn.trigger(); } @@ -602,9 +599,9 @@ export class RoomListStoreClass extends AsyncStoreWithClient { } export default class RoomListStore { - private static internalInstance: RoomListStoreClass; + private static internalInstance: Interface; - public static get instance(): RoomListStoreClass { + public static get instance(): Interface { if (!RoomListStore.internalInstance) { RoomListStore.internalInstance = new RoomListStoreClass(); } diff --git a/test/components/views/rooms/RoomList-test.tsx b/test/components/views/rooms/RoomList-test.tsx index 7b9d3ee3113..6fa3fe22cf4 100644 --- a/test/components/views/rooms/RoomList-test.tsx +++ b/test/components/views/rooms/RoomList-test.tsx @@ -137,7 +137,7 @@ describe('RoomList', () => { client.getRoom.mockImplementation((roomId) => roomMap[roomId]); // Now that everything has been set up, prepare and update the store - await RoomListStore.instance.makeReady(client); + await (RoomListStore.instance as RoomListStoreClass).makeReady(client); done(); }); @@ -150,7 +150,7 @@ describe('RoomList', () => { } await RoomListLayoutStore.instance.resetLayouts(); - await RoomListStore.instance.resetStore(); + await (RoomListStore.instance as RoomListStoreClass).resetStore(); done(); }); From 9ed555050185457f2dc80f452de534c30fc72146 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Wed, 10 Aug 2022 08:51:54 +0200 Subject: [PATCH 12/64] Implement GroupCallUtils (#9131) * Implement GroupCallUtils * Trigger CI * Use UnstableValue for new call event types * Implement PR feedback --- src/utils/GroupCallUtils.ts | 175 ++++++++ test/utils/GroupCallUtils-test.ts | 673 ++++++++++++++++++++++++++++++ 2 files changed, 848 insertions(+) create mode 100644 src/utils/GroupCallUtils.ts create mode 100644 test/utils/GroupCallUtils-test.ts diff --git a/src/utils/GroupCallUtils.ts b/src/utils/GroupCallUtils.ts new file mode 100644 index 00000000000..3af6a2b07a0 --- /dev/null +++ b/src/utils/GroupCallUtils.ts @@ -0,0 +1,175 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { EventTimeline, MatrixClient, MatrixEvent, RoomState } from "matrix-js-sdk/src/matrix"; +import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; +import { deepCopy } from "matrix-js-sdk/src/utils"; + +export const STUCK_DEVICE_TIMEOUT_MS = 1000 * 60 * 60; // 1 hour + +export const CALL_STATE_EVENT_TYPE = new UnstableValue("m.call", "org.matrix.msc3401.call"); +export const CALL_MEMBER_STATE_EVENT_TYPE = new UnstableValue("m.call.member", "org.matrix.msc3401.call.member"); +const CALL_STATE_EVENT_TERMINATED = "m.terminated"; + +interface MDevice { + ["m.device_id"]: string; +} + +interface MCall { + ["m.call_id"]: string; + ["m.devices"]: Array; +} + +interface MCallMemberContent { + ["m.expires_ts"]: number; + ["m.calls"]: Array; +} + +const getRoomState = (client: MatrixClient, roomId: string): RoomState => { + return client.getRoom(roomId) + ?.getLiveTimeline() + ?.getState?.(EventTimeline.FORWARDS); +}; + +/** + * Returns all room state events for the stable and unstable type value. + */ +const getRoomStateEvents = ( + client: MatrixClient, + roomId: string, + type: UnstableValue, +): MatrixEvent[] => { + const roomState = getRoomState(client, roomId); + if (!roomState) return []; + + return [ + ...roomState.getStateEvents(type.name), + ...roomState.getStateEvents(type.altName), + ]; +}; + +/** + * Finds the latest, non-terminated call state event. + */ +export const getGroupCall = (client: MatrixClient, roomId: string): MatrixEvent => { + return getRoomStateEvents(client, roomId, CALL_STATE_EVENT_TYPE) + .sort((a: MatrixEvent, b: MatrixEvent) => b.getTs() - a.getTs()) + .find((event: MatrixEvent) => { + return !(CALL_STATE_EVENT_TERMINATED in event.getContent()); + }); +}; + +/** + * Finds the "m.call.member" events for an "m.call" event. + * + * @returns {MatrixEvent[]} non-expired "m.call.member" events for the call + */ +export const useConnectedMembers = (client: MatrixClient, callEvent: MatrixEvent): MatrixEvent[] => { + if (!CALL_STATE_EVENT_TYPE.matches(callEvent.getType())) return []; + + const callId = callEvent.getStateKey(); + const now = Date.now(); + + return getRoomStateEvents(client, callEvent.getRoomId(), CALL_MEMBER_STATE_EVENT_TYPE) + .filter((callMemberEvent: MatrixEvent): boolean => { + const { + ["m.expires_ts"]: expiresTs, + ["m.calls"]: calls, + } = callMemberEvent.getContent(); + + // state event expired + if (expiresTs && expiresTs < now) return false; + + return !!calls?.find((call: MCall) => call["m.call_id"] === callId); + }) || []; +}; + +/** + * Removes a list of devices from a call. + * Only works for the current user's devices. + */ +const removeDevices = async (client: MatrixClient, callEvent: MatrixEvent, deviceIds: string[]): Promise => { + if (!CALL_STATE_EVENT_TYPE.matches(callEvent.getType())) return; + + const roomId = callEvent.getRoomId(); + const roomState = getRoomState(client, roomId); + if (!roomState) return; + + const callMemberEvent = roomState.getStateEvents(CALL_MEMBER_STATE_EVENT_TYPE.name, client.getUserId()) + ?? roomState.getStateEvents(CALL_MEMBER_STATE_EVENT_TYPE.altName, client.getUserId()); + const callMemberEventContent = callMemberEvent?.getContent(); + if ( + !Array.isArray(callMemberEventContent?.["m.calls"]) + || callMemberEventContent?.["m.calls"].length === 0 + ) { + return; + } + + // copy the content to prevent mutations + const newContent = deepCopy(callMemberEventContent); + const callId = callEvent.getStateKey(); + let changed = false; + + newContent["m.calls"].forEach((call: MCall) => { + // skip other calls + if (call["m.call_id"] !== callId) return; + + call["m.devices"] = call["m.devices"]?.filter((device: MDevice) => { + if (deviceIds.includes(device["m.device_id"])) { + changed = true; + return false; + } + + return true; + }); + }); + + if (changed) { + // only send a new state event if there has been a change + newContent["m.expires_ts"] = Date.now() + STUCK_DEVICE_TIMEOUT_MS; + await client.sendStateEvent( + roomId, + CALL_MEMBER_STATE_EVENT_TYPE.name, + newContent, + client.getUserId(), + ); + } +}; + +/** + * Removes the current device from a call. + */ +export const removeOurDevice = async (client: MatrixClient, callEvent: MatrixEvent) => { + return removeDevices(client, callEvent, [client.getDeviceId()]); +}; + +/** + * Removes all devices of the current user that have not been seen within the STUCK_DEVICE_TIMEOUT_MS. + * Does per default not remove the current device unless includeCurrentDevice is true. + * + * @param {boolean} includeCurrentDevice - Whether to include the current device of this session here. + */ +export const fixStuckDevices = async (client: MatrixClient, callEvent: MatrixEvent, includeCurrentDevice: boolean) => { + const now = Date.now(); + const { devices: myDevices } = await client.getDevices(); + const currentDeviceId = client.getDeviceId(); + const devicesToBeRemoved = myDevices.filter(({ last_seen_ts: lastSeenTs, device_id: deviceId }) => { + return lastSeenTs + && (deviceId !== currentDeviceId || includeCurrentDevice) + && (now - lastSeenTs) > STUCK_DEVICE_TIMEOUT_MS; + }).map(d => d.device_id); + return removeDevices(client, callEvent, devicesToBeRemoved); +}; diff --git a/test/utils/GroupCallUtils-test.ts b/test/utils/GroupCallUtils-test.ts new file mode 100644 index 00000000000..971527e8037 --- /dev/null +++ b/test/utils/GroupCallUtils-test.ts @@ -0,0 +1,673 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { mocked } from "jest-mock"; +import { IMyDevice, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; + +import { + CALL_MEMBER_STATE_EVENT_TYPE, + CALL_STATE_EVENT_TYPE, + fixStuckDevices, + getGroupCall, + removeOurDevice, + STUCK_DEVICE_TIMEOUT_MS, + useConnectedMembers, +} from "../../src/utils/GroupCallUtils"; +import { createTestClient, mkEvent } from "../test-utils"; + +[ + { + callStateEventType: CALL_STATE_EVENT_TYPE.name, + callMemberStateEventType: CALL_MEMBER_STATE_EVENT_TYPE.name, + }, + { + callStateEventType: CALL_STATE_EVENT_TYPE.altName, + callMemberStateEventType: CALL_MEMBER_STATE_EVENT_TYPE.altName, + }, +].forEach(({ callStateEventType, callMemberStateEventType }) => { + describe(`GroupCallUtils (${callStateEventType}, ${callMemberStateEventType})`, () => { + const roomId = "!room:example.com"; + let client: MatrixClient; + let callEvent: MatrixEvent; + const callId = "test call"; + const callId2 = "test call 2"; + const userId1 = "@user1:example.com"; + const now = 1654616071686; + + const setUpNonCallStateEvent = () => { + callEvent = mkEvent({ + room: roomId, + user: userId1, + event: true, + type: "test", + skey: userId1, + content: {}, + }); + }; + + const setUpEmptyStateKeyCallEvent = () => { + callEvent = mkEvent({ + room: roomId, + user: userId1, + event: true, + type: callStateEventType, + skey: "", + content: {}, + }); + }; + + const setUpValidCallEvent = () => { + callEvent = mkEvent({ + room: roomId, + user: userId1, + event: true, + type: callStateEventType, + skey: callId, + content: {}, + }); + }; + + beforeEach(() => { + client = createTestClient(); + }); + + describe("getGroupCall", () => { + describe("for a non-existing room", () => { + beforeEach(() => { + mocked(client.getRoom).mockReturnValue(null); + }); + + it("should return null", () => { + expect(getGroupCall(client, roomId)).toBeUndefined(); + }); + }); + + describe("for an existing room", () => { + let room: Room; + + beforeEach(() => { + room = new Room(roomId, client, client.getUserId()); + mocked(client.getRoom).mockImplementation((rid: string) => { + return rid === roomId + ? room + : null; + }); + }); + + it("should return null if no 'call' state event exist", () => { + expect(getGroupCall(client, roomId)).toBeUndefined(); + }); + + describe("with call state events", () => { + let callEvent1: MatrixEvent; + let callEvent2: MatrixEvent; + let callEvent3: MatrixEvent; + + beforeEach(() => { + callEvent1 = mkEvent({ + room: roomId, + user: client.getUserId(), + event: true, + type: callStateEventType, + content: {}, + ts: 150, + skey: "call1", + }); + room.getLiveTimeline().addEvent(callEvent1, { + toStartOfTimeline: false, + }); + + callEvent2 = mkEvent({ + room: roomId, + user: client.getUserId(), + event: true, + type: callStateEventType, + content: {}, + ts: 100, + skey: "call2", + }); + room.getLiveTimeline().addEvent(callEvent2, { + toStartOfTimeline: false, + }); + + // terminated call - should never be returned + callEvent3 = mkEvent({ + room: roomId, + user: client.getUserId(), + event: true, + type: callStateEventType, + content: { + ["m.terminated"]: "time's up", + }, + ts: 500, + skey: "call3", + }); + room.getLiveTimeline().addEvent(callEvent3, { + toStartOfTimeline: false, + }); + }); + + it("should return the newest call state event (1)", () => { + expect(getGroupCall(client, roomId)).toBe(callEvent1); + }); + + it("should return the newest call state event (2)", () => { + callEvent2.getTs = () => 200; + expect(getGroupCall(client, roomId)).toBe(callEvent2); + }); + }); + }); + }); + + describe("useConnectedMembers", () => { + describe("for a non-call event", () => { + beforeEach(() => { + setUpNonCallStateEvent(); + }); + + it("should return an empty list", () => { + expect(useConnectedMembers(client, callEvent)).toEqual([]); + }); + }); + + describe("for an empty state key", () => { + beforeEach(() => { + setUpEmptyStateKeyCallEvent(); + }); + + it("should return an empty list", () => { + expect(useConnectedMembers(client, callEvent)).toEqual([]); + }); + }); + + describe("for a valid call state event", () => { + beforeEach(() => { + setUpValidCallEvent(); + }); + + describe("and a non-existing room", () => { + beforeEach(() => { + mocked(client.getRoom).mockReturnValue(null); + }); + + it("should return an empty list", () => { + expect(useConnectedMembers(client, callEvent)).toEqual([]); + }); + }); + + describe("and an existing room", () => { + let room: Room; + + beforeEach(() => { + room = new Room(roomId, client, client.getUserId()); + mocked(client.getRoom).mockImplementation((rid: string) => { + return rid === roomId + ? room + : null; + }); + }); + + it("should return an empty list if no call member state events exist", () => { + expect(useConnectedMembers(client, callEvent)).toEqual([]); + }); + + describe("and some call member state events", () => { + const userId2 = "@user2:example.com"; + const userId3 = "@user3:example.com"; + const userId4 = "@user4:example.com"; + let expectedEvent1: MatrixEvent; + let expectedEvent2: MatrixEvent; + + beforeEach(() => { + jest.useFakeTimers() + .setSystemTime(now); + + expectedEvent1 = mkEvent({ + event: true, + room: roomId, + user: userId1, + skey: userId1, + type: callMemberStateEventType, + content: { + ["m.expires_ts"]: now + 100, + ["m.calls"]: [ + { + ["m.call_id"]: callId2, + }, + { + ["m.call_id"]: callId, + }, + ], + }, + }); + room.getLiveTimeline().addEvent(expectedEvent1, { toStartOfTimeline: false }); + + expectedEvent2 = mkEvent({ + event: true, + room: roomId, + user: userId2, + skey: userId2, + type: callMemberStateEventType, + content: { + ["m.expires_ts"]: now + 100, + ["m.calls"]: [ + { + ["m.call_id"]: callId, + }, + ], + }, + }); + room.getLiveTimeline().addEvent(expectedEvent2, { toStartOfTimeline: false }); + + // expired event + const event3 = mkEvent({ + event: true, + room: roomId, + user: userId3, + skey: userId3, + type: callMemberStateEventType, + content: { + ["m.expires_ts"]: now - 100, + ["m.calls"]: [ + { + ["m.call_id"]: callId, + }, + ], + }, + }); + room.getLiveTimeline().addEvent(event3, { toStartOfTimeline: false }); + + // other call + const event4 = mkEvent({ + event: true, + room: roomId, + user: userId4, + skey: userId4, + type: callMemberStateEventType, + content: { + ["m.expires_ts"]: now + 100, + ["m.calls"]: [ + { + ["m.call_id"]: callId2, + }, + ], + }, + }); + room.getLiveTimeline().addEvent(event4, { toStartOfTimeline: false }); + + // empty calls + const event5 = mkEvent({ + event: true, + room: roomId, + user: userId4, + skey: userId4, + type: callMemberStateEventType, + content: { + ["m.expires_ts"]: now + 100, + ["m.calls"]: [], + }, + }); + room.getLiveTimeline().addEvent(event5, { toStartOfTimeline: false }); + + // no calls prop + const event6 = mkEvent({ + event: true, + room: roomId, + user: userId4, + skey: userId4, + type: callMemberStateEventType, + content: { + ["m.expires_ts"]: now + 100, + }, + }); + room.getLiveTimeline().addEvent(event6, { toStartOfTimeline: false }); + }); + + it("should return the expected call member events", () => { + const callMemberEvents = useConnectedMembers(client, callEvent); + expect(callMemberEvents).toHaveLength(2); + expect(callMemberEvents).toContain(expectedEvent1); + expect(callMemberEvents).toContain(expectedEvent2); + }); + }); + }); + }); + }); + + describe("removeOurDevice", () => { + describe("for a non-call event", () => { + beforeEach(() => { + setUpNonCallStateEvent(); + }); + + it("should not update the state", () => { + removeOurDevice(client, callEvent); + expect(client.sendStateEvent).not.toHaveBeenCalled(); + }); + }); + + describe("for an empty state key", () => { + beforeEach(() => { + setUpEmptyStateKeyCallEvent(); + }); + + it("should not update the state", () => { + removeOurDevice(client, callEvent); + expect(client.sendStateEvent).not.toHaveBeenCalled(); + }); + }); + + describe("for a valid call state event", () => { + beforeEach(() => { + setUpValidCallEvent(); + }); + + describe("and a non-existing room", () => { + beforeEach(() => { + mocked(client.getRoom).mockReturnValue(null); + }); + + it("should not update the state", () => { + removeOurDevice(client, callEvent); + expect(client.sendStateEvent).not.toHaveBeenCalled(); + }); + }); + + describe("and an existing room", () => { + let room: Room; + + beforeEach(() => { + room = new Room(roomId, client, client.getUserId()); + room.getLiveTimeline().addEvent(callEvent, { toStartOfTimeline: false }); + mocked(client.getRoom).mockImplementation((rid: string) => { + return rid === roomId + ? room + : null; + }); + }); + + it("should not update the state if no call member event exists", () => { + removeOurDevice(client, callEvent); + expect(client.sendStateEvent).not.toHaveBeenCalled(); + }); + + describe("and a call member state event", () => { + beforeEach(() => { + jest.useFakeTimers() + .setSystemTime(now); + + const callMemberEvent = mkEvent({ + event: true, + room: roomId, + user: client.getUserId(), + skey: client.getUserId(), + type: callMemberStateEventType, + content: { + ["m.expires_ts"]: now - 100, + ["m.calls"]: [ + { + ["m.call_id"]: callId, + ["m.devices"]: [ + // device to be removed + { "m.device_id": client.getDeviceId() }, + { "m.device_id": "device 2" }, + ], + }, + { + // no device list + ["m.call_id"]: callId, + }, + { + // other call + ["m.call_id"]: callId2, + ["m.devices"]: [ + { "m.device_id": client.getDeviceId() }, + ], + }, + ], + }, + }); + room.getLiveTimeline().addEvent(callMemberEvent, { toStartOfTimeline: false }); + }); + + it("should remove the device from the call", async () => { + await removeOurDevice(client, callEvent); + expect(client.sendStateEvent).toHaveBeenCalledTimes(1); + expect(client.sendStateEvent).toHaveBeenCalledWith( + roomId, + CALL_MEMBER_STATE_EVENT_TYPE.name, + { + ["m.expires_ts"]: now + STUCK_DEVICE_TIMEOUT_MS, + ["m.calls"]: [ + { + ["m.call_id"]: callId, + ["m.devices"]: [ + { "m.device_id": "device 2" }, + ], + }, + { + // no device list + ["m.call_id"]: callId, + }, + { + // other call + ["m.call_id"]: callId2, + ["m.devices"]: [ + { "m.device_id": client.getDeviceId() }, + ], + }, + ], + }, + client.getUserId(), + ); + }); + }); + }); + }); + }); + + describe("fixStuckDevices", () => { + let thisDevice: IMyDevice; + let otherDevice: IMyDevice; + let noLastSeenTsDevice: IMyDevice; + let stuckDevice: IMyDevice; + + beforeEach(() => { + jest.useFakeTimers() + .setSystemTime(now); + + thisDevice = { device_id: "ABCDEFGHI", last_seen_ts: now - STUCK_DEVICE_TIMEOUT_MS - 100 }; + otherDevice = { device_id: "ABCDEFGHJ", last_seen_ts: now }; + noLastSeenTsDevice = { device_id: "ABCDEFGHK" }; + stuckDevice = { device_id: "ABCDEFGHL", last_seen_ts: now - STUCK_DEVICE_TIMEOUT_MS - 100 }; + + mocked(client.getDeviceId).mockReturnValue(thisDevice.device_id); + mocked(client.getDevices).mockResolvedValue({ + devices: [ + thisDevice, + otherDevice, + noLastSeenTsDevice, + stuckDevice, + ], + }); + }); + + describe("for a non-call event", () => { + beforeEach(() => { + setUpNonCallStateEvent(); + }); + + it("should not update the state", () => { + fixStuckDevices(client, callEvent, true); + expect(client.sendStateEvent).not.toHaveBeenCalled(); + }); + }); + + describe("for an empty state key", () => { + beforeEach(() => { + setUpEmptyStateKeyCallEvent(); + }); + + it("should not update the state", () => { + fixStuckDevices(client, callEvent, true); + expect(client.sendStateEvent).not.toHaveBeenCalled(); + }); + }); + + describe("for a valid call state event", () => { + beforeEach(() => { + setUpValidCallEvent(); + }); + + describe("and a non-existing room", () => { + beforeEach(() => { + mocked(client.getRoom).mockReturnValue(null); + }); + + it("should not update the state", () => { + fixStuckDevices(client, callEvent, true); + expect(client.sendStateEvent).not.toHaveBeenCalled(); + }); + }); + + describe("and an existing room", () => { + let room: Room; + + beforeEach(() => { + room = new Room(roomId, client, client.getUserId()); + room.getLiveTimeline().addEvent(callEvent, { toStartOfTimeline: false }); + mocked(client.getRoom).mockImplementation((rid: string) => { + return rid === roomId + ? room + : null; + }); + }); + + it("should not update the state if no call member event exists", () => { + fixStuckDevices(client, callEvent, true); + expect(client.sendStateEvent).not.toHaveBeenCalled(); + }); + + describe("and a call member state event", () => { + beforeEach(() => { + const callMemberEvent = mkEvent({ + event: true, + room: roomId, + user: client.getUserId(), + skey: client.getUserId(), + type: callMemberStateEventType, + content: { + ["m.expires_ts"]: now - 100, + ["m.calls"]: [ + { + ["m.call_id"]: callId, + ["m.devices"]: [ + { "m.device_id": thisDevice.device_id }, + { "m.device_id": otherDevice.device_id }, + { "m.device_id": noLastSeenTsDevice.device_id }, + { "m.device_id": stuckDevice.device_id }, + ], + }, + { + // no device list + ["m.call_id"]: callId, + }, + { + // other call + ["m.call_id"]: callId2, + ["m.devices"]: [ + { "m.device_id": stuckDevice.device_id }, + ], + }, + ], + }, + }); + room.getLiveTimeline().addEvent(callMemberEvent, { toStartOfTimeline: false }); + }); + + it("should remove stuck devices from the call, except this device", async () => { + await fixStuckDevices(client, callEvent, false); + expect(client.sendStateEvent).toHaveBeenCalledTimes(1); + expect(client.sendStateEvent).toHaveBeenCalledWith( + roomId, + CALL_MEMBER_STATE_EVENT_TYPE.name, + { + ["m.expires_ts"]: now + STUCK_DEVICE_TIMEOUT_MS, + ["m.calls"]: [ + { + ["m.call_id"]: callId, + ["m.devices"]: [ + { "m.device_id": thisDevice.device_id }, + { "m.device_id": otherDevice.device_id }, + { "m.device_id": noLastSeenTsDevice.device_id }, + ], + }, + { + // no device list + ["m.call_id"]: callId, + }, + { + // other call + ["m.call_id"]: callId2, + ["m.devices"]: [ + { "m.device_id": stuckDevice.device_id }, + ], + }, + ], + }, + client.getUserId(), + ); + }); + + it("should remove stuck devices from the call, including this device", async () => { + await fixStuckDevices(client, callEvent, true); + expect(client.sendStateEvent).toHaveBeenCalledTimes(1); + expect(client.sendStateEvent).toHaveBeenCalledWith( + roomId, + CALL_MEMBER_STATE_EVENT_TYPE.name, + { + ["m.expires_ts"]: now + STUCK_DEVICE_TIMEOUT_MS, + ["m.calls"]: [ + { + ["m.call_id"]: callId, + ["m.devices"]: [ + { "m.device_id": otherDevice.device_id }, + { "m.device_id": noLastSeenTsDevice.device_id }, + ], + }, + { + // no device list + ["m.call_id"]: callId, + }, + { + // other call + ["m.call_id"]: callId2, + ["m.devices"]: [ + { "m.device_id": stuckDevice.device_id }, + ], + }, + ], + }, + client.getUserId(), + ); + }); + }); + }); + }); + }); + }); +}); + From 3b64a7999c79ef2a2d8cdc91535756d034666e88 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 10 Aug 2022 08:20:29 +0100 Subject: [PATCH 13/64] Override the disambiguated profile colour in percy tests for screenshot consistency (#9161) --- res/css/views/messages/_DisambiguatedProfile.pcss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/res/css/views/messages/_DisambiguatedProfile.pcss b/res/css/views/messages/_DisambiguatedProfile.pcss index ef4bc7cfb30..4863dc3518c 100644 --- a/res/css/views/messages/_DisambiguatedProfile.pcss +++ b/res/css/views/messages/_DisambiguatedProfile.pcss @@ -34,3 +34,10 @@ limitations under the License. color: $primary-content; } } + +@media only percy { + .mx_DisambiguatedProfile_displayName { + /* Override the colour in percy tests for screenshot consistency */ + color: $username-variant1-color !important; + } +} From fdde6b1428e86d604c3577dea3b386109db62d9b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 10 Aug 2022 09:40:17 +0100 Subject: [PATCH 14/64] Cypress test stability improvements (#9156) * Make cypress hidden event test more reliable * Make timeline tests more stable --- cypress/e2e/timeline/timeline.spec.ts | 41 ++++++++++++++------------- cypress/support/settings.ts | 4 +-- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/cypress/e2e/timeline/timeline.spec.ts b/cypress/e2e/timeline/timeline.spec.ts index 6eacacfed23..73de28dd302 100644 --- a/cypress/e2e/timeline/timeline.spec.ts +++ b/cypress/e2e/timeline/timeline.spec.ts @@ -155,7 +155,7 @@ describe("Timeline", () => { cy.visit("/#/room/" + roomId); cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC); cy.contains(".mx_RoomView_body .mx_GenericEventListSummary[data-layout=irc] " + - ".mx_GenericEventListSummary_summary", "created and configured the room."); + ".mx_GenericEventListSummary_summary", "created and configured the room.").should("exist"); cy.get(".mx_Spinner").should("not.exist"); cy.percySnapshot("Configured room on IRC layout"); }); @@ -166,7 +166,7 @@ describe("Timeline", () => { // Wait until configuration is finished cy.contains(".mx_RoomView_body .mx_GenericEventListSummary " + - ".mx_GenericEventListSummary_summary", "created and configured the room."); + ".mx_GenericEventListSummary_summary", "created and configured the room.").should("exist"); // Click "expand" link button cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click(); @@ -193,14 +193,14 @@ describe("Timeline", () => { cy.visit("/#/room/" + roomId); cy.setSettingValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true); cy.contains(".mx_RoomView_body .mx_GenericEventListSummary .mx_GenericEventListSummary_summary", - "created and configured the room."); + "created and configured the room.").should("exist"); // Edit message cy.contains(".mx_RoomView_body .mx_EventTile .mx_EventTile_line", "Message").within(() => { cy.get('[aria-label="Edit"]').click({ force: true }); // Cypress has no ability to hover cy.get(".mx_BasicMessageComposer_input").type("Edit{enter}"); }); - cy.get(".mx_RoomView_body .mx_EventTile").contains(".mx_EventTile[data-scroll-tokens]", "MessageEdit"); + cy.contains(".mx_EventTile[data-scroll-tokens]", "MessageEdit").should("exist"); // Click timestamp to highlight hidden event line cy.get(".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp").click(); @@ -228,18 +228,19 @@ describe("Timeline", () => { cy.visit("/#/room/" + roomId); cy.setSettingValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true); cy.contains(".mx_RoomView_body .mx_GenericEventListSummary " + - ".mx_GenericEventListSummary_summary", "created and configured the room."); + ".mx_GenericEventListSummary_summary", "created and configured the room.").should("exist"); // Edit message cy.contains(".mx_RoomView_body .mx_EventTile .mx_EventTile_line", "Message").within(() => { cy.get('[aria-label="Edit"]').click({ force: true }); // Cypress has no ability to hover cy.get(".mx_BasicMessageComposer_input").type("Edit{enter}"); }); - cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "MessageEdit"); + cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "MessageEdit").should("exist"); // Click top left of the event toggle, which should not be covered by MessageActionBar's safe area - cy.get(".mx_EventTile .mx_ViewSourceEvent").realHover() - .get(".mx_EventTile .mx_ViewSourceEvent .mx_ViewSourceEvent_toggle").click('topLeft', { force: false }); + cy.get(".mx_EventTile .mx_ViewSourceEvent").realHover().within(() => { + cy.get(".mx_ViewSourceEvent_toggle").click('topLeft', { force: false }); + }); // Make sure the expand toggle worked cy.get(".mx_EventTile .mx_ViewSourceEvent_expanded .mx_ViewSourceEvent_toggle").should("be.visible"); @@ -249,17 +250,17 @@ describe("Timeline", () => { cy.visit("/#/room/" + roomId); cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); cy.contains(".mx_RoomView_body .mx_GenericEventListSummary[data-layout=bubble] " + - ".mx_GenericEventListSummary_summary", "created and configured the room."); + ".mx_GenericEventListSummary_summary", "created and configured the room.").should("exist"); // Click "expand" link button cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click(); // Click "collapse" link button on the first hovered info event line - cy.get(".mx_GenericEventListSummary_unstyledList .mx_EventTile_info:first-of-type").realHover() - .get(".mx_GenericEventListSummary_toggle[aria-expanded=true]").click({ force: false }); + cy.get(".mx_GenericEventListSummary_unstyledList .mx_EventTile_info:first-of-type").realHover(); + cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=true]").click({ force: false }); // Make sure "collapse" link button worked - cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]"); + cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").should("exist"); }); it("should highlight search result words regardless of formatting", () => { @@ -285,7 +286,7 @@ describe("Timeline", () => { cy.getComposer().type(`${MESSAGE}{enter}`); // Reply to the message - cy.get(".mx_RoomView_body .mx_EventTile").contains(".mx_EventTile_line", "Hello world").within(() => { + cy.get(".mx_RoomView_body").contains(".mx_EventTile_line", "Hello world").within(() => { cy.get('[aria-label="Reply"]').click({ force: true }); // Cypress has no ability to hover }); }; @@ -296,20 +297,22 @@ describe("Timeline", () => { cy.getComposer().type(`${reply}{enter}`); - cy.get(".mx_RoomView_body .mx_EventTile .mx_EventTile_line").find(".mx_ReplyTile .mx_MTextBody") + cy.get(".mx_RoomView_body .mx_EventTile .mx_EventTile_line .mx_ReplyTile .mx_MTextBody") .should("contain", MESSAGE); - cy.get(".mx_RoomView_body .mx_EventTile > .mx_EventTile_line > .mx_MTextBody").contains(reply) + cy.contains(".mx_RoomView_body .mx_EventTile > .mx_EventTile_line > .mx_MTextBody", reply) .should("have.length", 1); }); - xit("can reply with a voice message", () => { + it("can reply with a voice message", () => { viewRoomSendMessageAndSetupReply(); - cy.openMessageComposerOptions().find(`[aria-label="Voice Message"]`).click(); + cy.openMessageComposerOptions().within(() => { + cy.get(`[aria-label="Voice Message"]`).click(); + }); cy.wait(3000); - cy.getComposer().find(".mx_MessageComposer_sendMessage").click(); + cy.get(".mx_RoomView_body .mx_MessageComposer .mx_MessageComposer_sendMessage").click(); - cy.get(".mx_RoomView_body .mx_EventTile .mx_EventTile_line").find(".mx_ReplyTile .mx_MTextBody") + cy.get(".mx_RoomView_body .mx_EventTile .mx_EventTile_line .mx_ReplyTile .mx_MTextBody") .should("contain", MESSAGE); cy.get(".mx_RoomView_body .mx_EventTile > .mx_EventTile_line > .mx_MVoiceMessageBody") .should("have.length", 1); diff --git a/cypress/support/settings.ts b/cypress/support/settings.ts index 06ec815364b..ec07df93aa1 100644 --- a/cypress/support/settings.ts +++ b/cypress/support/settings.ts @@ -82,7 +82,7 @@ declare global { * @param {*} value The new value of the setting, may be null. * @return {Promise} Resolves when the setting has been changed. */ - setSettingValue(name: string, roomId: string, level: SettingLevel, value: any): Chainable; + setSettingValue(settingName: string, roomId: string, level: SettingLevel, value: any): Chainable; /** * Gets the value of a setting. The room ID is optional if the @@ -96,7 +96,7 @@ declare global { * value. * @return {*} The value, or null if not found */ - getSettingValue(name: string, roomId?: string, excludeDefault?: boolean): Chainable; + getSettingValue(settingName: string, roomId?: string, excludeDefault?: boolean): Chainable; } } } From 350341d13d6e6b2709ce19685e40228954056a6f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 10 Aug 2022 11:22:15 +0100 Subject: [PATCH 15/64] Fix inverted logic for showing UserWelcomeTop component (#9164) --- src/components/structures/HomePage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/HomePage.tsx b/src/components/structures/HomePage.tsx index 200c28d159f..613625ae702 100644 --- a/src/components/structures/HomePage.tsx +++ b/src/components/structures/HomePage.tsx @@ -97,8 +97,8 @@ const HomePage: React.FC = ({ justRegistered = false }) => { return ; } - let introSection; - if (justRegistered || !!OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_SIZE)) { + let introSection: JSX.Element; + if (justRegistered || !OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_SIZE)) { introSection = ; } else { const brandingConfig = SdkConfig.getObject("branding"); From 3d0982e9a6032eae70e2293de374928c7c36b723 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 10 Aug 2022 13:14:54 +0100 Subject: [PATCH 16/64] Space panel accessibility improvements (#9157) * Move the UserMenu out of the SpacePanel ul list * Apply aria-selected to the spacepanel treeview * Fix typing --- res/css/structures/_SpacePanel.pcss | 4 -- .../structures/AutoHideScrollbar.tsx | 40 ++++++++++--------- .../structures/IndicatorScrollbar.tsx | 19 ++++----- .../dialogs/AddExistingToSpaceDialog.tsx | 2 +- .../views/emojipicker/EmojiPicker.tsx | 2 +- src/components/views/spaces/SpacePanel.tsx | 12 +++--- .../views/spaces/SpaceTreeLevel.tsx | 4 +- 7 files changed, 44 insertions(+), 39 deletions(-) diff --git a/res/css/structures/_SpacePanel.pcss b/res/css/structures/_SpacePanel.pcss index 7fdb2500b45..72dbddf75e1 100644 --- a/res/css/structures/_SpacePanel.pcss +++ b/res/css/structures/_SpacePanel.pcss @@ -78,10 +78,6 @@ $activeBorderColor: $primary-content; margin: 0; list-style: none; padding: 0; - - > .mx_SpaceItem { - padding-left: 16px; - } } .mx_SpaceButton_toggleCollapse { diff --git a/src/components/structures/AutoHideScrollbar.tsx b/src/components/structures/AutoHideScrollbar.tsx index d2134c33fef..9a6f5b26a0a 100644 --- a/src/components/structures/AutoHideScrollbar.tsx +++ b/src/components/structures/AutoHideScrollbar.tsx @@ -15,18 +15,28 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { HTMLAttributes, WheelEvent } from "react"; +import classNames from "classnames"; +import React, { HTMLAttributes, ReactHTML, WheelEvent } from "react"; -interface IProps extends Omit, "onScroll"> { +type DynamicHtmlElementProps = + JSX.IntrinsicElements[T] extends HTMLAttributes<{}> ? DynamicElementProps : DynamicElementProps<"div">; +type DynamicElementProps = Partial>; + +export type IProps = DynamicHtmlElementProps & { + element?: T; className?: string; onScroll?: (event: Event) => void; onWheel?: (event: WheelEvent) => void; style?: React.CSSProperties; tabIndex?: number; wrappedRef?: (ref: HTMLDivElement) => void; -} +}; + +export default class AutoHideScrollbar extends React.Component> { + static defaultProps = { + element: 'div' as keyof ReactHTML, + }; -export default class AutoHideScrollbar extends React.Component { public readonly containerRef: React.RefObject = React.createRef(); public componentDidMount() { @@ -36,9 +46,7 @@ export default class AutoHideScrollbar extends React.Component { this.containerRef.current.addEventListener("scroll", this.props.onScroll, { passive: true }); } - if (this.props.wrappedRef) { - this.props.wrappedRef(this.containerRef.current); - } + this.props.wrappedRef?.(this.containerRef.current); } public componentWillUnmount() { @@ -49,19 +57,15 @@ export default class AutoHideScrollbar extends React.Component { public render() { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { className, onScroll, onWheel, style, tabIndex, wrappedRef, children, ...otherProps } = this.props; + const { element, className, onScroll, tabIndex, wrappedRef, children, ...otherProps } = this.props; - return (
- { children } -
); + tabIndex: tabIndex ?? -1, + }, children); } } diff --git a/src/components/structures/IndicatorScrollbar.tsx b/src/components/structures/IndicatorScrollbar.tsx index 4b122345b32..ea876f9ae29 100644 --- a/src/components/structures/IndicatorScrollbar.tsx +++ b/src/components/structures/IndicatorScrollbar.tsx @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ComponentProps, createRef } from "react"; +import React, { createRef } from "react"; -import AutoHideScrollbar from "./AutoHideScrollbar"; +import AutoHideScrollbar, { IProps as AutoHideScrollbarProps } from "./AutoHideScrollbar"; import UIStore, { UI_EVENTS } from "../../stores/UIStore"; -interface IProps extends Omit, "onWheel"> { +export type IProps = Omit, "onWheel"> & { // If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator // and mx_IndicatorScrollbar_rightOverflowIndicator elements to the list for positioning // by the parent element. @@ -31,21 +31,22 @@ interface IProps extends Omit, "onWheel verticalScrollsHorizontally?: boolean; children: React.ReactNode; - className: string; -} +}; interface IState { leftIndicatorOffset: string; rightIndicatorOffset: string; } -export default class IndicatorScrollbar extends React.Component { - private autoHideScrollbar = createRef(); +export default class IndicatorScrollbar< + T extends keyof JSX.IntrinsicElements, +> extends React.Component, IState> { + private autoHideScrollbar = createRef>(); private scrollElement: HTMLDivElement; private likelyTrackpadUser: boolean = null; private checkAgainForTrackpad = 0; // ts in milliseconds to recheck this._likelyTrackpadUser - constructor(props: IProps) { + constructor(props: IProps) { super(props); this.state = { @@ -65,7 +66,7 @@ export default class IndicatorScrollbar extends React.Component } }; - public componentDidUpdate(prevProps: IProps): void { + public componentDidUpdate(prevProps: IProps): void { const prevLen = React.Children.count(prevProps.children); const curLen = React.Children.count(this.props.children); // check overflow only if amount of children changes. diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 606f96553d7..6251faee41f 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -130,7 +130,7 @@ export const AddExistingToSpace: React.FC = ({ const cli = useContext(MatrixClientContext); const visibleRooms = useMemo(() => cli.getVisibleRooms().filter(r => r.getMyMembership() === "join"), [cli]); - const scrollRef = useRef(); + const scrollRef = useRef>(); const [scrollState, setScrollState] = useState({ // these are estimates which update as soon as it mounts scrollTop: 0, diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index e32f20b78b6..c178084826a 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -55,7 +55,7 @@ class EmojiPicker extends React.Component { private readonly memoizedDataByCategory: Record; private readonly categories: ICategory[]; - private scrollRef = React.createRef(); + private scrollRef = React.createRef>(); constructor(props: IProps) { super(props); diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index e1f62445f74..129e6f3584e 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -132,6 +132,7 @@ const MetaSpaceButton = ({ selected, isPanelCollapsed, ...props }: IMetaSpaceBut "collapsed": isPanelCollapsed, })} role="treeitem" + aria-selected={selected} > ; @@ -282,6 +283,9 @@ const InnerSpacePanel = React.memo(({ style={isDraggingOver ? { pointerEvents: "none", } : undefined} + element="ul" + role="tree" + aria-label={_t("Spaces")} > { metaSpacesSection } { invites.map(s => ( @@ -321,7 +325,7 @@ const InnerSpacePanel = React.memo(({ const SpacePanel = () => { const [isPanelCollapsed, setPanelCollapsed] = useState(true); - const ref = useRef(); + const ref = useRef(); useLayoutEffect(() => { UIStore.instance.trackElementDimensions("SpacePanel", ref.current); return () => UIStore.instance.stopTrackingElementDimensions("SpacePanel"); @@ -340,11 +344,9 @@ const SpacePanel = () => { }}> { ({ onKeyDownHandler }) => ( -
    @@ -381,7 +383,7 @@ const SpacePanel = () => { -
+
) } diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 80d678d3b61..cab7bc3c76b 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -315,6 +315,7 @@ export class SpaceItem extends React.PureComponent { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { tabIndex, ...restDragHandleProps } = dragHandleProps || {}; + const selected = activeSpaces.includes(space.roomId); return (
  • { className={itemClasses} ref={innerRef} aria-expanded={hasChildren ? !collapsed : undefined} + aria-selected={selected} role="treeitem" > Date: Wed, 10 Aug 2022 08:57:56 -0400 Subject: [PATCH 17/64] Implement MSC3819: Allowing widgets to send/receive to-device messages (#8885) * Implement MSC3819: Allowing widgets to send/receive to-device messages * Don't change the room events and state events drivers * Update to latest matrix-widget-api changes * Support sending encrypted to-device messages * Use queueToDevice for better reliability * Update types for latest WidgetDriver changes * Upgrade matrix-widget-api * Add tests * Test StopGapWidget * Fix a potential memory leak --- package.json | 2 +- src/stores/widgets/StopGapWidget.ts | 37 ++++++--- src/stores/widgets/StopGapWidgetDriver.ts | 48 ++++++++++- src/widgets/CapabilityText.tsx | 9 +- test/stores/widgets/StopGapWidget-test.ts | 70 ++++++++++++++++ .../widgets/StopGapWidgetDriver-test.ts | 79 ++++++++++++++++++ .../StopGapWidgetDriver-test.ts.snap | 82 +++++++++++++++++++ test/test-utils/test-utils.ts | 11 ++- yarn.lock | 8 +- 9 files changed, 322 insertions(+), 24 deletions(-) create mode 100644 test/stores/widgets/StopGapWidget-test.ts create mode 100644 test/stores/widgets/StopGapWidgetDriver-test.ts create mode 100644 test/stores/widgets/__snapshots__/StopGapWidgetDriver-test.ts.snap diff --git a/package.json b/package.json index 933ccfd7ada..ca203360c63 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "^0.0.1-beta.7", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", - "matrix-widget-api": "^0.1.0-beta.18", + "matrix-widget-api": "^1.0.0", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", "pako": "^2.0.3", diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index 7a1c5e0ba53..889a050ebfd 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -33,6 +33,7 @@ import { WidgetKind, } from "matrix-widget-api"; import { EventEmitter } from "events"; +import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event"; import { logger } from "matrix-js-sdk/src/logger"; import { ClientEvent } from "matrix-js-sdk/src/client"; @@ -148,6 +149,7 @@ export class ElementWidget extends Widget { } export class StopGapWidget extends EventEmitter { + private client: MatrixClient; private messaging: ClientWidgetApi; private mockWidget: ElementWidget; private scalarToken: string; @@ -157,12 +159,13 @@ export class StopGapWidget extends EventEmitter { constructor(private appTileProps: IAppTileProps) { super(); - let app = appTileProps.app; + this.client = MatrixClientPeg.get(); + let app = appTileProps.app; // Backwards compatibility: not all old widgets have a creatorUserId if (!app.creatorUserId) { app = objectShallowClone(app); // clone to prevent accidental mutation - app.creatorUserId = MatrixClientPeg.get().getUserId(); + app.creatorUserId = this.client.getUserId(); } this.mockWidget = new ElementWidget(app); @@ -203,7 +206,7 @@ export class StopGapWidget extends EventEmitter { const fromCustomisation = WidgetVariableCustomisations?.provideVariables?.() ?? {}; const defaults: ITemplateParams = { widgetRoomId: this.roomId, - currentUserId: MatrixClientPeg.get().getUserId(), + currentUserId: this.client.getUserId(), userDisplayName: OwnProfileStore.instance.displayName, userHttpAvatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(), clientId: ELEMENT_CLIENT_ID, @@ -260,8 +263,10 @@ export class StopGapWidget extends EventEmitter { */ public startMessaging(iframe: HTMLIFrameElement): any { if (this.started) return; + const allowedCapabilities = this.appTileProps.whitelistCapabilities || []; const driver = new StopGapWidgetDriver(allowedCapabilities, this.mockWidget, this.kind, this.roomId); + this.messaging = new ClientWidgetApi(this.mockWidget, iframe, driver); this.messaging.on("preparing", () => this.emit("preparing")); this.messaging.on("ready", () => this.emit("ready")); @@ -302,7 +307,7 @@ export class StopGapWidget extends EventEmitter { // Populate the map of "read up to" events for this widget with the current event in every room. // This is a bit inefficient, but should be okay. We do this for all rooms in case the widget // requests timeline capabilities in other rooms down the road. It's just easier to manage here. - for (const room of MatrixClientPeg.get().getRooms()) { + for (const room of this.client.getRooms()) { // Timelines are most recent last const events = room.getLiveTimeline()?.getEvents() || []; const roomEvent = events[events.length - 1]; @@ -311,8 +316,9 @@ export class StopGapWidget extends EventEmitter { } // Attach listeners for feeding events - the underlying widget classes handle permissions for us - MatrixClientPeg.get().on(ClientEvent.Event, this.onEvent); - MatrixClientPeg.get().on(MatrixEventEvent.Decrypted, this.onEventDecrypted); + this.client.on(ClientEvent.Event, this.onEvent); + this.client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted); + this.client.on(ClientEvent.ToDeviceEvent, this.onToDeviceEvent); this.messaging.on(`action:${WidgetApiFromWidgetAction.UpdateAlwaysOnScreen}`, (ev: CustomEvent) => { @@ -363,7 +369,7 @@ export class StopGapWidget extends EventEmitter { // noinspection JSIgnoredPromiseFromCall IntegrationManagers.sharedInstance().getPrimaryManager().open( - MatrixClientPeg.get().getRoom(RoomViewStore.instance.getRoomId()), + this.client.getRoom(RoomViewStore.instance.getRoomId()), `type_${integType}`, integId, ); @@ -428,14 +434,13 @@ export class StopGapWidget extends EventEmitter { WidgetMessagingStore.instance.stopMessaging(this.mockWidget, this.roomId); this.messaging = null; - if (MatrixClientPeg.get()) { - MatrixClientPeg.get().off(ClientEvent.Event, this.onEvent); - MatrixClientPeg.get().off(MatrixEventEvent.Decrypted, this.onEventDecrypted); - } + this.client.off(ClientEvent.Event, this.onEvent); + this.client.off(MatrixEventEvent.Decrypted, this.onEventDecrypted); + this.client.off(ClientEvent.ToDeviceEvent, this.onToDeviceEvent); } private onEvent = (ev: MatrixEvent) => { - MatrixClientPeg.get().decryptEventIfNeeded(ev); + this.client.decryptEventIfNeeded(ev); if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return; this.feedEvent(ev); }; @@ -445,6 +450,12 @@ export class StopGapWidget extends EventEmitter { this.feedEvent(ev); }; + private onToDeviceEvent = async (ev: MatrixEvent) => { + await this.client.decryptEventIfNeeded(ev); + if (ev.isDecryptionFailure()) return; + await this.messaging.feedToDevice(ev.getEffectiveEvent(), ev.isEncrypted()); + }; + private feedEvent(ev: MatrixEvent) { if (!this.messaging) return; @@ -465,7 +476,7 @@ export class StopGapWidget extends EventEmitter { // Timelines are most recent last, so reverse the order and limit ourselves to 100 events // to avoid overusing the CPU. - const timeline = MatrixClientPeg.get().getRoom(ev.getRoomId()).getLiveTimeline(); + const timeline = this.client.getRoom(ev.getRoomId()).getLiveTimeline(); const events = arrayFastClone(timeline.getEvents()).reverse().slice(0, 100); for (const timelineEvent of events) { diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index 3b617e6f314..ee69f0ca9c2 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -20,6 +20,7 @@ import { IOpenIDCredentials, IOpenIDUpdate, ISendEventDetails, + IRoomEvent, MatrixCapabilities, OpenIDRequestState, SimpleObservable, @@ -182,6 +183,49 @@ export class StopGapWidgetDriver extends WidgetDriver { return { roomId, eventId: r.event_id }; } + public async sendToDevice( + eventType: string, + encrypted: boolean, + contentMap: { [userId: string]: { [deviceId: string]: object } }, + ): Promise { + const client = MatrixClientPeg.get(); + + if (encrypted) { + const deviceInfoMap = await client.crypto.deviceList.downloadKeys(Object.keys(contentMap), false); + + await Promise.all( + Object.entries(contentMap).flatMap(([userId, userContentMap]) => + Object.entries(userContentMap).map(async ([deviceId, content]) => { + if (deviceId === "*") { + // Send the message to all devices we have keys for + await client.encryptAndSendToDevices( + Object.values(deviceInfoMap[userId]).map(deviceInfo => ({ + userId, deviceInfo, + })), + content, + ); + } else { + // Send the message to a specific device + await client.encryptAndSendToDevices( + [{ userId, deviceInfo: deviceInfoMap[userId][deviceId] }], + content, + ); + } + }), + ), + ); + } else { + await client.queueToDevice({ + eventType, + batch: Object.entries(contentMap).flatMap(([userId, userContentMap]) => + Object.entries(userContentMap).map(([deviceId, content]) => + ({ userId, deviceId, payload: content }), + ), + ), + }); + } + } + private pickRooms(roomIds: (string | Symbols.AnyRoom)[] = null): Room[] { const client = MatrixClientPeg.get(); if (!client) throw new Error("Not attached to a client"); @@ -197,7 +241,7 @@ export class StopGapWidgetDriver extends WidgetDriver { msgtype: string | undefined, limitPerRoom: number, roomIds: (string | Symbols.AnyRoom)[] = null, - ): Promise { + ): Promise { limitPerRoom = limitPerRoom > 0 ? Math.min(limitPerRoom, Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER; // relatively arbitrary const rooms = this.pickRooms(roomIds); @@ -224,7 +268,7 @@ export class StopGapWidgetDriver extends WidgetDriver { stateKey: string | undefined, limitPerRoom: number, roomIds: (string | Symbols.AnyRoom)[] = null, - ): Promise { + ): Promise { limitPerRoom = limitPerRoom > 0 ? Math.min(limitPerRoom, Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER; // relatively arbitrary const rooms = this.pickRooms(roomIds); diff --git a/src/widgets/CapabilityText.tsx b/src/widgets/CapabilityText.tsx index cd442f213b8..e4790eaad2a 100644 --- a/src/widgets/CapabilityText.tsx +++ b/src/widgets/CapabilityText.tsx @@ -17,6 +17,7 @@ limitations under the License. import { Capability, EventDirection, + EventKind, getTimelineRoomIDFromCapability, isTimelineCapability, isTimelineCapabilityFor, @@ -134,7 +135,7 @@ export class CapabilityText { }; private static bylineFor(eventCap: WidgetEventCapability): TranslatedString { - if (eventCap.isState) { + if (eventCap.kind === EventKind.State) { return !eventCap.keyStr ? _t("with an empty state key") : _t("with state key %(stateKey)s", { stateKey: eventCap.keyStr }); @@ -143,6 +144,8 @@ export class CapabilityText { } public static for(capability: Capability, kind: WidgetKind): TranslatedCapabilityText { + // TODO: Support MSC3819 (to-device capabilities) + // First see if we have a super simple line of text to provide back if (CapabilityText.simpleCaps[capability]) { const textForKind = CapabilityText.simpleCaps[capability]; @@ -184,13 +187,13 @@ export class CapabilityText { // Special case room messages so they show up a bit cleaner to the user. Result is // effectively "Send images" instead of "Send messages... of type images" if we were // to handle the msgtype nuances in this function. - if (!eventCap.isState && eventCap.eventType === EventType.RoomMessage) { + if (eventCap.kind === EventKind.Event && eventCap.eventType === EventType.RoomMessage) { return CapabilityText.forRoomMessageCap(eventCap, kind); } // See if we have a static line of text to provide for the given event type and // direction. The hope is that we do for common event types for friendlier copy. - const evSendRecv = eventCap.isState + const evSendRecv = eventCap.kind === EventKind.State ? CapabilityText.stateSendRecvCaps : CapabilityText.nonStateSendRecvCaps; if (evSendRecv[eventCap.eventType]) { diff --git a/test/stores/widgets/StopGapWidget-test.ts b/test/stores/widgets/StopGapWidget-test.ts new file mode 100644 index 00000000000..40292e451be --- /dev/null +++ b/test/stores/widgets/StopGapWidget-test.ts @@ -0,0 +1,70 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { mocked, MockedObject } from "jest-mock"; +import { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client"; +import { ClientWidgetApi } from "matrix-widget-api"; + +import { stubClient, mkRoom, mkEvent } from "../../test-utils"; +import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; +import { StopGapWidget } from "../../../src/stores/widgets/StopGapWidget"; + +jest.mock("matrix-widget-api/lib/ClientWidgetApi"); + +describe("StopGapWidget", () => { + let client: MockedObject; + let widget: StopGapWidget; + let messaging: MockedObject; + + beforeEach(() => { + stubClient(); + client = mocked(MatrixClientPeg.get()); + + widget = new StopGapWidget({ + app: { + id: "test", + creatorUserId: "@alice:example.org", + type: "example", + url: "https://example.org", + }, + room: mkRoom(client, "!1:example.org"), + userId: "@alice:example.org", + creatorUserId: "@alice:example.org", + waitForIframeLoad: true, + userWidget: false, + }); + // Start messaging without an iframe, since ClientWidgetApi is mocked + widget.startMessaging(null as unknown as HTMLIFrameElement); + messaging = mocked(mocked(ClientWidgetApi).mock.instances[0]); + }); + + afterEach(() => { + widget.stopMessaging(); + }); + + it("feeds incoming to-device messages to the widget", async () => { + const event = mkEvent({ + event: true, + type: "org.example.foo", + user: "@alice:example.org", + content: { hello: "world" }, + }); + + client.emit(ClientEvent.ToDeviceEvent, event); + await Promise.resolve(); // flush promises + expect(messaging.feedToDevice).toHaveBeenCalledWith(event.getEffectiveEvent(), false); + }); +}); diff --git a/test/stores/widgets/StopGapWidgetDriver-test.ts b/test/stores/widgets/StopGapWidgetDriver-test.ts new file mode 100644 index 00000000000..7dab35052b4 --- /dev/null +++ b/test/stores/widgets/StopGapWidgetDriver-test.ts @@ -0,0 +1,79 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { mocked, MockedObject } from "jest-mock"; +import { Widget, WidgetKind, WidgetDriver } from "matrix-widget-api"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; + +import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; +import { StopGapWidgetDriver } from "../../../src/stores/widgets/StopGapWidgetDriver"; +import { stubClient } from "../../test-utils"; + +describe("StopGapWidgetDriver", () => { + let client: MockedObject; + let driver: WidgetDriver; + + beforeEach(() => { + stubClient(); + client = mocked(MatrixClientPeg.get()); + + driver = new StopGapWidgetDriver( + [], + new Widget({ + id: "test", + creatorUserId: "@alice:example.org", + type: "example", + url: "https://example.org", + }), + WidgetKind.Room, + ); + }); + + describe("sendToDevice", () => { + const contentMap = { + "@alice:example.org": { + "*": { + hello: "alice", + }, + }, + "@bob:example.org": { + "bobDesktop": { + hello: "bob", + }, + }, + }; + + it("sends unencrypted messages", async () => { + await driver.sendToDevice("org.example.foo", false, contentMap); + expect(client.queueToDevice.mock.calls).toMatchSnapshot(); + }); + + it("sends encrypted messages", async () => { + const aliceWeb = new DeviceInfo("aliceWeb"); + const aliceMobile = new DeviceInfo("aliceMobile"); + const bobDesktop = new DeviceInfo("bobDesktop"); + + mocked(client.crypto.deviceList).downloadKeys.mockResolvedValue({ + "@alice:example.org": { aliceWeb, aliceMobile }, + "@bob:example.org": { bobDesktop }, + }); + + await driver.sendToDevice("org.example.foo", true, contentMap); + expect(client.encryptAndSendToDevices.mock.calls).toMatchSnapshot(); + }); + }); +}); diff --git a/test/stores/widgets/__snapshots__/StopGapWidgetDriver-test.ts.snap b/test/stores/widgets/__snapshots__/StopGapWidgetDriver-test.ts.snap new file mode 100644 index 00000000000..5f19dbb793d --- /dev/null +++ b/test/stores/widgets/__snapshots__/StopGapWidgetDriver-test.ts.snap @@ -0,0 +1,82 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`StopGapWidgetDriver sendToDevice sends encrypted messages 1`] = ` +Array [ + Array [ + Array [ + Object { + "deviceInfo": DeviceInfo { + "algorithms": undefined, + "deviceId": "aliceWeb", + "keys": Object {}, + "known": false, + "signatures": Object {}, + "unsigned": Object {}, + "verified": 0, + }, + "userId": "@alice:example.org", + }, + Object { + "deviceInfo": DeviceInfo { + "algorithms": undefined, + "deviceId": "aliceMobile", + "keys": Object {}, + "known": false, + "signatures": Object {}, + "unsigned": Object {}, + "verified": 0, + }, + "userId": "@alice:example.org", + }, + ], + Object { + "hello": "alice", + }, + ], + Array [ + Array [ + Object { + "deviceInfo": DeviceInfo { + "algorithms": undefined, + "deviceId": "bobDesktop", + "keys": Object {}, + "known": false, + "signatures": Object {}, + "unsigned": Object {}, + "verified": 0, + }, + "userId": "@bob:example.org", + }, + ], + Object { + "hello": "bob", + }, + ], +] +`; + +exports[`StopGapWidgetDriver sendToDevice sends unencrypted messages 1`] = ` +Array [ + Array [ + Object { + "batch": Array [ + Object { + "deviceId": "*", + "payload": Object { + "hello": "alice", + }, + "userId": "@alice:example.org", + }, + Object { + "deviceId": "bobDesktop", + "payload": Object { + "hello": "bob", + }, + "userId": "@bob:example.org", + }, + ], + "eventType": "org.example.foo", + }, + ], +] +`; diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 39f8e8a7ece..391683d5d15 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -91,6 +91,12 @@ export function createTestClient(): MatrixClient { removeRoom: jest.fn(), }, + crypto: { + deviceList: { + downloadKeys: jest.fn(), + }, + }, + getPushActionsForEvent: jest.fn(), getRoom: jest.fn().mockImplementation(mkStubRoom), getRooms: jest.fn().mockReturnValue([]), @@ -163,6 +169,9 @@ export function createTestClient(): MatrixClient { downloadKeys: jest.fn(), fetchRoomEvent: jest.fn(), makeTxnId: jest.fn().mockImplementation(() => `t${txnId++}`), + sendToDevice: jest.fn().mockResolvedValue(undefined), + queueToDevice: jest.fn().mockResolvedValue(undefined), + encryptAndSendToDevices: jest.fn().mockResolvedValue(undefined), } as unknown as MatrixClient; } @@ -176,7 +185,7 @@ type MakeEventPassThruProps = { type MakeEventProps = MakeEventPassThruProps & { type: string; content: IContent; - room: Room["roomId"]; + room?: Room["roomId"]; // to-device messages are roomless // eslint-disable-next-line camelcase prev_content?: IContent; unsigned?: IUnsigned; diff --git a/yarn.lock b/yarn.lock index f2f623b7edf..56d63e5a17a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6813,10 +6813,10 @@ matrix-web-i18n@^1.3.0: "@babel/traverse" "^7.18.5" walk "^2.3.15" -matrix-widget-api@^0.1.0-beta.18: - version "0.1.0-beta.18" - resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.18.tgz#4efd30edec3eeb4211285985464c062fcab59795" - integrity sha512-kCpcs6rrB94Mmr2/1gBJ+6auWyZ5UvOMOn5K2VFafz2/NDMzZg9OVWj9KFYnNAuwwBE5/tCztYEj6OQ+hgbwOQ== +matrix-widget-api@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.0.0.tgz#0cde6839cca66ad817ab12aca3490ccc8bac97d1" + integrity sha512-cy8p/8EteRPTFIAw7Q9EgPUJc2jD19ZahMR8bMKf2NkILDcjuPMC0UWnsJyB3fSnlGw+VbGepttRpULM31zX8Q== dependencies: "@types/events" "^3.0.0" events "^3.2.0" From 28ed87bffeea84bcb6bb3621c2f9efdefd3cb9d1 Mon Sep 17 00:00:00 2001 From: Robin Date: Wed, 10 Aug 2022 09:26:42 -0400 Subject: [PATCH 18/64] Implement MSC3846: Allowing widgets to access TURN servers (#9061) * Implement MSC3819: Allowing widgets to send/receive to-device messages * Don't change the room events and state events drivers * Implement MSC3846: Allowing widgets to access TURN servers * Update to latest matrix-widget-api changes * Support sending encrypted to-device messages * Yield a TURN server immediately * Use queueToDevice for better reliability * Update types for latest WidgetDriver changes * Upgrade matrix-widget-api * Add tests * Test StopGapWidget * Fix a potential memory leak * Add tests * Empty commit to retry CI --- src/stores/widgets/StopGapWidgetDriver.ts | 42 ++++++++++++- .../widgets/StopGapWidgetDriver-test.ts | 59 ++++++++++++++++++- test/test-utils/test-utils.ts | 9 ++- 3 files changed, 106 insertions(+), 4 deletions(-) diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index ee69f0ca9c2..8fe18dbc8c0 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 - 2021 The Matrix.org Foundation C.I.C. + * Copyright 2020 - 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import { IOpenIDCredentials, IOpenIDUpdate, ISendEventDetails, + ITurnServer, IRoomEvent, MatrixCapabilities, OpenIDRequestState, @@ -30,6 +31,7 @@ import { WidgetEventCapability, WidgetKind, } from "matrix-widget-api"; +import { ClientEvent, ITurnServer as IClientTurnServer } from "matrix-js-sdk/src/client"; import { EventType } from "matrix-js-sdk/src/@types/event"; import { IContent, IEvent, MatrixEvent } from "matrix-js-sdk/src/models/event"; import { Room } from "matrix-js-sdk/src/models/room"; @@ -62,6 +64,12 @@ function setRememberedCapabilitiesForWidget(widget: Widget, caps: Capability[]) localStorage.setItem(`widget_${widget.id}_approved_caps`, JSON.stringify(caps)); } +const normalizeTurnServer = ({ urls, username, credential }: IClientTurnServer): ITurnServer => ({ + uris: urls, + username, + password: credential, +}); + export class StopGapWidgetDriver extends WidgetDriver { private allowedCapabilities: Set; @@ -326,4 +334,36 @@ export class StopGapWidgetDriver extends WidgetDriver { public async navigate(uri: string): Promise { navigateToPermalink(uri); } + + public async* getTurnServers(): AsyncGenerator { + const client = MatrixClientPeg.get(); + if (!client.pollingTurnServers || !client.getTurnServers().length) return; + + let setTurnServer: (server: ITurnServer) => void; + let setError: (error: Error) => void; + + const onTurnServers = ([server]: IClientTurnServer[]) => setTurnServer(normalizeTurnServer(server)); + const onTurnServersError = (error: Error, fatal: boolean) => { if (fatal) setError(error); }; + + client.on(ClientEvent.TurnServers, onTurnServers); + client.on(ClientEvent.TurnServersError, onTurnServersError); + + try { + const initialTurnServer = client.getTurnServers()[0]; + yield normalizeTurnServer(initialTurnServer); + + // Repeatedly listen for new TURN servers until an error occurs or + // the caller stops this generator + while (true) { + yield await new Promise((resolve, reject) => { + setTurnServer = resolve; + setError = reject; + }); + } + } finally { + // The loop was broken - clean up + client.off(ClientEvent.TurnServers, onTurnServers); + client.off(ClientEvent.TurnServersError, onTurnServersError); + } + } } diff --git a/test/stores/widgets/StopGapWidgetDriver-test.ts b/test/stores/widgets/StopGapWidgetDriver-test.ts index 7dab35052b4..79046294282 100644 --- a/test/stores/widgets/StopGapWidgetDriver-test.ts +++ b/test/stores/widgets/StopGapWidgetDriver-test.ts @@ -15,8 +15,8 @@ limitations under the License. */ import { mocked, MockedObject } from "jest-mock"; -import { Widget, WidgetKind, WidgetDriver } from "matrix-widget-api"; -import { MatrixClient } from "matrix-js-sdk/src/client"; +import { Widget, WidgetKind, WidgetDriver, ITurnServer } from "matrix-widget-api"; +import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "matrix-js-sdk/src/client"; import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo"; import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; @@ -76,4 +76,59 @@ describe("StopGapWidgetDriver", () => { expect(client.encryptAndSendToDevices.mock.calls).toMatchSnapshot(); }); }); + + describe("getTurnServers", () => { + it("stops if VoIP isn't supported", async () => { + jest.spyOn(client, "pollingTurnServers", "get").mockReturnValue(false); + const servers = driver.getTurnServers(); + expect(await servers.next()).toEqual({ value: undefined, done: true }); + }); + + it("stops if the homeserver provides no TURN servers", async () => { + const servers = driver.getTurnServers(); + expect(await servers.next()).toEqual({ value: undefined, done: true }); + }); + + it("gets TURN servers", async () => { + const server1: ITurnServer = { + uris: [ + "turn:turn.example.com:3478?transport=udp", + "turn:10.20.30.40:3478?transport=tcp", + "turns:10.20.30.40:443?transport=tcp", + ], + username: "1443779631:@user:example.com", + password: "JlKfBy1QwLrO20385QyAtEyIv0=", + }; + const server2: ITurnServer = { + uris: [ + "turn:turn.example.com:3478?transport=udp", + "turn:10.20.30.40:3478?transport=tcp", + "turns:10.20.30.40:443?transport=tcp", + ], + username: "1448999322:@user:example.com", + password: "hunter2", + }; + const clientServer1: IClientTurnServer = { + urls: server1.uris, + username: server1.username, + credential: server1.password, + }; + const clientServer2: IClientTurnServer = { + urls: server2.uris, + username: server2.username, + credential: server2.password, + }; + + client.getTurnServers.mockReturnValue([clientServer1]); + const servers = driver.getTurnServers(); + expect(await servers.next()).toEqual({ value: server1, done: false }); + + const nextServer = servers.next(); + client.getTurnServers.mockReturnValue([clientServer2]); + client.emit(ClientEvent.TurnServers, [clientServer2]); + expect(await nextServer).toEqual({ value: server2, done: false }); + + await servers.return(undefined); + }); + }); }); diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 391683d5d15..fd120a077b0 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -74,7 +74,7 @@ export function createTestClient(): MatrixClient { const eventEmitter = new EventEmitter(); let txnId = 1; - return { + const client = { getHomeserverUrl: jest.fn(), getIdentityServerUrl: jest.fn(), getDomain: jest.fn().mockReturnValue("matrix.org"), @@ -118,6 +118,7 @@ export function createTestClient(): MatrixClient { getThirdpartyProtocols: jest.fn().mockResolvedValue({}), getClientWellKnown: jest.fn().mockReturnValue(null), supportsVoip: jest.fn().mockReturnValue(true), + getTurnServers: jest.fn().mockReturnValue([]), getTurnServersExpiry: jest.fn().mockReturnValue(2 ^ 32), getThirdpartyUser: jest.fn().mockResolvedValue([]), getAccountData: (type) => { @@ -173,6 +174,12 @@ export function createTestClient(): MatrixClient { queueToDevice: jest.fn().mockResolvedValue(undefined), encryptAndSendToDevices: jest.fn().mockResolvedValue(undefined), } as unknown as MatrixClient; + + Object.defineProperty(client, "pollingTurnServers", { + configurable: true, + get: () => true, + }); + return client; } type MakeEventPassThruProps = { From 2e32a4d4b655e94c821dabe71190474cc2c715c9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 10 Aug 2022 14:33:13 +0100 Subject: [PATCH 19/64] Fix invisible power levels tile when showing hidden events (#9162) * Fix invisible power levels tile when showing hidden events * Add regression test --- src/TextForEvent.tsx | 36 ++++++++++++++-------------- src/events/EventTileFactory.tsx | 4 ++++ test/events/EventTileFactory-test.ts | 36 ++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 18 deletions(-) create mode 100644 test/events/EventTileFactory-test.ts diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index 10530d7c9b4..9310391e3e2 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -434,29 +434,29 @@ function textForHistoryVisibilityEvent(event: MatrixEvent): () => string | null // Currently will only display a change if a user's power level is changed function textForPowerEvent(event: MatrixEvent): () => string | null { const senderName = getSenderName(event); - if (!event.getPrevContent() || !event.getPrevContent().users || - !event.getContent() || !event.getContent().users) { + if (!event.getPrevContent()?.users || !event.getContent()?.users) { return null; } - const previousUserDefault = event.getPrevContent().users_default || 0; - const currentUserDefault = event.getContent().users_default || 0; + const previousUserDefault: number = event.getPrevContent().users_default || 0; + const currentUserDefault: number = event.getContent().users_default || 0; // Construct set of userIds - const users = []; - Object.keys(event.getContent().users).forEach( - (userId) => { - if (users.indexOf(userId) === -1) users.push(userId); - }, - ); - Object.keys(event.getPrevContent().users).forEach( - (userId) => { - if (users.indexOf(userId) === -1) users.push(userId); - }, - ); - - const diffs = []; + const users: string[] = []; + Object.keys(event.getContent().users).forEach((userId) => { + if (users.indexOf(userId) === -1) users.push(userId); + }); + Object.keys(event.getPrevContent().users).forEach((userId) => { + if (users.indexOf(userId) === -1) users.push(userId); + }); + + const diffs: { + userId: string; + name: string; + from: number; + to: number; + }[] = []; users.forEach((userId) => { // Previous power level - let from = event.getPrevContent().users[userId]; + let from: number = event.getPrevContent().users[userId]; if (!Number.isInteger(from)) { from = previousUserDefault; } diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx index 9315741c593..88982b373fe 100644 --- a/src/events/EventTileFactory.tsx +++ b/src/events/EventTileFactory.tsx @@ -225,6 +225,10 @@ export function pickFactory( return noEventFactoryFactory(); // improper event type to render } + if (STATE_EVENT_TILE_TYPES[evType] === TextualEventFactory && !hasText(mxEvent, showHiddenEvents)) { + return noEventFactoryFactory(); + } + return STATE_EVENT_TILE_TYPES[evType] ?? noEventFactoryFactory(); } diff --git a/test/events/EventTileFactory-test.ts b/test/events/EventTileFactory-test.ts new file mode 100644 index 00000000000..ebda574dfc2 --- /dev/null +++ b/test/events/EventTileFactory-test.ts @@ -0,0 +1,36 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { EventType, MatrixEvent } from "matrix-js-sdk/src/matrix"; + +import { JSONEventFactory, pickFactory } from "../../src/events/EventTileFactory"; +import { createTestClient } from "../test-utils"; + +const roomId = "!room:example.com"; + +describe("pickFactory", () => { + it("should return JSONEventFactory for a no-op m.room.power_levels event", () => { + const cli = createTestClient(); + const event = new MatrixEvent({ + type: EventType.RoomPowerLevels, + state_key: "", + content: {}, + sender: cli.getUserId(), + room_id: roomId, + }); + expect(pickFactory(event, cli, true)).toBe(JSONEventFactory); + }); +}); From df016ff5f6524c27187d7d13757e10a79d32fbac Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 10 Aug 2022 16:10:08 +0100 Subject: [PATCH 20/64] Upgrade deps (#9165) --- yarn.lock | 313 +++++++++++++++++++++++++++++------------------------- 1 file changed, 171 insertions(+), 142 deletions(-) diff --git a/yarn.lock b/yarn.lock index 56d63e5a17a..c8ac572177f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1044,20 +1044,27 @@ source-map-support "^0.5.16" "@babel/runtime-corejs3@^7.10.2": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.18.6.tgz#6f02c5536911f4b445946a2179554b95c8838635" - integrity sha512-cOu5wH2JFBgMjje+a+fz2JNIWU4GzYpl05oSob3UDvBEh6EuIn+TXFHMmBbhSb+k/4HMzgKCQfEEDArAWNF9Cw== + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.18.9.tgz#7bacecd1cb2dd694eacd32a91fcf7021c20770ae" + integrity sha512-qZEWeccZCrHA2Au4/X05QW5CMdm4VjUDCrGq5gf1ZDcM4hRqreKrtwAn7yci9zfgAS9apvnsFXiGBHBAxZdK9A== dependencies: core-js-pure "^3.20.2" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.9", "@babel/runtime@^7.18.3", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.9", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.6.tgz#6a1ef59f838debd670421f8c7f2cbb8da9751580" integrity sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ== dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.10.2", "@babel/runtime@^7.18.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a" + integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.18.6", "@babel/template@^7.3.3": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.6.tgz#1283f4993e00b929d6e2d3c72fdc9168a2977a31" @@ -1678,96 +1685,96 @@ tslib "^2.4.0" webcrypto-core "^1.7.4" -"@percy/cli-build@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@percy/cli-build/-/cli-build-1.6.1.tgz#f49e6df1ca3b2f548c853c66f75ebb407e2302ea" - integrity sha512-YP4+uLfT14zI1bfjU+VE8fhu6RkwuhRK1AHIFcbonj/HlIhCdFWqyrWcmLp6HYU06JoiNtbI4QQNM3spxWrolg== +"@percy/cli-build@1.8.1": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@percy/cli-build/-/cli-build-1.8.1.tgz#15752ed826ffcfb4160ad5d931ec35b5bdb3e1ea" + integrity sha512-tVn2sDboRepzuf2n96hlyV84q0metkRwN81wusNicPcoZjXagKPfkbLIe/Js5JIDYFGWaTuFxErdFkLnaK2ogQ== dependencies: - "@percy/cli-command" "1.6.1" + "@percy/cli-command" "1.8.1" -"@percy/cli-command@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@percy/cli-command/-/cli-command-1.6.1.tgz#e41b47d038e9ce52ffeeabba19d707468670829d" - integrity sha512-5jeKsKM+it3gh93vV48dBi725ewVGOfxUsjZU8slKcK9IaohXkjr0/aWor9LWgQcug87sLS9VjNlc+UyhTW0KA== +"@percy/cli-command@1.8.1": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@percy/cli-command/-/cli-command-1.8.1.tgz#feee384f7414771482fa86cd1fb9e0f2e0b821b4" + integrity sha512-iCb5o2ZuiY9j3gQDN/Q+b+B2T/BKImfil5ve9kA4I5S2Uv8ujVX2u4tHznrb89hw2xlkBUTbN2nCETH9HR7gcA== dependencies: - "@percy/config" "1.6.1" - "@percy/core" "1.6.1" - "@percy/logger" "1.6.1" + "@percy/config" "1.8.1" + "@percy/core" "1.8.1" + "@percy/logger" "1.8.1" -"@percy/cli-config@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@percy/cli-config/-/cli-config-1.6.1.tgz#f34c72fda6f9903770bb725693b8c7dbbad21150" - integrity sha512-fUO14gra2EdvY0RT5eSxH8QhammDtclcb2vjTwMG6QUW+sGDQQCu8M8s7YBxRKo/aTq+tJy1hfqd6W/rsx0mWQ== +"@percy/cli-config@1.8.1": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@percy/cli-config/-/cli-config-1.8.1.tgz#e4aafac25676ed9dcdeaf44824c95f2a34fe2ab7" + integrity sha512-cjb7rZMjnvnGGJD+z4yCX8u2//Q2CAI3MbltG7ipuEmj2MtzdNwQS1Y8v57fO2g0b0ALp2PqwTXacwhoIKiWCg== dependencies: - "@percy/cli-command" "1.6.1" + "@percy/cli-command" "1.8.1" -"@percy/cli-exec@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@percy/cli-exec/-/cli-exec-1.6.1.tgz#736e5f2c09080e19ebbcf84c1ded13c5e7db2b0b" - integrity sha512-F8j+reJWPu0s+zQuEU2Gm8SZ1QP7cf04MS5hmL1CaE7G7O88ec+yrS6eiSlNrY1+D45B7NYy7l6zWzsP+nQ3TA== +"@percy/cli-exec@1.8.1": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@percy/cli-exec/-/cli-exec-1.8.1.tgz#8452c515d5d4a20e8837171f63b1313ffa4a3907" + integrity sha512-zN3ulxYexsmMDwnx6BfNzaRbsKg08zCyIYF3Kv7Ut18J2FnqZXuF89fO2k/BQnvwzCIvwlkB4YzZxEP27jPORg== dependencies: - "@percy/cli-command" "1.6.1" + "@percy/cli-command" "1.8.1" cross-spawn "^7.0.3" which "^2.0.2" -"@percy/cli-snapshot@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@percy/cli-snapshot/-/cli-snapshot-1.6.1.tgz#0da90a1cf6f7e79898f4f68f1e180c8c7c8f2894" - integrity sha512-lrZR/CIL1zkZdeOPmnidkVtWY6foOq3ekWbaoMgDu0agMjf2lxEX2DvkCS/WwdmF/JvDcquLKO6gS5EHE6mbBQ== +"@percy/cli-snapshot@1.8.1": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@percy/cli-snapshot/-/cli-snapshot-1.8.1.tgz#404d25467069befaf77bcd93118dc7ee22c1c66e" + integrity sha512-VvV+MTTkd6GRkkCLB02BAGKleCt1j3xcyVF2EgUaX8av+9LruONwR8oIpeGc+rCSwlzcpQKAsCduq9cyiz26iw== dependencies: - "@percy/cli-command" "1.6.1" + "@percy/cli-command" "1.8.1" yaml "^2.0.0" -"@percy/cli-upload@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@percy/cli-upload/-/cli-upload-1.6.1.tgz#ffd95ca03d15081a77d9090d1c33b32044db849e" - integrity sha512-F+n7gy5CTfR0BUSsFdxEpfb9CNDnnhnFO0AN9v2FfyiSEei3CXrS2Te6DJS/YXnVI58YeFRbiW6G1LKhP+Vi4g== +"@percy/cli-upload@1.8.1": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@percy/cli-upload/-/cli-upload-1.8.1.tgz#01f08f513933c91618de324e12be6a5a19f4e44f" + integrity sha512-2R4pEfOhYMTp2cdQeBzssoHESjTVcfdtLN2cQiAcV86jpBC+A5tVW7FQnRBjrefOdnLWRCO9HQn69sRdxrH28g== dependencies: - "@percy/cli-command" "1.6.1" + "@percy/cli-command" "1.8.1" fast-glob "^3.2.11" image-size "^1.0.0" "@percy/cli@^1.3.0": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@percy/cli/-/cli-1.6.1.tgz#d3910dd63d2b2b67e57e4c9c675eafb4d471322a" - integrity sha512-BwCcigFixUhi2Wn6X+oucJrqnk/6e4FOsYQI4+lDzZU416+WpsYF0CjmhQVpa9Us278L+qc7eIsDHJwuJeurFw== - dependencies: - "@percy/cli-build" "1.6.1" - "@percy/cli-command" "1.6.1" - "@percy/cli-config" "1.6.1" - "@percy/cli-exec" "1.6.1" - "@percy/cli-snapshot" "1.6.1" - "@percy/cli-upload" "1.6.1" - "@percy/client" "1.6.1" - "@percy/logger" "1.6.1" - -"@percy/client@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@percy/client/-/client-1.6.1.tgz#6bb136c2fe0ab0490acc898de21f8ab3508420a5" - integrity sha512-e5ToG88O1gDDOuQ+i4989vKCfF+CsPSGG9VA06IKSYNP4QBATe9/RYERZ5jk118uEutjxi8lIjzet0eg8rv7BA== - dependencies: - "@percy/env" "1.6.1" - "@percy/logger" "1.6.1" - -"@percy/config@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@percy/config/-/config-1.6.1.tgz#11fb960dca4ecc0575349382a973b6c5c941363a" - integrity sha512-UiB4gpt01VgPUF4ObZBixR1Wwi/ZUaMXBUxmE3wOa3zZrtZXOzbZwQGcntw5ToEq6OQBP100q1vetnP8xhFQtQ== - dependencies: - "@percy/logger" "1.6.1" + version "1.8.1" + resolved "https://registry.yarnpkg.com/@percy/cli/-/cli-1.8.1.tgz#adf3e47ced38a563b065a3f4c153f40f57045ac9" + integrity sha512-vVpLC8fgPAabw9APNIuM00+f9bwgR3BEFln9LJVtqp1L9ILspKLoUl2qt6nO0SgmxPcHJ3/HO6EGoVFoMClZTQ== + dependencies: + "@percy/cli-build" "1.8.1" + "@percy/cli-command" "1.8.1" + "@percy/cli-config" "1.8.1" + "@percy/cli-exec" "1.8.1" + "@percy/cli-snapshot" "1.8.1" + "@percy/cli-upload" "1.8.1" + "@percy/client" "1.8.1" + "@percy/logger" "1.8.1" + +"@percy/client@1.8.1": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@percy/client/-/client-1.8.1.tgz#bbc81c2a41ab44e5f79c93aac6afa68c17a90af8" + integrity sha512-2T0SdIUFwMCsnaUTM0hNQx5lrkg2qPHPKBL/MRrbbTPJVJe3J9PzeR84RJtKviyy+ciEkkm6UaQsWBngbyXXsQ== + dependencies: + "@percy/env" "1.8.1" + "@percy/logger" "1.8.1" + +"@percy/config@1.8.1": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@percy/config/-/config-1.8.1.tgz#46d73b8cdb0e1d7400e8d91fe12e9f6ea08d2b31" + integrity sha512-yKdKx0kh5xyVxBdNVExXsoNuSmKcmpma31DkqqacDUu2nYSjuCGxr3j3y8BRAKURfj59fdhwpFNmjVVB1xiVWA== + dependencies: + "@percy/logger" "1.8.1" ajv "^8.6.2" cosmiconfig "^7.0.0" yaml "^2.0.0" -"@percy/core@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@percy/core/-/core-1.6.1.tgz#f18681ba4ba1d8f25cfe2c624424749ae0b2590a" - integrity sha512-a4EeoynE4pU7qExfLr56nmZNu3ozeCNFk6rCBQqoXbSJy6UVG8nj7U8AWEPeBHqtdk2M+30YAVr6mJCQjkZZ7w== +"@percy/core@1.8.1": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@percy/core/-/core-1.8.1.tgz#b1e419da98251b370cdcea436bb702ee3461d203" + integrity sha512-wwTSJWoKK8tr8AbORlJGJ7uz55v8oUdOrfZtCnRPssWiceaWFXecRbjLXcNCCz0/2nT6Y1ilDsEOjlExYnKhww== dependencies: - "@percy/client" "1.6.1" - "@percy/config" "1.6.1" - "@percy/dom" "1.6.1" - "@percy/logger" "1.6.1" + "@percy/client" "1.8.1" + "@percy/config" "1.8.1" + "@percy/dom" "1.8.1" + "@percy/logger" "1.8.1" content-disposition "^0.5.4" cross-spawn "^7.0.3" extract-zip "^2.0.1" @@ -1785,20 +1792,20 @@ dependencies: "@percy/sdk-utils" "^1.3.1" -"@percy/dom@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@percy/dom/-/dom-1.6.1.tgz#8b8e82817dd88f8497ffce2d8fc5480466cbfef3" - integrity sha512-TAVGiE/7imR3Q6z1ogZFufX+SsPrElBOYNmj+MOFVJyzJ/cTjA9B510Blx1nXTu2VNdK2GAmRGQPbZDty34YMg== +"@percy/dom@1.8.1": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@percy/dom/-/dom-1.8.1.tgz#b3daa0f6f8d95a5b72df4edfdca615ed04f14f93" + integrity sha512-h9XJV+VcVHrMkfIgJ6sJLujtYLzkvRy3aBvUY/iS7yh88qM16pSuTgEHG9fzbOp8ZNZpu6zZdxy4QmeACqzGzw== -"@percy/env@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@percy/env/-/env-1.6.1.tgz#5119de7526e006242b76688c24e94822113af139" - integrity sha512-AguYuqRcKHkCnWndvq1pTrxeUXT0mNULSi9p7D0nN1744036RcdVrE/EhZbgK2fshSHxIrnfPSj0Lnt4Of46lg== +"@percy/env@1.8.1": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@percy/env/-/env-1.8.1.tgz#338d87479dc48fc2ca3e0a66c98f0b5235f322c2" + integrity sha512-noxlV3fesivvXxnWEODUzdoToIurOolvwlEk+ETmpB933zKTpo55UFD5leV5LWP5Oxwrv4uen2i7ZdHRXPD47Q== -"@percy/logger@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@percy/logger/-/logger-1.6.1.tgz#754e3bdaa4419aadc2d7fa95b63f4ef757483cd4" - integrity sha512-cOMzDbg6Or1SnyzT9xCnwIiMDQ4sUR7Ha91CE6NWpV273Ef5PtvBA0joIB7wWSe7t2/a8hQrYJ6eGU4rUcsuTw== +"@percy/logger@1.8.1": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@percy/logger/-/logger-1.8.1.tgz#91e1fb3ec2952dfd170201ab101aa1a7f505c778" + integrity sha512-O1GpuuN6pzBk/dso57LS90APbRNfc4y0qsGEIph+8f1zK6dBZmalwKLzhgrvb267rLe3pduYM5fCiYel0pqh7Q== "@percy/sdk-utils@^1.3.1": version "1.6.1" @@ -1869,9 +1876,9 @@ tslib "^1.9.3" "@sinclair/typebox@^0.24.1": - version "0.24.19" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.19.tgz#5297278e0d8a1aea084685a3216074910ac6c113" - integrity sha512-gHJu8cdYTD5p4UqmQHrxaWrtb/jkH5imLXzuBypWhKzNkW0qfmgz+w1xaJccWVuJta1YYUdlDiPHXRTR4Ku0MQ== + version "0.24.27" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.27.tgz#d55643516a1546174e10da681a8aaa81e757452d" + integrity sha512-K7C7IlQ3zLePEZleUN21ceBA2aLcMnLHTLph8QWk1JK37L90obdpY+QGY8bXMKxf1ht1Z0MNewvXxWv0oGDYFg== "@sinonjs/commons@^1.7.0": version "1.8.3" @@ -2038,9 +2045,9 @@ "@types/node" "*" "@types/geojson@^7946.0.8": - version "7946.0.9" - resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.9.tgz#dd5d4c39c989c01f49c1b05df5826ffd4ddae1e9" - integrity sha512-snVY+R7d0VfUEHdfEe/NTFnz0+Qei0kEGIV8ErryWQwDXNS7uzhVEMcUtZ+uBHXSBcJa0zBcevBE0u41acbodQ== + version "7946.0.10" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.10.tgz#6dfbf5ea17142f7f9a043809f1cd4c448cb68249" + integrity sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA== "@types/graceful-fs@^4.1.2": version "4.1.5" @@ -2115,14 +2122,14 @@ integrity sha512-jhMOZSS0UGYTS9pqvt6q3wtT3uvOSve5piTEmTMx3zzTuBLvSIMxSIBIc3d5lajVD5h4xc41AMZD2M5orN3PxA== "@types/node@*": - version "18.0.3" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.3.tgz#463fc47f13ec0688a33aec75d078a0541a447199" - integrity sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ== + version "18.6.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.6.5.tgz#06caea822caf9e59d5034b695186ee74154d2802" + integrity sha512-Xjt5ZGUa5WusGZJ4WJPbOT8QOqp6nDynVFRKcUt32bOgvXEoc6o085WNkYTMO7ifAj2isEfQQ2cseE+wT6jsRw== "@types/node@^14.14.22", "@types/node@^14.14.31": - version "14.18.21" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.21.tgz#0155ee46f6be28b2ff0342ca1a9b9fd4468bef41" - integrity sha512-x5W9s+8P4XteaxT/jKF0PSb7XEvo5VmqEWgsMlyeY4ZlLK8I6aH6g5TPPyDlLAep+GYf4kefb7HFyc7PAO3m+Q== + version "14.18.23" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.23.tgz#70f5f20b0b1b38f696848c1d3647bb95694e615e" + integrity sha512-MhbCWN18R4GhO8ewQWAFK4TGQdBpXWByukz7cWyJmXhvRuCIaM/oWytGPqVmDzgEnnaIc9ss6HbU5mUi+vyZPA== "@types/normalize-package-data@^2.4.0": version "2.4.1" @@ -2267,9 +2274,9 @@ "@types/yargs-parser" "*" "@types/yargs@^17.0.8": - version "17.0.10" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.10.tgz#591522fce85d8739bca7b8bb90d048e4478d186a" - integrity sha512-gmEaFwpj/7f/ROdtIlci1R1VYU1J4j95m8T+Tj3iBgiBFKg1foE/PSl93bBd5T9LDXNPo8UlNN6W0qwD8O5OaA== + version "17.0.11" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.11.tgz#5e10ca33e219807c0eee0f08b5efcba9b6a42c06" + integrity sha512-aB4y9UDUXTSMxmM4MH+YnuR0g5Cph3FLQBoWoMB21DSvFVAxRVEHEMx3TLh+zUZYMCQtKiqazz0Q4Rre31f/OA== dependencies: "@types/yargs-parser" "*" @@ -2714,11 +2721,6 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== -axe-core@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.2.tgz#dcf7fb6dea866166c3eab33d68208afe4d5f670c" - integrity sha512-LVAaGp/wkkgYJcjmHsoKx4juT1aQvJyPcW09MLCjVTh3V2cc6PnyempiLMNH5iMdfIX/zdbjUx2KDjMLCTdPeA== - axe-core@^4.4.3: version "4.4.3" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.3.tgz#11c74d23d5013c0fa5d183796729bc3482bd2f6f" @@ -3411,9 +3413,9 @@ core-js-compat@^3.21.0, core-js-compat@^3.22.1: semver "7.0.0" core-js-pure@^3.20.2: - version "3.23.4" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.23.4.tgz#aba5c7fb297063444f6bf93afb0362151679a012" - integrity sha512-lizxkcgj3XDmi7TUBFe+bQ1vNpD5E4t76BrBWI3HdUxdw/Mq1VF4CkiHzIKyieECKtcODK2asJttoofEeUKICQ== + version "3.24.1" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.24.1.tgz#8839dde5da545521bf282feb7dc6d0b425f39fd3" + integrity sha512-r1nJk41QLLPyozHUUPmILCEMtMw24NG4oWK6RbsDdjzQgg9ZvrUsPBj1MnG0wXXp1DCDU6j+wUvEmBSrtRbLXg== core-js@^1.0.0: version "1.2.7" @@ -3560,9 +3562,9 @@ cypress-real-events@^1.7.1: integrity sha512-/Bg15RgJ0SYsuXc6lPqH08x19z6j2vmhWN4wXfJqm3z8BTAFiK2MvipZPzxT8Z0jJP0q7kuniWrLIvz/i/8lCQ== cypress@^10.3.0: - version "10.3.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-10.3.0.tgz#fae8d32f0822fcfb938e79c7c31ef344794336ae" - integrity sha512-txkQWKzvBVnWdCuKs5Xc08gjpO89W2Dom2wpZgT9zWZT5jXxqPIxqP/NC1YArtkpmp3fN5HW8aDjYBizHLUFvg== + version "10.4.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-10.4.0.tgz#bb5b3b6588ad49eff172fecf5778cc0da2980e4e" + integrity sha512-OM7F8MRE01SHQRVVzunid1ZK1m90XTxYnl+7uZfIrB4CYqUDCrZEeSyCXzIbsS6qcaijVCAhqDL60SxG8N6hew== dependencies: "@cypress/request" "^2.88.10" "@cypress/xvfb" "^1.2.4" @@ -3642,9 +3644,9 @@ date-names@^0.1.11: integrity sha512-IxxoeD9tdx8pXVcmqaRlPvrXIsSrSrIZzfzlOkm9u+hyzKp5Wk/odt9O/gd7Ockzy8n/WHeEpTVJ2bF3mMV4LA== dayjs@^1.10.4: - version "1.11.3" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.3.tgz#4754eb694a624057b9ad2224b67b15d552589258" - integrity sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A== + version "1.11.4" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.4.tgz#3b3c10ca378140d8917e06ebc13a4922af4f433e" + integrity sha512-Zj/lPM5hOvQ1Bf7uAvewDaUcsJoI6JmNqmHhHl3nyumwe0XHwt8sWdOVAPACJzCebL8gQCi+K49w7iKWnGwX9g== debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" @@ -3756,9 +3758,9 @@ detect-node-es@^1.1.0: integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== diff-dom@^4.2.2: - version "4.2.3" - resolved "https://registry.yarnpkg.com/diff-dom/-/diff-dom-4.2.3.tgz#c6234b49c1b49e41601d2f08dbb26cd57842de45" - integrity sha512-8OZPIbTWVhkQVlUlsb+VuMEMpTpKKhO5FTwds2bYVIaBiPNJCG1YW5qXUyLWMux5gC2UGyYXjtX05SPivnGMCw== + version "4.2.5" + resolved "https://registry.yarnpkg.com/diff-dom/-/diff-dom-4.2.5.tgz#5e093486d4ce706c702f0151c1b674aa015ac0a6" + integrity sha512-muGbiH5Mkj+bCigiG4x8tGES1JQQHp8UpAEaemOqfQkiwtCxKqDYPOeqBzoTRG+L7mKwHgTPY2WBlgOnnnUmAw== diff-match-patch@^1.0.5: version "1.0.5" @@ -4201,20 +4203,20 @@ eslint-plugin-import@^2.25.4: tsconfig-paths "^3.14.1" eslint-plugin-jsx-a11y@^6.5.1: - version "6.6.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.0.tgz#2c5ac12e013eb98337b9aa261c3b355275cc6415" - integrity sha512-kTeLuIzpNhXL2CwLlc8AHI0aFRwWHcg483yepO9VQiHzM9bZwJdzTkzBszbuPrbgGmq2rlX/FaT2fJQsjUSHsw== + version "6.6.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.1.tgz#93736fc91b83fdc38cc8d115deedfc3091aef1ff" + integrity sha512-sXgFVNHiWffBq23uiS/JaP6eVR622DqwB4yTzKvGZGcPq6/yZ3WmOZfuBks/vHWo9GaFOqC2ZK4i6+C35knx7Q== dependencies: - "@babel/runtime" "^7.18.3" + "@babel/runtime" "^7.18.9" aria-query "^4.2.2" array-includes "^3.1.5" ast-types-flow "^0.0.7" - axe-core "^4.4.2" + axe-core "^4.4.3" axobject-query "^2.2.0" damerau-levenshtein "^1.0.8" emoji-regex "^9.2.2" has "^1.0.3" - jsx-ast-utils "^3.3.1" + jsx-ast-utils "^3.3.2" language-tags "^1.0.5" minimatch "^3.1.2" semver "^6.3.0" @@ -4380,9 +4382,9 @@ event-emitter@^0.3.5: es5-ext "~0.10.14" eventemitter2@^6.4.3: - version "6.4.6" - resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.6.tgz#92d56569cc147a4d9b9da9e942e89b20ce236b0a" - integrity sha512-OHqo4wbHX5VbvlbB6o6eDwhYmiTjrpWACjF8Pmof/GTD6rdBNdZFNck3xlhqOiQFGCOoq3uzHvA0cQpFHIGVAQ== + version "6.4.7" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.7.tgz#a7f6c4d7abf28a14c1ef3442f21cb306a054271d" + integrity sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg== events@^3.2.0: version "3.3.0" @@ -6424,7 +6426,7 @@ jsprim@^2.0.2: json-schema "0.4.0" verror "1.10.0" -"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.1: +"jsx-ast-utils@^2.4.1 || ^3.0.0": version "3.3.2" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.2.tgz#afe5efe4332cd3515c065072bd4d6b0aa22152bd" integrity sha512-4ZCADZHRkno244xlNnn4AOG6sRQ7iBZ5BbgZ4vW4y5IZw7cVUD1PPeblm1xx/nfmMxPdt/LHsXZW8z/j58+l9Q== @@ -6432,10 +6434,18 @@ jsprim@^2.0.2: array-includes "^3.1.5" object.assign "^4.1.2" +jsx-ast-utils@^3.3.2: + version "3.3.3" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz#76b3e6e6cece5c69d49a5792c3d01bd1a0cdc7ea" + integrity sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw== + dependencies: + array-includes "^3.1.5" + object.assign "^4.1.3" + jszip@^3.7.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.0.tgz#faf3db2b4b8515425e34effcdbb086750a346061" - integrity sha512-LDfVtOLtOxb9RXkYOwPyNBTQDL4eUbqahtoY6x07GiDJHwSYvn8sHHIw8wINImV3MqbMNve2gSuM1DDqEKk09Q== + version "3.10.1" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" + integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g== dependencies: lie "~3.3.0" pako "~1.0.2" @@ -6793,9 +6803,9 @@ matrix-events-sdk@^0.0.1-beta.7: unhomoglyph "^1.0.6" matrix-mock-request@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/matrix-mock-request/-/matrix-mock-request-2.1.0.tgz#86f5b0ef846865d0767d3a8e64f5bcd6ca94c178" - integrity sha512-Cjpl3yP6h0yu5GKG89m1XZXZlm69Kg/qHV41N/t6SrQsgcfM3Bfavqx9YrtG0UnuXGy4bBSZIe1QiWVeFPZw1A== + version "2.1.2" + resolved "https://registry.yarnpkg.com/matrix-mock-request/-/matrix-mock-request-2.1.2.tgz#11e38ed1233dced88a6f2bfba1684d5c5b3aa2c2" + integrity sha512-/OXCIzDGSLPJ3fs+uzDrtaOHI/Sqp4iEuniRn31U8S06mPXbvAnXknHqJ4c6A/KVwJj/nPFbGXpK4wPM038I6A== dependencies: expect "^28.1.0" @@ -7157,7 +7167,7 @@ object-visit@^1.0.0: dependencies: isobject "^3.0.0" -object.assign@^4.1.0, object.assign@^4.1.2: +object.assign@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== @@ -7167,6 +7177,16 @@ object.assign@^4.1.0, object.assign@^4.1.2: has-symbols "^1.0.1" object-keys "^1.1.1" +object.assign@^4.1.2, object.assign@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.3.tgz#d36b7700ddf0019abb6b1df1bb13f6445f79051f" + integrity sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + has-symbols "^1.0.3" + object-keys "^1.1.1" + object.entries@^1.1.1, object.entries@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.5.tgz#e1acdd17c4de2cd96d5a08487cfb9db84d881861" @@ -7531,7 +7551,16 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.3.11, postcss@^8.4.14: +postcss@^8.3.11: + version "8.4.16" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.16.tgz#33a1d675fac39941f5f445db0de4db2b6e01d43c" + integrity sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ== + dependencies: + nanoid "^3.3.4" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +postcss@^8.4.14: version "8.4.14" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf" integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig== @@ -7857,9 +7886,9 @@ react-test-renderer@^17.0.0, react-test-renderer@^17.0.2: scheduler "^0.20.2" react-transition-group@^4.4.1: - version "4.4.2" - resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470" - integrity sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg== + version "4.4.5" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" + integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== dependencies: "@babel/runtime" "^7.5.5" dom-helpers "^5.0.1" @@ -8228,9 +8257,9 @@ sane@^4.0.3: walker "~1.0.5" sanitize-html@^2.3.2: - version "2.7.0" - resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.7.0.tgz#e106205b468aca932e2f9baf241f24660d34e279" - integrity sha512-jfQelabOn5voO7FAfnQF7v+jsA6z9zC/O4ec0z3E35XPEtHYJT/OdUziVWlKW4irCr2kXaQAyXTXDHWAibg1tA== + version "2.7.1" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.7.1.tgz#a6c2c1a88054a79eeacfac9b0a43f1b393476901" + integrity sha512-oOpe8l4J8CaBk++2haoN5yNI5beekjuHv3JRPKUx/7h40Rdr85pemn4NkvUB3TcBP7yjat574sPlcMAyv4UQig== dependencies: deepmerge "^4.2.2" escape-string-regexp "^4.0.0" @@ -9422,9 +9451,9 @@ ws@^7.4.6: integrity sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw== ws@^8.0.0: - version "8.8.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.0.tgz#8e71c75e2f6348dbf8d78005107297056cb77769" - integrity sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ== + version "8.8.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.1.tgz#5dbad0feb7ade8ecc99b830c1d77c913d4955ff0" + integrity sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA== xml-name-validator@^3.0.0: version "3.0.0" From 4e30d3c0fc4b62ef1271989c68dbd6539f3b2726 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 10 Aug 2022 16:29:53 +0100 Subject: [PATCH 21/64] Fix space panel subspace indentation going missing (#9167) * Fix space panel subspace indentation going missing * Add cypress test around subspaces in space panel * Add cypress test around subspaces in space panel * Fix bad selector * Fix aria axe violation heading-order * Fix test * Remove it.only --- cypress/e2e/spaces/spaces.spec.ts | 38 ++++++++++++++++++++++++++ res/css/structures/_HomePage.pcss | 6 ++-- res/css/structures/_SpacePanel.pcss | 5 ++++ src/components/structures/HomePage.tsx | 6 ++-- 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/cypress/e2e/spaces/spaces.spec.ts b/cypress/e2e/spaces/spaces.spec.ts index 0a8212ab8dd..e7767de9421 100644 --- a/cypress/e2e/spaces/spaces.spec.ts +++ b/cypress/e2e/spaces/spaces.spec.ts @@ -237,4 +237,42 @@ describe("Spaces", () => { cy.contains(".mx_SpaceHierarchy_roomTile", "Gaming").should("exist"); }); }); + + it("should render subspaces in the space panel only when expanded", () => { + cy.injectAxe(); + + cy.createSpace({ + name: "Child Space", + initial_state: [], + }).then(spaceId => { + cy.createSpace({ + name: "Root Space", + initial_state: [ + spaceChildInitialState(spaceId), + ], + }).as("spaceId"); + }); + cy.get('.mx_SpacePanel .mx_SpaceButton[aria-label="Root Space"]').should("exist"); + cy.get('.mx_SpacePanel .mx_SpaceButton[aria-label="Child Space"]').should("not.exist"); + + const axeOptions = { + rules: { + // Disable this check as it triggers on nested roving tab index elements which are in practice fine + 'nested-interactive': { + enabled: false, + }, + }, + }; + cy.checkA11y(undefined, axeOptions); + cy.get(".mx_SpacePanel").percySnapshotElement("Space panel collapsed", { widths: [68] }); + + cy.get(".mx_SpaceButton_toggleCollapse").click({ force: true }); + cy.get(".mx_SpacePanel:not(.collapsed)").should("exist"); + + cy.contains(".mx_SpaceItem", "Root Space").should("exist") + .contains(".mx_SpaceItem", "Child Space").should("exist"); + + cy.checkA11y(undefined, axeOptions); + cy.get(".mx_SpacePanel").percySnapshotElement("Space panel expanded", { widths: [258] }); + }); }); diff --git a/res/css/structures/_HomePage.pcss b/res/css/structures/_HomePage.pcss index 6bfabd9c87f..f35de9919ce 100644 --- a/res/css/structures/_HomePage.pcss +++ b/res/css/structures/_HomePage.pcss @@ -37,15 +37,15 @@ limitations under the License. } h1 { - font-weight: 600; + font-weight: $font-semi-bold; font-size: $font-32px; line-height: $font-44px; margin-bottom: 4px; } - h4 { + h2 { margin-top: 4px; - font-weight: 600; + font-weight: $font-semi-bold; font-size: $font-18px; line-height: $font-25px; color: $muted-fg-color; diff --git a/res/css/structures/_SpacePanel.pcss b/res/css/structures/_SpacePanel.pcss index 72dbddf75e1..6517aec5e10 100644 --- a/res/css/structures/_SpacePanel.pcss +++ b/res/css/structures/_SpacePanel.pcss @@ -286,6 +286,11 @@ $activeBorderColor: $primary-content; visibility: hidden; } } + + .mx_SpaceTreeLevel { + // Indent subspaces + padding-left: 16px; + } } .mx_SpaceButton_avatarWrapper { diff --git a/src/components/structures/HomePage.tsx b/src/components/structures/HomePage.tsx index 613625ae702..b2596cee435 100644 --- a/src/components/structures/HomePage.tsx +++ b/src/components/structures/HomePage.tsx @@ -85,7 +85,7 @@ const UserWelcomeTop = () => {

    { _tDom("Welcome %(name)s", { name: ownProfile.displayName }) }

    -

    { _tDom("Now, let's help you get started") }

    +

    { _tDom("Now, let's help you get started") }

  • ; }; @@ -107,11 +107,11 @@ const HomePage: React.FC = ({ justRegistered = false }) => { introSection = {config.brand}

    { _tDom("Welcome to %(appName)s", { appName: config.brand }) }

    -

    { _tDom("Own your conversations.") }

    +

    { _tDom("Own your conversations.") }

    ; } - return + return
    { introSection }
    From b7872f2ff7abef6b5cc28d7866d2265f403ec937 Mon Sep 17 00:00:00 2001 From: Kerry Date: Wed, 10 Aug 2022 18:14:59 +0200 Subject: [PATCH 22/64] Device manager - data fetching (PSG-637) (#9151) * add session manager tab to user settings * fussy import ordering * i18n * extract device fetching logic into hook * use new extended device type in device tile, add verified metadata * add current session section, test * tidy * update types for DeviceWithVerification --- .../views/settings/DevicesPanelEntry.tsx | 9 +- .../views/settings/devices/DeviceTile.tsx | 10 +- .../views/settings/devices/useOwnDevices.ts | 105 ++++++++++++ .../settings/shared/SettingsSubsection.tsx | 8 +- .../settings/tabs/user/SessionManagerTab.tsx | 16 +- src/i18n/strings/en_EN.json | 2 + .../settings/devices/DeviceTile-test.tsx | 6 + .../devices/SelectableDeviceTile-test.tsx | 1 + .../__snapshots__/DeviceTile-test.tsx.snap | 54 +++++++ .../SelectableDeviceTile-test.tsx.snap | 7 + .../tabs/user/SessionManagerTab-test.tsx | 152 ++++++++++++++++++ .../SessionManagerTab-test.tsx.snap | 77 +++++++++ 12 files changed, 434 insertions(+), 13 deletions(-) create mode 100644 src/components/views/settings/devices/useOwnDevices.ts create mode 100644 test/components/views/settings/tabs/user/SessionManagerTab-test.tsx create mode 100644 test/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap diff --git a/src/components/views/settings/DevicesPanelEntry.tsx b/src/components/views/settings/DevicesPanelEntry.tsx index b0301214b9b..beb7a8e86e6 100644 --- a/src/components/views/settings/DevicesPanelEntry.tsx +++ b/src/components/views/settings/DevicesPanelEntry.tsx @@ -154,12 +154,17 @@ export default class DevicesPanelEntry extends React.Component { ; + const deviceWithVerification = { + ...this.props.device, + isVerified: this.props.verified, + }; + if (this.props.isOwnDevice) { return
    - + { buttons }
    ; @@ -167,7 +172,7 @@ export default class DevicesPanelEntry extends React.Component { return (
    - + { buttons }
    diff --git a/src/components/views/settings/devices/DeviceTile.tsx b/src/components/views/settings/devices/DeviceTile.tsx index 33f9fc40a85..9e9a520fcea 100644 --- a/src/components/views/settings/devices/DeviceTile.tsx +++ b/src/components/views/settings/devices/DeviceTile.tsx @@ -15,21 +15,21 @@ limitations under the License. */ import React, { Fragment } from "react"; -import { IMyDevice } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../../languageHandler"; import { formatDate, formatRelativeTime } from "../../../../DateUtils"; import TooltipTarget from "../../elements/TooltipTarget"; import { Alignment } from "../../elements/Tooltip"; import Heading from "../../typography/Heading"; +import { DeviceWithVerification } from "./useOwnDevices"; export interface DeviceTileProps { - device: IMyDevice; + device: DeviceWithVerification; children?: React.ReactNode; onClick?: () => void; } -const DeviceTileName: React.FC<{ device: IMyDevice }> = ({ device }) => { +const DeviceTileName: React.FC<{ device: DeviceWithVerification }> = ({ device }) => { if (device.display_name) { return = ({ value, id }) const DeviceTile: React.FC = ({ device, children, onClick }) => { const lastActivity = device.last_seen_ts && `${_t('Last activity')} ${formatLastActivity(device.last_seen_ts)}`; + const verificationStatus = device.isVerified ? _t('Verified') : _t('Unverified'); const metadata = [ + { id: 'isVerified', value: verificationStatus }, { id: 'lastActivity', value: lastActivity }, { id: 'lastSeenIp', value: device.last_seen_ip }, ]; - return
    + return
    diff --git a/src/components/views/settings/devices/useOwnDevices.ts b/src/components/views/settings/devices/useOwnDevices.ts new file mode 100644 index 00000000000..ad9523cc14f --- /dev/null +++ b/src/components/views/settings/devices/useOwnDevices.ts @@ -0,0 +1,105 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { useContext, useEffect, useState } from "react"; +import { IMyDevice, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning"; +import { logger } from "matrix-js-sdk/src/logger"; + +import MatrixClientContext from "../../../../contexts/MatrixClientContext"; + +export type DeviceWithVerification = IMyDevice & { isVerified: boolean | null }; + +const isDeviceVerified = ( + matrixClient: MatrixClient, + crossSigningInfo: CrossSigningInfo, + device: IMyDevice, +): boolean | null => { + try { + const deviceInfo = matrixClient.getStoredDevice(matrixClient.getUserId(), device.device_id); + return crossSigningInfo.checkDeviceTrust( + crossSigningInfo, + deviceInfo, + false, + true, + ).isCrossSigningVerified(); + } catch (error) { + logger.error("Error getting device cross-signing info", error); + return null; + } +}; + +const fetchDevicesWithVerification = async (matrixClient: MatrixClient): Promise => { + const { devices } = await matrixClient.getDevices(); + const crossSigningInfo = matrixClient.getStoredCrossSigningForUser(matrixClient.getUserId()); + + const devicesDict = devices.reduce((acc, device: IMyDevice) => ({ + ...acc, + [device.device_id]: { + ...device, + isVerified: isDeviceVerified(matrixClient, crossSigningInfo, device), + }, + }), {}); + + return devicesDict; +}; +export enum OwnDevicesError { + Unsupported = 'Unsupported', + Default = 'Default', +} +type DevicesState = { + devices: Record; + currentDeviceId: string; + isLoading: boolean; + error?: OwnDevicesError; +}; +export const useOwnDevices = (): DevicesState => { + const matrixClient = useContext(MatrixClientContext); + + const currentDeviceId = matrixClient.getDeviceId(); + + const [devices, setDevices] = useState({}); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(); + + useEffect(() => { + const getDevicesAsync = async () => { + setIsLoading(true); + try { + const devices = await fetchDevicesWithVerification(matrixClient); + setDevices(devices); + setIsLoading(false); + } catch (error) { + if (error.httpStatus == 404) { + // 404 probably means the HS doesn't yet support the API. + setError(OwnDevicesError.Unsupported); + } else { + logger.error("Error loading sessions:", error); + setError(OwnDevicesError.Default); + } + setIsLoading(false); + } + }; + getDevicesAsync(); + }, [matrixClient]); + + return { + devices, + currentDeviceId, + isLoading, + error, + }; +}; diff --git a/src/components/views/settings/shared/SettingsSubsection.tsx b/src/components/views/settings/shared/SettingsSubsection.tsx index 5dcdc9dad6f..6d23a080caa 100644 --- a/src/components/views/settings/shared/SettingsSubsection.tsx +++ b/src/components/views/settings/shared/SettingsSubsection.tsx @@ -14,18 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { HTMLAttributes } from "react"; import Heading from "../../typography/Heading"; -export interface SettingsSubsectionProps { +export interface SettingsSubsectionProps extends HTMLAttributes { heading: string; description?: string | React.ReactNode; children?: React.ReactNode; } -const SettingsSubsection: React.FC = ({ heading, description, children }) => ( -
    +const SettingsSubsection: React.FC = ({ heading, description, children, ...rest }) => ( +
    { heading } { !!description &&
    { description }
    }
    diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index 17c09aeb7a3..7d65ce83da0 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -17,16 +17,26 @@ limitations under the License. import React from 'react'; import { _t } from "../../../../../languageHandler"; +import Spinner from '../../../elements/Spinner'; +import { useOwnDevices } from '../../devices/useOwnDevices'; +import DeviceTile from '../../devices/DeviceTile'; import SettingsSubsection from '../../shared/SettingsSubsection'; import SettingsTab from '../SettingsTab'; const SessionManagerTab: React.FC = () => { + const { devices, currentDeviceId, isLoading } = useOwnDevices(); + + const currentDevice = devices[currentDeviceId]; return + data-testid='current-session-section' + > + { isLoading && } + { !!currentDevice && } + ; }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e601003ecb4..c15c81273cf 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1693,6 +1693,8 @@ "Verification code": "Verification code", "Discovery options will appear once you have added a phone number above.": "Discovery options will appear once you have added a phone number above.", "Last activity": "Last activity", + "Verified": "Verified", + "Unverified": "Unverified", "Unable to remove contact information": "Unable to remove contact information", "Remove %(email)s?": "Remove %(email)s?", "Invalid Email Address": "Invalid Email Address", diff --git a/test/components/views/settings/devices/DeviceTile-test.tsx b/test/components/views/settings/devices/DeviceTile-test.tsx index d688eca9135..4083945fd61 100644 --- a/test/components/views/settings/devices/DeviceTile-test.tsx +++ b/test/components/views/settings/devices/DeviceTile-test.tsx @@ -24,6 +24,7 @@ describe('', () => { const defaultProps = { device: { device_id: '123', + isVerified: false, }, }; const getComponent = (props = {}) => ( @@ -43,6 +44,11 @@ describe('', () => { expect(container).toMatchSnapshot(); }); + it('renders a verified device with no metadata', () => { + const { container } = render(getComponent()); + expect(container).toMatchSnapshot(); + }); + it('renders display name with a tooltip', () => { const device: IMyDevice = { device_id: '123', diff --git a/test/components/views/settings/devices/SelectableDeviceTile-test.tsx b/test/components/views/settings/devices/SelectableDeviceTile-test.tsx index 77dad3e1383..5c0fe47828b 100644 --- a/test/components/views/settings/devices/SelectableDeviceTile-test.tsx +++ b/test/components/views/settings/devices/SelectableDeviceTile-test.tsx @@ -25,6 +25,7 @@ describe('', () => { display_name: 'My Device', device_id: 'my-device', last_seen_ip: '123.456.789', + isVerified: false, }; const defaultProps = { onClick: jest.fn(), diff --git a/test/components/views/settings/devices/__snapshots__/DeviceTile-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/DeviceTile-test.tsx.snap index 299d72348c8..cafd47a8a74 100644 --- a/test/components/views/settings/devices/__snapshots__/DeviceTile-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/DeviceTile-test.tsx.snap @@ -4,6 +4,7 @@ exports[` renders a device with no metadata 1`] = `
    renders a device with no metadata 1`] = ` +
    +
    +
    +
    +`; + +exports[` renders a verified device with no metadata 1`] = ` +
    +
    +
    +

    + 123 +

    +
    @@ -30,6 +70,7 @@ exports[` renders display name with a tooltip 1`] = `
    renders display name with a tooltip 1`] = `
    @@ -60,6 +107,7 @@ exports[` separates metadata with a dot 1`] = `
    separates metadata with a dot 1`] = `