From 6961c256035bed0b7640a6e5907652c806968478 Mon Sep 17 00:00:00 2001 From: Callum Brown Date: Tue, 24 Jan 2023 12:02:07 +0000 Subject: [PATCH 01/97] Support registration tokens (#7275) * Support token authenticated registration https://spec.matrix.org/v1.2/client-server-api/#token-authenticated-registration Signed-off-by: Callum Brown * Backwards compatibility with unstable auth type Some server installs are not updated to use the stable version of the registration token authentication type, so also handle the unstable version defined in MSC3231. Signed-off-by: Callum Brown * Make LOGIN_TYPE public and readonly Co-authored-by: Travis Ralston * Remove line related to skinning Signed-off-by: Callum Brown * Change empty string to null Signed-off-by: Callum Brown * Use "public"s for new code style Signed-off-by: Callum Brown * Change input to AccessibleButton Signed-off-by: Callum Brown * Add more detail regarding source of token Signed-off-by: Callum Brown * Fix lint error The text and button type will be the same every time for registration tokens, unlike (possibly) for SSO. Signed-off-by: Callum Brown * Change null back to "" Due to the following warning when attempting to test: > Warning: `value` prop on `input` should not be null. > Consider using an empty string to clear the component or > `undefined` for uncontrolled components. Signed-off-by: Callum Brown * Disable submit button when no token entered Signed-off-by: Callum Brown * Add test for registration tokens Adapted from test/components/views/dialogs/InteractiveAuthDialog-test.tsx Signed-off-by: Callum Brown * Fix linting errors Signed-off-by: Callum Brown * Fix test for registration tokens Signed-off-by: Callum Brown Signed-off-by: Callum Brown Co-authored-by: Travis Ralston Co-authored-by: Andy Balaam --- .../auth/InteractiveAuthEntryComponents.tsx | 88 ++++++++++++++- src/i18n/strings/en_EN.json | 2 + .../views/auth/RegistrationToken-test.tsx | 106 ++++++++++++++++++ 3 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 test/components/views/auth/RegistrationToken-test.tsx diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index 78fcdf7c2ee..4a995e4d06b 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -692,6 +692,89 @@ export class MsisdnAuthEntry extends React.Component { + public static readonly LOGIN_TYPE = AuthType.RegistrationToken; + + public constructor(props: IAuthEntryProps) { + super(props); + + this.state = { + registrationToken: "", + }; + } + + public componentDidMount(): void { + this.props.onPhaseChange(DEFAULT_PHASE); + } + + private onSubmit = (e: FormEvent): void => { + e.preventDefault(); + if (this.props.busy) return; + + this.props.submitAuthDict({ + // Could be AuthType.RegistrationToken or AuthType.UnstableRegistrationToken + type: this.props.loginType, + token: this.state.registrationToken, + }); + }; + + private onRegistrationTokenFieldChange = (ev: ChangeEvent): void => { + // enable the submit button if the registration token is non-empty + this.setState({ + registrationToken: ev.target.value, + }); + }; + + public render(): JSX.Element { + const registrationTokenBoxClass = classNames({ + error: this.props.errorText, + }); + + let submitButtonOrSpinner; + if (this.props.busy) { + submitButtonOrSpinner = ; + } else { + submitButtonOrSpinner = ( + + {_t("Continue")} + + ); + } + + let errorSection; + if (this.props.errorText) { + errorSection = ( +
+ {this.props.errorText} +
+ ); + } + + return ( +
+

{_t("Enter a registration token provided by the homeserver administrator.")}

+
+ + {errorSection} +
{submitButtonOrSpinner}
+ +
+ ); + } +} + interface ISSOAuthEntryProps extends IAuthEntryProps { continueText?: string; continueKind?: string; @@ -713,7 +796,7 @@ export class SSOAuthEntry extends React.Component mount(); + + beforeEach(function () { + jest.clearAllMocks(); + }); + + afterAll(() => { + unmockClientPeg(); + }); + + const getSubmitButton = (wrapper: ReactWrapper) => wrapper.find('AccessibleButton[kind="primary"]').at(0); + const getRegistrationTokenInput = (wrapper: ReactWrapper) => + wrapper.find('input[name="registrationTokenField"]').at(0); + + it("Should successfully complete a registration token flow", async () => { + const onAuthFinished = jest.fn(); + const makeRequest = jest.fn().mockResolvedValue({ a: 1 }); + + const authData = { + session: "sess", + flows: [{ stages: ["m.login.registration_token"] }], + }; + + const wrapper = getComponent({ makeRequest, onAuthFinished, authData }); + + const registrationTokenNode = getRegistrationTokenInput(wrapper); + const submitNode = getSubmitButton(wrapper); + const formNode = wrapper.find("form").at(0); + + expect(registrationTokenNode).toBeTruthy(); + expect(submitNode).toBeTruthy(); + expect(formNode).toBeTruthy(); + + // submit should be disabled + expect(submitNode.props().disabled).toBe(true); + + // put something in the registration token box + act(() => { + registrationTokenNode.simulate("change", { target: { value: "s3kr3t" } }); + wrapper.setProps({}); + }); + + expect(getRegistrationTokenInput(wrapper).props().value).toEqual("s3kr3t"); + expect(getSubmitButton(wrapper).props().disabled).toBe(false); + + // hit enter; that should trigger a request + act(() => { + formNode.simulate("submit"); + }); + + // wait for auth request to resolve + await flushPromises(); + + expect(makeRequest).toHaveBeenCalledTimes(1); + expect(makeRequest).toBeCalledWith( + expect.objectContaining({ + session: "sess", + type: "m.login.registration_token", + token: "s3kr3t", + }), + ); + + expect(onAuthFinished).toBeCalledTimes(1); + expect(onAuthFinished).toBeCalledWith( + true, + { a: 1 }, + { clientSecret: "t35tcl1Ent5ECr3T", emailSid: undefined }, + ); + }); +}); From 3f766b3f0f195a2ffaa837acd4e1a2df0eb61ccb Mon Sep 17 00:00:00 2001 From: Mustafa Kapadia <60058032+mustafa-kapadia1483@users.noreply.github.com> Date: Wed, 25 Jan 2023 13:21:07 +0530 Subject: [PATCH 02/97] Issue Fix: Misaligned reply preview in thread composer (#9977) --- res/css/views/rooms/_ReplyPreview.pcss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_ReplyPreview.pcss b/res/css/views/rooms/_ReplyPreview.pcss index 96b0321f298..95a061c4042 100644 --- a/res/css/views/rooms/_ReplyPreview.pcss +++ b/res/css/views/rooms/_ReplyPreview.pcss @@ -26,7 +26,7 @@ limitations under the License. display: flex; flex-flow: column; row-gap: $spacing-8; - padding: $spacing-8 $spacing-8 0 $spacing-8; + padding: $spacing-8 $spacing-8 0 0; .mx_ReplyPreview_header { display: flex; From 441ad40e5549470d390ef20fc347e8cb2af4f4f5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 25 Jan 2023 12:33:44 +0000 Subject: [PATCH 03/97] Update dependency ua-parser-js to v1.0.33 [SECURITY] (#9982) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index bef6f4eb3ad..d9d770a0330 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8511,9 +8511,9 @@ ua-parser-js@^0.7.30: integrity sha512-f9BESNVhzlhEFf2CHMSj40NWOjYPl1YKYbrvIr/hFTDEmLq7SRbWvm7FcdcpCYT95zrOhC7gZSxjdnnTpBcwVw== ua-parser-js@^1.0.2: - version "1.0.32" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.32.tgz#786bf17df97de159d5b1c9d5e8e9e89806f8a030" - integrity sha512-dXVsz3M4j+5tTiovFVyVqssXBu5HM47//YSOeZ9fQkdDKkfzv2v3PP1jmH6FUyPW+yCSn7aBVK1fGGKNhowdDA== + version "1.0.33" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.33.tgz#f21f01233e90e7ed0f059ceab46eb190ff17f8f4" + integrity sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ== unbox-primitive@^1.0.2: version "1.0.2" From 222f8a919dd90867bec77a471f90a55a22fb85a5 Mon Sep 17 00:00:00 2001 From: Clark Fischer <439978+clarkf@users.noreply.github.com> Date: Wed, 25 Jan 2023 18:14:15 +0000 Subject: [PATCH 04/97] Fix failing userAgentParser tests (#9988) --- test/utils/device/parseUserAgent-test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/utils/device/parseUserAgent-test.ts b/test/utils/device/parseUserAgent-test.ts index e6a1a228fa5..2aabdc479db 100644 --- a/test/utils/device/parseUserAgent-test.ts +++ b/test/utils/device/parseUserAgent-test.ts @@ -70,7 +70,7 @@ const DESKTOP_UA = [ "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2022091301 Chrome/104.0.5112.102 Electron/20.1.1 Safari/537.36", ]; const DESKTOP_EXPECTED_RESULT = [ - makeDeviceExtendedInfo(DeviceType.Desktop, undefined, "Mac OS", "Electron", "20.1.1"), + makeDeviceExtendedInfo(DeviceType.Desktop, "Apple Macintosh", "Mac OS", "Electron", "20.1.1"), makeDeviceExtendedInfo(DeviceType.Desktop, undefined, "Windows", "Electron", "20.1.1"), ]; @@ -88,10 +88,10 @@ const WEB_UA = [ ]; const WEB_EXPECTED_RESULT = [ - makeDeviceExtendedInfo(DeviceType.Web, undefined, "Mac OS", "Chrome", "104.0.5112.102"), + makeDeviceExtendedInfo(DeviceType.Web, "Apple Macintosh", "Mac OS", "Chrome", "104.0.5112.102"), makeDeviceExtendedInfo(DeviceType.Web, undefined, "Windows", "Chrome", "104.0.5112.102"), - makeDeviceExtendedInfo(DeviceType.Web, undefined, "Mac OS", "Firefox", "39.0"), - makeDeviceExtendedInfo(DeviceType.Web, undefined, "Mac OS", "Safari", "8.0.3"), + makeDeviceExtendedInfo(DeviceType.Web, "Apple Macintosh", "Mac OS", "Firefox", "39.0"), + makeDeviceExtendedInfo(DeviceType.Web, "Apple Macintosh", "Mac OS", "Safari", "8.0.3"), makeDeviceExtendedInfo(DeviceType.Web, undefined, "Windows", "Firefox", "40.0"), makeDeviceExtendedInfo(DeviceType.Web, undefined, "Windows", "Edge", "12.246"), // using mobile browser From 406edfc27d5a38a9f59214560d132467b00cd281 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 26 Jan 2023 11:08:23 +0100 Subject: [PATCH 05/97] Fix link creation with backward selection (#9986) Fix link creation with backward selection --- cypress/e2e/composer/composer.spec.ts | 22 ++++++++++++++- .../rooms/wysiwyg_composer/ComposerContext.ts | 2 +- .../hooks/useComposerFunctions.ts | 1 + .../wysiwyg_composer/hooks/useSelection.ts | 4 +++ .../views/rooms/wysiwyg_composer/types.ts | 4 ++- .../rooms/wysiwyg_composer/utils/selection.ts | 9 +++++-- .../SendWysiwygComposer-test.tsx | 27 +++++++++++++++++++ .../components/LinkModal-test.tsx | 1 + 8 files changed, 65 insertions(+), 5 deletions(-) diff --git a/cypress/e2e/composer/composer.spec.ts b/cypress/e2e/composer/composer.spec.ts index 289d865ba64..48467ac6564 100644 --- a/cypress/e2e/composer/composer.spec.ts +++ b/cypress/e2e/composer/composer.spec.ts @@ -98,7 +98,7 @@ describe("Composer", () => { }); }); - describe("WYSIWYG", () => { + describe("Rich text editor", () => { beforeEach(() => { cy.enableLabsFeature("feature_wysiwyg_composer"); cy.initTestUser(homeserver, "Janet").then(() => { @@ -165,5 +165,25 @@ describe("Composer", () => { cy.contains(".mx_EventTile_body", "my message 3"); }); }); + + describe("links", () => { + it("create link with a forward selection", () => { + // Type a message + cy.get("div[contenteditable=true]").type("my message 0{selectAll}"); + + // Open link modal + cy.get('button[aria-label="Link"]').click(); + // Fill the link field + cy.get('input[label="Link"]').type("https://matrix.org/"); + // Click on save + cy.get('button[type="submit"]').click(); + // Send the message + cy.get('div[aria-label="Send message"]').click(); + + // It was sent + cy.contains(".mx_EventTile_body a", "my message 0"); + cy.get(".mx_EventTile_body a").should("have.attr", "href").and("include", "https://matrix.org/"); + }); + }); }); }); diff --git a/src/components/views/rooms/wysiwyg_composer/ComposerContext.ts b/src/components/views/rooms/wysiwyg_composer/ComposerContext.ts index c8a369ead29..582c883dfee 100644 --- a/src/components/views/rooms/wysiwyg_composer/ComposerContext.ts +++ b/src/components/views/rooms/wysiwyg_composer/ComposerContext.ts @@ -20,7 +20,7 @@ import { SubSelection } from "./types"; export function getDefaultContextValue(): { selection: SubSelection } { return { - selection: { anchorNode: null, anchorOffset: 0, focusNode: null, focusOffset: 0 }, + selection: { anchorNode: null, anchorOffset: 0, focusNode: null, focusOffset: 0, isForward: true }, }; } diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions.ts index cad2e130745..7578fb7389d 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions.ts @@ -44,6 +44,7 @@ export function useComposerFunctions( anchorOffset: anchorOffset + text.length, focusNode: ref.current.firstChild, focusOffset: focusOffset + text.length, + isForward: true, }); setContent(ref.current.innerHTML); } diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useSelection.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useSelection.ts index 7f828cee391..92823773c35 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useSelection.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useSelection.ts @@ -23,11 +23,15 @@ function setSelectionContext(composerContext: ComposerContextState): void { const selection = document.getSelection(); if (selection) { + const range = selection.getRangeAt(0); + const isForward = range.startContainer === selection.anchorNode && range.startOffset === selection.anchorOffset; + composerContext.selection = { anchorNode: selection.anchorNode, anchorOffset: selection.anchorOffset, focusNode: selection.focusNode, focusOffset: selection.focusOffset, + isForward, }; } } diff --git a/src/components/views/rooms/wysiwyg_composer/types.ts b/src/components/views/rooms/wysiwyg_composer/types.ts index 6505825b286..97d3fd91a5e 100644 --- a/src/components/views/rooms/wysiwyg_composer/types.ts +++ b/src/components/views/rooms/wysiwyg_composer/types.ts @@ -19,4 +19,6 @@ export type ComposerFunctions = { insertText: (text: string) => void; }; -export type SubSelection = Pick; +export type SubSelection = Pick & { + isForward: boolean; +}; diff --git a/src/components/views/rooms/wysiwyg_composer/utils/selection.ts b/src/components/views/rooms/wysiwyg_composer/utils/selection.ts index e6a594451bb..ca515d06eef 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/selection.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/selection.ts @@ -19,9 +19,14 @@ import { SubSelection } from "../types"; export function setSelection(selection: SubSelection): Promise { if (selection.anchorNode && selection.focusNode) { const range = new Range(); - range.setStart(selection.anchorNode, selection.anchorOffset); - range.setEnd(selection.focusNode, selection.focusOffset); + if (selection.isForward) { + range.setStart(selection.anchorNode, selection.anchorOffset); + range.setEnd(selection.focusNode, selection.focusOffset); + } else { + range.setStart(selection.focusNode, selection.focusOffset); + range.setEnd(selection.anchorNode, selection.anchorOffset); + } document.getSelection()?.removeAllRanges(); document.getSelection()?.addRange(range); } diff --git a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx index 7f04bf21ffb..aa39de7a573 100644 --- a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx @@ -299,6 +299,7 @@ describe("SendWysiwygComposer", () => { anchorOffset: 2, focusNode: textNode, focusOffset: 2, + isForward: true, }); // the event is not automatically fired by jest document.dispatchEvent(new CustomEvent("selectionchange")); @@ -308,6 +309,32 @@ describe("SendWysiwygComposer", () => { // Then await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent(/wo🦫rd/)); }); + + it("Should add an emoji when a word is selected", async () => { + // When + screen.getByRole("textbox").focus(); + screen.getByRole("textbox").innerHTML = "word"; + fireEvent.input(screen.getByRole("textbox"), { + data: "word", + inputType: "insertText", + }); + + const textNode = screen.getByRole("textbox").firstChild; + await setSelection({ + anchorNode: textNode, + anchorOffset: 3, + focusNode: textNode, + focusOffset: 2, + isForward: false, + }); + // the event is not automatically fired by jest + document.dispatchEvent(new CustomEvent("selectionchange")); + + emojiButton.click(); + + // Then + await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent(/wo🦫d/)); + }); }, ); }); diff --git a/test/components/views/rooms/wysiwyg_composer/components/LinkModal-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/LinkModal-test.tsx index 527f7e7b3d6..852354f0ba5 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/LinkModal-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/LinkModal-test.tsx @@ -35,6 +35,7 @@ describe("LinkModal", () => { anchorNode: null, focusOffset: 3, anchorOffset: 4, + isForward: true, }; const customRender = (isTextEnabled: boolean, onClose: () => void, isEditing = false) => { From 5807c649907ddf38ac2e8ced522da343a71f68a9 Mon Sep 17 00:00:00 2001 From: Marco Bartelt Date: Fri, 27 Jan 2023 08:57:24 +0100 Subject: [PATCH 06/97] Improve avatar settings accessibility (#9985) Co-authored-by: Germain --- .../views/settings/AvatarSetting.tsx | 27 ++++++--- .../views/settings/AvatarSetting-test.tsx | 55 +++++++++++++++++++ 2 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 test/components/views/settings/AvatarSetting-test.tsx diff --git a/src/components/views/settings/AvatarSetting.tsx b/src/components/views/settings/AvatarSetting.tsx index 5c311195f40..33efe8f9d8c 100644 --- a/src/components/views/settings/AvatarSetting.tsx +++ b/src/components/views/settings/AvatarSetting.tsx @@ -14,17 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useState } from "react"; +import React, { useRef, useState } from "react"; import classNames from "classnames"; import { _t } from "../../../languageHandler"; -import AccessibleButton from "../elements/AccessibleButton"; +import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; interface IProps { avatarUrl?: string; avatarName: string; // name of user/room the avatar belongs to - uploadAvatar?: (e: React.MouseEvent) => void; - removeAvatar?: (e: React.MouseEvent) => void; + uploadAvatar?: (e: ButtonEvent) => void; + removeAvatar?: (e: ButtonEvent) => void; avatarAltText: string; } @@ -34,12 +34,16 @@ const AvatarSetting: React.FC = ({ avatarUrl, avatarAltText, avatarName, onMouseEnter: () => setIsHovering(true), onMouseLeave: () => setIsHovering(false), }; + // TODO: Use useId() as soon as we're using React 18. + // Prevents ID collisions when this component is used more than once on the same page. + const a11yId = useRef(`hover-text-${Math.random()}`); let avatarElement = ( ); @@ -50,7 +54,7 @@ const AvatarSetting: React.FC = ({ avatarUrl, avatarAltText, avatarName, src={avatarUrl} alt={avatarAltText} aria-label={avatarAltText} - onClick={uploadAvatar} + onClick={uploadAvatar ?? null} {...hoveringProps} /> ); @@ -60,7 +64,12 @@ const AvatarSetting: React.FC = ({ avatarUrl, avatarAltText, avatarName, if (uploadAvatar) { // insert an empty div to be the host for a css mask containing the upload.svg uploadAvatarBtn = ( - + ); } @@ -80,9 +89,9 @@ const AvatarSetting: React.FC = ({ avatarUrl, avatarAltText, avatarName, return (
{avatarElement} -
+ ); @@ -167,6 +190,8 @@ const SSOButtons: React.FC = ({ matrixClient, flow, loginType, fragmentA idp={idp} mini={true} primary={primary} + action={action} + flow={flow} /> ))}
diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx index 89d070210bd..b1744e80cca 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx @@ -20,6 +20,7 @@ import React from "react"; import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types"; import { IThreepid } from "matrix-js-sdk/src/@types/threepids"; import { logger } from "matrix-js-sdk/src/logger"; +import { IDelegatedAuthConfig, M_AUTHENTICATION } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../../../languageHandler"; import ProfileSettings from "../../ProfileSettings"; @@ -79,6 +80,7 @@ interface IState { loading3pids: boolean; // whether or not the emails and msisdns have been loaded canChangePassword: boolean; idServerName: string; + externalAccountManagementUrl?: string; } export default class GeneralUserSettingsTab extends React.Component { @@ -106,6 +108,7 @@ export default class GeneralUserSettingsTab extends React.Component(cli.getClientWellKnown()); + const externalAccountManagementUrl = delegatedAuthConfig?.account; + + this.setState({ serverSupportsSeparateAddAndBind, canChangePassword, externalAccountManagementUrl }); } private async getThreepidState(): Promise { @@ -348,9 +354,37 @@ export default class GeneralUserSettingsTab extends React.Component +

+ {_t( + "Your account details are managed separately at %(hostname)s.", + { hostname }, + { code: (sub) => {sub} }, + )} +

+ + {_t("Manage account")} + + + ); + } return (
{_t("Account")} + {externalAccountManagement}

{passwordChangeText}

{passwordChangeForm} {threepidSection} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d2ca5a48b00..45db7cb23f2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1532,6 +1532,8 @@ "Email addresses": "Email addresses", "Phone numbers": "Phone numbers", "Set a new account password...": "Set a new account password...", + "Your account details are managed separately at %(hostname)s.": "Your account details are managed separately at %(hostname)s.", + "Manage account": "Manage account", "Account": "Account", "Language and region": "Language and region", "Spell check": "Spell check", diff --git a/test/components/structures/auth/Login-test.tsx b/test/components/structures/auth/Login-test.tsx index 48df7f410fc..25f3263749f 100644 --- a/test/components/structures/auth/Login-test.tsx +++ b/test/components/structures/auth/Login-test.tsx @@ -19,6 +19,7 @@ import { fireEvent, render, screen, waitForElementToBeRemoved } from "@testing-l import { mocked, MockedObject } from "jest-mock"; import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix"; import fetchMock from "fetch-mock-jest"; +import { DELEGATED_OIDC_COMPATIBILITY, IdentityProviderBrand } from "matrix-js-sdk/src/@types/auth"; import SdkConfig from "../../../../src/SdkConfig"; import { mkServerConfig, mockPlatformPeg, unmockPlatformPeg } from "../../../test-utils"; @@ -192,4 +193,64 @@ describe("Login", function () { fireEvent.click(container.querySelector(".mx_SSOButton")); expect(platform.startSingleSignOn.mock.calls[1][0].baseUrl).toBe("https://server2"); }); + + it("should show single Continue button if OIDC MSC3824 compatibility is given by server", async () => { + mockClient.loginFlows.mockResolvedValue({ + flows: [ + { + type: "m.login.sso", + [DELEGATED_OIDC_COMPATIBILITY.name]: true, + }, + { + type: "m.login.password", + }, + ], + }); + + const { container } = getComponent(); + await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading...")); + + const ssoButtons = container.querySelectorAll(".mx_SSOButton"); + + expect(ssoButtons.length).toBe(1); + expect(ssoButtons[0].textContent).toBe("Continue"); + + // no password form visible + expect(container.querySelector("form")).toBeFalsy(); + }); + + it("should show branded SSO buttons", async () => { + const idpsWithIcons = Object.values(IdentityProviderBrand).map((brand) => ({ + id: brand, + brand, + name: `Provider ${brand}`, + })); + + mockClient.loginFlows.mockResolvedValue({ + flows: [ + { + type: "m.login.sso", + identity_providers: [ + ...idpsWithIcons, + { + id: "foo", + name: "Provider foo", + }, + ], + }, + ], + }); + + const { container } = getComponent(); + await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading...")); + + for (const idp of idpsWithIcons) { + const ssoButton = container.querySelector(`.mx_SSOButton.mx_SSOButton_brand_${idp.brand}`); + expect(ssoButton).toBeTruthy(); + expect(ssoButton?.querySelector(`img[alt="${idp.brand}"]`)).toBeTruthy(); + } + + const ssoButtons = container.querySelectorAll(".mx_SSOButton"); + expect(ssoButtons.length).toBe(idpsWithIcons.length + 1); + }); }); diff --git a/test/components/views/settings/tabs/user/GeneralUserSettingsTab-test.tsx b/test/components/views/settings/tabs/user/GeneralUserSettingsTab-test.tsx new file mode 100644 index 00000000000..863f1fbe557 --- /dev/null +++ b/test/components/views/settings/tabs/user/GeneralUserSettingsTab-test.tsx @@ -0,0 +1,77 @@ +/* +Copyright 2023 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 { render } from "@testing-library/react"; +import React from "react"; +import { M_AUTHENTICATION } from "matrix-js-sdk/src/matrix"; + +import GeneralUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/GeneralUserSettingsTab"; +import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext"; +import SettingsStore from "../../../../../../src/settings/SettingsStore"; +import { + getMockClientWithEventEmitter, + mockClientMethodsServer, + mockClientMethodsUser, + mockPlatformPeg, + flushPromises, +} from "../../../../../test-utils"; + +describe("", () => { + const defaultProps = { + closeSettingsFn: jest.fn(), + }; + + const userId = "@alice:server.org"; + const mockClient = getMockClientWithEventEmitter({ + ...mockClientMethodsUser(userId), + ...mockClientMethodsServer(), + }); + + const getComponent = () => ( + + + + ); + + jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); + const clientWellKnownSpy = jest.spyOn(mockClient, "getClientWellKnown"); + + beforeEach(() => { + mockPlatformPeg(); + jest.clearAllMocks(); + clientWellKnownSpy.mockReturnValue({}); + }); + + it("does not show account management link when not available", () => { + const { queryByTestId } = render(getComponent()); + + expect(queryByTestId("external-account-management-outer")).toBeFalsy(); + expect(queryByTestId("external-account-management-link")).toBeFalsy(); + }); + + it("show account management link in expected format", async () => { + const accountManagementLink = "https://id.server.org/my-account"; + clientWellKnownSpy.mockReturnValue({ + [M_AUTHENTICATION.name]: { + issuer: "https://id.server.org", + account: accountManagementLink, + }, + }); + const { getByTestId } = render(getComponent()); + + // wait for well-known call to settle + await flushPromises(); + + expect(getByTestId("external-account-management-outer").textContent).toMatch(/.*id\.server\.org/); + expect(getByTestId("external-account-management-link").getAttribute("href")).toMatch(accountManagementLink); + }); +}); From a69b805156126708c78e2a0ed7c5fab3919a7f82 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 27 Jan 2023 12:17:31 +0000 Subject: [PATCH 10/97] Revert "Update sentry-javascript monorepo to v7.32.1 (#9896)" This update caused "Cannot redeclare block-scoped variable '__DEBUG_BUILD__' " error This reverts commit b3586cf1941d906d13698f62a3560fd7cb0eb211. --- yarn.lock | 65 +++++++++++++++++++++---------------------------------- 1 file changed, 25 insertions(+), 40 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6946cd23759..d9d770a0330 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1903,60 +1903,45 @@ integrity sha512-/nPyK4NCjFGYNVQ7vOivfuEYveOJhA4gWzB7w2PjCkw/Y3kCtu+axRpUiDPEybTz2H6RTvr+I526DbtUYguqVw== "@sentry/browser@^7.0.0": - version "7.34.0" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.34.0.tgz#6a521c5d95d535e6e89cf4eae85153f90c37d17a" - integrity sha512-5Jmjj0DLxx+31o12T+VH4U+gO7wz3L+ftjuTxcQaC8GeFVe5qCyXZoDmWKNV9NEyREiZ3azV62bJc5wojZrIIg== - dependencies: - "@sentry/core" "7.34.0" - "@sentry/replay" "7.34.0" - "@sentry/types" "7.34.0" - "@sentry/utils" "7.34.0" - tslib "^1.9.3" - -"@sentry/core@7.34.0": - version "7.34.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.34.0.tgz#bfda8d386cf7343200aa9fb7a7a26e99b839fc0c" - integrity sha512-J1oxsYZX1N0tkEcaHt/uuDqk6zOnaivyampp+EvBsUMCdemjg7rwKvawlRB0ZtBEQu3HAhi8zecm03mlpWfCDw== + version "7.23.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.23.0.tgz#ca2a01ce2b00727036906158efaa1c7af1395cc0" + integrity sha512-2/dLGOSaM5AvlRdMgYxDyxPxkUUqYyxF7QZ0NicdIXkKXa0fM38IdibeXrE8XzC7rF2B7DQZ6U7uDb1Yry60ig== dependencies: - "@sentry/types" "7.34.0" - "@sentry/utils" "7.34.0" + "@sentry/core" "7.23.0" + "@sentry/types" "7.23.0" + "@sentry/utils" "7.23.0" tslib "^1.9.3" -"@sentry/replay@7.34.0": - version "7.34.0" - resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.34.0.tgz#66d63b1e04d7e8068cac0c623a607f470d000751" - integrity sha512-4L4YZfWt8mcVNcI99RxHORPb308URI1R9xsFj97fagk0ATjexLKr5QCA2ApnKaSn8Q0q1Zdzd4XmFtW9anU45Q== +"@sentry/core@7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.23.0.tgz#d320b2b6e5620b41f345bc01d69b547cdf28f78d" + integrity sha512-oNLGsscSdMs1urCbpwe868NsoJWyeTOQXOm5w2e78yE7G6zm2Ra473NQio3lweaEvjQgSGpFyEfAn/3ubZbtPw== dependencies: - "@sentry/core" "7.34.0" - "@sentry/types" "7.34.0" - "@sentry/utils" "7.34.0" + "@sentry/types" "7.23.0" + "@sentry/utils" "7.23.0" + tslib "^1.9.3" "@sentry/tracing@^7.0.0": - version "7.34.0" - resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.34.0.tgz#bc084389cad4f1e8520311ac195b070eced72b40" - integrity sha512-JtfSWBfcWslfIujcpGEPF5oOiAOCd5shMoWYrdTvCfruHhYjp4w5kv/ndkvq2EpFkcQYhdmtQEytXEO8IJIqRw== + version "7.23.0" + resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.23.0.tgz#9b6c5d3761d7664b6e40c476912281589d7cbe43" + integrity sha512-sbwvf6gjLgUTkBwZQOV7RkZPah7KnnpeVcwnNl+vigq6FNgNtejz53FFCo6t4mNGZSerfWbEy/c3C1LMX9AaXw== dependencies: - "@sentry/core" "7.34.0" - "@sentry/types" "7.34.0" - "@sentry/utils" "7.34.0" + "@sentry/core" "7.23.0" + "@sentry/types" "7.23.0" + "@sentry/utils" "7.23.0" tslib "^1.9.3" -"@sentry/types@7.34.0": - version "7.34.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.34.0.tgz#e0dc6a927dd13e4cacbca7bfee67a088885e8309" - integrity sha512-K+OeHIrl35PSYn6Zwqe4b8WWyAJQoI5NeWxHVkM7oQTGJ1YLG4BvLsR+UiUXnKdR5krE4EDtEA5jLsDlBEyPvw== - -"@sentry/types@^7.2.0": +"@sentry/types@7.23.0", "@sentry/types@^7.2.0": version "7.23.0" resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.23.0.tgz#5d2ce94d81d7c1fad702645306f3c0932708cad5" integrity sha512-fZ5XfVRswVZhKoCutQ27UpIHP16tvyc6ws+xq+njHv8Jg8gFBCoOxlJxuFhegD2xxylAn1aiSHNAErFWdajbpA== -"@sentry/utils@7.34.0": - version "7.34.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.34.0.tgz#32fb6db8b352477d219ddff8200372959c68b445" - integrity sha512-VIHHXEBw0htzqxnU8A7WkXKvmsG2pZVqHlAn0H9W/yyFQtXMuP1j1i0NsjADB/3JXUKK83kTNWGzScXvp0o+Jg== +"@sentry/utils@7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.23.0.tgz#5f38640fe49f5abac88f048b92d3e83375d7ddf7" + integrity sha512-ad/XXH03MfgDH/7N7FjKEOVaKrfQWdMaE0nCxZCr2RrvlitlmGQmPpms95epr1CpzSU3BDRImlILx6+TlrXOgg== dependencies: - "@sentry/types" "7.34.0" + "@sentry/types" "7.23.0" tslib "^1.9.3" "@sinclair/typebox@^0.24.1": From e0e149be3de8364127f3dce6844bc3bc3686dbeb Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Fri, 27 Jan 2023 14:58:38 +0100 Subject: [PATCH 11/97] Ensure room is actually in space hierarchy when resolving its latest version Relates to: vector-im/element-web#24329 --- src/components/structures/SpaceHierarchy.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index af6f2983826..f99cf73f5f5 100644 --- a/src/components/structures/SpaceHierarchy.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -413,9 +413,18 @@ interface IHierarchyLevelProps { onToggleClick?(parentId: string, childId: string): void; } -const toLocalRoom = (cli: MatrixClient, room: IHierarchyRoom): IHierarchyRoom => { +const toLocalRoom = (cli: MatrixClient, room: IHierarchyRoom, hierarchy: RoomHierarchy): IHierarchyRoom => { const history = cli.getRoomUpgradeHistory(room.room_id, true); - const cliRoom = history[history.length - 1]; + + // Pick latest room that is actually part of the hierarchy + let cliRoom = null; + for (let idx = history.length - 1; idx >= 0; --idx) { + if (hierarchy.roomMap[history[idx].roomId]) { + cliRoom = history[idx]; + break; + } + } + if (cliRoom) { return { ...room, @@ -461,7 +470,7 @@ export const HierarchyLevel: React.FC = ({ (result, ev: IHierarchyRelation) => { const room = hierarchy.roomMap.get(ev.state_key); if (room && roomSet.has(room)) { - result[room.room_type === RoomType.Space ? 0 : 1].push(toLocalRoom(cli, room)); + result[room.room_type === RoomType.Space ? 0 : 1].push(toLocalRoom(cli, room, hierarchy)); } return result; }, From d84509d8d335d96cb865773f45a29924c923fccf Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 27 Jan 2023 14:07:05 +0000 Subject: [PATCH 12/97] Implement MSC3946 for AdvancedRoomSettingsTab (#9995) --- .../tabs/room/AdvancedRoomSettingsTab.tsx | 20 +++++---- src/i18n/strings/en_EN.json | 2 + src/settings/Settings.tsx | 9 ++++ .../room/AdvancedRoomSettingsTab-test.tsx | 42 +++++++++++++++++++ 4 files changed, 64 insertions(+), 9 deletions(-) diff --git a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx index 809da069baf..d9acba8524a 100644 --- a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx @@ -19,13 +19,14 @@ import { EventType } from "matrix-js-sdk/src/@types/event"; import { _t } from "../../../../../languageHandler"; import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; -import AccessibleButton from "../../../elements/AccessibleButton"; +import AccessibleButton, { ButtonEvent } from "../../../elements/AccessibleButton"; import RoomUpgradeDialog from "../../../dialogs/RoomUpgradeDialog"; import Modal from "../../../../../Modal"; import dis from "../../../../../dispatcher/dispatcher"; import { Action } from "../../../../../dispatcher/actions"; import CopyableText from "../../../elements/CopyableText"; import { ViewRoomPayload } from "../../../../../dispatcher/payloads/ViewRoomPayload"; +import SettingsStore from "../../../../../settings/SettingsStore"; interface IProps { roomId: string; @@ -46,9 +47,11 @@ interface IState { } export default class AdvancedRoomSettingsTab extends React.Component { - public constructor(props, context) { + public constructor(props: IProps, context: any) { super(props, context); + const msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors"); + this.state = { // This is eventually set to the value of room.getRecommendedVersion() upgradeRecommendation: null, @@ -60,11 +63,10 @@ export default class AdvancedRoomSettingsTab extends React.Component = {}; - const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, ""); - const predecessor = createEvent ? createEvent.getContent().predecessor : null; - if (predecessor && predecessor.room_id) { - additionalStateChanges.oldRoomId = predecessor.room_id; - additionalStateChanges.oldEventId = predecessor.event_id; + const predecessor = room.findPredecessor(msc3946ProcessDynamicPredecessor); + if (predecessor) { + additionalStateChanges.oldRoomId = predecessor.roomId; + additionalStateChanges.oldEventId = predecessor.eventId; } this.setState({ @@ -75,12 +77,12 @@ export default class AdvancedRoomSettingsTab extends React.Component { + private upgradeRoom = (): void => { const room = MatrixClientPeg.get().getRoom(this.props.roomId); Modal.createDialog(RoomUpgradeDialog, { room }); }; - private onOldRoomClicked = (e): void => { + private onOldRoomClicked = (e: ButtonEvent): void => { e.preventDefault(); e.stopPropagation(); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 45db7cb23f2..6d315bf38d2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -955,6 +955,8 @@ "New group call experience": "New group call experience", "Live Location Sharing": "Live Location Sharing", "Temporary implementation. Locations persist in room history.": "Temporary implementation. Locations persist in room history.", + "Dynamic room predecessors": "Dynamic room predecessors", + "Enable MSC3946 (to support late-arriving room archives)": "Enable MSC3946 (to support late-arriving room archives)", "Favourite Messages": "Favourite Messages", "Under active development.": "Under active development.", "Force 15s voice broadcast chunk length": "Force 15s voice broadcast chunk length", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index bfd40e96ce1..fdd3a857c12 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -439,6 +439,15 @@ export const SETTINGS: { [setting: string]: ISetting } = { shouldWarn: true, default: false, }, + "feature_dynamic_room_predecessors": { + isFeature: true, + labsGroup: LabGroup.Rooms, + supportedLevels: LEVELS_FEATURE, + displayName: _td("Dynamic room predecessors"), + description: _td("Enable MSC3946 (to support late-arriving room archives)"), + shouldWarn: true, + default: false, + }, "feature_favourite_messages": { isFeature: true, labsGroup: LabGroup.Messaging, diff --git a/test/components/views/settings/tabs/room/AdvancedRoomSettingsTab-test.tsx b/test/components/views/settings/tabs/room/AdvancedRoomSettingsTab-test.tsx index dafd555966e..cb4b373e4fb 100644 --- a/test/components/views/settings/tabs/room/AdvancedRoomSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/room/AdvancedRoomSettingsTab-test.tsx @@ -26,6 +26,7 @@ import { mkEvent, mkStubRoom, stubClient } from "../../../../../test-utils"; import dis from "../../../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../../../src/dispatcher/actions"; import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg"; +import SettingsStore from "../../../../../../src/settings/SettingsStore"; jest.mock("../../../../../../src/dispatcher/dispatcher"); @@ -43,6 +44,12 @@ describe("AdvancedRoomSettingsTab", () => { cli = MatrixClientPeg.get(); room = mkStubRoom(roomId, "test room", cli); mocked(cli.getRoom).mockReturnValue(room); + mocked(dis.dispatch).mockReset(); + mocked(room.findPredecessor).mockImplementation((msc3946: boolean) => + msc3946 + ? { roomId: "old_room_id_via_predecessor", eventId: null } + : { roomId: "old_room_id", eventId: "tombstone_event_id" }, + ); }); it("should render as expected", () => { @@ -71,6 +78,17 @@ describe("AdvancedRoomSettingsTab", () => { room: room.roomId, }); + // Because we're mocking Room.findPredecessor, it may not be necessary + // to provide the actual event here, but we do need the create event, + // and in future this may be needed, so included for symmetry. + const predecessorEvent = mkEvent({ + event: true, + user: "@a:b.com", + type: EventType.RoomPredecessor, + content: { predecessor_room_id: "old_room_id_via_predecessor" }, + room: room.roomId, + }); + type GetStateEvents2Args = (eventType: EventType | string, stateKey: string) => MatrixEvent | null; const getStateEvents = jest.spyOn( @@ -82,6 +100,8 @@ describe("AdvancedRoomSettingsTab", () => { switch (eventType) { case EventType.RoomCreate: return createEvent; + case EventType.RoomPredecessor: + return predecessorEvent; default: return null; } @@ -101,4 +121,26 @@ describe("AdvancedRoomSettingsTab", () => { metricsViaKeyboard: false, }); }); + + describe("When MSC3946 support is enabled", () => { + beforeEach(() => { + jest.spyOn(SettingsStore, "getValue") + .mockReset() + .mockImplementation((settingName) => settingName === "feature_dynamic_room_predecessors"); + }); + + it("should link to predecessor room via MSC3946 if enabled", async () => { + mockStateEvents(room); + const tab = renderTab(); + const link = await tab.findByText("View older messages in test room."); + fireEvent.click(link); + expect(dis.dispatch).toHaveBeenCalledWith({ + action: Action.ViewRoom, + event_id: null, + room_id: "old_room_id_via_predecessor", + metricsTrigger: "WebPredecessorSettings", + metricsViaKeyboard: false, + }); + }); + }); }); From c557162592a38e99bf907a48f89d87eda4ca7d93 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Fri, 27 Jan 2023 15:14:20 +0100 Subject: [PATCH 13/97] Try to appease the linter --- src/components/structures/SpaceHierarchy.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index f99cf73f5f5..84ec987337f 100644 --- a/src/components/structures/SpaceHierarchy.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -419,7 +419,7 @@ const toLocalRoom = (cli: MatrixClient, room: IHierarchyRoom, hierarchy: RoomHie // Pick latest room that is actually part of the hierarchy let cliRoom = null; for (let idx = history.length - 1; idx >= 0; --idx) { - if (hierarchy.roomMap[history[idx].roomId]) { + if (hierarchy.roomMap.get(history[idx].roomId)) { cliRoom = history[idx]; break; } @@ -432,7 +432,7 @@ const toLocalRoom = (cli: MatrixClient, room: IHierarchyRoom, hierarchy: RoomHie room_type: cliRoom.getType(), name: cliRoom.name, topic: cliRoom.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent().topic, - avatar_url: cliRoom.getMxcAvatarUrl(), + avatar_url: cliRoom.getMxcAvatarUrl() ?? undefined, canonical_alias: cliRoom.getCanonicalAlias(), aliases: cliRoom.getAltAliases(), world_readable: From 0d0d7a3e2cfe5793c60bb3f3b4e110ea66eec758 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Fri, 27 Jan 2023 15:25:10 +0100 Subject: [PATCH 14/97] Try to appease the linter --- src/components/structures/SpaceHierarchy.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index 84ec987337f..f358e378897 100644 --- a/src/components/structures/SpaceHierarchy.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -432,8 +432,8 @@ const toLocalRoom = (cli: MatrixClient, room: IHierarchyRoom, hierarchy: RoomHie room_type: cliRoom.getType(), name: cliRoom.name, topic: cliRoom.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent().topic, - avatar_url: cliRoom.getMxcAvatarUrl() ?? undefined, - canonical_alias: cliRoom.getCanonicalAlias(), + avatar_url: cliRoom.getMxcAvatarUrl(), + canonical_alias: cliRoom.getCanonicalAlias() ?? undefined, aliases: cliRoom.getAltAliases(), world_readable: cliRoom.currentState.getStateEvents(EventType.RoomHistoryVisibility, "")?.getContent() From 364c453907afa169c54ec9c8e3bfc42d07f71406 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 27 Jan 2023 15:23:23 +0000 Subject: [PATCH 15/97] Tests for RoomCreate (#9997) * Tests for RoomCreate tile * Prefer screen instead of holding the return from render * use userEvent instead of fireEvent --- src/components/views/messages/RoomCreate.tsx | 6 +- .../views/messages/RoomCreate-test.tsx | 87 +++++++++++++++++++ .../__snapshots__/RoomCreate-test.tsx.snap | 24 +++++ 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 test/components/views/messages/RoomCreate-test.tsx create mode 100644 test/components/views/messages/__snapshots__/RoomCreate-test.tsx.snap diff --git a/src/components/views/messages/RoomCreate.tsx b/src/components/views/messages/RoomCreate.tsx index a9035ca03cb..8bff5dfdcc5 100644 --- a/src/components/views/messages/RoomCreate.tsx +++ b/src/components/views/messages/RoomCreate.tsx @@ -27,11 +27,15 @@ import EventTileBubble from "./EventTileBubble"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; interface IProps { - /* the MatrixEvent to show */ + /** The m.room.create MatrixEvent that this tile represents */ mxEvent: MatrixEvent; timestamp?: JSX.Element; } +/** + * A message tile showing that this room was created as an upgrade of a previous + * room. + */ export default class RoomCreate extends React.Component { private onLinkClicked = (e: React.MouseEvent): void => { e.preventDefault(); diff --git a/test/components/views/messages/RoomCreate-test.tsx b/test/components/views/messages/RoomCreate-test.tsx new file mode 100644 index 00000000000..31763f9ae89 --- /dev/null +++ b/test/components/views/messages/RoomCreate-test.tsx @@ -0,0 +1,87 @@ +/* +Copyright 2023 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 { act, render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { mocked } from "jest-mock"; +import { EventType, MatrixEvent } from "matrix-js-sdk/src/matrix"; + +import dis from "../../../../src/dispatcher/dispatcher"; +import SettingsStore from "../../../../src/settings/SettingsStore"; +import RoomCreate from "../../../../src/components/views/messages/RoomCreate"; +import { stubClient } from "../../../test-utils/test-utils"; +import { Action } from "../../../../src/dispatcher/actions"; + +jest.mock("../../../../src/dispatcher/dispatcher"); + +describe("", () => { + const userId = "@alice:server.org"; + const roomId = "!room:server.org"; + const createEvent = new MatrixEvent({ + type: EventType.RoomCreate, + sender: userId, + room_id: roomId, + content: { + predecessor: { room_id: "old_room_id", event_id: "tombstone_event_id" }, + }, + event_id: "$create", + }); + + beforeEach(() => { + jest.clearAllMocks(); + mocked(dis.dispatch).mockReset(); + jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); + jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined); + stubClient(); + }); + + afterAll(() => { + jest.spyOn(SettingsStore, "getValue").mockRestore(); + jest.spyOn(SettingsStore, "setValue").mockRestore(); + }); + + it("Renders as expected", () => { + const roomCreate = render(); + expect(roomCreate.asFragment()).toMatchSnapshot(); + }); + + it("Links to the old version of the room", () => { + render(); + expect(screen.getByText("Click here to see older messages.")).toHaveAttribute( + "href", + "https://matrix.to/#/old_room_id/tombstone_event_id", + ); + }); + + it("Opens the old room on click", async () => { + render(); + const link = screen.getByText("Click here to see older messages."); + + await act(() => userEvent.click(link)); + + await waitFor(() => + expect(dis.dispatch).toHaveBeenCalledWith({ + action: Action.ViewRoom, + event_id: "tombstone_event_id", + highlighted: true, + room_id: "old_room_id", + metricsTrigger: "Predecessor", + metricsViaKeyboard: false, + }), + ); + }); +}); diff --git a/test/components/views/messages/__snapshots__/RoomCreate-test.tsx.snap b/test/components/views/messages/__snapshots__/RoomCreate-test.tsx.snap new file mode 100644 index 00000000000..97c1cee66f6 --- /dev/null +++ b/test/components/views/messages/__snapshots__/RoomCreate-test.tsx.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` Renders as expected 1`] = ` + +
+
+ This room is a continuation of another conversation. +
+ +
+
+`; From 7788d50b02456d34d12d5f94aed4db7b18cf225a Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Fri, 27 Jan 2023 20:20:01 +0100 Subject: [PATCH 16/97] Add tests --- src/components/structures/SpaceHierarchy.tsx | 2 +- .../structures/SpaceHierarchy-test.tsx | 48 ++++++++++++++++++- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index f358e378897..7d6888a1979 100644 --- a/src/components/structures/SpaceHierarchy.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -413,7 +413,7 @@ interface IHierarchyLevelProps { onToggleClick?(parentId: string, childId: string): void; } -const toLocalRoom = (cli: MatrixClient, room: IHierarchyRoom, hierarchy: RoomHierarchy): IHierarchyRoom => { +export const toLocalRoom = (cli: MatrixClient, room: IHierarchyRoom, hierarchy: RoomHierarchy): IHierarchyRoom => { const history = cli.getRoomUpgradeHistory(room.room_id, true); // Pick latest room that is actually part of the hierarchy diff --git a/test/components/structures/SpaceHierarchy-test.tsx b/test/components/structures/SpaceHierarchy-test.tsx index 91818028142..a2a63ae0b82 100644 --- a/test/components/structures/SpaceHierarchy-test.tsx +++ b/test/components/structures/SpaceHierarchy-test.tsx @@ -17,11 +17,12 @@ limitations under the License. import { MatrixClient } from "matrix-js-sdk/src/client"; import { Room } from "matrix-js-sdk/src/models/room"; import { RoomHierarchy } from "matrix-js-sdk/src/room-hierarchy"; +import { IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces"; import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; -import { stubClient } from "../../test-utils"; +import { mkStubRoom, stubClient } from "../../test-utils"; import dispatcher from "../../../src/dispatcher/dispatcher"; -import { showRoom } from "../../../src/components/structures/SpaceHierarchy"; +import { showRoom, toLocalRoom } from "../../../src/components/structures/SpaceHierarchy"; import { Action } from "../../../src/dispatcher/actions"; describe("SpaceHierarchy", () => { @@ -67,4 +68,47 @@ describe("SpaceHierarchy", () => { }); }); }); + + describe("toLocalRoom", () => { + let client: MatrixClient; + let roomV1: Room; + let roomV2: Room; + let roomV3: Room; + + beforeEach(() => { + stubClient(); + client = MatrixClientPeg.get(); + roomV1 = mkStubRoom("room-id-1", "Room V1", client); + roomV2 = mkStubRoom("room-id-2", "Room V2", client); + roomV3 = mkStubRoom("room-id-3", "Room V3", client); + jest.spyOn(client, "getRoomUpgradeHistory").mockReturnValue([roomV1, roomV2, roomV3]); + }); + + it("grabs last room that is in hierarchy when latest version is in hierarchy", () => { + const hierarchy = { roomMap: new Map([ + [roomV1.roomId, { room_id: roomV1.roomId } as IHierarchyRoom], + [roomV2.roomId, { room_id: roomV2.roomId } as IHierarchyRoom], + [roomV3.roomId, { room_id: roomV3.roomId } as IHierarchyRoom], + ]) } as RoomHierarchy; + const localRoomV1 = toLocalRoom(client, { room_id: roomV1.roomId } as IHierarchyRoom, hierarchy); + expect(localRoomV1.room_id).toEqual(roomV3.roomId); + const localRoomV2 = toLocalRoom(client, { room_id: roomV2.roomId } as IHierarchyRoom, hierarchy); + expect(localRoomV2.room_id).toEqual(roomV3.roomId); + const localRoomV3 = toLocalRoom(client, { room_id: roomV3.roomId } as IHierarchyRoom, hierarchy); + expect(localRoomV3.room_id).toEqual(roomV3.roomId); + }); + + it("grabs last room that is in hierarchy when latest version is *not* in hierarchy", () => { + const hierarchy = { roomMap: new Map([ + [roomV1.roomId, { room_id: roomV1.roomId } as IHierarchyRoom], + [roomV2.roomId, { room_id: roomV2.roomId } as IHierarchyRoom] + ]) } as RoomHierarchy; + const localRoomV1 = toLocalRoom(client, { room_id: roomV1.roomId } as IHierarchyRoom, hierarchy); + expect(localRoomV1.room_id).toEqual(roomV2.roomId); + const localRoomV2 = toLocalRoom(client, { room_id: roomV2.roomId } as IHierarchyRoom, hierarchy); + expect(localRoomV2.room_id).toEqual(roomV2.roomId); + const localRoomV3 = toLocalRoom(client, { room_id: roomV3.roomId } as IHierarchyRoom, hierarchy); + expect(localRoomV3.room_id).toEqual(roomV2.roomId); + }); + }); }); From 6f4581943b003c533303e41d746b9f4112cbc40c Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Fri, 27 Jan 2023 20:29:50 +0100 Subject: [PATCH 17/97] Prettify it --- .../structures/SpaceHierarchy-test.tsx | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/test/components/structures/SpaceHierarchy-test.tsx b/test/components/structures/SpaceHierarchy-test.tsx index a2a63ae0b82..796e8848d48 100644 --- a/test/components/structures/SpaceHierarchy-test.tsx +++ b/test/components/structures/SpaceHierarchy-test.tsx @@ -85,11 +85,13 @@ describe("SpaceHierarchy", () => { }); it("grabs last room that is in hierarchy when latest version is in hierarchy", () => { - const hierarchy = { roomMap: new Map([ - [roomV1.roomId, { room_id: roomV1.roomId } as IHierarchyRoom], - [roomV2.roomId, { room_id: roomV2.roomId } as IHierarchyRoom], - [roomV3.roomId, { room_id: roomV3.roomId } as IHierarchyRoom], - ]) } as RoomHierarchy; + const hierarchy = { + roomMap: new Map([ + [roomV1.roomId, { room_id: roomV1.roomId } as IHierarchyRoom], + [roomV2.roomId, { room_id: roomV2.roomId } as IHierarchyRoom], + [roomV3.roomId, { room_id: roomV3.roomId } as IHierarchyRoom], + ]), + } as RoomHierarchy; const localRoomV1 = toLocalRoom(client, { room_id: roomV1.roomId } as IHierarchyRoom, hierarchy); expect(localRoomV1.room_id).toEqual(roomV3.roomId); const localRoomV2 = toLocalRoom(client, { room_id: roomV2.roomId } as IHierarchyRoom, hierarchy); @@ -99,10 +101,12 @@ describe("SpaceHierarchy", () => { }); it("grabs last room that is in hierarchy when latest version is *not* in hierarchy", () => { - const hierarchy = { roomMap: new Map([ - [roomV1.roomId, { room_id: roomV1.roomId } as IHierarchyRoom], - [roomV2.roomId, { room_id: roomV2.roomId } as IHierarchyRoom] - ]) } as RoomHierarchy; + const hierarchy = { + roomMap: new Map([ + [roomV1.roomId, { room_id: roomV1.roomId } as IHierarchyRoom], + [roomV2.roomId, { room_id: roomV2.roomId } as IHierarchyRoom], + ]), + } as RoomHierarchy; const localRoomV1 = toLocalRoom(client, { room_id: roomV1.roomId } as IHierarchyRoom, hierarchy); expect(localRoomV1.room_id).toEqual(roomV2.roomId); const localRoomV2 = toLocalRoom(client, { room_id: roomV2.roomId } as IHierarchyRoom, hierarchy); From 64cec319812e9a72c386c1643d20db17d1bbe480 Mon Sep 17 00:00:00 2001 From: Nawaraj Shah Date: Sun, 29 Jan 2023 15:48:38 +0545 Subject: [PATCH 18/97] changing the color of message time stamp (#10016) --- res/css/views/right_panel/_ThreadPanel.pcss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/right_panel/_ThreadPanel.pcss b/res/css/views/right_panel/_ThreadPanel.pcss index aff186fca5d..c1f1daaca1d 100644 --- a/res/css/views/right_panel/_ThreadPanel.pcss +++ b/res/css/views/right_panel/_ThreadPanel.pcss @@ -130,7 +130,7 @@ limitations under the License. } .mx_MessageTimestamp { - color: $secondary-content; + color: $event-timestamp-color; } .mx_BaseCard_footer { From d0de2a6851ff21d383ed18f054f8a32f37b06619 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Mon, 30 Jan 2023 09:53:41 +0100 Subject: [PATCH 19/97] Add test case for no-version-in-hierarchy situation --- test/components/structures/SpaceHierarchy-test.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/components/structures/SpaceHierarchy-test.tsx b/test/components/structures/SpaceHierarchy-test.tsx index 796e8848d48..27e55e95c10 100644 --- a/test/components/structures/SpaceHierarchy-test.tsx +++ b/test/components/structures/SpaceHierarchy-test.tsx @@ -114,5 +114,15 @@ describe("SpaceHierarchy", () => { const localRoomV3 = toLocalRoom(client, { room_id: roomV3.roomId } as IHierarchyRoom, hierarchy); expect(localRoomV3.room_id).toEqual(roomV2.roomId); }); + + it("returns specified room when none of the versions is in hierarchy", () => { + const hierarchy = { roomMap: new Map([]) } as RoomHierarchy; + const localRoomV1 = toLocalRoom(client, { room_id: roomV1.roomId } as IHierarchyRoom, hierarchy); + expect(localRoomV1.room_id).toEqual(roomV1.roomId); + const localRoomV2 = toLocalRoom(client, { room_id: roomV2.roomId } as IHierarchyRoom, hierarchy); + expect(localRoomV2.room_id).toEqual(roomV2.roomId); + const localRoomV3 = toLocalRoom(client, { room_id: roomV3.roomId } as IHierarchyRoom, hierarchy); + expect(localRoomV3.room_id).toEqual(roomV3.roomId); + }); }); }); From a8aa4de4b4c14a6a995071aab3f2f871b3ff2388 Mon Sep 17 00:00:00 2001 From: Clark Fischer <439978+clarkf@users.noreply.github.com> Date: Mon, 30 Jan 2023 09:50:08 +0000 Subject: [PATCH 20/97] Member avatars without canvas (#9990) * Strict typechecking fixes for Base/Member/Avatar Update the core avatar files to pass `--strict --noImplicitAny` typechecks. Signed-off-by: Clark Fischer * Add tests for Base/Member/Avatar More thoroughly test the core avatar files. Not necessarily the most thorough, but an improvement. Signed-off-by: Clark Fischer * Extract TextAvatar from BaseAvatar Extracted the fallback/textual avatar into its own component. Signed-off-by: Clark Fischer * Use standard HTML for non-image avatars Firefox users with `resistFingerprinting` enabled were seeing random noise for rooms and users without avatars. There's no real reason to use data URLs to present flat colors. This converts non-image avatars to inline blocks with background colors. See https://github.com/vector-im/element-web/issues/23936 Signed-off-by: Clark Fischer * Have pills use solid backgrounds rather than colored images Similar to room and member avatars, pills now use colored pseudo-elements rather than background images. Signed-off-by: Clark Fischer --------- Signed-off-by: Clark Fischer Co-authored-by: Andy Balaam --- .../views/rooms/_BasicMessageComposer.pcss | 2 +- src/Avatar.ts | 64 ++++-- src/components/views/avatars/BaseAvatar.tsx | 128 ++++++----- src/components/views/avatars/MemberAvatar.tsx | 22 +- src/components/views/avatars/RoomAvatar.tsx | 3 +- src/dispatcher/payloads/ViewUserPayload.ts | 7 + src/editor/parts.ts | 40 ++-- test/Avatar-test.ts | 121 ++++++++--- .../__snapshots__/RoomView-test.tsx.snap | 98 +++------ .../__snapshots__/UserMenu-test.tsx.snap | 14 +- .../views/avatars/BaseAvatar-test.tsx | 201 ++++++++++++++++++ .../views/avatars/MemberAvatar-test.tsx | 94 ++++++-- .../views/avatars/RoomAvatar-test.tsx | 10 +- .../__snapshots__/BaseAvatar-test.tsx.snap | 72 +++++++ .../__snapshots__/MemberAvatar-test.tsx.snap | 14 ++ .../__snapshots__/RoomAvatar-test.tsx.snap | 42 ++-- .../__snapshots__/BeaconMarker-test.tsx.snap | 16 +- .../views/rooms/RoomHeader-test.tsx | 10 +- .../RoomPreviewBar-test.tsx.snap | 28 +-- .../__snapshots__/RoomTile-test.tsx.snap | 14 +- test/editor/__snapshots__/parts-test.ts.snap | 45 ++++ test/editor/parts-test.ts | 72 ++++++- 22 files changed, 806 insertions(+), 311 deletions(-) create mode 100644 test/components/views/avatars/BaseAvatar-test.tsx create mode 100644 test/components/views/avatars/__snapshots__/BaseAvatar-test.tsx.snap create mode 100644 test/components/views/avatars/__snapshots__/MemberAvatar-test.tsx.snap create mode 100644 test/editor/__snapshots__/parts-test.ts.snap diff --git a/res/css/views/rooms/_BasicMessageComposer.pcss b/res/css/views/rooms/_BasicMessageComposer.pcss index 7b88a058153..32e7c5288f6 100644 --- a/res/css/views/rooms/_BasicMessageComposer.pcss +++ b/res/css/views/rooms/_BasicMessageComposer.pcss @@ -78,7 +78,7 @@ limitations under the License. min-width: $font-16px; /* ensure the avatar is not compressed */ height: $font-16px; margin-inline-end: 0.24rem; - background: var(--avatar-background), $background; + background: var(--avatar-background); color: $avatar-initial-color; background-repeat: no-repeat; background-size: $font-16px; diff --git a/src/Avatar.ts b/src/Avatar.ts index 8a3f10a22ca..3e6b18dbc79 100644 --- a/src/Avatar.ts +++ b/src/Avatar.ts @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd +Copyright 2015, 2016, 2023 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. @@ -24,16 +24,19 @@ import DMRoomMap from "./utils/DMRoomMap"; import { mediaFromMxc } from "./customisations/Media"; import { isLocalRoom } from "./utils/localRoom/isLocalRoom"; +const DEFAULT_COLORS: Readonly = ["#0DBD8B", "#368bd6", "#ac3ba8"]; + // Not to be used for BaseAvatar urls as that has similar default avatar fallback already export function avatarUrlForMember( - member: RoomMember, + member: RoomMember | null | undefined, width: number, height: number, resizeMethod: ResizeMethod, ): string { - let url: string; - if (member?.getMxcAvatarUrl()) { - url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod); + let url: string | undefined; + const mxcUrl = member?.getMxcAvatarUrl(); + if (mxcUrl) { + url = mediaFromMxc(mxcUrl).getThumbnailOfSourceHttp(width, height, resizeMethod); } if (!url) { // member can be null here currently since on invites, the JS SDK @@ -44,6 +47,17 @@ export function avatarUrlForMember( return url; } +export function getMemberAvatar( + member: RoomMember | null | undefined, + width: number, + height: number, + resizeMethod: ResizeMethod, +): string | undefined { + const mxcUrl = member?.getMxcAvatarUrl(); + if (!mxcUrl) return undefined; + return mediaFromMxc(mxcUrl).getThumbnailOfSourceHttp(width, height, resizeMethod); +} + export function avatarUrlForUser( user: Pick, width: number, @@ -86,18 +100,10 @@ function urlForColor(color: string): string { // hard to install a listener here, even if there were a clear event to listen to const colorToDataURLCache = new Map(); -export function defaultAvatarUrlForString(s: string): string { +export function defaultAvatarUrlForString(s: string | undefined): string { if (!s) return ""; // XXX: should never happen but empirically does by evidence of a rageshake - const defaultColors = ["#0DBD8B", "#368bd6", "#ac3ba8"]; - let total = 0; - for (let i = 0; i < s.length; ++i) { - total += s.charCodeAt(i); - } - const colorIndex = total % defaultColors.length; - // overwritten color value in custom themes - const cssVariable = `--avatar-background-colors_${colorIndex}`; - const cssValue = document.body.style.getPropertyValue(cssVariable); - const color = cssValue || defaultColors[colorIndex]; + + const color = getColorForString(s); let dataUrl = colorToDataURLCache.get(color); if (!dataUrl) { // validate color as this can come from account_data @@ -112,13 +118,23 @@ export function defaultAvatarUrlForString(s: string): string { return dataUrl; } +export function getColorForString(input: string): string { + const charSum = [...input].reduce((s, c) => s + c.charCodeAt(0), 0); + const index = charSum % DEFAULT_COLORS.length; + + // overwritten color value in custom themes + const cssVariable = `--avatar-background-colors_${index}`; + const cssValue = document.body.style.getPropertyValue(cssVariable); + return cssValue || DEFAULT_COLORS[index]!; +} + /** * returns the first (non-sigil) character of 'name', * converted to uppercase * @param {string} name * @return {string} the first letter */ -export function getInitialLetter(name: string): string { +export function getInitialLetter(name: string): string | undefined { if (!name) { // XXX: We should find out what causes the name to sometimes be falsy. console.trace("`name` argument to `getInitialLetter` not supplied"); @@ -134,19 +150,20 @@ export function getInitialLetter(name: string): string { } // rely on the grapheme cluster splitter in lodash so that we don't break apart compound emojis - return split(name, "", 1)[0].toUpperCase(); + return split(name, "", 1)[0]!.toUpperCase(); } export function avatarUrlForRoom( - room: Room, + room: Room | undefined, width: number, height: number, resizeMethod?: ResizeMethod, ): string | null { if (!room) return null; // null-guard - if (room.getMxcAvatarUrl()) { - return mediaFromMxc(room.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod); + const mxcUrl = room.getMxcAvatarUrl(); + if (mxcUrl) { + return mediaFromMxc(mxcUrl).getThumbnailOfSourceHttp(width, height, resizeMethod); } // space rooms cannot be DMs so skip the rest @@ -159,8 +176,9 @@ export function avatarUrlForRoom( // If there are only two members in the DM use the avatar of the other member const otherMember = room.getAvatarFallbackMember(); - if (otherMember?.getMxcAvatarUrl()) { - return mediaFromMxc(otherMember.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod); + const otherMemberMxc = otherMember?.getMxcAvatarUrl(); + if (otherMemberMxc) { + return mediaFromMxc(otherMemberMxc).getThumbnailOfSourceHttp(width, height, resizeMethod); } return null; } diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx index 025cb9d2711..d1dbe7743de 100644 --- a/src/components/views/avatars/BaseAvatar.tsx +++ b/src/components/views/avatars/BaseAvatar.tsx @@ -1,8 +1,6 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2018 New Vector Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2015, 2016, 2018, 2019, 2020, 2023 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. @@ -21,34 +19,41 @@ import React, { useCallback, useContext, useEffect, useState } from "react"; import classNames from "classnames"; import { ResizeMethod } from "matrix-js-sdk/src/@types/partials"; import { ClientEvent } from "matrix-js-sdk/src/client"; +import { SyncState } from "matrix-js-sdk/src/sync"; import * as AvatarLogic from "../../../Avatar"; -import SettingsStore from "../../../settings/SettingsStore"; import AccessibleButton from "../elements/AccessibleButton"; import RoomContext from "../../../contexts/RoomContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { useTypedEventEmitter } from "../../../hooks/useEventEmitter"; import { toPx } from "../../../utils/units"; import { _t } from "../../../languageHandler"; +import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; interface IProps { - name: string; // The name (first initial used as default) - idName?: string; // ID for generating hash colours - title?: string; // onHover title text - url?: string; // highest priority of them all, shortcut to set in urls[0] - urls?: string[]; // [highest_priority, ... , lowest_priority] + /** The name (first initial used as default) */ + name: string; + /** ID for generating hash colours */ + idName?: string; + /** onHover title text */ + title?: string; + /** highest priority of them all, shortcut to set in urls[0] */ + url?: string; + /** [highest_priority, ... , lowest_priority] */ + urls?: string[]; width?: number; height?: number; - // XXX: resizeMethod not actually used. + /** @deprecated not actually used */ resizeMethod?: ResizeMethod; - defaultToInitialLetter?: boolean; // true to add default url - onClick?: React.MouseEventHandler; + /** true to add default url */ + defaultToInitialLetter?: boolean; + onClick?: React.ComponentPropsWithoutRef["onClick"]; inputRef?: React.RefObject; className?: string; tabIndex?: number; } -const calculateUrls = (url: string, urls: string[], lowBandwidth: boolean): string[] => { +const calculateUrls = (url: string | undefined, urls: string[] | undefined, lowBandwidth: boolean): string[] => { // work out the full set of urls to try to load. This is formed like so: // imageUrls: [ props.url, ...props.urls ] @@ -66,11 +71,26 @@ const calculateUrls = (url: string, urls: string[], lowBandwidth: boolean): stri return Array.from(new Set(_urls)); }; -const useImageUrl = ({ url, urls }): [string, () => void] => { +/** + * Hook for cycling through a changing set of images. + * + * The set of images is updated whenever `url` or `urls` change, the user's + * `lowBandwidth` preference changes, or the client reconnects. + * + * Returns `[imageUrl, onError]`. When `onError` is called, the next image in + * the set will be displayed. + */ +const useImageUrl = ({ + url, + urls, +}: { + url: string | undefined; + urls: string[] | undefined; +}): [string | undefined, () => void] => { // Since this is a hot code path and the settings store can be slow, we // use the cached lowBandwidth value from the room context if it exists const roomContext = useContext(RoomContext); - const lowBandwidth = roomContext ? roomContext.lowBandwidth : SettingsStore.getValue("lowBandwidth"); + const lowBandwidth = roomContext.lowBandwidth; const [imageUrls, setUrls] = useState(calculateUrls(url, urls, lowBandwidth)); const [urlsIndex, setIndex] = useState(0); @@ -85,10 +105,10 @@ const useImageUrl = ({ url, urls }): [string, () => void] => { }, [url, JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps const cli = useContext(MatrixClientContext); - const onClientSync = useCallback((syncState, prevState) => { + const onClientSync = useCallback((syncState: SyncState, prevState: SyncState | null) => { // Consider the client reconnected if there is no error with syncing. // This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP. - const reconnected = syncState !== "ERROR" && prevState !== syncState; + const reconnected = syncState !== SyncState.Error && prevState !== syncState; if (reconnected) { setIndex(0); } @@ -108,46 +128,18 @@ const BaseAvatar: React.FC = (props) => { urls, width = 40, height = 40, - resizeMethod = "crop", // eslint-disable-line @typescript-eslint/no-unused-vars defaultToInitialLetter = true, onClick, inputRef, className, + resizeMethod: _unused, // to keep it from being in `otherProps` ...otherProps } = props; const [imageUrl, onError] = useImageUrl({ url, urls }); if (!imageUrl && defaultToInitialLetter && name) { - const initialLetter = AvatarLogic.getInitialLetter(name); - const textNode = ( - - ); - const imgNode = ( - - ); + const avatar = ; if (onClick) { return ( @@ -159,9 +151,12 @@ const BaseAvatar: React.FC = (props) => { className={classNames("mx_BaseAvatar", className)} onClick={onClick} inputRef={inputRef} + style={{ + width: toPx(width), + height: toPx(height), + }} > - {textNode} - {imgNode} + {avatar} ); } else { @@ -170,10 +165,13 @@ const BaseAvatar: React.FC = (props) => { className={classNames("mx_BaseAvatar", className)} ref={inputRef} {...otherProps} + style={{ + width: toPx(width), + height: toPx(height), + }} role="presentation" > - {textNode} - {imgNode} + {avatar} ); } @@ -220,3 +218,31 @@ const BaseAvatar: React.FC = (props) => { export default BaseAvatar; export type BaseAvatarType = React.FC; + +const TextAvatar: React.FC<{ + name: string; + idName?: string; + width: number; + height: number; + title?: string; +}> = ({ name, idName, width, height, title }) => { + const initialLetter = AvatarLogic.getInitialLetter(name); + + return ( + + ); +}; diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx index 48138714559..f493c58f8cf 100644 --- a/src/components/views/avatars/MemberAvatar.tsx +++ b/src/components/views/avatars/MemberAvatar.tsx @@ -1,6 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2019 - 2022 The Matrix.org Foundation C.I.C. +Copyright 2015, 2016, 2019 - 2023 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. @@ -26,6 +25,7 @@ import { mediaFromMxc } from "../../../customisations/Media"; import { CardContext } from "../right_panel/context"; import UserIdentifierCustomisations from "../../../customisations/UserIdentifier"; import { useRoomMemberProfile } from "../../../hooks/room/useRoomMemberProfile"; +import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload"; interface IProps extends Omit, "name" | "idName" | "url"> { member: RoomMember | null; @@ -33,14 +33,13 @@ interface IProps extends Omit, "name" | width: number; height: number; resizeMethod?: ResizeMethod; - // The onClick to give the avatar - onClick?: React.MouseEventHandler; - // Whether the onClick of the avatar should be overridden to dispatch `Action.ViewUser` + /** Whether the onClick of the avatar should be overridden to dispatch `Action.ViewUser` */ viewUserOnClick?: boolean; pushUserOnClick?: boolean; title?: string; - style?: any; - forceHistorical?: boolean; // true to deny `useOnlyCurrentProfiles` usage. Default false. + style?: React.CSSProperties; + /** true to deny `useOnlyCurrentProfiles` usage. Default false. */ + forceHistorical?: boolean; hideTitle?: boolean; } @@ -77,8 +76,8 @@ export default function MemberAvatar({ if (!title) { title = - UserIdentifierCustomisations.getDisplayUserIdentifier(member?.userId ?? "", { - roomId: member?.roomId ?? "", + UserIdentifierCustomisations.getDisplayUserIdentifier!(member.userId, { + roomId: member.roomId, }) ?? fallbackUserId; } } @@ -88,7 +87,6 @@ export default function MemberAvatar({ {...props} width={width} height={height} - resizeMethod={resizeMethod} name={name ?? ""} title={hideTitle ? undefined : title} idName={member?.userId ?? fallbackUserId} @@ -96,9 +94,9 @@ export default function MemberAvatar({ onClick={ viewUserOnClick ? () => { - dis.dispatch({ + dis.dispatch({ action: Action.ViewUser, - member: propsMember, + member: propsMember || undefined, push: card.isCard, }); } diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx index 50389c77491..4abfdbbf67e 100644 --- a/src/components/views/avatars/RoomAvatar.tsx +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -109,7 +109,8 @@ export default class RoomAvatar extends React.Component { } private onRoomAvatarClick = (): void => { - const avatarUrl = Avatar.avatarUrlForRoom(this.props.room, null, null, null); + const avatarMxc = this.props.room?.getMxcAvatarUrl(); + const avatarUrl = avatarMxc ? mediaFromMxc(avatarMxc).srcHttp : null; const params = { src: avatarUrl, name: this.props.room.name, diff --git a/src/dispatcher/payloads/ViewUserPayload.ts b/src/dispatcher/payloads/ViewUserPayload.ts index 20df21beb49..a09804babee 100644 --- a/src/dispatcher/payloads/ViewUserPayload.ts +++ b/src/dispatcher/payloads/ViewUserPayload.ts @@ -28,4 +28,11 @@ export interface ViewUserPayload extends ActionPayload { * should be shown (hide whichever relevant components). */ member?: RoomMember | User; + + /** + * Should this event be pushed as a card into the right panel? + * + * @see RightPanelStore#pushCard + */ + push?: boolean; } diff --git a/src/editor/parts.ts b/src/editor/parts.ts index 306d86dbc94..0157cd738a0 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -1,6 +1,5 @@ /* -Copyright 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2023 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. @@ -295,9 +294,9 @@ export abstract class PillPart extends BasePart implements IPillPart { } // helper method for subclasses - protected setAvatarVars(node: HTMLElement, avatarUrl: string, initialLetter: string): void { - const avatarBackground = `url('${avatarUrl}')`; - const avatarLetter = `'${initialLetter}'`; + protected setAvatarVars(node: HTMLElement, avatarBackground: string, initialLetter: string | undefined): void { + // const avatarBackground = `url('${avatarUrl}')`; + const avatarLetter = `'${initialLetter || ""}'`; // check if the value is changing, // otherwise the avatars flicker on every keystroke while updating. if (node.style.getPropertyValue("--avatar-background") !== avatarBackground) { @@ -413,13 +412,15 @@ class RoomPillPart extends PillPart { } protected setAvatar(node: HTMLElement): void { - let initialLetter = ""; - let avatarUrl = Avatar.avatarUrlForRoom(this.room, 16, 16, "crop"); - if (!avatarUrl) { - initialLetter = Avatar.getInitialLetter(this.room?.name || this.resourceId); - avatarUrl = Avatar.defaultAvatarUrlForString(this.room?.roomId ?? this.resourceId); + const avatarUrl = Avatar.avatarUrlForRoom(this.room, 16, 16, "crop"); + if (avatarUrl) { + this.setAvatarVars(node, `url('${avatarUrl}')`, ""); + return; } - this.setAvatarVars(node, avatarUrl, initialLetter); + + const initialLetter = Avatar.getInitialLetter(this.room?.name || this.resourceId); + const color = Avatar.getColorForString(this.room?.roomId ?? this.resourceId); + this.setAvatarVars(node, color, initialLetter); } public get type(): IPillPart["type"] { @@ -465,14 +466,17 @@ class UserPillPart extends PillPart { if (!this.member) { return; } - const name = this.member.name || this.member.userId; - const defaultAvatarUrl = Avatar.defaultAvatarUrlForString(this.member.userId); - const avatarUrl = Avatar.avatarUrlForMember(this.member, 16, 16, "crop"); - let initialLetter = ""; - if (avatarUrl === defaultAvatarUrl) { - initialLetter = Avatar.getInitialLetter(name); + + const avatar = Avatar.getMemberAvatar(this.member, 16, 16, "crop"); + if (avatar) { + this.setAvatarVars(node, `url('${avatar}')`, ""); + return; } - this.setAvatarVars(node, avatarUrl, initialLetter); + + const name = this.member.name || this.member.userId; + const initialLetter = Avatar.getInitialLetter(name); + const color = Avatar.getColorForString(this.member.userId); + this.setAvatarVars(node, color, initialLetter); } protected onClick = (): void => { diff --git a/test/Avatar-test.ts b/test/Avatar-test.ts index 0ff064ed57d..8b4ee03b7fc 100644 --- a/test/Avatar-test.ts +++ b/test/Avatar-test.ts @@ -1,5 +1,5 @@ /* -Copyright 2022 The Matrix.org Foundation C.I.C. +Copyright 2022 - 2023 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. @@ -15,33 +15,106 @@ limitations under the License. */ import { mocked } from "jest-mock"; -import { Room, RoomMember, RoomType } from "matrix-js-sdk/src/matrix"; - -import { avatarUrlForRoom } from "../src/Avatar"; -import { Media, mediaFromMxc } from "../src/customisations/Media"; +import { Room, RoomMember, RoomType, User } from "matrix-js-sdk/src/matrix"; + +import { + avatarUrlForMember, + avatarUrlForRoom, + avatarUrlForUser, + defaultAvatarUrlForString, + getColorForString, + getInitialLetter, +} from "../src/Avatar"; +import { mediaFromMxc } from "../src/customisations/Media"; import DMRoomMap from "../src/utils/DMRoomMap"; - -jest.mock("../src/customisations/Media", () => ({ - mediaFromMxc: jest.fn(), -})); +import { filterConsole, stubClient } from "./test-utils"; const roomId = "!room:example.com"; const avatarUrl1 = "https://example.com/avatar1"; const avatarUrl2 = "https://example.com/avatar2"; +describe("avatarUrlForMember", () => { + let member: RoomMember; + + beforeEach(() => { + stubClient(); + member = new RoomMember(roomId, "@user:example.com"); + }); + + it("returns the member's url", () => { + const mxc = "mxc://example.com/a/b/c/d/avatar.gif"; + jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue(mxc); + + expect(avatarUrlForMember(member, 32, 32, "crop")).toBe( + mediaFromMxc(mxc).getThumbnailOfSourceHttp(32, 32, "crop"), + ); + }); + + it("returns a default if the member has no avatar", () => { + jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue(undefined); + + expect(avatarUrlForMember(member, 32, 32, "crop")).toMatch(/^data:/); + }); +}); + +describe("avatarUrlForUser", () => { + let user: User; + + beforeEach(() => { + stubClient(); + user = new User("@user:example.com"); + }); + + it("should return the user's avatar", () => { + const mxc = "mxc://example.com/a/b/c/d/avatar.gif"; + user.avatarUrl = mxc; + + expect(avatarUrlForUser(user, 64, 64, "scale")).toBe( + mediaFromMxc(mxc).getThumbnailOfSourceHttp(64, 64, "scale"), + ); + }); + + it("should not provide a fallback", () => { + expect(avatarUrlForUser(user, 64, 64, "scale")).toBeNull(); + }); +}); + +describe("defaultAvatarUrlForString", () => { + it.each(["a", "abc", "abcde", "@".repeat(150)])("should return a value for %s", (s) => { + expect(defaultAvatarUrlForString(s)).not.toBe(""); + }); +}); + +describe("getColorForString", () => { + it.each(["a", "abc", "abcde", "@".repeat(150)])("should return a value for %s", (s) => { + expect(getColorForString(s)).toMatch(/^#\w+$/); + }); + + it("should return different values for different strings", () => { + expect(getColorForString("a")).not.toBe(getColorForString("b")); + }); +}); + +describe("getInitialLetter", () => { + filterConsole("argument to `getInitialLetter` not supplied"); + + it.each(["a", "abc", "abcde", "@".repeat(150)])("should return a value for %s", (s) => { + expect(getInitialLetter(s)).not.toBe(""); + }); + + it("should return undefined for empty strings", () => { + expect(getInitialLetter("")).toBeUndefined(); + }); +}); + describe("avatarUrlForRoom", () => { - let getThumbnailOfSourceHttp: jest.Mock; let room: Room; let roomMember: RoomMember; let dmRoomMap: DMRoomMap; beforeEach(() => { - getThumbnailOfSourceHttp = jest.fn(); - mocked(mediaFromMxc).mockImplementation((): Media => { - return { - getThumbnailOfSourceHttp, - } as unknown as Media; - }); + stubClient(); + room = { roomId, getMxcAvatarUrl: jest.fn(), @@ -59,14 +132,14 @@ describe("avatarUrlForRoom", () => { }); it("should return null for a null room", () => { - expect(avatarUrlForRoom(null, 128, 128)).toBeNull(); + expect(avatarUrlForRoom(undefined, 128, 128)).toBeNull(); }); it("should return the HTTP source if the room provides a MXC url", () => { mocked(room.getMxcAvatarUrl).mockReturnValue(avatarUrl1); - getThumbnailOfSourceHttp.mockReturnValue(avatarUrl2); - expect(avatarUrlForRoom(room, 128, 256, "crop")).toEqual(avatarUrl2); - expect(getThumbnailOfSourceHttp).toHaveBeenCalledWith(128, 256, "crop"); + expect(avatarUrlForRoom(room, 128, 256, "crop")).toBe( + mediaFromMxc(avatarUrl1).getThumbnailOfSourceHttp(128, 256, "crop"), + ); }); it("should return null for a space room", () => { @@ -83,7 +156,7 @@ describe("avatarUrlForRoom", () => { it("should return null if there is no other member in the room", () => { mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue("@user:example.com"); - mocked(room.getAvatarFallbackMember).mockReturnValue(null); + mocked(room.getAvatarFallbackMember).mockReturnValue(undefined); expect(avatarUrlForRoom(room, 128, 128)).toBeNull(); }); @@ -97,8 +170,8 @@ describe("avatarUrlForRoom", () => { mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue("@user:example.com"); mocked(room.getAvatarFallbackMember).mockReturnValue(roomMember); mocked(roomMember.getMxcAvatarUrl).mockReturnValue(avatarUrl2); - getThumbnailOfSourceHttp.mockReturnValue(avatarUrl2); - expect(avatarUrlForRoom(room, 128, 256, "crop")).toEqual(avatarUrl2); - expect(getThumbnailOfSourceHttp).toHaveBeenCalledWith(128, 256, "crop"); + expect(avatarUrlForRoom(room, 128, 256, "crop")).toEqual( + mediaFromMxc(avatarUrl2).getThumbnailOfSourceHttp(128, 256, "crop"), + ); }); }); diff --git a/test/components/structures/__snapshots__/RoomView-test.tsx.snap b/test/components/structures/__snapshots__/RoomView-test.tsx.snap index 47318525d56..c81e180c421 100644 --- a/test/components/structures/__snapshots__/RoomView-test.tsx.snap +++ b/test/components/structures/__snapshots__/RoomView-test.tsx.snap @@ -20,22 +20,16 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1 -
@@ -119,22 +113,16 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`] - @@ -215,23 +203,17 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`] aria-live="off" class="mx_AccessibleButton mx_BaseAvatar" role="button" + style="width: 52px; height: 52px;" tabindex="0" > -

@user:example.com @@ -314,22 +296,16 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] = - @@ -410,23 +386,17 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] = aria-live="off" class="mx_AccessibleButton mx_BaseAvatar" role="button" + style="width: 52px; height: 52px;" tabindex="0" > -

@user:example.com @@ -581,22 +551,16 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t - @@ -672,23 +636,17 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t aria-live="off" class="mx_AccessibleButton mx_BaseAvatar" role="button" + style="width: 52px; height: 52px;" tabindex="0" > -

@user:example.com diff --git a/test/components/structures/__snapshots__/UserMenu-test.tsx.snap b/test/components/structures/__snapshots__/UserMenu-test.tsx.snap index 769711434a8..0546900abb3 100644 --- a/test/components/structures/__snapshots__/UserMenu-test.tsx.snap +++ b/test/components/structures/__snapshots__/UserMenu-test.tsx.snap @@ -20,22 +20,16 @@ exports[` when rendered should render as expected 1`] = ` - diff --git a/test/components/views/avatars/BaseAvatar-test.tsx b/test/components/views/avatars/BaseAvatar-test.tsx new file mode 100644 index 00000000000..294a64c4362 --- /dev/null +++ b/test/components/views/avatars/BaseAvatar-test.tsx @@ -0,0 +1,201 @@ +/* +Copyright 2023 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 { ClientEvent, PendingEventOrdering } from "matrix-js-sdk/src/client"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import React from "react"; +import { act } from "react-dom/test-utils"; +import { SyncState } from "matrix-js-sdk/src/sync"; + +import type { MatrixClient } from "matrix-js-sdk/src/client"; +import RoomContext from "../../../../src/contexts/RoomContext"; +import { getRoomContext } from "../../../test-utils/room"; +import { stubClient } from "../../../test-utils/test-utils"; +import BaseAvatar from "../../../../src/components/views/avatars/BaseAvatar"; +import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; + +type Props = React.ComponentPropsWithoutRef; + +describe("", () => { + let client: MatrixClient; + let room: Room; + let member: RoomMember; + + function getComponent(props: Partial) { + return ( + + + + + + ); + } + + function failLoadingImg(container: HTMLElement): void { + const img = container.querySelector("img")!; + expect(img).not.toBeNull(); + act(() => { + fireEvent.error(img); + }); + } + + function emitReconnect(): void { + act(() => { + client.emit(ClientEvent.Sync, SyncState.Prepared, SyncState.Reconnecting); + }); + } + + beforeEach(() => { + client = stubClient(); + + room = new Room("!room:example.com", client, client.getUserId() ?? "", { + pendingEventOrdering: PendingEventOrdering.Detached, + }); + + member = new RoomMember(room.roomId, "@bob:example.org"); + jest.spyOn(room, "getMember").mockReturnValue(member); + }); + + it("renders with minimal properties", () => { + const { container } = render(getComponent({})); + + expect(container.querySelector(".mx_BaseAvatar")).not.toBeNull(); + }); + + it("matches snapshot (avatar)", () => { + const { container } = render( + getComponent({ + name: "CoolUser22", + title: "Hover title", + url: "https://example.com/images/avatar.gif", + className: "mx_SomethingArbitrary", + }), + ); + + expect(container).toMatchSnapshot(); + }); + + it("matches snapshot (avatar + click)", () => { + const { container } = render( + getComponent({ + name: "CoolUser22", + title: "Hover title", + url: "https://example.com/images/avatar.gif", + className: "mx_SomethingArbitrary", + onClick: () => {}, + }), + ); + + expect(container).toMatchSnapshot(); + }); + + it("matches snapshot (no avatar)", () => { + const { container } = render( + getComponent({ + name: "xX_Element_User_Xx", + title: ":kiss:", + defaultToInitialLetter: true, + className: "big-and-bold", + }), + ); + + expect(container).toMatchSnapshot(); + }); + + it("matches snapshot (no avatar + click)", () => { + const { container } = render( + getComponent({ + name: "xX_Element_User_Xx", + title: ":kiss:", + defaultToInitialLetter: true, + className: "big-and-bold", + onClick: () => {}, + }), + ); + + expect(container).toMatchSnapshot(); + }); + + it("uses fallback images", () => { + const images = [...Array(10)].map((_, i) => `https://example.com/images/${i}.webp`); + + const { container } = render( + getComponent({ + url: images[0], + urls: images.slice(1), + }), + ); + + for (const image of images) { + expect(container.querySelector("img")!.src).toBe(image); + failLoadingImg(container); + } + }); + + it("re-renders on reconnect", () => { + const primary = "https://example.com/image.jpeg"; + const fallback = "https://example.com/fallback.png"; + const { container } = render( + getComponent({ + url: primary, + urls: [fallback], + }), + ); + + failLoadingImg(container); + expect(container.querySelector("img")!.src).toBe(fallback); + + emitReconnect(); + expect(container.querySelector("img")!.src).toBe(primary); + }); + + it("renders with an image", () => { + const url = "https://example.com/images/small/avatar.gif?size=realBig"; + const { container } = render(getComponent({ url })); + + const img = container.querySelector("img"); + expect(img!.src).toBe(url); + }); + + it("renders the initial letter", () => { + const { container } = render(getComponent({ name: "Yellow", defaultToInitialLetter: true })); + + const avatar = container.querySelector(".mx_BaseAvatar_initial")!; + expect(avatar.innerHTML).toBe("Y"); + }); + + it.each([{}, { name: "CoolUser22" }, { name: "XxElement_FanxX", defaultToInitialLetter: true }])( + "includes a click handler", + (props: Partial) => { + const onClick = jest.fn(); + + const { container } = render( + getComponent({ + ...props, + onClick, + }), + ); + + act(() => { + fireEvent.click(container.querySelector(".mx_BaseAvatar")!); + }); + + expect(onClick).toHaveBeenCalled(); + }, + ); +}); diff --git a/test/components/views/avatars/MemberAvatar-test.tsx b/test/components/views/avatars/MemberAvatar-test.tsx index 4895b70f217..3dc793bd929 100644 --- a/test/components/views/avatars/MemberAvatar-test.tsx +++ b/test/components/views/avatars/MemberAvatar-test.tsx @@ -1,5 +1,5 @@ /* -Copyright 2022 The Matrix.org Foundation C.I.C. +Copyright 2022 - 2023 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. @@ -14,19 +14,25 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { getByTestId, render, waitFor } from "@testing-library/react"; -import { mocked } from "jest-mock"; +import { fireEvent, getByTestId, render } from "@testing-library/react"; import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client"; import { Room } from "matrix-js-sdk/src/models/room"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import React from "react"; +import { act } from "react-dom/test-utils"; import MemberAvatar from "../../../../src/components/views/avatars/MemberAvatar"; import RoomContext from "../../../../src/contexts/RoomContext"; -import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; +import { mediaFromMxc } from "../../../../src/customisations/Media"; +import { ViewUserPayload } from "../../../../src/dispatcher/payloads/ViewUserPayload"; +import defaultDispatcher from "../../../../src/dispatcher/dispatcher"; +import { SettingLevel } from "../../../../src/settings/SettingLevel"; import SettingsStore from "../../../../src/settings/SettingsStore"; import { getRoomContext } from "../../../test-utils/room"; import { stubClient } from "../../../test-utils/test-utils"; +import { Action } from "../../../../src/dispatcher/actions"; + +type Props = React.ComponentPropsWithoutRef; describe("MemberAvatar", () => { const ROOM_ID = "roomId"; @@ -35,7 +41,7 @@ describe("MemberAvatar", () => { let room: Room; let member: RoomMember; - function getComponent(props) { + function getComponent(props: Partial) { return ( @@ -44,10 +50,7 @@ describe("MemberAvatar", () => { } beforeEach(() => { - jest.clearAllMocks(); - - stubClient(); - mockClient = mocked(MatrixClientPeg.get()); + mockClient = stubClient(); room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", { pendingEventOrdering: PendingEventOrdering.Detached, @@ -55,22 +58,77 @@ describe("MemberAvatar", () => { member = new RoomMember(ROOM_ID, "@bob:example.org"); jest.spyOn(room, "getMember").mockReturnValue(member); + }); + + it("supports 'null' members", () => { + const { container } = render(getComponent({ member: null })); + + expect(container.querySelector("img")).not.toBeNull(); + }); + + it("matches the snapshot", () => { jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue("http://placekitten.com/400/400"); + const { container } = render( + getComponent({ + member, + fallbackUserId: "Fallback User ID", + title: "Hover title", + style: { + color: "pink", + }, + }), + ); + + expect(container).toMatchSnapshot(); }); - it("shows an avatar for useOnlyCurrentProfiles", async () => { - jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => { - return settingName === "useOnlyCurrentProfiles"; - }); + it("shows an avatar for useOnlyCurrentProfiles", () => { + jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue("http://placekitten.com/400/400"); + + SettingsStore.setValue("useOnlyCurrentProfiles", null, SettingLevel.DEVICE, true); const { container } = render(getComponent({})); - let avatar: HTMLElement; - await waitFor(() => { - avatar = getByTestId(container, "avatar-img"); - expect(avatar).toBeInTheDocument(); + const avatar = getByTestId(container, "avatar-img"); + expect(avatar).toBeInTheDocument(); + expect(avatar.getAttribute("src")).not.toBe(""); + }); + + it("uses the member's configured avatar", () => { + const mxcUrl = "mxc://example.com/avatars/user.tiff"; + jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue(mxcUrl); + + const { container } = render(getComponent({ member })); + + const img = container.querySelector("img"); + expect(img).not.toBeNull(); + expect(img!.src).toBe(mediaFromMxc(mxcUrl).srcHttp); + }); + + it("uses a fallback when the member has no avatar", () => { + jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue(undefined); + + const { container } = render(getComponent({ member })); + + const img = container.querySelector(".mx_BaseAvatar_image"); + expect(img).not.toBeNull(); + }); + + it("dispatches on click", () => { + const { container } = render(getComponent({ member, viewUserOnClick: true })); + + const spy = jest.spyOn(defaultDispatcher, "dispatch"); + + act(() => { + fireEvent.click(container.querySelector(".mx_BaseAvatar")!); }); - expect(avatar!.getAttribute("src")).not.toBe(""); + expect(spy).toHaveBeenCalled(); + const [payload] = spy.mock.lastCall!; + expect(payload).toStrictEqual({ + action: Action.ViewUser, + member, + push: false, + }); }); }); diff --git a/test/components/views/avatars/RoomAvatar-test.tsx b/test/components/views/avatars/RoomAvatar-test.tsx index e23cd96f02d..7be7dd65e90 100644 --- a/test/components/views/avatars/RoomAvatar-test.tsx +++ b/test/components/views/avatars/RoomAvatar-test.tsx @@ -39,7 +39,7 @@ describe("RoomAvatar", () => { const dmRoomMap = new DMRoomMap(client); jest.spyOn(dmRoomMap, "getUserIdForRoomId"); jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap); - jest.spyOn(AvatarModule, "defaultAvatarUrlForString"); + jest.spyOn(AvatarModule, "getColorForString"); }); afterAll(() => { @@ -48,14 +48,14 @@ describe("RoomAvatar", () => { afterEach(() => { mocked(DMRoomMap.shared().getUserIdForRoomId).mockReset(); - mocked(AvatarModule.defaultAvatarUrlForString).mockClear(); + mocked(AvatarModule.getColorForString).mockClear(); }); it("should render as expected for a Room", () => { const room = new Room("!room:example.com", client, client.getSafeUserId()); room.name = "test room"; expect(render().container).toMatchSnapshot(); - expect(AvatarModule.defaultAvatarUrlForString).toHaveBeenCalledWith(room.roomId); + expect(AvatarModule.getColorForString).toHaveBeenCalledWith(room.roomId); }); it("should render as expected for a DM room", () => { @@ -64,7 +64,7 @@ describe("RoomAvatar", () => { room.name = "DM room"; mocked(DMRoomMap.shared().getUserIdForRoomId).mockReturnValue(userId); expect(render().container).toMatchSnapshot(); - expect(AvatarModule.defaultAvatarUrlForString).toHaveBeenCalledWith(userId); + expect(AvatarModule.getColorForString).toHaveBeenCalledWith(userId); }); it("should render as expected for a LocalRoom", () => { @@ -73,6 +73,6 @@ describe("RoomAvatar", () => { localRoom.name = "local test room"; localRoom.targets.push(new DirectoryMember({ user_id: userId })); expect(render().container).toMatchSnapshot(); - expect(AvatarModule.defaultAvatarUrlForString).toHaveBeenCalledWith(userId); + expect(AvatarModule.getColorForString).toHaveBeenCalledWith(userId); }); }); diff --git a/test/components/views/avatars/__snapshots__/BaseAvatar-test.tsx.snap b/test/components/views/avatars/__snapshots__/BaseAvatar-test.tsx.snap new file mode 100644 index 00000000000..da62540b90e --- /dev/null +++ b/test/components/views/avatars/__snapshots__/BaseAvatar-test.tsx.snap @@ -0,0 +1,72 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` matches snapshot (avatar + click) 1`] = ` +
+ Avatar +
+`; + +exports[` matches snapshot (avatar) 1`] = ` +
+ +
+`; + +exports[` matches snapshot (no avatar + click) 1`] = ` +
+ + + +
+`; + +exports[` matches snapshot (no avatar) 1`] = ` +
+ + + +
+`; diff --git a/test/components/views/avatars/__snapshots__/MemberAvatar-test.tsx.snap b/test/components/views/avatars/__snapshots__/MemberAvatar-test.tsx.snap new file mode 100644 index 00000000000..feda79035cd --- /dev/null +++ b/test/components/views/avatars/__snapshots__/MemberAvatar-test.tsx.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MemberAvatar matches the snapshot 1`] = ` +
+ +
+`; diff --git a/test/components/views/avatars/__snapshots__/RoomAvatar-test.tsx.snap b/test/components/views/avatars/__snapshots__/RoomAvatar-test.tsx.snap index 6bffa157b63..699113689e4 100644 --- a/test/components/views/avatars/__snapshots__/RoomAvatar-test.tsx.snap +++ b/test/components/views/avatars/__snapshots__/RoomAvatar-test.tsx.snap @@ -5,22 +5,16 @@ exports[`RoomAvatar should render as expected for a DM room 1`] = ` - `; @@ -30,22 +24,16 @@ exports[`RoomAvatar should render as expected for a LocalRoom 1`] = ` - `; @@ -55,22 +43,16 @@ exports[`RoomAvatar should render as expected for a Room 1`] = ` - `; diff --git a/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap b/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap index b42ccb83ee3..b965f50b2f9 100644 --- a/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap +++ b/test/components/views/beacon/__snapshots__/BeaconMarker-test.tsx.snap @@ -13,23 +13,17 @@ exports[` renders marker when beacon has location 1`] = ` - diff --git a/test/components/views/rooms/RoomHeader-test.tsx b/test/components/views/rooms/RoomHeader-test.tsx index 5857e282957..c27d4c0c20f 100644 --- a/test/components/views/rooms/RoomHeader-test.tsx +++ b/test/components/views/rooms/RoomHeader-test.tsx @@ -72,7 +72,7 @@ describe("RoomHeader (Enzyme)", () => { // And there is no image avatar (because it's not set on this room) const image = findImg(rendered, ".mx_BaseAvatar_image"); - expect(image.prop("src")).toEqual("data:image/png;base64,00"); + expect(image).toBeTruthy(); }); it("shows the room avatar in a room with 2 people", () => { @@ -86,7 +86,7 @@ describe("RoomHeader (Enzyme)", () => { // And there is no image avatar (because it's not set on this room) const image = findImg(rendered, ".mx_BaseAvatar_image"); - expect(image.prop("src")).toEqual("data:image/png;base64,00"); + expect(image).toBeTruthy(); }); it("shows the room avatar in a room with >2 people", () => { @@ -100,7 +100,7 @@ describe("RoomHeader (Enzyme)", () => { // And there is no image avatar (because it's not set on this room) const image = findImg(rendered, ".mx_BaseAvatar_image"); - expect(image.prop("src")).toEqual("data:image/png;base64,00"); + expect(image).toBeTruthy(); }); it("shows the room avatar in a DM with only ourselves", () => { @@ -114,7 +114,7 @@ describe("RoomHeader (Enzyme)", () => { // And there is no image avatar (because it's not set on this room) const image = findImg(rendered, ".mx_BaseAvatar_image"); - expect(image.prop("src")).toEqual("data:image/png;base64,00"); + expect(image).toBeTruthy(); }); it("shows the user avatar in a DM with 2 people", () => { @@ -148,7 +148,7 @@ describe("RoomHeader (Enzyme)", () => { // And there is no image avatar (because it's not set on this room) const image = findImg(rendered, ".mx_BaseAvatar_image"); - expect(image.prop("src")).toEqual("data:image/png;base64,00"); + expect(image).toBeTruthy(); }); it("renders call buttons normally", () => { diff --git a/test/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap b/test/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap index f35467e1efd..a78a452e890 100644 --- a/test/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap +++ b/test/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap @@ -161,22 +161,16 @@ exports[` with an invite without an invited email for a dm roo -

@@ -236,22 +230,16 @@ exports[` with an invite without an invited email for a non-dm -

diff --git a/test/components/views/rooms/__snapshots__/RoomTile-test.tsx.snap b/test/components/views/rooms/__snapshots__/RoomTile-test.tsx.snap index bcbb7932c69..557d97c243e 100644 --- a/test/components/views/rooms/__snapshots__/RoomTile-test.tsx.snap +++ b/test/components/views/rooms/__snapshots__/RoomTile-test.tsx.snap @@ -15,22 +15,16 @@ exports[`RoomTile should render the room 1`] = ` -

+ !room:example.com + +`; + +exports[`RoomPillPart matches snapshot (no avatar) 1`] = ` + + !room:example.com + +`; + +exports[`UserPillPart matches snapshot (avatar) 1`] = ` + + DisplayName + +`; + +exports[`UserPillPart matches snapshot (no avatar) 1`] = ` + + DisplayName + +`; diff --git a/test/editor/parts-test.ts b/test/editor/parts-test.ts index 534221ece3a..31c620c94ad 100644 --- a/test/editor/parts-test.ts +++ b/test/editor/parts-test.ts @@ -1,5 +1,5 @@ /* -Copyright 2022 The Matrix.org Foundation C.I.C. +Copyright 2022 - 2023 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. @@ -14,7 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { EmojiPart, PlainPart } from "../../src/editor/parts"; +import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix"; + +import { EmojiPart, PartCreator, PlainPart } from "../../src/editor/parts"; +import DMRoomMap from "../../src/utils/DMRoomMap"; +import { stubClient } from "../test-utils"; import { createPartCreator } from "./mock"; describe("editor/parts", () => { @@ -40,3 +44,67 @@ describe("editor/parts", () => { expect(() => part.toDOMNode()).not.toThrow(); }); }); + +describe("UserPillPart", () => { + const roomId = "!room:example.com"; + let client: MatrixClient; + let room: Room; + let creator: PartCreator; + + beforeEach(() => { + client = stubClient(); + room = new Room(roomId, client, "@me:example.com"); + creator = new PartCreator(room, client); + }); + + it("matches snapshot (no avatar)", () => { + jest.spyOn(room, "getMember").mockReturnValue(new RoomMember(room.roomId, "@user:example.com")); + const pill = creator.userPill("DisplayName", "@user:example.com"); + const el = pill.toDOMNode(); + + expect(el).toMatchSnapshot(); + }); + + it("matches snapshot (avatar)", () => { + const member = new RoomMember(room.roomId, "@user:example.com"); + jest.spyOn(room, "getMember").mockReturnValue(member); + jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue("mxc://www.example.com/avatar.png"); + + const pill = creator.userPill("DisplayName", "@user:example.com"); + const el = pill.toDOMNode(); + + expect(el).toMatchSnapshot(); + }); +}); + +describe("RoomPillPart", () => { + const roomId = "!room:example.com"; + let client: jest.Mocked; + let room: Room; + let creator: PartCreator; + + beforeEach(() => { + client = stubClient() as jest.Mocked; + DMRoomMap.makeShared(); + + room = new Room(roomId, client, "@me:example.com"); + client.getRoom.mockReturnValue(room); + creator = new PartCreator(room, client); + }); + + it("matches snapshot (no avatar)", () => { + jest.spyOn(room, "getMxcAvatarUrl").mockReturnValue(null); + const pill = creator.roomPill("super-secret clubhouse"); + const el = pill.toDOMNode(); + + expect(el).toMatchSnapshot(); + }); + + it("matches snapshot (avatar)", () => { + jest.spyOn(room, "getMxcAvatarUrl").mockReturnValue("mxc://www.example.com/avatars/room1.jpeg"); + const pill = creator.roomPill("cool chat club"); + const el = pill.toDOMNode(); + + expect(el).toMatchSnapshot(); + }); +}); From c7b01af49e648482707792d4292a6e7f211feb2c Mon Sep 17 00:00:00 2001 From: AHMAD KADRI <52747422+ahmadkadri@users.noreply.github.com> Date: Mon, 30 Jan 2023 10:54:05 +0100 Subject: [PATCH 21/97] Should open new 1:1 chat room after leaving the old one (#9880) * should open new 1:1 chat room after leaving the old one Signed-off-by: Ahmad Kadri * change the copyright * update the test Signed-off-by: AHMAD KADRI <52747422+ahmadkadri@users.noreply.github.com> --------- Signed-off-by: Ahmad Kadri Signed-off-by: AHMAD KADRI <52747422+ahmadkadri@users.noreply.github.com> Co-authored-by: Oliver Sand Co-authored-by: Dominik Henneke --- .../one-to-one-chat/one-to-one-chat.spec.ts | 61 +++++++++++++++++++ src/components/structures/MatrixChat.tsx | 8 +-- 2 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 cypress/e2e/one-to-one-chat/one-to-one-chat.spec.ts diff --git a/cypress/e2e/one-to-one-chat/one-to-one-chat.spec.ts b/cypress/e2e/one-to-one-chat/one-to-one-chat.spec.ts new file mode 100644 index 00000000000..897b916105e --- /dev/null +++ b/cypress/e2e/one-to-one-chat/one-to-one-chat.spec.ts @@ -0,0 +1,61 @@ +/* +Copyright 2023 Ahmad Kadri +Copyright 2023 Nordeck IT + Consulting GmbH. + +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 { HomeserverInstance } from "../../plugins/utils/homeserver"; +import { Credentials } from "../../support/homeserver"; + +describe("1:1 chat room", () => { + let homeserver: HomeserverInstance; + let user2: Credentials; + + const username = "user1234"; + const password = "p4s5W0rD"; + + beforeEach(() => { + cy.startHomeserver("default").then((data) => { + homeserver = data; + + cy.initTestUser(homeserver, "Jeff"); + cy.registerUser(homeserver, username, password).then((credential) => { + user2 = credential; + cy.visit(`/#/user/${user2.userId}?action=chat`); + }); + }); + }); + + afterEach(() => { + cy.stopHomeserver(homeserver); + }); + + it("should open new 1:1 chat room after leaving the old one", () => { + // leave 1:1 chat room + cy.contains(".mx_RoomHeader_nametext", username).click(); + cy.contains('[role="menuitem"]', "Leave").click(); + cy.get('[data-testid="dialog-primary-button"]').click(); + + // wait till the room was left + cy.get('[role="group"][aria-label="Historical"]').within(() => { + cy.contains(".mx_RoomTile", username); + }); + + // open new 1:1 chat room + cy.visit(`/#/user/${user2.userId}?action=chat`); + cy.contains(".mx_RoomHeader_nametext", username); + }); +}); diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 429adb6f504..e517aaaf83d 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -138,6 +138,7 @@ import { cleanUpBroadcasts, VoiceBroadcastResumer } from "../../voice-broadcast" import GenericToast from "../views/toasts/GenericToast"; import { Linkify } from "../views/elements/Linkify"; import RovingSpotlightDialog, { Filter } from "../views/dialogs/spotlight/SpotlightDialog"; +import { findDMForUser } from "../../utils/dm/findDMForUser"; // legacy export export { default as Views } from "../../Views"; @@ -1101,13 +1102,12 @@ export default class MatrixChat extends React.PureComponent { // TODO: Immutable DMs replaces this const client = MatrixClientPeg.get(); - const dmRoomMap = new DMRoomMap(client); - const dmRooms = dmRoomMap.getDMRoomsForUserId(userId); + const dmRoom = findDMForUser(client, userId); - if (dmRooms.length > 0) { + if (dmRoom) { dis.dispatch({ action: Action.ViewRoom, - room_id: dmRooms[0], + room_id: dmRoom.roomId, metricsTrigger: "MessageUser", }); } else { From a21929dba00491e026926d2cdea51a4457b18313 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Mon, 30 Jan 2023 10:02:32 +0000 Subject: [PATCH 22/97] Convert RoomCreate to a functional component (#9999) --- src/components/views/messages/RoomCreate.tsx | 78 +++++++++---------- src/events/EventTileFactory.tsx | 4 +- .../views/messages/RoomCreate-test.tsx | 2 +- 3 files changed, 42 insertions(+), 42 deletions(-) diff --git a/src/components/views/messages/RoomCreate.tsx b/src/components/views/messages/RoomCreate.tsx index 8bff5dfdcc5..25cfb90a488 100644 --- a/src/components/views/messages/RoomCreate.tsx +++ b/src/components/views/messages/RoomCreate.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { useCallback } from "react"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import dis from "../../../dispatcher/dispatcher"; @@ -36,44 +36,44 @@ interface IProps { * A message tile showing that this room was created as an upgrade of a previous * room. */ -export default class RoomCreate extends React.Component { - private onLinkClicked = (e: React.MouseEvent): void => { - e.preventDefault(); +export const RoomCreate: React.FC = ({ mxEvent, timestamp }) => { + const onLinkClicked = useCallback( + (e: React.MouseEvent): void => { + e.preventDefault(); - const predecessor = this.props.mxEvent.getContent()["predecessor"]; + const predecessor = mxEvent.getContent()["predecessor"]; - dis.dispatch({ - action: Action.ViewRoom, - event_id: predecessor["event_id"], - highlighted: true, - room_id: predecessor["room_id"], - metricsTrigger: "Predecessor", - metricsViaKeyboard: e.type !== "click", - }); - }; - - public render(): JSX.Element { - const predecessor = this.props.mxEvent.getContent()["predecessor"]; - if (predecessor === undefined) { - return
; // We should never have been instantiated in this case - } - const prevRoom = MatrixClientPeg.get().getRoom(predecessor["room_id"]); - const permalinkCreator = new RoomPermalinkCreator(prevRoom, predecessor["room_id"]); - permalinkCreator.load(); - const predecessorPermalink = permalinkCreator.forEvent(predecessor["event_id"]); - const link = ( - - {_t("Click here to see older messages.")} - - ); - - return ( - - ); + dis.dispatch({ + action: Action.ViewRoom, + event_id: predecessor["event_id"], + highlighted: true, + room_id: predecessor["room_id"], + metricsTrigger: "Predecessor", + metricsViaKeyboard: e.type !== "click", + }); + }, + [mxEvent], + ); + const predecessor = mxEvent.getContent()["predecessor"]; + if (predecessor === undefined) { + return
; // We should never have been instantiated in this case } -} + const prevRoom = MatrixClientPeg.get().getRoom(predecessor["room_id"]); + const permalinkCreator = new RoomPermalinkCreator(prevRoom, predecessor["room_id"]); + permalinkCreator.load(); + const predecessorPermalink = permalinkCreator.forEvent(predecessor["event_id"]); + const link = ( + + {_t("Click here to see older messages.")} + + ); + + return ( + + ); +}; diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx index 1d30416f0f9..d6b5e18ad53 100644 --- a/src/events/EventTileFactory.tsx +++ b/src/events/EventTileFactory.tsx @@ -33,7 +33,7 @@ import LegacyCallEvent from "../components/views/messages/LegacyCallEvent"; import { CallEvent } from "../components/views/messages/CallEvent"; import TextualEvent from "../components/views/messages/TextualEvent"; import EncryptionEvent from "../components/views/messages/EncryptionEvent"; -import RoomCreate from "../components/views/messages/RoomCreate"; +import { RoomCreate } from "../components/views/messages/RoomCreate"; import RoomAvatarEvent from "../components/views/messages/RoomAvatarEvent"; import { WIDGET_LAYOUT_EVENT_TYPE } from "../stores/widgets/WidgetLayoutStore"; import { ALL_RULE_TYPES } from "../mjolnir/BanList"; @@ -101,7 +101,7 @@ const EVENT_TILE_TYPES = new Map([ const STATE_EVENT_TILE_TYPES = new Map([ [EventType.RoomEncryption, (ref, props) => ], [EventType.RoomCanonicalAlias, TextualEventFactory], - [EventType.RoomCreate, (ref, props) => ], + [EventType.RoomCreate, (_ref, props) => ], [EventType.RoomMember, TextualEventFactory], [EventType.RoomName, TextualEventFactory], [EventType.RoomAvatar, (ref, props) => ], diff --git a/test/components/views/messages/RoomCreate-test.tsx b/test/components/views/messages/RoomCreate-test.tsx index 31763f9ae89..09f17e2ae4f 100644 --- a/test/components/views/messages/RoomCreate-test.tsx +++ b/test/components/views/messages/RoomCreate-test.tsx @@ -22,7 +22,7 @@ import { EventType, MatrixEvent } from "matrix-js-sdk/src/matrix"; import dis from "../../../../src/dispatcher/dispatcher"; import SettingsStore from "../../../../src/settings/SettingsStore"; -import RoomCreate from "../../../../src/components/views/messages/RoomCreate"; +import { RoomCreate } from "../../../../src/components/views/messages/RoomCreate"; import { stubClient } from "../../../test-utils/test-utils"; import { Action } from "../../../../src/dispatcher/actions"; From 3e2bf5640e17ff1c096a746089c87f8cf93e174a Mon Sep 17 00:00:00 2001 From: Germain Date: Mon, 30 Jan 2023 12:20:11 +0000 Subject: [PATCH 23/97] Update to supportsThreads (#9907) --- src/MatrixClientPeg.ts | 2 +- src/utils/EventUtils.ts | 6 +----- test/Notifier-test.ts | 2 +- test/Unread-test.ts | 2 +- test/components/structures/RoomSearchView-test.tsx | 2 +- test/components/structures/ThreadPanel-test.tsx | 2 +- test/components/structures/ThreadView-test.tsx | 2 +- test/components/structures/TimelinePanel-test.tsx | 4 ++-- .../components/views/right_panel/RoomHeaderButtons-test.tsx | 2 +- test/components/views/rooms/EventTile-test.tsx | 2 +- .../NotificationBadge/UnreadNotificationBadge-test.tsx | 2 +- test/components/views/settings/Notifications-test.tsx | 2 +- test/stores/RoomViewStore-test.ts | 2 +- test/test-utils/test-utils.ts | 2 +- test/utils/EventUtils-test.ts | 2 +- 15 files changed, 16 insertions(+), 20 deletions(-) diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index f674892bf7c..19a9eb5fde8 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -218,7 +218,7 @@ class MatrixClientPegClass implements IMatrixClientPeg { opts.pendingEventOrdering = PendingEventOrdering.Detached; opts.lazyLoadMembers = true; opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours - opts.experimentalThreadSupport = SettingsStore.getValue("feature_threadenabled"); + opts.threadSupport = SettingsStore.getValue("feature_threadenabled"); if (SettingsStore.getValue("feature_sliding_sync")) { const proxyUrl = SettingsStore.getValue("feature_sliding_sync_proxy_url"); diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts index e80ffb83e20..5e650018a0e 100644 --- a/src/utils/EventUtils.ts +++ b/src/utils/EventUtils.ts @@ -229,11 +229,7 @@ export async function fetchInitialEvent( initialEvent = null; } - if ( - client.supportsExperimentalThreads() && - initialEvent?.isRelation(THREAD_RELATION_TYPE.name) && - !initialEvent.getThread() - ) { + if (client.supportsThreads() && initialEvent?.isRelation(THREAD_RELATION_TYPE.name) && !initialEvent.getThread()) { const threadId = initialEvent.threadRootId; const room = client.getRoom(roomId); const mapper = client.getEventMapper(); diff --git a/test/Notifier-test.ts b/test/Notifier-test.ts index 20b2c2e3610..e2b867ee7f5 100644 --- a/test/Notifier-test.ts +++ b/test/Notifier-test.ts @@ -109,7 +109,7 @@ describe("Notifier", () => { decryptEventIfNeeded: jest.fn(), getRoom: jest.fn(), getPushActionsForEvent: jest.fn(), - supportsExperimentalThreads: jest.fn().mockReturnValue(false), + supportsThreads: jest.fn().mockReturnValue(false), }); mockClient.pushRules = { diff --git a/test/Unread-test.ts b/test/Unread-test.ts index 8ff759b142b..4cfb7f265a7 100644 --- a/test/Unread-test.ts +++ b/test/Unread-test.ts @@ -124,7 +124,7 @@ describe("Unread", () => { const myId = client.getUserId()!; beforeAll(() => { - client.supportsExperimentalThreads = () => true; + client.supportsThreads = () => true; }); beforeEach(() => { diff --git a/test/components/structures/RoomSearchView-test.tsx b/test/components/structures/RoomSearchView-test.tsx index 26786956b5e..35e297b7a1a 100644 --- a/test/components/structures/RoomSearchView-test.tsx +++ b/test/components/structures/RoomSearchView-test.tsx @@ -48,7 +48,7 @@ describe("", () => { beforeEach(async () => { stubClient(); client = MatrixClientPeg.get(); - client.supportsExperimentalThreads = jest.fn().mockReturnValue(true); + client.supportsThreads = jest.fn().mockReturnValue(true); room = new Room("!room:server", client, client.getUserId()); mocked(client.getRoom).mockReturnValue(room); permalinkCreator = new RoomPermalinkCreator(room, room.roomId); diff --git a/test/components/structures/ThreadPanel-test.tsx b/test/components/structures/ThreadPanel-test.tsx index b868549da4e..20c6cf03973 100644 --- a/test/components/structures/ThreadPanel-test.tsx +++ b/test/components/structures/ThreadPanel-test.tsx @@ -161,7 +161,7 @@ describe("ThreadPanel", () => { Thread.setServerSideSupport(FeatureSupport.Stable); Thread.setServerSideListSupport(FeatureSupport.Stable); Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable); - jest.spyOn(mockClient, "supportsExperimentalThreads").mockReturnValue(true); + jest.spyOn(mockClient, "supportsThreads").mockReturnValue(true); room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", { pendingEventOrdering: PendingEventOrdering.Detached, diff --git a/test/components/structures/ThreadView-test.tsx b/test/components/structures/ThreadView-test.tsx index c578121e461..ef6b195a2dc 100644 --- a/test/components/structures/ThreadView-test.tsx +++ b/test/components/structures/ThreadView-test.tsx @@ -117,7 +117,7 @@ describe("ThreadView", () => { stubClient(); mockPlatformPeg(); mockClient = mocked(MatrixClientPeg.get()); - jest.spyOn(mockClient, "supportsExperimentalThreads").mockReturnValue(true); + jest.spyOn(mockClient, "supportsThreads").mockReturnValue(true); room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", { pendingEventOrdering: PendingEventOrdering.Detached, diff --git a/test/components/structures/TimelinePanel-test.tsx b/test/components/structures/TimelinePanel-test.tsx index e66a015a5c5..eaeb19d8ec0 100644 --- a/test/components/structures/TimelinePanel-test.tsx +++ b/test/components/structures/TimelinePanel-test.tsx @@ -362,7 +362,7 @@ describe("TimelinePanel", () => { client = MatrixClientPeg.get(); Thread.hasServerSideSupport = FeatureSupport.Stable; - client.supportsExperimentalThreads = () => true; + client.supportsThreads = () => true; const getValueCopy = SettingsStore.getValue; SettingsStore.getValue = jest.fn().mockImplementation((name: string) => { if (name === "feature_threadenabled") return true; @@ -524,7 +524,7 @@ describe("TimelinePanel", () => { const client = MatrixClientPeg.get(); client.isRoomEncrypted = () => true; - client.supportsExperimentalThreads = () => true; + client.supportsThreads = () => true; client.decryptEventIfNeeded = () => Promise.resolve(); const authorId = client.getUserId()!; const room = new Room("roomId", client, authorId, { diff --git a/test/components/views/right_panel/RoomHeaderButtons-test.tsx b/test/components/views/right_panel/RoomHeaderButtons-test.tsx index 06192ccc232..8df11a76028 100644 --- a/test/components/views/right_panel/RoomHeaderButtons-test.tsx +++ b/test/components/views/right_panel/RoomHeaderButtons-test.tsx @@ -38,7 +38,7 @@ describe("RoomHeaderButtons-test.tsx", function () { stubClient(); client = MatrixClientPeg.get(); - client.supportsExperimentalThreads = () => true; + client.supportsThreads = () => true; room = new Room(ROOM_ID, client, client.getUserId() ?? "", { pendingEventOrdering: PendingEventOrdering.Detached, }); diff --git a/test/components/views/rooms/EventTile-test.tsx b/test/components/views/rooms/EventTile-test.tsx index c3c0b55ab5b..e3f28d851ae 100644 --- a/test/components/views/rooms/EventTile-test.tsx +++ b/test/components/views/rooms/EventTile-test.tsx @@ -92,7 +92,7 @@ describe("EventTile", () => { describe("EventTile thread summary", () => { beforeEach(() => { - jest.spyOn(client, "supportsExperimentalThreads").mockReturnValue(true); + jest.spyOn(client, "supportsThreads").mockReturnValue(true); }); it("removes the thread summary when thread is deleted", async () => { diff --git a/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx b/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx index cfa44165765..9c60d26e265 100644 --- a/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx +++ b/test/components/views/rooms/NotificationBadge/UnreadNotificationBadge-test.tsx @@ -48,7 +48,7 @@ describe("UnreadNotificationBadge", () => { } beforeAll(() => { - client.supportsExperimentalThreads = () => true; + client.supportsThreads = () => true; }); beforeEach(() => { diff --git a/test/components/views/settings/Notifications-test.tsx b/test/components/views/settings/Notifications-test.tsx index b33f72838ad..aedb96fb130 100644 --- a/test/components/views/settings/Notifications-test.tsx +++ b/test/components/views/settings/Notifications-test.tsx @@ -225,7 +225,7 @@ describe("", () => { }), setAccountData: jest.fn(), sendReadReceipt: jest.fn(), - supportsExperimentalThreads: jest.fn().mockReturnValue(true), + supportsThreads: jest.fn().mockReturnValue(true), }); mockClient.getPushRules.mockResolvedValue(pushRules); diff --git a/test/stores/RoomViewStore-test.ts b/test/stores/RoomViewStore-test.ts index 7c2b1ec81b3..370fb95bcd8 100644 --- a/test/stores/RoomViewStore-test.ts +++ b/test/stores/RoomViewStore-test.ts @@ -93,7 +93,7 @@ describe("RoomViewStore", function () { getSafeUserId: jest.fn().mockReturnValue(userId), getDeviceId: jest.fn().mockReturnValue("ABC123"), sendStateEvent: jest.fn().mockResolvedValue({}), - supportsExperimentalThreads: jest.fn(), + supportsThreads: jest.fn(), }); const room = new Room(roomId, mockClient, userId); const room2 = new Room(roomId2, mockClient, userId); diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index ef7bc1dcef3..5938f89cfe3 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -166,7 +166,7 @@ export function createTestClient(): MatrixClient { decryptEventIfNeeded: () => Promise.resolve(), isUserIgnored: jest.fn().mockReturnValue(false), getCapabilities: jest.fn().mockResolvedValue({}), - supportsExperimentalThreads: () => false, + supportsThreads: () => false, getRoomUpgradeHistory: jest.fn().mockReturnValue([]), getOpenIdToken: jest.fn().mockResolvedValue(undefined), registerWithIdentityServer: jest.fn().mockResolvedValue({}), diff --git a/test/utils/EventUtils-test.ts b/test/utils/EventUtils-test.ts index decf42931a8..9c4012a4c4c 100644 --- a/test/utils/EventUtils-test.ts +++ b/test/utils/EventUtils-test.ts @@ -436,7 +436,7 @@ describe("EventUtils", () => { pendingEventOrdering: PendingEventOrdering.Detached, }); - jest.spyOn(client, "supportsExperimentalThreads").mockReturnValue(true); + jest.spyOn(client, "supportsThreads").mockReturnValue(true); jest.spyOn(client, "getRoom").mockReturnValue(room); jest.spyOn(client, "fetchRoomEvent").mockImplementation(async (roomId, eventId) => { return events[eventId] ?? Promise.reject(); From 4c1e4f5127bd7fc8a3ab852d630242a704ffbda2 Mon Sep 17 00:00:00 2001 From: Clark Fischer <439978+clarkf@users.noreply.github.com> Date: Mon, 30 Jan 2023 14:31:32 +0000 Subject: [PATCH 24/97] Fix "[object Promise]" appearing in HTML exports (#9975) Fixes https://github.com/vector-im/element-web/issues/24272 --- src/DateUtils.ts | 2 +- src/components/structures/MessagePanel.tsx | 4 +- .../dialogs/MessageEditHistoryDialog.tsx | 2 +- .../views/rooms/SearchResultTile.tsx | 7 +- src/utils/exportUtils/HtmlExport.tsx | 43 ++- src/utils/exportUtils/exportCSS.ts | 4 +- .../dialogs/MessageEditHistoryDialog-test.tsx | 83 +++++ .../MessageEditHistoryDialog-test.tsx.snap | 322 ++++++++++++++++++ .../views/rooms/SearchResultTile-test.tsx | 106 +++--- test/test-utils/test-utils.ts | 4 + test/utils/exportUtils/HTMLExport-test.ts | 286 +++++++++++++++- .../__snapshots__/HTMLExport-test.ts.snap | 86 +++++ test/utils/exportUtils/exportCSS-test.ts | 26 ++ 13 files changed, 893 insertions(+), 82 deletions(-) create mode 100644 test/components/views/dialogs/MessageEditHistoryDialog-test.tsx create mode 100644 test/components/views/dialogs/__snapshots__/MessageEditHistoryDialog-test.tsx.snap create mode 100644 test/utils/exportUtils/exportCSS-test.ts diff --git a/src/DateUtils.ts b/src/DateUtils.ts index 5973a7c5f2e..c1aa69aacd6 100644 --- a/src/DateUtils.ts +++ b/src/DateUtils.ts @@ -175,7 +175,7 @@ function withinCurrentYear(prevDate: Date, nextDate: Date): boolean { return prevDate.getFullYear() === nextDate.getFullYear(); } -export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): boolean { +export function wantsDateSeparator(prevEventDate: Date | undefined, nextEventDate: Date | undefined): boolean { if (!nextEventDate || !prevEventDate) { return false; } diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 98e8f79ec72..2dd432cb928 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -72,7 +72,7 @@ const groupedStateEvents = [ // check if there is a previous event and it has the same sender as this event // and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL export function shouldFormContinuation( - prevEvent: MatrixEvent, + prevEvent: MatrixEvent | null, mxEvent: MatrixEvent, showHiddenEvents: boolean, threadsEnabled: boolean, @@ -821,7 +821,7 @@ export default class MessagePanel extends React.Component { // here. return !this.props.canBackPaginate; } - return wantsDateSeparator(prevEvent.getDate(), nextEventDate); + return wantsDateSeparator(prevEvent.getDate() || undefined, nextEventDate); } // Get a list of read receipts that should be shown next to this event diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.tsx b/src/components/views/dialogs/MessageEditHistoryDialog.tsx index 943e7f58d2d..8775b4eb5c3 100644 --- a/src/components/views/dialogs/MessageEditHistoryDialog.tsx +++ b/src/components/views/dialogs/MessageEditHistoryDialog.tsx @@ -130,7 +130,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent { - if (!lastEvent || wantsDateSeparator(lastEvent.getDate(), e.getDate())) { + if (!lastEvent || wantsDateSeparator(lastEvent.getDate() || undefined, e.getDate() || undefined)) { nodes.push(
  • diff --git a/src/components/views/rooms/SearchResultTile.tsx b/src/components/views/rooms/SearchResultTile.tsx index 067cbaee383..3ec68b989f4 100644 --- a/src/components/views/rooms/SearchResultTile.tsx +++ b/src/components/views/rooms/SearchResultTile.tsx @@ -84,7 +84,7 @@ export default class SearchResultTile extends React.Component { // is this a continuation of the previous message? const continuation = prevEv && - !wantsDateSeparator(prevEv.getDate(), mxEv.getDate()) && + !wantsDateSeparator(prevEv.getDate() || undefined, mxEv.getDate() || undefined) && shouldFormContinuation( prevEv, mxEv, @@ -96,7 +96,10 @@ export default class SearchResultTile extends React.Component { let lastInSection = true; const nextEv = timeline[j + 1]; if (nextEv) { - const willWantDateSeparator = wantsDateSeparator(mxEv.getDate(), nextEv.getDate()); + const willWantDateSeparator = wantsDateSeparator( + mxEv.getDate() || undefined, + nextEv.getDate() || undefined, + ); lastInSection = willWantDateSeparator || mxEv.getSender() !== nextEv.getSender() || diff --git a/src/utils/exportUtils/HtmlExport.tsx b/src/utils/exportUtils/HtmlExport.tsx index 6b4240375c6..e915d180250 100644 --- a/src/utils/exportUtils/HtmlExport.tsx +++ b/src/utils/exportUtils/HtmlExport.tsx @@ -1,5 +1,5 @@ /* -Copyright 2021 The Matrix.org Foundation C.I.C. +Copyright 2021, 2023 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. @@ -66,7 +66,7 @@ export default class HTMLExporter extends Exporter { } protected async getRoomAvatar(): Promise { - let blob: Blob; + let blob: Blob | undefined = undefined; const avatarUrl = Avatar.avatarUrlForRoom(this.room, 32, 32, "crop"); const avatarPath = "room.png"; if (avatarUrl) { @@ -85,7 +85,7 @@ export default class HTMLExporter extends Exporter { height={32} name={this.room.name} title={this.room.name} - url={blob ? avatarPath : null} + url={blob ? avatarPath : ""} resizeMethod="crop" /> ); @@ -96,9 +96,9 @@ export default class HTMLExporter extends Exporter { const roomAvatar = await this.getRoomAvatar(); const exportDate = formatFullDateNoDayNoTime(new Date()); const creator = this.room.currentState.getStateEvents(EventType.RoomCreate, "")?.getSender(); - const creatorName = this.room?.getMember(creator)?.rawDisplayName || creator; - const exporter = this.client.getUserId(); - const exporterName = this.room?.getMember(exporter)?.rawDisplayName; + const creatorName = (creator ? this.room.getMember(creator)?.rawDisplayName : creator) || creator; + const exporter = this.client.getUserId()!; + const exporterName = this.room.getMember(exporter)?.rawDisplayName; const topic = this.room.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic || ""; const createdText = _t("%(creatorName)s created this room.", { creatorName, @@ -217,20 +217,19 @@ export default class HTMLExporter extends Exporter { `; } - protected getAvatarURL(event: MatrixEvent): string { + protected getAvatarURL(event: MatrixEvent): string | undefined { const member = event.sender; - return ( - member.getMxcAvatarUrl() && mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(30, 30, "crop") - ); + const avatarUrl = member?.getMxcAvatarUrl(); + return avatarUrl ? mediaFromMxc(avatarUrl).getThumbnailOfSourceHttp(30, 30, "crop") : undefined; } protected async saveAvatarIfNeeded(event: MatrixEvent): Promise { - const member = event.sender; + const member = event.sender!; if (!this.avatars.has(member.userId)) { try { const avatarUrl = this.getAvatarURL(event); this.avatars.set(member.userId, true); - const image = await fetch(avatarUrl); + const image = await fetch(avatarUrl!); const blob = await image.blob(); this.addFile(`users/${member.userId.replace(/:/g, "-")}.png`, blob); } catch (err) { @@ -239,19 +238,19 @@ export default class HTMLExporter extends Exporter { } } - protected async getDateSeparator(event: MatrixEvent): Promise { + protected getDateSeparator(event: MatrixEvent): string { const ts = event.getTs(); const dateSeparator = (
  • - +
  • ); return renderToStaticMarkup(dateSeparator); } - protected async needsDateSeparator(event: MatrixEvent, prevEvent: MatrixEvent): Promise { - if (prevEvent == null) return true; - return wantsDateSeparator(prevEvent.getDate(), event.getDate()); + protected needsDateSeparator(event: MatrixEvent, prevEvent: MatrixEvent | null): boolean { + if (!prevEvent) return true; + return wantsDateSeparator(prevEvent.getDate() || undefined, event.getDate() || undefined); } public getEventTile(mxEv: MatrixEvent, continuation: boolean): JSX.Element { @@ -264,9 +263,7 @@ export default class HTMLExporter extends Exporter { isRedacted={mxEv.isRedacted()} replacingEventId={mxEv.replacingEventId()} forExport={true} - readReceipts={null} alwaysShowTimestamps={true} - readReceiptMap={null} showUrlPreview={false} checkUnmounting={() => false} isTwelveHour={false} @@ -275,7 +272,6 @@ export default class HTMLExporter extends Exporter { permalinkCreator={this.permalinkCreator} lastSuccessful={false} isSelectedEvent={false} - getRelationsForEvent={null} showReactions={false} layout={Layout.Group} showReadReceipts={false} @@ -286,7 +282,8 @@ export default class HTMLExporter extends Exporter { } protected async getEventTileMarkup(mxEv: MatrixEvent, continuation: boolean, filePath?: string): Promise { - const hasAvatar = !!this.getAvatarURL(mxEv); + const avatarUrl = this.getAvatarURL(mxEv); + const hasAvatar = !!avatarUrl; if (hasAvatar) await this.saveAvatarIfNeeded(mxEv); const EventTile = this.getEventTile(mxEv, continuation); let eventTileMarkup: string; @@ -312,8 +309,8 @@ export default class HTMLExporter extends Exporter { eventTileMarkup = eventTileMarkup.replace(/.*?<\/span>/, ""); if (hasAvatar) { eventTileMarkup = eventTileMarkup.replace( - encodeURI(this.getAvatarURL(mxEv)).replace(/&/g, "&"), - `users/${mxEv.sender.userId.replace(/:/g, "-")}.png`, + encodeURI(avatarUrl).replace(/&/g, "&"), + `users/${mxEv.sender!.userId.replace(/:/g, "-")}.png`, ); } return eventTileMarkup; diff --git a/src/utils/exportUtils/exportCSS.ts b/src/utils/exportUtils/exportCSS.ts index f92e339b023..2a6a098a14e 100644 --- a/src/utils/exportUtils/exportCSS.ts +++ b/src/utils/exportUtils/exportCSS.ts @@ -58,8 +58,8 @@ const getExportCSS = async (usedClasses: Set): Promise => { // If the light theme isn't loaded we will have to fetch & parse it manually if (!stylesheets.some(isLightTheme)) { - const href = document.querySelector('link[rel="stylesheet"][href$="theme-light.css"]').href; - stylesheets.push(await getRulesFromCssFile(href)); + const href = document.querySelector('link[rel="stylesheet"][href$="theme-light.css"]')?.href; + if (href) stylesheets.push(await getRulesFromCssFile(href)); } let css = ""; diff --git a/test/components/views/dialogs/MessageEditHistoryDialog-test.tsx b/test/components/views/dialogs/MessageEditHistoryDialog-test.tsx new file mode 100644 index 00000000000..cadb92e488c --- /dev/null +++ b/test/components/views/dialogs/MessageEditHistoryDialog-test.tsx @@ -0,0 +1,83 @@ +/* +Copyright 2023 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, RenderResult } from "@testing-library/react"; +import { EventType, MatrixEvent } from "matrix-js-sdk/src/matrix"; + +import type { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { flushPromises, mkMessage, stubClient } from "../../../test-utils"; +import MessageEditHistoryDialog from "../../../../src/components/views/dialogs/MessageEditHistoryDialog"; + +describe("", () => { + const roomId = "!aroom:example.com"; + let client: jest.Mocked; + let event: MatrixEvent; + + beforeEach(() => { + client = stubClient() as jest.Mocked; + event = mkMessage({ + event: true, + user: "@user:example.com", + room: "!room:example.com", + msg: "My Great Message", + }); + }); + + async function renderComponent(): Promise { + const result = render(); + await flushPromises(); + return result; + } + + function mockEdits(...edits: { msg: string; ts: number | undefined }[]) { + client.relations.mockImplementation(() => + Promise.resolve({ + events: edits.map( + (e) => + new MatrixEvent({ + type: EventType.RoomMessage, + room_id: roomId, + origin_server_ts: e.ts, + content: { + body: e.msg, + }, + }), + ), + }), + ); + } + + it("should match the snapshot", async () => { + mockEdits({ msg: "My Great Massage", ts: 1234 }); + + const { container } = await renderComponent(); + + expect(container).toMatchSnapshot(); + }); + + it("should support events with ", async () => { + mockEdits( + { msg: "My Great Massage", ts: undefined }, + { msg: "My Great Massage?", ts: undefined }, + { msg: "My Great Missage", ts: undefined }, + ); + + const { container } = await renderComponent(); + + expect(container).toMatchSnapshot(); + }); +}); diff --git a/test/components/views/dialogs/__snapshots__/MessageEditHistoryDialog-test.tsx.snap b/test/components/views/dialogs/__snapshots__/MessageEditHistoryDialog-test.tsx.snap new file mode 100644 index 00000000000..0eb2683003d --- /dev/null +++ b/test/components/views/dialogs/__snapshots__/MessageEditHistoryDialog-test.tsx.snap @@ -0,0 +1,322 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should match the snapshot 1`] = ` +
    +
    + @@ -623,11 +617,9 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
    -
    - -
    +
    diff --git a/test/components/views/elements/Linkify-test.tsx b/test/components/views/elements/Linkify-test.tsx deleted file mode 100644 index 2fa841b1f11..00000000000 --- a/test/components/views/elements/Linkify-test.tsx +++ /dev/null @@ -1,82 +0,0 @@ -/* -Copyright 2021 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, { useState } from "react"; - -import { Linkify } from "../../../../src/components/views/elements/Linkify"; - -describe("Linkify", () => { - it("linkifies the context", () => { - const { container } = render(https://perdu.com); - expect(container.innerHTML).toBe( - '", - ); - }); - - it("correctly linkifies a room alias", () => { - const { container } = render(#element-web:matrix.org); - expect(container.innerHTML).toBe( - "", - ); - }); - - it("changes the root tag name", () => { - const TAG_NAME = "p"; - - const { container } = render(Hello world!); - - expect(container.querySelectorAll("p")).toHaveLength(1); - }); - - it("relinkifies on update", () => { - function DummyTest() { - const [n, setN] = useState(0); - function onClick() { - setN(n + 1); - } - - // upon clicking the element, change the content, and expect - // linkify to update - return ( -
    - {n % 2 === 0 ? "https://perdu.com" : "https://matrix.org"} -
    - ); - } - - const { container } = render(); - - expect(container.innerHTML).toBe( - "", - ); - - fireEvent.click(container.querySelector("div")); - - expect(container.innerHTML).toBe( - "", - ); - }); -}); diff --git a/yarn.lock b/yarn.lock index f5f4930289a..b548d9f9136 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6258,6 +6258,11 @@ linkify-element@4.0.0-beta.4: resolved "https://registry.yarnpkg.com/linkify-element/-/linkify-element-4.0.0-beta.4.tgz#31bb5dff7430c4debc34030466bd8f3e297793a7" integrity sha512-dsu5qxk6MhQHxXUlPjul33JknQPx7Iv/N8zisH4JtV31qVk0qZg/5gn10Hr76GlMuixcdcxVvGHNfVcvbut13w== +linkify-react@4.0.0-beta.4: + version "4.0.0-beta.4" + resolved "https://registry.yarnpkg.com/linkify-react/-/linkify-react-4.0.0-beta.4.tgz#75311ade523a52d43054dd841d724d746d43f60d" + integrity sha512-o4vFe28vtk6i8a6tbtkLyusIyhLJSYoHC3gEpmJEVqi6Hy3aguVEenYmtaOjmAQehDrBYeHv9s4qcneZOf7SWQ== + linkify-string@4.0.0-beta.4: version "4.0.0-beta.4" resolved "https://registry.yarnpkg.com/linkify-string/-/linkify-string-4.0.0-beta.4.tgz#0982509bc6ce81c554bff8d7121057193b84ea32" From b7cd28bd2983d50ab140084a196f4d1ad9dcd928 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 3 Feb 2023 09:14:44 +0000 Subject: [PATCH 68/97] Support MSC3946 in RoomListStore (#10054) --- src/stores/room-list/RoomListStore.ts | 8 +-- test/stores/room-list/RoomListStore-test.ts | 56 +++++++++++++++++++-- 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 39d66aa9bf1..09d9cca2e4e 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -286,9 +286,9 @@ export class RoomListStoreClass extends AsyncStoreWithClient implements // If we're joining an upgraded room, we'll want to make sure we don't proliferate // the dead room in the list. const roomState: RoomState = membershipPayload.room.currentState; - const createEvent = roomState.getStateEvents(EventType.RoomCreate, ""); - if (createEvent && createEvent.getContent()["predecessor"]) { - const prevRoom = this.matrixClient.getRoom(createEvent.getContent()["predecessor"]["room_id"]); + const predecessor = roomState.findPredecessor(SettingsStore.getValue("feature_dynamic_room_predecessors")); + if (predecessor) { + const prevRoom = this.matrixClient.getRoom(predecessor.roomId); if (prevRoom) { const isSticky = this.algorithm.stickyRoom === prevRoom; if (isSticky) { @@ -298,6 +298,8 @@ export class RoomListStoreClass extends AsyncStoreWithClient implements // Note: we hit the algorithm instead of our handleRoomUpdate() function to // avoid redundant updates. this.algorithm.handleRoomUpdate(prevRoom, RoomUpdateCause.RoomRemoved); + } else { + logger.warn(`Unable to find predecessor room with id ${predecessor.roomId}`); } } diff --git a/test/stores/room-list/RoomListStore-test.ts b/test/stores/room-list/RoomListStore-test.ts index 7ceb6393a27..8ce3dff22a3 100644 --- a/test/stores/room-list/RoomListStore-test.ts +++ b/test/stores/room-list/RoomListStore-test.ts @@ -17,6 +17,7 @@ limitations under the License. import { EventType, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import { MatrixDispatcher } from "../../../src/dispatcher/dispatcher"; +import SettingsStore from "../../../src/settings/SettingsStore"; import { ListAlgorithm, SortAlgorithm } from "../../../src/stores/room-list/algorithms/models"; import { OrderedDefaultTagIDs, RoomUpdateCause } from "../../../src/stores/room-list/models"; import RoomListStore, { RoomListStoreClass } from "../../../src/stores/room-list/RoomListStore"; @@ -24,14 +25,14 @@ import { stubClient, upsertRoomStateEvents } from "../../test-utils"; describe("RoomListStore", () => { const client = stubClient(); - const roomWithCreatePredecessorId = "!roomid:example.com"; + const newRoomId = "!roomid:example.com"; const roomNoPredecessorId = "!roomnopreid:example.com"; const oldRoomId = "!oldroomid:example.com"; const userId = "@user:example.com"; const createWithPredecessor = new MatrixEvent({ type: EventType.RoomCreate, sender: userId, - room_id: roomWithCreatePredecessorId, + room_id: newRoomId, content: { predecessor: { room_id: oldRoomId, event_id: "tombstone_event_id" }, }, @@ -41,19 +42,32 @@ describe("RoomListStore", () => { const createNoPredecessor = new MatrixEvent({ type: EventType.RoomCreate, sender: userId, - room_id: roomWithCreatePredecessorId, + room_id: newRoomId, content: {}, event_id: "$create", state_key: "", }); - const roomWithCreatePredecessor = new Room(roomWithCreatePredecessorId, client, userId, {}); + const predecessor = new MatrixEvent({ + type: EventType.RoomPredecessor, + sender: userId, + room_id: newRoomId, + content: { + predecessor_room_id: oldRoomId, + last_known_event_id: "tombstone_event_id", + }, + event_id: "$pred", + state_key: "", + }); + const roomWithPredecessorEvent = new Room(newRoomId, client, userId, {}); + upsertRoomStateEvents(roomWithPredecessorEvent, [predecessor]); + const roomWithCreatePredecessor = new Room(newRoomId, client, userId, {}); upsertRoomStateEvents(roomWithCreatePredecessor, [createWithPredecessor]); const roomNoPredecessor = new Room(roomNoPredecessorId, client, userId, {}); upsertRoomStateEvents(roomNoPredecessor, [createNoPredecessor]); const oldRoom = new Room(oldRoomId, client, userId, {}); client.getRoom = jest.fn().mockImplementation((roomId) => { switch (roomId) { - case roomWithCreatePredecessorId: + case newRoomId: return roomWithCreatePredecessor; case oldRoomId: return oldRoom; @@ -123,4 +137,36 @@ describe("RoomListStore", () => { // And no other updates happen expect(handleRoomUpdate).toHaveBeenCalledTimes(1); }); + + describe("When feature_dynamic_room_predecessors = true", () => { + beforeEach(() => { + jest.spyOn(SettingsStore, "getValue").mockImplementation( + (settingName) => settingName === "feature_dynamic_room_predecessors", + ); + }); + + afterEach(() => { + jest.spyOn(SettingsStore, "getValue").mockReset(); + }); + + it("Removes old room if it finds a predecessor in the m.predecessor event", () => { + // Given a store we can spy on + const { store, handleRoomUpdate } = createStore(); + + // When we tell it we joined a new room that has an old room as + // predecessor in the create event + const payload = { + oldMembership: "invite", + membership: "join", + room: roomWithPredecessorEvent, + }; + store.onDispatchMyMembership(payload); + + // Then the old room is removed + expect(handleRoomUpdate).toHaveBeenCalledWith(oldRoom, RoomUpdateCause.RoomRemoved); + + // And the new room is added + expect(handleRoomUpdate).toHaveBeenCalledWith(roomWithPredecessorEvent, RoomUpdateCause.NewRoom); + }); + }); }); From 469228f45e80ecef612fea5ffc62da58c671fd55 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 3 Feb 2023 10:48:12 +0100 Subject: [PATCH 69/97] Fix new line created when enter is pressed (#10064) --- .../hooks/useInputEventProcessor.ts | 6 ++++-- .../components/WysiwygComposer-test.tsx | 20 ++++++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts index 52d5f19e33d..405539fc709 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts @@ -168,13 +168,15 @@ function handleInputEvent(event: InputEvent, send: Send, isCtrlEnterToSend: bool case "insertParagraph": if (!isCtrlEnterToSend) { send(); + return null; } - return null; + break; case "sendMessage": if (isCtrlEnterToSend) { send(); + return null; } - return null; + break; } return event; diff --git a/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx index 44f2a5a996c..4d485b5a3fa 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx @@ -149,8 +149,10 @@ describe("WysiwygComposer", () => { it("Should not call onSend when Enter is pressed", async () => { // When + const textbox = screen.getByRole("textbox"); + fireEvent( - screen.getByRole("textbox"), + textbox, new InputEvent("input", { inputType: "insertParagraph", }), @@ -158,6 +160,22 @@ describe("WysiwygComposer", () => { // Then it does not send a message await waitFor(() => expect(onSend).toBeCalledTimes(0)); + + fireEvent( + textbox, + new InputEvent("input", { + inputType: "insertText", + data: "other", + }), + ); + + // The focus is on the last text node + await waitFor(() => { + const selection = document.getSelection(); + if (selection) { + expect(selection.focusNode?.textContent).toEqual("other"); + } + }); }); it("Should send a message when Ctrl+Enter is pressed", async () => { From 6dd578e5a7cad0abe2fadcdfcd075b4fe6d3f621 Mon Sep 17 00:00:00 2001 From: Germain Date: Fri, 3 Feb 2023 10:07:24 +0000 Subject: [PATCH 70/97] Devtools for stuck notifications (#10042) --- .../views/dialogs/DevtoolsDialog.tsx | 4 +- .../dialogs/devtools/RoomNotifications.tsx | 180 ++++++++++++++++++ .../dialogs/devtools/VerificationExplorer.tsx | 4 +- src/i18n/strings/en_EN.json | 29 ++- src/stores/notifications/NotificationColor.ts | 19 ++ .../DevtoolsDialog-test.tsx.snap | 5 + .../devtools/RoomNotifications-test.tsx | 50 +++++ .../RoomNotifications-test.tsx.snap | 62 ++++++ .../notifications/NotificationColor-test.ts | 31 +++ 9 files changed, 378 insertions(+), 6 deletions(-) create mode 100644 src/components/views/dialogs/devtools/RoomNotifications.tsx create mode 100644 test/components/views/dialogs/devtools/RoomNotifications-test.tsx create mode 100644 test/components/views/dialogs/devtools/__snapshots__/RoomNotifications-test.tsx.snap create mode 100644 test/stores/notifications/NotificationColor-test.ts diff --git a/src/components/views/dialogs/DevtoolsDialog.tsx b/src/components/views/dialogs/DevtoolsDialog.tsx index f16dd94f6ba..0df6ce42065 100644 --- a/src/components/views/dialogs/DevtoolsDialog.tsx +++ b/src/components/views/dialogs/DevtoolsDialog.tsx @@ -33,6 +33,7 @@ import { SettingLevel } from "../../../settings/SettingLevel"; import ServerInfo from "./devtools/ServerInfo"; import { Features } from "../../../settings/Settings"; import CopyableText from "../elements/CopyableText"; +import RoomNotifications from "./devtools/RoomNotifications"; enum Category { Room, @@ -44,13 +45,14 @@ const categoryLabels: Record = { [Category.Other]: _td("Other"), }; -export type Tool = React.FC; +export type Tool = React.FC | ((props: IDevtoolsProps) => JSX.Element); const Tools: Record = { [Category.Room]: [ [_td("Send custom timeline event"), TimelineEventEditor], [_td("Explore room state"), RoomStateExplorer], [_td("Explore room account data"), RoomAccountDataExplorer], [_td("View servers in room"), ServersInRoom], + [_td("Notifications debug"), RoomNotifications], [_td("Verification explorer"), VerificationExplorer], [_td("Active Widgets"), WidgetExplorer], ], diff --git a/src/components/views/dialogs/devtools/RoomNotifications.tsx b/src/components/views/dialogs/devtools/RoomNotifications.tsx new file mode 100644 index 00000000000..7ddfb5d8baa --- /dev/null +++ b/src/components/views/dialogs/devtools/RoomNotifications.tsx @@ -0,0 +1,180 @@ +/* +Copyright 2023 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 { NotificationCountType, Room } from "matrix-js-sdk/src/models/room"; +import { Thread } from "matrix-js-sdk/src/models/thread"; +import React, { useContext } from "react"; + +import MatrixClientContext from "../../../../contexts/MatrixClientContext"; +import { useNotificationState } from "../../../../hooks/useRoomNotificationState"; +import { _t } from "../../../../languageHandler"; +import { determineUnreadState } from "../../../../RoomNotifs"; +import { humanReadableNotificationColor } from "../../../../stores/notifications/NotificationColor"; +import { doesRoomOrThreadHaveUnreadMessages } from "../../../../Unread"; +import BaseTool, { DevtoolsContext, IDevtoolsProps } from "./BaseTool"; + +export default function RoomNotifications({ onBack }: IDevtoolsProps): JSX.Element { + const { room } = useContext(DevtoolsContext); + const cli = useContext(MatrixClientContext); + + const { color, count } = determineUnreadState(room); + const [notificationState] = useNotificationState(room); + + return ( + +
    +

    {_t("Room status")}

    +
      +
    • + {_t("Room unread status: ")} + {humanReadableNotificationColor(color)} + {count > 0 && ( + <> + {_t(", count:")} {count} + + )} +
    • +
    • + {_t("Notification state is")} {notificationState} +
    • +
    • + {_t("Room is ")} + + {cli.isRoomEncrypted(room.roomId!) ? _t("encrypted ✅") : _t("not encrypted 🚨")} + +
    • +
    +
    + +
    +

    {_t("Main timeline")}

    + +
      +
    • + {_t("Total: ")} {room.getRoomUnreadNotificationCount(NotificationCountType.Total)} +
    • +
    • + {_t("Highlight: ")} {room.getRoomUnreadNotificationCount(NotificationCountType.Highlight)} +
    • +
    • + {_t("Dot: ")} {doesRoomOrThreadHaveUnreadMessages(room) + ""} +
    • + {roomHasUnread(room) && ( + <> +
    • + {_t("User read up to: ")} + + {room.getReadReceiptForUserId(cli.getSafeUserId())?.eventId ?? + _t("No receipt found")} + +
    • +
    • + {_t("Last event:")} +
        +
      • + {_t("ID: ")} {room.timeline[room.timeline.length - 1].getId()} +
      • +
      • + {_t("Type: ")}{" "} + {room.timeline[room.timeline.length - 1].getType()} +
      • +
      • + {_t("Sender: ")}{" "} + {room.timeline[room.timeline.length - 1].getSender()} +
      • +
      +
    • + + )} +
    +
    + +
    +

    {_t("Threads timeline")}

    +
      + {room + .getThreads() + .filter((thread) => threadHasUnread(thread)) + .map((thread) => ( +
    • + {_t("Thread Id: ")} {thread.id} +
        +
      • + {_t("Total: ")} + + {room.getThreadUnreadNotificationCount( + thread.id, + NotificationCountType.Total, + )} + +
      • +
      • + {_t("Highlight: ")} + + {room.getThreadUnreadNotificationCount( + thread.id, + NotificationCountType.Highlight, + )} + +
      • +
      • + {_t("Dot: ")} {doesRoomOrThreadHaveUnreadMessages(thread) + ""} +
      • +
      • + {_t("User read up to: ")} + + {thread.getReadReceiptForUserId(cli.getSafeUserId())?.eventId ?? + _t("No receipt found")} + +
      • +
      • + {_t("Last event:")} +
          +
        • + {_t("ID: ")} {thread.lastReply()?.getId()} +
        • +
        • + {_t("Type: ")} {thread.lastReply()?.getType()} +
        • +
        • + {_t("Sender: ")} {thread.lastReply()?.getSender()} +
        • +
        +
      • +
      +
    • + ))} +
    +
    +
    + ); +} + +function threadHasUnread(thread: Thread): boolean { + const total = thread.room.getThreadUnreadNotificationCount(thread.id, NotificationCountType.Total); + const highlight = thread.room.getThreadUnreadNotificationCount(thread.id, NotificationCountType.Highlight); + const dot = doesRoomOrThreadHaveUnreadMessages(thread); + + return total > 0 || highlight > 0 || dot; +} + +function roomHasUnread(room: Room): boolean { + const total = room.getRoomUnreadNotificationCount(NotificationCountType.Total); + const highlight = room.getRoomUnreadNotificationCount(NotificationCountType.Highlight); + const dot = doesRoomOrThreadHaveUnreadMessages(room); + + return total > 0 || highlight > 0 || dot; +} diff --git a/src/components/views/dialogs/devtools/VerificationExplorer.tsx b/src/components/views/dialogs/devtools/VerificationExplorer.tsx index 7092d87f645..c535d32b327 100644 --- a/src/components/views/dialogs/devtools/VerificationExplorer.tsx +++ b/src/components/views/dialogs/devtools/VerificationExplorer.tsx @@ -25,7 +25,7 @@ import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import { useTypedEventEmitter, useTypedEventEmitterState } from "../../../../hooks/useEventEmitter"; import { _t, _td } from "../../../../languageHandler"; import MatrixClientContext from "../../../../contexts/MatrixClientContext"; -import BaseTool, { DevtoolsContext } from "./BaseTool"; +import BaseTool, { DevtoolsContext, IDevtoolsProps } from "./BaseTool"; import { Tool } from "../DevtoolsDialog"; const PHASE_MAP: Record = { @@ -81,7 +81,7 @@ const VerificationRequestExplorer: React.FC<{ ); }; -const VerificationExplorer: Tool = ({ onBack }) => { +const VerificationExplorer: Tool = ({ onBack }: IDevtoolsProps) => { const cli = useContext(MatrixClientContext); const context = useContext(DevtoolsContext); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f61c21b028f..c1aeeab2de5 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -902,6 +902,12 @@ "Room information": "Room information", "Room members": "Room members", "Back to thread": "Back to thread", + "None": "None", + "Bold": "Bold", + "Grey": "Grey", + "Red": "Red", + "Unsent": "Unsent", + "unknown": "unknown", "Change notification settings": "Change notification settings", "Messaging": "Messaging", "Profile": "Profile", @@ -1582,7 +1588,6 @@ "Error removing ignored user/server": "Error removing ignored user/server", "Error unsubscribing from list": "Error unsubscribing from list", "Please try again or view your console for hints.": "Please try again or view your console for hints.", - "None": "None", "Ban list rules - %(roomName)s": "Ban list rules - %(roomName)s", "Server rules": "Server rules", "User rules": "User rules", @@ -1942,7 +1947,6 @@ "Poll": "Poll", "Hide formatting": "Hide formatting", "Show formatting": "Show formatting", - "Bold": "Bold", "Italics": "Italics", "Strikethrough": "Strikethrough", "Code block": "Code block", @@ -2773,6 +2777,7 @@ "Explore room state": "Explore room state", "Explore room account data": "Explore room account data", "View servers in room": "View servers in room", + "Notifications debug": "Notifications debug", "Verification explorer": "Verification explorer", "Active Widgets": "Active Widgets", "Explore account data": "Explore account data", @@ -3152,6 +3157,25 @@ "Event Content": "Event Content", "Filter results": "Filter results", "No results found": "No results found", + "Room status": "Room status", + "Room unread status: ": "Room unread status: ", + ", count:": ", count:", + "Notification state is": "Notification state is", + "Room is ": "Room is ", + "encrypted ✅": "encrypted ✅", + "not encrypted 🚨": "not encrypted 🚨", + "Main timeline": "Main timeline", + "Total: ": "Total: ", + "Highlight: ": "Highlight: ", + "Dot: ": "Dot: ", + "User read up to: ": "User read up to: ", + "No receipt found": "No receipt found", + "Last event:": "Last event:", + "ID: ": "ID: ", + "Type: ": "Type: ", + "Sender: ": "Sender: ", + "Threads timeline": "Threads timeline", + "Thread Id: ": "Thread Id: ", "<%(count)s spaces>|other": "<%(count)s spaces>", "<%(count)s spaces>|one": "", "<%(count)s spaces>|zero": "", @@ -3182,7 +3206,6 @@ "Value": "Value", "Value in this room": "Value in this room", "Edit setting": "Edit setting", - "Unsent": "Unsent", "Requested": "Requested", "Ready": "Ready", "Started": "Started", diff --git a/src/stores/notifications/NotificationColor.ts b/src/stores/notifications/NotificationColor.ts index 58737866df5..f89bb1728d1 100644 --- a/src/stores/notifications/NotificationColor.ts +++ b/src/stores/notifications/NotificationColor.ts @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { _t } from "../../languageHandler"; + export enum NotificationColor { // Inverted (None -> Red) because we do integer comparisons on this None, // nothing special @@ -23,3 +25,20 @@ export enum NotificationColor { Red, // unread pings Unsent, // some messages failed to send } + +export function humanReadableNotificationColor(color: NotificationColor): string { + switch (color) { + case NotificationColor.None: + return _t("None"); + case NotificationColor.Bold: + return _t("Bold"); + case NotificationColor.Grey: + return _t("Grey"); + case NotificationColor.Red: + return _t("Red"); + case NotificationColor.Unsent: + return _t("Unsent"); + default: + return _t("unknown"); + } +} diff --git a/test/components/views/dialogs/__snapshots__/DevtoolsDialog-test.tsx.snap b/test/components/views/dialogs/__snapshots__/DevtoolsDialog-test.tsx.snap index 62014e2d2b6..5e859a6c630 100644 --- a/test/components/views/dialogs/__snapshots__/DevtoolsDialog-test.tsx.snap +++ b/test/components/views/dialogs/__snapshots__/DevtoolsDialog-test.tsx.snap @@ -75,6 +75,11 @@ exports[`DevtoolsDialog renders the devtools dialog 1`] = ` > View servers in room + +
    + +`; diff --git a/test/stores/notifications/NotificationColor-test.ts b/test/stores/notifications/NotificationColor-test.ts new file mode 100644 index 00000000000..1125c47bff8 --- /dev/null +++ b/test/stores/notifications/NotificationColor-test.ts @@ -0,0 +1,31 @@ +/* +Copyright 2023 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 { humanReadableNotificationColor, NotificationColor } from "../../../src/stores/notifications/NotificationColor"; + +describe("NotificationColor", () => { + describe("humanReadableNotificationColor", () => { + it.each([ + [NotificationColor.None, "None"], + [NotificationColor.Bold, "Bold"], + [NotificationColor.Grey, "Grey"], + [NotificationColor.Red, "Red"], + [NotificationColor.Unsent, "Unsent"], + ])("correctly maps the output", (color, output) => { + expect(humanReadableNotificationColor(color)).toBe(output); + }); + }); +}); From 27bd04a875bcd98531252abf0251b84438a74dc6 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Fri, 3 Feb 2023 13:00:33 +0100 Subject: [PATCH 71/97] Use findPredecessor in RoomNotifs#getUnreadNotificationCount (#10067) --- src/RoomNotifs.ts | 10 ++-- test/RoomNotifs-test.ts | 130 +++++++++++++++++++++++++++++++--------- 2 files changed, 108 insertions(+), 32 deletions(-) diff --git a/src/RoomNotifs.ts b/src/RoomNotifs.ts index 0962e45cb9c..c29105b7809 100644 --- a/src/RoomNotifs.ts +++ b/src/RoomNotifs.ts @@ -17,7 +17,6 @@ limitations under the License. import { PushProcessor } from "matrix-js-sdk/src/pushprocessor"; import { NotificationCountType } from "matrix-js-sdk/src/models/room"; import { ConditionKind, PushRuleActionName, PushRuleKind, TweakName } from "matrix-js-sdk/src/@types/PushRules"; -import { EventType } from "matrix-js-sdk/src/@types/event"; import type { IPushRule } from "matrix-js-sdk/src/@types/PushRules"; import type { Room } from "matrix-js-sdk/src/models/room"; @@ -27,6 +26,7 @@ import { NotificationColor } from "./stores/notifications/NotificationColor"; import { getUnsentMessages } from "./components/structures/RoomStatusBar"; import { doesRoomHaveUnreadMessages, doesRoomOrThreadHaveUnreadMessages } from "./Unread"; import { EffectiveMembership, getEffectiveMembership } from "./utils/membership"; +import SettingsStore from "./settings/SettingsStore"; export enum RoomNotifState { AllMessagesLoud = "all_messages_loud", @@ -86,11 +86,11 @@ export function getUnreadNotificationCount(room: Room, type: NotificationCountTy // Check notification counts in the old room just in case there's some lost // there. We only go one level down to avoid performance issues, and theory // is that 1st generation rooms will have already been read by the 3rd generation. - const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, ""); - const predecessor = createEvent?.getContent().predecessor; + const msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors"); + const predecessor = room.findPredecessor(msc3946ProcessDynamicPredecessor); // Exclude threadId, as the same thread can't continue over a room upgrade - if (!threadId && predecessor) { - const oldRoomId = predecessor.room_id; + if (!threadId && predecessor?.roomId) { + const oldRoomId = predecessor.roomId; const oldRoom = MatrixClientPeg.get().getRoom(oldRoomId); if (oldRoom) { // We only ever care if there's highlights in the old room. No point in diff --git a/test/RoomNotifs-test.ts b/test/RoomNotifs-test.ts index c50cdb86677..963bfd77ed7 100644 --- a/test/RoomNotifs-test.ts +++ b/test/RoomNotifs-test.ts @@ -17,10 +17,10 @@ limitations under the License. import { mocked } from "jest-mock"; import { PushRuleActionName, TweakName } from "matrix-js-sdk/src/@types/PushRules"; import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room"; -import { EventStatus, PendingEventOrdering } from "matrix-js-sdk/src/matrix"; +import { EventStatus, EventType, MatrixEvent, PendingEventOrdering } from "matrix-js-sdk/src/matrix"; import type { MatrixClient } from "matrix-js-sdk/src/matrix"; -import { mkEvent, mkRoom, muteRoom, stubClient } from "./test-utils"; +import { mkEvent, mkRoom, muteRoom, stubClient, upsertRoomStateEvents } from "./test-utils"; import { getRoomNotifsState, RoomNotifState, @@ -28,6 +28,7 @@ import { determineUnreadState, } from "../src/RoomNotifs"; import { NotificationColor } from "../src/stores/notifications/NotificationColor"; +import SettingsStore from "../src/settings/SettingsStore"; describe("RoomNotifs test", () => { let client: jest.Mocked; @@ -105,36 +106,111 @@ describe("RoomNotifs test", () => { expect(getUnreadNotificationCount(room, NotificationCountType.Highlight)).toBe(1); }); - it("counts predecessor highlight", () => { - room.setUnreadNotificationCount(NotificationCountType.Total, 2); - room.setUnreadNotificationCount(NotificationCountType.Highlight, 1); - + describe("when there is a room predecessor", () => { const OLD_ROOM_ID = "!oldRoomId:example.org"; - const oldRoom = new Room(OLD_ROOM_ID, client, client.getUserId()!); - oldRoom.setUnreadNotificationCount(NotificationCountType.Total, 10); - oldRoom.setUnreadNotificationCount(NotificationCountType.Highlight, 6); + const mkCreateEvent = (predecessorId?: string): MatrixEvent => { + return mkEvent({ + event: true, + type: "m.room.create", + room: ROOM_ID, + user: client.getUserId()!, + content: { + ...(predecessorId ? { predecessor: { room_id: predecessorId, event_id: "$someevent" } } : {}), + creator: client.getUserId(), + room_version: "5", + }, + ts: Date.now(), + }); + }; + + const mkPredecessorEvent = (predecessorId: string): MatrixEvent => { + return mkEvent({ + event: true, + type: EventType.RoomPredecessor, + room: ROOM_ID, + user: client.getUserId()!, + skey: "", + content: { + predecessor_room_id: predecessorId, + }, + ts: Date.now(), + }); + }; + + const itShouldCountPredecessorHighlightWhenThereIsAPredecessorInTheCreateEvent = (): void => { + it("and there is a predecessor in the create event, it should count predecessor highlight", () => { + room.addLiveEvents([mkCreateEvent(OLD_ROOM_ID)]); + + expect(getUnreadNotificationCount(room, NotificationCountType.Total)).toBe(8); + expect(getUnreadNotificationCount(room, NotificationCountType.Highlight)).toBe(7); + }); + }; + + const itShouldCountPredecessorHighlightWhenThereIsAPredecessorEvent = (): void => { + it("and there is a predecessor event, it should count predecessor highlight", () => { + client.getVisibleRooms(); + room.addLiveEvents([mkCreateEvent(OLD_ROOM_ID)]); + upsertRoomStateEvents(room, [mkPredecessorEvent(OLD_ROOM_ID)]); + + expect(getUnreadNotificationCount(room, NotificationCountType.Total)).toBe(8); + expect(getUnreadNotificationCount(room, NotificationCountType.Highlight)).toBe(7); + }); + }; + + beforeEach(() => { + room.setUnreadNotificationCount(NotificationCountType.Total, 2); + room.setUnreadNotificationCount(NotificationCountType.Highlight, 1); + + const oldRoom = new Room(OLD_ROOM_ID, client, client.getUserId()!); + oldRoom.setUnreadNotificationCount(NotificationCountType.Total, 10); + oldRoom.setUnreadNotificationCount(NotificationCountType.Highlight, 6); + + client.getRoom.mockImplementation((roomId: string | undefined): Room | null => { + if (roomId === room.roomId) return room; + if (roomId === OLD_ROOM_ID) return oldRoom; + return null; + }); + }); - client.getRoom.mockReset().mockReturnValue(oldRoom); + describe("and dynamic room predecessors are disabled", () => { + itShouldCountPredecessorHighlightWhenThereIsAPredecessorInTheCreateEvent(); + itShouldCountPredecessorHighlightWhenThereIsAPredecessorEvent(); - const predecessorEvent = mkEvent({ - event: true, - type: "m.room.create", - room: ROOM_ID, - user: client.getUserId()!, - content: { - creator: client.getUserId(), - room_version: "5", - predecessor: { - room_id: OLD_ROOM_ID, - event_id: "$someevent", - }, - }, - ts: Date.now(), + it("and there is only a predecessor event, it should not count predecessor highlight", () => { + room.addLiveEvents([mkCreateEvent()]); + upsertRoomStateEvents(room, [mkPredecessorEvent(OLD_ROOM_ID)]); + + expect(getUnreadNotificationCount(room, NotificationCountType.Total)).toBe(2); + expect(getUnreadNotificationCount(room, NotificationCountType.Highlight)).toBe(1); + }); }); - room.addLiveEvents([predecessorEvent]); - expect(getUnreadNotificationCount(room, NotificationCountType.Total)).toBe(8); - expect(getUnreadNotificationCount(room, NotificationCountType.Highlight)).toBe(7); + describe("and dynamic room predecessors are enabled", () => { + beforeEach(() => { + jest.spyOn(SettingsStore, "getValue").mockImplementation( + (settingName) => settingName === "feature_dynamic_room_predecessors", + ); + }); + + itShouldCountPredecessorHighlightWhenThereIsAPredecessorInTheCreateEvent(); + itShouldCountPredecessorHighlightWhenThereIsAPredecessorEvent(); + + it("and there is only a predecessor event, it should count predecessor highlight", () => { + room.addLiveEvents([mkCreateEvent()]); + upsertRoomStateEvents(room, [mkPredecessorEvent(OLD_ROOM_ID)]); + + expect(getUnreadNotificationCount(room, NotificationCountType.Total)).toBe(8); + expect(getUnreadNotificationCount(room, NotificationCountType.Highlight)).toBe(7); + }); + + it("and there is an unknown room in the predecessor event, it should not count predecessor highlight", () => { + room.addLiveEvents([mkCreateEvent()]); + upsertRoomStateEvents(room, [mkPredecessorEvent("!unknon:example.com")]); + + expect(getUnreadNotificationCount(room, NotificationCountType.Total)).toBe(2); + expect(getUnreadNotificationCount(room, NotificationCountType.Highlight)).toBe(1); + }); + }); }); it("counts thread notification type", () => { From cfba1b07c65cba3463c9b0aa2640d6bde65cb9d5 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Fri, 3 Feb 2023 13:49:43 +0100 Subject: [PATCH 72/97] Broadcast time left should never be negative (#10070) --- .../models/VoiceBroadcastPlayback.ts | 4 +++- .../models/VoiceBroadcastPlayback-test.tsx | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/voice-broadcast/models/VoiceBroadcastPlayback.ts b/src/voice-broadcast/models/VoiceBroadcastPlayback.ts index 94ccaac1fb2..0a5442cb62c 100644 --- a/src/voice-broadcast/models/VoiceBroadcastPlayback.ts +++ b/src/voice-broadcast/models/VoiceBroadcastPlayback.ts @@ -441,7 +441,9 @@ export class VoiceBroadcastPlayback } public get timeLeftSeconds(): number { - return Math.round(this.durationSeconds) - this.timeSeconds; + // Sometimes the meta data and the audio files are a little bit out of sync. + // Be sure it never returns a negative value. + return Math.max(0, Math.round(this.durationSeconds) - this.timeSeconds); } public async skipTo(timeSeconds: number): Promise { diff --git a/test/voice-broadcast/models/VoiceBroadcastPlayback-test.tsx b/test/voice-broadcast/models/VoiceBroadcastPlayback-test.tsx index ac4217e0485..e7f4c8afcc1 100644 --- a/test/voice-broadcast/models/VoiceBroadcastPlayback-test.tsx +++ b/test/voice-broadcast/models/VoiceBroadcastPlayback-test.tsx @@ -525,6 +525,20 @@ describe("VoiceBroadcastPlayback", () => { it("should update the time", () => { expect(playback.timeSeconds).toBe(11); + expect(playback.timeLeftSeconds).toBe(2); + }); + }); + + describe("and the chunk playback progresses across the actual time", () => { + // This can be the case if the meta data is out of sync with the actual audio data. + + beforeEach(() => { + chunk1Playback.clockInfo.liveData.update([15]); + }); + + it("should update the time", () => { + expect(playback.timeSeconds).toBe(15); + expect(playback.timeLeftSeconds).toBe(0); }); }); From 41c8ab5e592ceff92c22e689180f308318099b79 Mon Sep 17 00:00:00 2001 From: alunturner <56027671+alunturner@users.noreply.github.com> Date: Fri, 3 Feb 2023 13:55:12 +0000 Subject: [PATCH 73/97] fix paragraph display for MD from plain text editor (#10071) --- res/css/views/rooms/_EventTile.pcss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index 3b00103581a..5425697ff9f 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -640,7 +640,7 @@ $left-gutter: 64px; } /* Remove top and bottom margin for better display in rich text editor output */ - :is(p, ol, ul) { + :is(blockquote > p, ol, ul) { margin-top: 0; margin-bottom: 0; } From add23e4d5d0a01f6f4e20fa31f5b87b0e457da10 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 3 Feb 2023 14:09:31 +0000 Subject: [PATCH 74/97] Pass the dynamic predecessor feature flag when listing rooms (#10068) --- src/stores/room-list/RoomListStore.ts | 21 +++- test/stores/room-list/RoomListStore-test.ts | 111 +++++++++++++++++++- 2 files changed, 127 insertions(+), 5 deletions(-) diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 09d9cca2e4e..0fd511e8a81 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -56,6 +56,8 @@ export class RoomListStoreClass extends AsyncStoreWithClient implements public static TEST_MODE = false; private initialListsGenerated = false; + private msc3946ProcessDynamicPredecessor: boolean; + private msc3946SettingWatcherRef: string; private algorithm = new Algorithm(); private prefilterConditions: IFilterCondition[] = []; private updateFn = new MarkedExecution(() => { @@ -69,6 +71,20 @@ export class RoomListStoreClass extends AsyncStoreWithClient implements super(dis); this.setMaxListeners(20); // RoomList + LeftPanel + 8xRoomSubList + spares this.algorithm.start(); + + this.msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors"); + this.msc3946SettingWatcherRef = SettingsStore.watchSetting( + "feature_dynamic_room_predecessors", + null, + (_settingName, _roomId, _level, _newValAtLevel, newVal) => { + this.msc3946ProcessDynamicPredecessor = newVal; + this.regenerateAllLists({ trigger: true }); + }, + ); + } + + public componentWillUnmount(): void { + SettingsStore.unwatchSetting(this.msc3946SettingWatcherRef); } private setupWatchers(): void { @@ -286,7 +302,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient implements // If we're joining an upgraded room, we'll want to make sure we don't proliferate // the dead room in the list. const roomState: RoomState = membershipPayload.room.currentState; - const predecessor = roomState.findPredecessor(SettingsStore.getValue("feature_dynamic_room_predecessors")); + const predecessor = roomState.findPredecessor(this.msc3946ProcessDynamicPredecessor); if (predecessor) { const prevRoom = this.matrixClient.getRoom(predecessor.roomId); if (prevRoom) { @@ -496,7 +512,8 @@ export class RoomListStoreClass extends AsyncStoreWithClient implements private getPlausibleRooms(): Room[] { if (!this.matrixClient) return []; - let rooms = this.matrixClient.getVisibleRooms().filter((r) => VisibilityProvider.instance.isRoomVisible(r)); + let rooms = this.matrixClient.getVisibleRooms(this.msc3946ProcessDynamicPredecessor); + rooms = rooms.filter((r) => VisibilityProvider.instance.isRoomVisible(r)); if (this.prefilterConditions.length > 0) { rooms = rooms.filter((r) => { diff --git a/test/stores/room-list/RoomListStore-test.ts b/test/stores/room-list/RoomListStore-test.ts index 8ce3dff22a3..e0ee766f914 100644 --- a/test/stores/room-list/RoomListStore-test.ts +++ b/test/stores/room-list/RoomListStore-test.ts @@ -14,13 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { EventType, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { EventType, MatrixEvent, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix"; import { MatrixDispatcher } from "../../../src/dispatcher/dispatcher"; -import SettingsStore from "../../../src/settings/SettingsStore"; +import { SettingLevel } from "../../../src/settings/SettingLevel"; +import SettingsStore, { CallbackFn } from "../../../src/settings/SettingsStore"; import { ListAlgorithm, SortAlgorithm } from "../../../src/stores/room-list/algorithms/models"; import { OrderedDefaultTagIDs, RoomUpdateCause } from "../../../src/stores/room-list/models"; import RoomListStore, { RoomListStoreClass } from "../../../src/stores/room-list/RoomListStore"; +import DMRoomMap from "../../../src/utils/DMRoomMap"; import { stubClient, upsertRoomStateEvents } from "../../test-utils"; describe("RoomListStore", () => { @@ -62,7 +64,9 @@ describe("RoomListStore", () => { upsertRoomStateEvents(roomWithPredecessorEvent, [predecessor]); const roomWithCreatePredecessor = new Room(newRoomId, client, userId, {}); upsertRoomStateEvents(roomWithCreatePredecessor, [createWithPredecessor]); - const roomNoPredecessor = new Room(roomNoPredecessorId, client, userId, {}); + const roomNoPredecessor = new Room(roomNoPredecessorId, client, userId, { + pendingEventOrdering: PendingEventOrdering.Detached, + }); upsertRoomStateEvents(roomNoPredecessor, [createNoPredecessor]); const oldRoom = new Room(oldRoomId, client, userId, {}); client.getRoom = jest.fn().mockImplementation((roomId) => { @@ -138,6 +142,93 @@ describe("RoomListStore", () => { expect(handleRoomUpdate).toHaveBeenCalledTimes(1); }); + it("Lists all rooms that the client says are visible", () => { + // Given 3 rooms that are visible according to the client + const room1 = new Room("!r1:e.com", client, userId, { pendingEventOrdering: PendingEventOrdering.Detached }); + const room2 = new Room("!r2:e.com", client, userId, { pendingEventOrdering: PendingEventOrdering.Detached }); + const room3 = new Room("!r3:e.com", client, userId, { pendingEventOrdering: PendingEventOrdering.Detached }); + room1.updateMyMembership("join"); + room2.updateMyMembership("join"); + room3.updateMyMembership("join"); + DMRoomMap.makeShared(); + const { store } = createStore(); + client.getVisibleRooms = jest.fn().mockReturnValue([room1, room2, room3]); + + // When we make the list of rooms + store.regenerateAllLists({ trigger: false }); + + // Then the list contains all 3 + expect(store.orderedLists).toMatchObject({ + "im.vector.fake.recent": [room1, room2, room3], + }); + + // We asked not to use MSC3946 when we asked the client for the visible rooms + expect(client.getVisibleRooms).toHaveBeenCalledWith(false); + expect(client.getVisibleRooms).toHaveBeenCalledTimes(1); + }); + + it("Watches the feature flag setting", () => { + jest.spyOn(SettingsStore, "watchSetting").mockReturnValue("dyn_pred_ref"); + jest.spyOn(SettingsStore, "unwatchSetting"); + + // When we create a store + const { store } = createStore(); + + // Then we watch the feature flag + expect(SettingsStore.watchSetting).toHaveBeenCalledWith( + "feature_dynamic_room_predecessors", + null, + expect.any(Function), + ); + + // And when we unmount it + store.componentWillUnmount(); + + // Then we unwatch it. + expect(SettingsStore.unwatchSetting).toHaveBeenCalledWith("dyn_pred_ref"); + }); + + it("Regenerates all lists when the feature flag is set", () => { + // Given a store allowing us to spy on any use of SettingsStore + let featureFlagValue = false; + jest.spyOn(SettingsStore, "getValue").mockImplementation(() => featureFlagValue); + + let watchCallback: CallbackFn | undefined; + jest.spyOn(SettingsStore, "watchSetting").mockImplementation( + (_settingName: string, _roomId: string | null, callbackFn: CallbackFn) => { + watchCallback = callbackFn; + return "dyn_pred_ref"; + }, + ); + jest.spyOn(SettingsStore, "unwatchSetting"); + + const { store } = createStore(); + client.getVisibleRooms = jest.fn().mockReturnValue([]); + // Sanity: no calculation has happened yet + expect(client.getVisibleRooms).toHaveBeenCalledTimes(0); + + // When we calculate for the first time + store.regenerateAllLists({ trigger: false }); + + // Then we use the current feature flag value (false) + expect(client.getVisibleRooms).toHaveBeenCalledWith(false); + expect(client.getVisibleRooms).toHaveBeenCalledTimes(1); + + // But when we update the feature flag + featureFlagValue = true; + watchCallback( + "feature_dynamic_room_predecessors", + "", + SettingLevel.DEFAULT, + featureFlagValue, + featureFlagValue, + ); + + // Then we recalculate and passed the updated value (true) + expect(client.getVisibleRooms).toHaveBeenCalledWith(true); + expect(client.getVisibleRooms).toHaveBeenCalledTimes(2); + }); + describe("When feature_dynamic_room_predecessors = true", () => { beforeEach(() => { jest.spyOn(SettingsStore, "getValue").mockImplementation( @@ -168,5 +259,19 @@ describe("RoomListStore", () => { // And the new room is added expect(handleRoomUpdate).toHaveBeenCalledWith(roomWithPredecessorEvent, RoomUpdateCause.NewRoom); }); + + it("Passes the feature flag on to the client when asking for visible rooms", () => { + // Given a store that we can ask for a room list + DMRoomMap.makeShared(); + const { store } = createStore(); + client.getVisibleRooms = jest.fn().mockReturnValue([]); + + // When we make the list of rooms + store.regenerateAllLists({ trigger: false }); + + // We asked to use MSC3946 when we asked the client for the visible rooms + expect(client.getVisibleRooms).toHaveBeenCalledWith(true); + expect(client.getVisibleRooms).toHaveBeenCalledTimes(1); + }); }); }); From 97506cbcdbd8589512ae6258a9a7e342084dc3c4 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Fri, 3 Feb 2023 15:47:07 +0100 Subject: [PATCH 75/97] Migrate SpotlightDialog for dynamic room predecessors (#10076) --- .../dialogs/spotlight/SpotlightDialog.tsx | 21 ++++++++++------ .../views/dialogs/SpotlightDialog-test.tsx | 25 +++++++++++++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx index 7fb84292e65..bf255695682 100644 --- a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx +++ b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx @@ -50,7 +50,6 @@ import { useDebouncedCallback } from "../../../../hooks/spotlight/useDebouncedCa import { useRecentSearches } from "../../../../hooks/spotlight/useRecentSearches"; import { useProfileInfo } from "../../../../hooks/useProfileInfo"; import { usePublicRoomDirectory } from "../../../../hooks/usePublicRoomDirectory"; -import { useFeatureEnabled } from "../../../../hooks/useSettings"; import { useSpaceResults } from "../../../../hooks/useSpaceResults"; import { useUserDirectory } from "../../../../hooks/useUserDirectory"; import { getKeyBindingsManager } from "../../../../KeyBindingsManager"; @@ -93,6 +92,7 @@ import { TooltipOption } from "./TooltipOption"; import { isLocalRoom } from "../../../../utils/localRoom/isLocalRoom"; import { shouldShowFeedback } from "../../../../utils/Feedback"; import RoomAvatar from "../../avatars/RoomAvatar"; +import { useFeatureEnabled } from "../../../../hooks/useSettings"; const MAX_RECENT_SEARCHES = 10; const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons @@ -242,8 +242,8 @@ export const useWebSearchMetrics = (numResults: number, queryLength: number, via }, [numResults, queryLength, viaSpotlight]); }; -const findVisibleRooms = (cli: MatrixClient): Room[] => { - return cli.getVisibleRooms().filter((room) => { +const findVisibleRooms = (cli: MatrixClient, msc3946ProcessDynamicPredecessor: boolean): Room[] => { + return cli.getVisibleRooms(msc3946ProcessDynamicPredecessor).filter((room) => { // Do not show local rooms if (isLocalRoom(room)) return false; @@ -252,9 +252,13 @@ const findVisibleRooms = (cli: MatrixClient): Room[] => { }); }; -const findVisibleRoomMembers = (cli: MatrixClient, filterDMs = true): RoomMember[] => { +const findVisibleRoomMembers = ( + cli: MatrixClient, + msc3946ProcessDynamicPredecessor: boolean, + filterDMs = true, +): RoomMember[] => { return Object.values( - findVisibleRooms(cli) + findVisibleRooms(cli, msc3946ProcessDynamicPredecessor) .filter((room) => !filterDMs || !DMRoomMap.shared().getUserIdForRoomId(room.roomId)) .reduce((members, room) => { for (const member of room.getJoinedMembers()) { @@ -304,6 +308,7 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n const memberScores = buildMemberScores(cli); return compareMembers(activityScores, memberScores); }, [cli]); + const msc3946ProcessDynamicPredecessor = useFeatureEnabled("feature_dynamic_room_predecessors"); const ownInviteLink = makeUserPermalink(cli.getUserId()); const [inviteLinkCopied, setInviteLinkCopied] = useState(false); @@ -339,7 +344,7 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n const possibleResults = useMemo(() => { const userResults: IMemberResult[] = []; - const roomResults = findVisibleRooms(cli).map(toRoomResult); + const roomResults = findVisibleRooms(cli, msc3946ProcessDynamicPredecessor).map(toRoomResult); // If we already have a DM with the user we're looking for, we will // show that DM instead of the user themselves const alreadyAddedUserIds = roomResults.reduce((userIds, result) => { @@ -349,7 +354,7 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n userIds.add(userId); return userIds; }, new Set()); - for (const user of [...findVisibleRoomMembers(cli), ...users]) { + for (const user of [...findVisibleRoomMembers(cli, msc3946ProcessDynamicPredecessor), ...users]) { // Make sure we don't have any user more than once if (alreadyAddedUserIds.has(user.userId)) continue; alreadyAddedUserIds.add(user.userId); @@ -381,7 +386,7 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n ), ...publicRooms.map(toPublicRoomResult), ].filter((result) => filter === null || result.filter.includes(filter)); - }, [cli, users, profile, publicRooms, filter]); + }, [cli, users, profile, publicRooms, filter, msc3946ProcessDynamicPredecessor]); const results = useMemo>(() => { const results: Record = { diff --git a/test/components/views/dialogs/SpotlightDialog-test.tsx b/test/components/views/dialogs/SpotlightDialog-test.tsx index 724fe1b0e99..d46561422e6 100644 --- a/test/components/views/dialogs/SpotlightDialog-test.tsx +++ b/test/components/views/dialogs/SpotlightDialog-test.tsx @@ -30,6 +30,8 @@ import { DirectoryMember, startDmOnFirstMessage } from "../../../../src/utils/di import DMRoomMap from "../../../../src/utils/DMRoomMap"; import { mkRoom, stubClient } from "../../../test-utils"; import { shouldShowFeedback } from "../../../../src/utils/Feedback"; +import SettingsStore from "../../../../src/settings/SettingsStore"; +import { SettingLevel } from "../../../../src/settings/SettingLevel"; jest.mock("../../../../src/utils/Feedback"); @@ -202,6 +204,26 @@ describe("Spotlight Dialog", () => { }); }); + describe("when MSC3946 dynamic room predecessors is enabled", () => { + beforeEach(() => { + SettingsStore.setValue("feature_dynamic_room_predecessors", null, SettingLevel.DEVICE, true); + }); + + afterEach(() => { + SettingsStore.setValue("feature_dynamic_room_predecessors", null, SettingLevel.DEVICE, null); + }); + + it("should call getVisibleRooms with MSC3946 dynamic room predecessors", async () => { + const wrapper = mount( null} />); + await act(async () => { + await sleep(1); + }); + wrapper.update(); + expect(mockedClient.getVisibleRooms).toHaveBeenCalledWith(true); + wrapper.unmount(); + }); + }); + describe("should apply manually selected filter", () => { it("with public rooms", async () => { const wrapper = mount( null} />); @@ -224,6 +246,9 @@ describe("Spotlight Dialog", () => { expect(options.length).toBe(1); expect(options.first().text()).toContain(testPublicRoom.name); + // assert that getVisibleRooms is called without MSC3946 dynamic room predecessors + expect(mockedClient.getVisibleRooms).toHaveBeenCalledWith(false); + wrapper.unmount(); }); it("with people", async () => { From 97438523809c7175946bec487fe9ea985b57d7ab Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 3 Feb 2023 15:27:47 +0000 Subject: [PATCH 76/97] Improve typescript null checking in places (#10073 (#10073 * Improve typescript null checking in places * Iterate * Fix Timer.ts --- src/Avatar.ts | 6 ++-- src/BasePlatform.ts | 10 +++---- src/ContentMessages.ts | 2 +- src/DecryptionFailureTracker.ts | 4 +-- src/DeviceListener.ts | 6 ++-- src/ImageUtils.ts | 7 ++++- src/accessibility/KeyboardShortcutUtils.ts | 8 ++--- src/accessibility/RovingTabIndex.tsx | 16 +++++----- src/actions/MatrixActionCreators.ts | 2 +- src/audio/PlaybackQueue.ts | 6 ++-- src/autocomplete/AutocompleteProvider.tsx | 4 +-- src/autocomplete/Autocompleter.ts | 4 +-- src/autocomplete/CommandProvider.tsx | 6 ++-- src/autocomplete/RoomProvider.tsx | 8 ++--- src/components/structures/MatrixChat.tsx | 2 +- .../messages/MKeyVerificationConclusion.tsx | 8 ++--- src/createRoom.ts | 12 ++++---- src/customisations/Media.ts | 14 ++++----- src/customisations/Security.ts | 6 ++-- .../models/IMediaEventContent.ts | 2 +- src/editor/caret.ts | 8 ++--- src/editor/deserialize.ts | 2 +- src/editor/history.ts | 4 +-- src/editor/model.ts | 10 +++---- src/editor/parts.ts | 4 +-- src/editor/position.ts | 4 +++ src/editor/render.ts | 30 +++++++++---------- src/stores/OwnProfileStore.ts | 4 +-- src/stores/ReadyWatchingStore.ts | 4 +-- src/stores/UIStore.ts | 6 ++-- src/stores/spaces/flattenSpaceHierarchy.ts | 2 +- src/toasts/UpdateToast.tsx | 8 ++--- src/utils/DialogOpener.ts | 6 ++-- src/utils/RoomUpgrade.ts | 12 ++++---- src/utils/ShieldUtils.ts | 2 +- src/utils/StorageManager.ts | 8 ++--- src/utils/Timer.ts | 14 ++++----- src/utils/UrlUtils.ts | 2 +- src/utils/arrays.ts | 4 +-- src/utils/leave-behaviour.ts | 2 +- src/utils/permalinks/Permalinks.ts | 30 +++++++------------ src/utils/space.tsx | 6 ++-- src/utils/tooltipify.tsx | 4 +-- 43 files changed, 155 insertions(+), 154 deletions(-) diff --git a/src/Avatar.ts b/src/Avatar.ts index 8a3f10a22ca..986ae6306a4 100644 --- a/src/Avatar.ts +++ b/src/Avatar.ts @@ -31,7 +31,7 @@ export function avatarUrlForMember( height: number, resizeMethod: ResizeMethod, ): string { - let url: string; + let url: string | null | undefined; if (member?.getMxcAvatarUrl()) { url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod); } @@ -118,7 +118,7 @@ export function defaultAvatarUrlForString(s: string): string { * @param {string} name * @return {string} the first letter */ -export function getInitialLetter(name: string): string { +export function getInitialLetter(name: string): string | undefined { if (!name) { // XXX: We should find out what causes the name to sometimes be falsy. console.trace("`name` argument to `getInitialLetter` not supplied"); @@ -146,7 +146,7 @@ export function avatarUrlForRoom( if (!room) return null; // null-guard if (room.getMxcAvatarUrl()) { - return mediaFromMxc(room.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod); + return mediaFromMxc(room.getMxcAvatarUrl() || undefined).getThumbnailOfSourceHttp(width, height, resizeMethod); } // space rooms cannot be DMs so skip the rest diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index ab8bca4b5fa..46f964995af 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -130,7 +130,7 @@ export default abstract class BasePlatform { if (MatrixClientPeg.userRegisteredWithinLastHours(24)) return false; try { - const [version, deferUntil] = JSON.parse(localStorage.getItem(UPDATE_DEFER_KEY)); + const [version, deferUntil] = JSON.parse(localStorage.getItem(UPDATE_DEFER_KEY)!); return newVersion !== version || Date.now() > deferUntil; } catch (e) { return true; @@ -211,7 +211,7 @@ export default abstract class BasePlatform { metricsTrigger: "Notification", }; - if (ev.getThread()) { + if (ev?.getThread()) { payload.event_id = ev.getId(); } @@ -255,7 +255,7 @@ export default abstract class BasePlatform { return false; } - public getSettingValue(settingName: string): Promise { + public async getSettingValue(settingName: string): Promise { return undefined; } @@ -278,7 +278,7 @@ export default abstract class BasePlatform { public setSpellCheckEnabled(enabled: boolean): void {} public async getSpellCheckEnabled(): Promise { - return null; + return false; } public setSpellCheckLanguages(preferredLangs: string[]): void {} @@ -333,7 +333,7 @@ export default abstract class BasePlatform { // persist hs url and is url for when the user is returned to the app with the login token localStorage.setItem(SSO_HOMESERVER_URL_KEY, mxClient.getHomeserverUrl()); if (mxClient.getIdentityServerUrl()) { - localStorage.setItem(SSO_ID_SERVER_URL_KEY, mxClient.getIdentityServerUrl()); + localStorage.setItem(SSO_ID_SERVER_URL_KEY, mxClient.getIdentityServerUrl()!); } if (idpId) { localStorage.setItem(SSO_IDP_ID_KEY, idpId); diff --git a/src/ContentMessages.ts b/src/ContentMessages.ts index 1381e9431ec..85ca90067d5 100644 --- a/src/ContentMessages.ts +++ b/src/ContentMessages.ts @@ -372,7 +372,7 @@ export default class ContentMessages { const replyToEvent = SdkContextClass.instance.roomViewStore.getQuotingEvent(); if (!this.mediaConfig) { // hot-path optimization to not flash a spinner if we don't need to - const modal = Modal.createDialog(Spinner, null, "mx_Dialog_spinner"); + const modal = Modal.createDialog(Spinner, undefined, "mx_Dialog_spinner"); await this.ensureMediaConfigFetched(matrixClient); modal.close(); } diff --git a/src/DecryptionFailureTracker.ts b/src/DecryptionFailureTracker.ts index c275a176eb7..7329c665bc2 100644 --- a/src/DecryptionFailureTracker.ts +++ b/src/DecryptionFailureTracker.ts @@ -83,8 +83,8 @@ export class DecryptionFailureTracker { public trackedEvents: Set = new Set(); // Set to an interval ID when `start` is called - public checkInterval: number = null; - public trackInterval: number = null; + public checkInterval: number | null = null; + public trackInterval: number | null = null; // Spread the load on `Analytics` by tracking at a low frequency, `TRACK_INTERVAL_MS`. public static TRACK_INTERVAL_MS = 60000; diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index be48717415f..eccfc1c0e3e 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -58,12 +58,12 @@ export default class DeviceListener { private dismissedThisDeviceToast = false; // cache of the key backup info private keyBackupInfo: IKeyBackupInfo | null = null; - private keyBackupFetchedAt: number = null; + private keyBackupFetchedAt: number | null = null; private keyBackupStatusChecked = false; // We keep a list of our own device IDs so we can batch ones that were already // there the last time the app launched into a single toast, but display new // ones in their own toasts. - private ourDeviceIdsAtStart: Set = null; + private ourDeviceIdsAtStart: Set | null = null; // The set of device IDs we're currently displaying toasts for private displayingToastsForDeviceIds = new Set(); private running = false; @@ -203,7 +203,7 @@ export default class DeviceListener { } }; - private onSync = (state: SyncState, prevState?: SyncState): void => { + private onSync = (state: SyncState, prevState: SyncState | null): void => { if (state === "PREPARED" && prevState === null) { this.recheck(); } diff --git a/src/ImageUtils.ts b/src/ImageUtils.ts index 42db71ebab4..e8564fb0172 100644 --- a/src/ImageUtils.ts +++ b/src/ImageUtils.ts @@ -28,7 +28,12 @@ limitations under the License. * consume in the timeline, when performing scroll offset calculations * (e.g. scroll locking) */ -export function thumbHeight(fullWidth: number, fullHeight: number, thumbWidth: number, thumbHeight: number): number { +export function thumbHeight( + fullWidth: number, + fullHeight: number, + thumbWidth: number, + thumbHeight: number, +): number | null { if (!fullWidth || !fullHeight) { // Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even // log this because it's spammy diff --git a/src/accessibility/KeyboardShortcutUtils.ts b/src/accessibility/KeyboardShortcutUtils.ts index 394daf7ef90..bb42f7c1ce9 100644 --- a/src/accessibility/KeyboardShortcutUtils.ts +++ b/src/accessibility/KeyboardShortcutUtils.ts @@ -72,7 +72,7 @@ const getUIOnlyShortcuts = (): IKeyboardShortcuts => { }, }; - if (PlatformPeg.get().overrideBrowserShortcuts()) { + if (PlatformPeg.get()?.overrideBrowserShortcuts()) { // XXX: This keyboard shortcut isn't manually added to // KeyBindingDefaults as it can't be easily handled by the // KeyBindingManager @@ -92,7 +92,7 @@ const getUIOnlyShortcuts = (): IKeyboardShortcuts => { * This function gets keyboard shortcuts that can be consumed by the KeyBindingDefaults. */ export const getKeyboardShortcuts = (): IKeyboardShortcuts => { - const overrideBrowserShortcuts = PlatformPeg.get().overrideBrowserShortcuts(); + const overrideBrowserShortcuts = PlatformPeg.get()?.overrideBrowserShortcuts(); return Object.keys(KEYBOARD_SHORTCUTS) .filter((k: KeyBindingAction) => { @@ -120,11 +120,11 @@ export const getKeyboardShortcutsForUI = (): IKeyboardShortcuts => { }, {} as IKeyboardShortcuts); }; -export const getKeyboardShortcutValue = (name: string): KeyCombo => { +export const getKeyboardShortcutValue = (name: string): KeyCombo | undefined => { return getKeyboardShortcutsForUI()[name]?.default; }; -export const getKeyboardShortcutDisplayName = (name: string): string | null => { +export const getKeyboardShortcutDisplayName = (name: string): string | undefined => { const keyboardShortcutDisplayName = getKeyboardShortcutsForUI()[name]?.displayName; return keyboardShortcutDisplayName && _t(keyboardShortcutDisplayName); }; diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index e90aed87a90..605ffb1f5b5 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -56,7 +56,7 @@ export function checkInputableElement(el: HTMLElement): boolean { } export interface IState { - activeRef: Ref; + activeRef?: Ref; refs: Ref[]; } @@ -67,7 +67,6 @@ interface IContext { export const RovingTabIndexContext = createContext({ state: { - activeRef: null, refs: [], // list of refs in DOM order }, dispatch: () => {}, @@ -102,7 +101,7 @@ export const reducer: Reducer = (state: IState, action: IAction return 0; } - const position = a.current.compareDocumentPosition(b.current); + const position = a.current!.compareDocumentPosition(b.current!); if (position & Node.DOCUMENT_POSITION_FOLLOWING || position & Node.DOCUMENT_POSITION_CONTAINED_BY) { return -1; @@ -167,7 +166,7 @@ export const findSiblingElement = ( refs: RefObject[], startIndex: number, backwards = false, -): RefObject => { +): RefObject | undefined => { if (backwards) { for (let i = startIndex; i < refs.length && i >= 0; i--) { if (refs[i].current?.offsetParent !== null) { @@ -191,7 +190,6 @@ export const RovingTabIndexProvider: React.FC = ({ onKeyDown, }) => { const [state, dispatch] = useReducer>(reducer, { - activeRef: null, refs: [], }); @@ -208,7 +206,7 @@ export const RovingTabIndexProvider: React.FC = ({ let handled = false; const action = getKeyBindingsManager().getAccessibilityAction(ev); - let focusRef: RefObject; + let focusRef: RefObject | undefined; // Don't interfere with input default keydown behaviour // but allow people to move focus from it with Tab. if (checkInputableElement(ev.target as HTMLElement)) { @@ -216,7 +214,7 @@ export const RovingTabIndexProvider: React.FC = ({ case KeyBindingAction.Tab: handled = true; if (context.state.refs.length > 0) { - const idx = context.state.refs.indexOf(context.state.activeRef); + const idx = context.state.refs.indexOf(context.state.activeRef!); focusRef = findSiblingElement( context.state.refs, idx + (ev.shiftKey ? -1 : 1), @@ -252,7 +250,7 @@ export const RovingTabIndexProvider: React.FC = ({ ) { handled = true; if (context.state.refs.length > 0) { - const idx = context.state.refs.indexOf(context.state.activeRef); + const idx = context.state.refs.indexOf(context.state.activeRef!); focusRef = findSiblingElement(context.state.refs, idx + 1); } } @@ -266,7 +264,7 @@ export const RovingTabIndexProvider: React.FC = ({ ) { handled = true; if (context.state.refs.length > 0) { - const idx = context.state.refs.indexOf(context.state.activeRef); + const idx = context.state.refs.indexOf(context.state.activeRef!); focusRef = findSiblingElement(context.state.refs, idx - 1, true); } } diff --git a/src/actions/MatrixActionCreators.ts b/src/actions/MatrixActionCreators.ts index 88de8b3d17f..bb40a463baa 100644 --- a/src/actions/MatrixActionCreators.ts +++ b/src/actions/MatrixActionCreators.ts @@ -221,7 +221,7 @@ function createRoomTimelineAction( action: "MatrixActions.Room.timeline", event: timelineEvent, isLiveEvent: data.liveEvent, - isLiveUnfilteredRoomTimelineEvent: room && data.timeline.getTimelineSet() === room.getUnfilteredTimelineSet(), + isLiveUnfilteredRoomTimelineEvent: data.timeline.getTimelineSet() === room?.getUnfilteredTimelineSet(), room, }; } diff --git a/src/audio/PlaybackQueue.ts b/src/audio/PlaybackQueue.ts index 7c521b9ca65..0828a3df1d8 100644 --- a/src/audio/PlaybackQueue.ts +++ b/src/audio/PlaybackQueue.ts @@ -45,7 +45,7 @@ export class PlaybackQueue { private playbacks = new Map(); // keyed by event ID private clockStates = new Map(); // keyed by event ID private playbackIdOrder: string[] = []; // event IDs, last == current - private currentPlaybackId: string; // event ID, broken out from above for ease of use + private currentPlaybackId: string | null = null; // event ID, broken out from above for ease of use private recentFullPlays = new Set(); // event IDs public constructor(private room: Room) { @@ -68,7 +68,7 @@ export class PlaybackQueue { const room = cli.getRoom(roomId); if (!room) throw new Error("Unknown room"); if (PlaybackQueue.queues.has(room.roomId)) { - return PlaybackQueue.queues.get(room.roomId); + return PlaybackQueue.queues.get(room.roomId)!; } const queue = new PlaybackQueue(room); PlaybackQueue.queues.set(room.roomId, queue); @@ -101,7 +101,7 @@ export class PlaybackQueue { const wasLastPlaying = this.currentPlaybackId === mxEvent.getId(); if (newState === PlaybackState.Stopped && this.clockStates.has(mxEvent.getId()) && !wasLastPlaying) { // noinspection JSIgnoredPromiseFromCall - playback.skipTo(this.clockStates.get(mxEvent.getId())); + playback.skipTo(this.clockStates.get(mxEvent.getId())!); } else if (newState === PlaybackState.Stopped) { // Remove the now-useless clock for some space savings this.clockStates.delete(mxEvent.getId()); diff --git a/src/autocomplete/AutocompleteProvider.tsx b/src/autocomplete/AutocompleteProvider.tsx index 546e052f583..32c2d80c6fc 100644 --- a/src/autocomplete/AutocompleteProvider.tsx +++ b/src/autocomplete/AutocompleteProvider.tsx @@ -70,7 +70,7 @@ export default abstract class AutocompleteProvider { * @param {boolean} force True if the user is forcing completion * @return {object} { command, range } where both objects fields are null if no match */ - public getCurrentCommand(query: string, selection: ISelectionRange, force = false): ICommand { + public getCurrentCommand(query: string, selection: ISelectionRange, force = false): ICommand | null { let commandRegex = this.commandRegex; if (force && this.shouldForceComplete()) { @@ -83,7 +83,7 @@ export default abstract class AutocompleteProvider { commandRegex.lastIndex = 0; - let match: RegExpExecArray; + let match: RegExpExecArray | null; while ((match = commandRegex.exec(query)) !== null) { const start = match.index; const end = start + match[0].length; diff --git a/src/autocomplete/Autocompleter.ts b/src/autocomplete/Autocompleter.ts index b609f265f13..c769faf1553 100644 --- a/src/autocomplete/Autocompleter.ts +++ b/src/autocomplete/Autocompleter.ts @@ -87,7 +87,7 @@ export default class Autocompleter { to predict whether an action will actually do what is intended */ // list of results from each provider, each being a list of completions or null if it times out - const completionsList: ICompletion[][] = await Promise.all( + const completionsList: Array = await Promise.all( this.providers.map(async (provider): Promise => { return timeout( provider.getCompletions(query, selection, force, limit), @@ -113,6 +113,6 @@ export default class Autocompleter { command: this.providers[i].getCurrentCommand(query, selection, force), }; }) - .filter(Boolean); + .filter(Boolean) as IProviderCompletions[]; } } diff --git a/src/autocomplete/CommandProvider.tsx b/src/autocomplete/CommandProvider.tsx index caafe98f088..113c9287901 100644 --- a/src/autocomplete/CommandProvider.tsx +++ b/src/autocomplete/CommandProvider.tsx @@ -56,10 +56,10 @@ export default class CommandProvider extends AutocompleteProvider { if (command[0] !== command[1]) { // The input looks like a command with arguments, perform exact match const name = command[1].slice(1); // strip leading `/` - if (CommandMap.has(name) && CommandMap.get(name).isEnabled()) { + if (CommandMap.has(name) && CommandMap.get(name)!.isEnabled()) { // some commands, namely `me` don't suit having the usage shown whilst typing their arguments - if (CommandMap.get(name).hideCompletionAfterSpace) return []; - matches = [CommandMap.get(name)]; + if (CommandMap.get(name)!.hideCompletionAfterSpace) return []; + matches = [CommandMap.get(name)!]; } } else { if (query === "/") { diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx index bf102d55bc0..9cde5013650 100644 --- a/src/autocomplete/RoomProvider.tsx +++ b/src/autocomplete/RoomProvider.tsx @@ -39,12 +39,12 @@ function canonicalScore(displayedAlias: string, room: Room): number { function matcherObject( room: Room, - displayedAlias: string, + displayedAlias: string | null, matchName = "", ): { room: Room; matchName: string; - displayedAlias: string; + displayedAlias: string | null; } { return { room, @@ -58,7 +58,7 @@ export default class RoomProvider extends AutocompleteProvider { public constructor(room: Room, renderingType?: TimelineRenderingType) { super({ commandRegex: ROOM_REGEX, renderingType }); - this.matcher = new QueryMatcher([], { + this.matcher = new QueryMatcher>([], { keys: ["displayedAlias", "matchName"], }); } @@ -79,7 +79,7 @@ export default class RoomProvider extends AutocompleteProvider { const { command, range } = this.getCurrentCommand(query, selection, force); if (command) { // the only reason we need to do this is because Fuse only matches on properties - let matcherObjects = this.getRooms().reduce((aliases, room) => { + let matcherObjects = this.getRooms().reduce[]>((aliases, room) => { if (room.getCanonicalAlias()) { aliases = aliases.concat(matcherObject(room, room.getCanonicalAlias(), room.name)); } diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 44424619a75..cc07f28e4a1 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -648,7 +648,7 @@ export default class MatrixChat extends React.PureComponent { onFinished: (confirm) => { if (confirm) { // FIXME: controller shouldn't be loading a view :( - const modal = Modal.createDialog(Spinner, null, "mx_Dialog_spinner"); + const modal = Modal.createDialog(Spinner, undefined, "mx_Dialog_spinner"); MatrixClientPeg.get() .leave(payload.room_id) diff --git a/src/components/views/messages/MKeyVerificationConclusion.tsx b/src/components/views/messages/MKeyVerificationConclusion.tsx index 656745997c8..24b925544e3 100644 --- a/src/components/views/messages/MKeyVerificationConclusion.tsx +++ b/src/components/views/messages/MKeyVerificationConclusion.tsx @@ -72,7 +72,7 @@ export default class MKeyVerificationConclusion extends React.Component this.forceUpdate(); }; - public static shouldRender(mxEvent: MatrixEvent, request: VerificationRequest): boolean { + public static shouldRender(mxEvent: MatrixEvent, request?: VerificationRequest): boolean { // normally should not happen if (!request) { return false; @@ -99,9 +99,9 @@ export default class MKeyVerificationConclusion extends React.Component return true; } - public render(): JSX.Element { + public render(): JSX.Element | null { const { mxEvent } = this.props; - const request = mxEvent.verificationRequest; + const request = mxEvent.verificationRequest!; if (!MKeyVerificationConclusion.shouldRender(mxEvent, request)) { return null; @@ -110,7 +110,7 @@ export default class MKeyVerificationConclusion extends React.Component const client = MatrixClientPeg.get(); const myUserId = client.getUserId(); - let title; + let title: string | undefined; if (request.done) { title = _t("You verified %(name)s", { diff --git a/src/createRoom.ts b/src/createRoom.ts index 2bd49a4bb67..f4d9769d90e 100644 --- a/src/createRoom.ts +++ b/src/createRoom.ts @@ -268,7 +268,7 @@ export default async function createRoom(opts: IOpts): Promise { } let modal; - if (opts.spinner) modal = Modal.createDialog(Spinner, null, "mx_Dialog_spinner"); + if (opts.spinner) modal = Modal.createDialog(Spinner, undefined, "mx_Dialog_spinner"); let roomId: string; let room: Promise; @@ -417,9 +417,9 @@ export async function ensureVirtualRoomExists( client: MatrixClient, userId: string, nativeRoomId: string, -): Promise { +): Promise { const existingDMRoom = findDMForUser(client, userId); - let roomId; + let roomId: string | null; if (existingDMRoom) { roomId = existingDMRoom.roomId; } else { @@ -440,13 +440,13 @@ export async function ensureVirtualRoomExists( return roomId; } -export async function ensureDMExists(client: MatrixClient, userId: string): Promise { +export async function ensureDMExists(client: MatrixClient, userId: string): Promise { const existingDMRoom = findDMForUser(client, userId); - let roomId; + let roomId: string | null; if (existingDMRoom) { roomId = existingDMRoom.roomId; } else { - let encryption: boolean = undefined; + let encryption: boolean | undefined; if (privateShouldBeEncrypted()) { encryption = await canEncryptToAllUsers(client, [userId]); } diff --git a/src/customisations/Media.ts b/src/customisations/Media.ts index 7c9ca139a2c..c3debef86f7 100644 --- a/src/customisations/Media.ts +++ b/src/customisations/Media.ts @@ -75,7 +75,7 @@ export class Media { /** * The HTTP URL for the source media. */ - public get srcHttp(): string { + public get srcHttp(): string | null { // eslint-disable-next-line no-restricted-properties return this.client.mxcUrlToHttp(this.srcMxc); } @@ -87,7 +87,7 @@ export class Media { public get thumbnailHttp(): string | undefined | null { if (!this.hasThumbnail) return null; // eslint-disable-next-line no-restricted-properties - return this.client.mxcUrlToHttp(this.thumbnailMxc); + return this.client.mxcUrlToHttp(this.thumbnailMxc!); } /** @@ -98,13 +98,13 @@ export class Media { * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale. * @returns {string} The HTTP URL which points to the thumbnail. */ - public getThumbnailHttp(width: number, height: number, mode: ResizeMethod = "scale"): string | null | undefined { + public getThumbnailHttp(width: number, height: number, mode: ResizeMethod = "scale"): string | null { if (!this.hasThumbnail) return null; // scale using the device pixel ratio to keep images clear width = Math.floor(width * window.devicePixelRatio); height = Math.floor(height * window.devicePixelRatio); // eslint-disable-next-line no-restricted-properties - return this.client.mxcUrlToHttp(this.thumbnailMxc, width, height, mode); + return this.client.mxcUrlToHttp(this.thumbnailMxc!, width, height, mode); } /** @@ -114,7 +114,7 @@ export class Media { * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale. * @returns {string} The HTTP URL which points to the thumbnail. */ - public getThumbnailOfSourceHttp(width: number, height: number, mode: ResizeMethod = "scale"): string { + public getThumbnailOfSourceHttp(width: number, height: number, mode: ResizeMethod = "scale"): string | null { // scale using the device pixel ratio to keep images clear width = Math.floor(width * window.devicePixelRatio); height = Math.floor(height * window.devicePixelRatio); @@ -128,7 +128,7 @@ export class Media { * @param {number} dim The desired width and height. * @returns {string} An HTTP URL for the thumbnail. */ - public getSquareThumbnailHttp(dim: number): string { + public getSquareThumbnailHttp(dim: number): string | null { dim = Math.floor(dim * window.devicePixelRatio); // scale using the device pixel ratio to keep images clear if (this.hasThumbnail) { return this.getThumbnailHttp(dim, dim, "crop"); @@ -161,6 +161,6 @@ export function mediaFromContent(content: Partial, client?: * @param {MatrixClient} client? Optional client to use. * @returns {Media} The media object. */ -export function mediaFromMxc(mxc: string, client?: MatrixClient): Media { +export function mediaFromMxc(mxc?: string, client?: MatrixClient): Media { return mediaFromContent({ url: mxc }, client); } diff --git a/src/customisations/Security.ts b/src/customisations/Security.ts index bff7fc19f6a..bfc2caadddd 100644 --- a/src/customisations/Security.ts +++ b/src/customisations/Security.ts @@ -30,19 +30,19 @@ function persistCredentials(credentials: IMatrixClientCreds): void { } /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -function createSecretStorageKey(): Uint8Array { +function createSecretStorageKey(): Uint8Array | null { // E.g. generate or retrieve secret storage key somehow return null; } /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -function getSecretStorageKey(): Uint8Array { +function getSecretStorageKey(): Uint8Array | null { // E.g. retrieve secret storage key from some other place return null; } /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -function getDehydrationKey(keyInfo: ISecretStorageKeyInfo): Promise { +function getDehydrationKey(keyInfo: ISecretStorageKeyInfo): Promise { return Promise.resolve(null); } diff --git a/src/customisations/models/IMediaEventContent.ts b/src/customisations/models/IMediaEventContent.ts index 299432eaaaa..bd17ba2204c 100644 --- a/src/customisations/models/IMediaEventContent.ts +++ b/src/customisations/models/IMediaEventContent.ts @@ -72,7 +72,7 @@ export interface IMediaObject { * @throws Throws if the given content cannot be packaged into a prepared media object. */ export function prepEventContentAsMedia(content: Partial): IPreparedMedia { - let thumbnail: IMediaObject = null; + let thumbnail: IMediaObject | undefined; if (content?.info?.thumbnail_url) { thumbnail = { mxc: content.info.thumbnail_url, diff --git a/src/editor/caret.ts b/src/editor/caret.ts index ea6777c0579..61c6a856400 100644 --- a/src/editor/caret.ts +++ b/src/editor/caret.ts @@ -32,7 +32,7 @@ export function setSelection(editor: HTMLDivElement, model: EditorModel, selecti } function setDocumentRangeSelection(editor: HTMLDivElement, model: EditorModel, range: Range): void { - const sel = document.getSelection(); + const sel = document.getSelection()!; sel.removeAllRanges(); const selectionRange = document.createRange(); const start = getNodeAndOffsetForPosition(editor, model, range.start); @@ -50,7 +50,7 @@ export function setCaretPosition(editor: HTMLDivElement, model: EditorModel, car range.setStart(node, offset); range.collapse(true); - const sel = document.getSelection(); + const sel = document.getSelection()!; if (sel.rangeCount === 1) { const existingRange = sel.getRangeAt(0); if ( @@ -124,7 +124,7 @@ function findNodeInLineForPart(parts: Part[], partIndex: number): { lineIndex: n let lineIndex = 0; let nodeIndex = -1; - let prevPart = null; + let prevPart: Part | undefined; // go through to parts up till (and including) the index // to find newline parts for (let i = 0; i <= partIndex; ++i) { @@ -132,7 +132,7 @@ function findNodeInLineForPart(parts: Part[], partIndex: number): { lineIndex: n if (part.type === Type.Newline) { lineIndex += 1; nodeIndex = -1; - prevPart = null; + prevPart = undefined; } else { nodeIndex += 1; if (needsCaretNodeBefore(part, prevPart)) { diff --git a/src/editor/deserialize.ts b/src/editor/deserialize.ts index f8ecd19c160..d4d0de6ed8d 100644 --- a/src/editor/deserialize.ts +++ b/src/editor/deserialize.ts @@ -51,7 +51,7 @@ export function longestBacktickSequence(text: string): number { } function isListChild(n: Node): boolean { - return LIST_TYPES.includes(n.parentNode?.nodeName); + return LIST_TYPES.includes(n.parentNode?.nodeName || ""); } function parseAtRoomMentions(text: string, pc: PartCreator, opts: IParseOptions): Part[] { diff --git a/src/editor/history.ts b/src/editor/history.ts index dd9bd19b26e..2f0260025dc 100644 --- a/src/editor/history.ts +++ b/src/editor/history.ts @@ -31,7 +31,7 @@ export default class HistoryManager { private newlyTypedCharCount = 0; private currentIndex = -1; private changedSinceLastPush = false; - private lastCaret: Caret = null; + private lastCaret: Caret | null = null; private nonWordBoundarySinceLastPush = false; private addedSinceLastPush = false; private removedSinceLastPush = false; @@ -65,7 +65,7 @@ export default class HistoryManager { // as long as you've only been adding or removing since the last push if (this.addedSinceLastPush !== this.removedSinceLastPush) { // add steps by word boundary, up to MAX_STEP_LENGTH characters - const str = diff.added ? diff.added : diff.removed; + const str = diff.added ? diff.added : diff.removed!; const isWordBoundary = str === " " || str === "\t" || str === "\n"; if (this.nonWordBoundarySinceLastPush && isWordBoundary) { return true; diff --git a/src/editor/model.ts b/src/editor/model.ts index 4ec76bf0082..f4c59e36653 100644 --- a/src/editor/model.ts +++ b/src/editor/model.ts @@ -51,13 +51,13 @@ type ManualTransformCallback = () => Caret; export default class EditorModel { private _parts: Part[]; private readonly _partCreator: PartCreator; - private activePartIdx: number = null; - private _autoComplete: AutocompleteWrapperModel = null; - private autoCompletePartIdx: number = null; + private activePartIdx: number | null = null; + private _autoComplete: AutocompleteWrapperModel | null = null; + private autoCompletePartIdx: number | null = null; private autoCompletePartCount = 0; - private transformCallback: TransformCallback = null; + private transformCallback: TransformCallback | null = null; - public constructor(parts: Part[], partCreator: PartCreator, private updateCallback: UpdateCallback = null) { + public constructor(parts: Part[], partCreator: PartCreator, private updateCallback: UpdateCallback | null = null) { this._parts = parts; this._partCreator = partCreator; this.transformCallback = null; diff --git a/src/editor/parts.ts b/src/editor/parts.ts index 2c8596b492e..8202f765d33 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -455,7 +455,7 @@ class AtRoomPillPart extends RoomPillPart { } class UserPillPart extends PillPart { - public constructor(userId, displayName, private member: RoomMember) { + public constructor(userId, displayName, private member?: RoomMember) { super(userId, displayName); } @@ -625,7 +625,7 @@ export class PartCreator { public userPill(displayName: string, userId: string): UserPillPart { const member = this.room.getMember(userId); - return new UserPillPart(userId, displayName, member); + return new UserPillPart(userId, displayName, member || undefined); } private static isRegionalIndicator(c: string): boolean { diff --git a/src/editor/position.ts b/src/editor/position.ts index e2a08752b22..3f23016a8a6 100644 --- a/src/editor/position.ts +++ b/src/editor/position.ts @@ -79,6 +79,8 @@ export default class DocumentPosition implements IPosition { offset = 0; } } + + return this; // impossible but Typescript doesn't believe us } public backwardsWhile(model: EditorModel, predicate: Predicate): DocumentPosition { @@ -104,6 +106,8 @@ export default class DocumentPosition implements IPosition { offset = parts[index].text.length; } } + + return this; // impossible but Typescript doesn't believe us } public asOffset(model: EditorModel): DocumentOffset { diff --git a/src/editor/render.ts b/src/editor/render.ts index 317218cd8a7..bb7b0887805 100644 --- a/src/editor/render.ts +++ b/src/editor/render.ts @@ -18,7 +18,7 @@ limitations under the License. import { Part, Type } from "./parts"; import EditorModel from "./model"; -export function needsCaretNodeBefore(part: Part, prevPart: Part): boolean { +export function needsCaretNodeBefore(part: Part, prevPart?: Part): boolean { const isFirst = !prevPart || prevPart.type === Type.Newline; return !part.acceptsCaret && (isFirst || !prevPart.acceptsCaret); } @@ -30,9 +30,9 @@ export function needsCaretNodeAfter(part: Part, isLastOfLine: boolean): boolean function insertAfter(node: HTMLElement, nodeToInsert: HTMLElement): void { const next = node.nextSibling; if (next) { - node.parentElement.insertBefore(nodeToInsert, next); + node.parentElement!.insertBefore(nodeToInsert, next); } else { - node.parentElement.appendChild(nodeToInsert); + node.parentElement!.appendChild(nodeToInsert); } } @@ -58,11 +58,11 @@ function updateCaretNode(node: HTMLElement): void { } } -export function isCaretNode(node: HTMLElement): boolean { - return node && node.tagName === "SPAN" && node.className === "caretNode"; +export function isCaretNode(node?: Node | null): node is HTMLElement { + return !!node && node instanceof HTMLElement && node.tagName === "SPAN" && node.className === "caretNode"; } -function removeNextSiblings(node: ChildNode): void { +function removeNextSiblings(node: ChildNode | null): void { if (!node) { return; } @@ -83,13 +83,13 @@ function removeChildren(parent: HTMLElement): void { } function reconcileLine(lineContainer: ChildNode, parts: Part[]): void { - let currentNode; - let prevPart; + let currentNode: ChildNode | null = null; + let prevPart: Part | undefined; const lastPart = parts[parts.length - 1]; for (const part of parts) { const isFirst = !prevPart; - currentNode = isFirst ? lineContainer.firstChild : currentNode.nextSibling; + currentNode = isFirst ? lineContainer.firstChild : currentNode!.nextSibling; if (needsCaretNodeBefore(part, prevPart)) { if (isCaretNode(currentNode)) { @@ -109,18 +109,18 @@ function reconcileLine(lineContainer: ChildNode, parts: Part[]): void { if (currentNode && part) { part.updateDOMNode(currentNode); } else if (part) { - currentNode = part.toDOMNode(); + currentNode = part.toDOMNode() as ChildNode; // hooks up nextSibling for next iteration lineContainer.appendChild(currentNode); } if (needsCaretNodeAfter(part, part === lastPart)) { - if (isCaretNode(currentNode.nextSibling)) { - currentNode = currentNode.nextSibling; - updateCaretNode(currentNode); + if (isCaretNode(currentNode?.nextSibling)) { + currentNode = currentNode!.nextSibling; + updateCaretNode(currentNode as HTMLElement); } else { const caretNode = createCaretNode(); - insertAfter(currentNode, caretNode); + insertAfter(currentNode as HTMLElement, caretNode); currentNode = caretNode; } } @@ -150,7 +150,7 @@ function reconcileEmptyLine(lineContainer: HTMLElement): void { } export function renderModel(editor: HTMLDivElement, model: EditorModel): void { - const lines = model.parts.reduce( + const lines = model.parts.reduce( (linesArr, part) => { if (part.type === Type.Newline) { linesArr.push([]); diff --git a/src/stores/OwnProfileStore.ts b/src/stores/OwnProfileStore.ts index 1aad5fac9a8..1ad90b9ee40 100644 --- a/src/stores/OwnProfileStore.ts +++ b/src/stores/OwnProfileStore.ts @@ -50,8 +50,8 @@ export class OwnProfileStore extends AsyncStoreWithClient { // round-trip after the client is ready, and we often load widgets in that time, and we'd // and up passing them an incorrect display name super(defaultDispatcher, { - displayName: window.localStorage.getItem(KEY_DISPLAY_NAME), - avatarUrl: window.localStorage.getItem(KEY_AVATAR_URL), + displayName: window.localStorage.getItem(KEY_DISPLAY_NAME) || undefined, + avatarUrl: window.localStorage.getItem(KEY_AVATAR_URL) || undefined, }); } diff --git a/src/stores/ReadyWatchingStore.ts b/src/stores/ReadyWatchingStore.ts index a233a83ea3f..24117bab374 100644 --- a/src/stores/ReadyWatchingStore.ts +++ b/src/stores/ReadyWatchingStore.ts @@ -25,7 +25,7 @@ import { IDestroyable } from "../utils/IDestroyable"; import { Action } from "../dispatcher/actions"; export abstract class ReadyWatchingStore extends EventEmitter implements IDestroyable { - protected matrixClient: MatrixClient; + protected matrixClient: MatrixClient | null = null; private dispatcherRef: string | null = null; public constructor(protected readonly dispatcher: Dispatcher) { @@ -42,7 +42,7 @@ export abstract class ReadyWatchingStore extends EventEmitter implements IDestro } } - public get mxClient(): MatrixClient { + public get mxClient(): MatrixClient | null { return this.matrixClient; // for external readonly access } diff --git a/src/stores/UIStore.ts b/src/stores/UIStore.ts index f7a6e76d0b9..b1cdbae9517 100644 --- a/src/stores/UIStore.ts +++ b/src/stores/UIStore.ts @@ -21,7 +21,7 @@ export enum UI_EVENTS { } export default class UIStore extends EventEmitter { - private static _instance: UIStore = null; + private static _instance: UIStore | null = null; private resizeObserver: ResizeObserver; @@ -58,7 +58,7 @@ export default class UIStore extends EventEmitter { } } - public getElementDimensions(name: string): DOMRectReadOnly { + public getElementDimensions(name: string): DOMRectReadOnly | undefined { return this.uiElementDimensions.get(name); } @@ -68,7 +68,7 @@ export default class UIStore extends EventEmitter { } public stopTrackingElementDimensions(name: string): void { - let trackedElement: Element; + let trackedElement: Element | undefined; this.trackedUiElements.forEach((trackedElementName, element) => { if (trackedElementName === name) { trackedElement = element; diff --git a/src/stores/spaces/flattenSpaceHierarchy.ts b/src/stores/spaces/flattenSpaceHierarchy.ts index 0fc6e4fd31f..758681a60d8 100644 --- a/src/stores/spaces/flattenSpaceHierarchy.ts +++ b/src/stores/spaces/flattenSpaceHierarchy.ts @@ -66,7 +66,7 @@ export const flattenSpaceHierarchyWithCache = useCache = true, ): Set => { if (useCache && cache.has(spaceId)) { - return cache.get(spaceId); + return cache.get(spaceId)!; } const result = flattenSpaceHierarchy(spaceEntityMap, spaceDescendantMap, spaceId); cache.set(spaceId, result); diff --git a/src/toasts/UpdateToast.tsx b/src/toasts/UpdateToast.tsx index c14c57ea791..2783e169432 100644 --- a/src/toasts/UpdateToast.tsx +++ b/src/toasts/UpdateToast.tsx @@ -37,12 +37,12 @@ function checkVersion(ver: string): boolean { } function installUpdate(): void { - PlatformPeg.get().installUpdate(); + PlatformPeg.get()?.installUpdate(); } export const showToast = (version: string, newVersion: string, releaseNotes?: string): void => { function onReject(): void { - PlatformPeg.get().deferUpdate(newVersion); + PlatformPeg.get()?.deferUpdate(newVersion); } let onAccept; @@ -55,7 +55,7 @@ export const showToast = (version: string, newVersion: string, releaseNotes?: st button: _t("Update"), onFinished: (update) => { if (update && PlatformPeg.get()) { - PlatformPeg.get().installUpdate(); + PlatformPeg.get()!.installUpdate(); } }, }); @@ -67,7 +67,7 @@ export const showToast = (version: string, newVersion: string, releaseNotes?: st newVersion, onFinished: (update) => { if (update && PlatformPeg.get()) { - PlatformPeg.get().installUpdate(); + PlatformPeg.get()!.installUpdate(); } }, }); diff --git a/src/utils/DialogOpener.ts b/src/utils/DialogOpener.ts index ac38eb50182..0121fb7c180 100644 --- a/src/utils/DialogOpener.ts +++ b/src/utils/DialogOpener.ts @@ -62,7 +62,7 @@ export class DialogOpener { roomId: payload.room_id || SdkContextClass.instance.roomViewStore.getRoomId(), initialTabId: payload.initial_tab_id, }, - /*className=*/ null, + /*className=*/ undefined, /*isPriority=*/ false, /*isStatic=*/ true, ); @@ -90,7 +90,7 @@ export class DialogOpener { initialTabId: payload.initalTabId, space: payload.space, }, - null, + undefined, false, true, ); @@ -102,7 +102,7 @@ export class DialogOpener { matrixClient: payload.space.client, space: payload.space, }, - /*className=*/ null, + /*className=*/ undefined, /*isPriority=*/ false, /*isStatic=*/ true, ); diff --git a/src/utils/RoomUpgrade.ts b/src/utils/RoomUpgrade.ts index d534c12e2f3..669a8e8bfe6 100644 --- a/src/utils/RoomUpgrade.ts +++ b/src/utils/RoomUpgrade.ts @@ -62,9 +62,9 @@ export async function upgradeRoom( progressCallback?: (progress: IProgress) => void, ): Promise { const cli = room.client; - let spinnerModal: IHandle; + let spinnerModal: IHandle | undefined; if (!progressCallback) { - spinnerModal = Modal.createDialog(Spinner, null, "mx_Dialog_spinner"); + spinnerModal = Modal.createDialog(Spinner, undefined, "mx_Dialog_spinner"); } let toInvite: string[] = []; @@ -78,7 +78,9 @@ export async function upgradeRoom( if (updateSpaces) { parentsToRelink = Array.from(SpaceStore.instance.getKnownParents(room.roomId)) .map((roomId) => cli.getRoom(roomId)) - .filter((parent) => parent?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId())); + .filter((parent) => + parent?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()!), + ) as Room[]; } const progress: IProgress = { @@ -117,7 +119,7 @@ export async function upgradeRoom( if (toInvite.length > 0) { // Errors are handled internally to this function await inviteUsersToRoom(newRoomId, toInvite, false, () => { - progress.inviteUsersProgress++; + progress.inviteUsersProgress!++; progressCallback?.(progress); }); } @@ -137,7 +139,7 @@ export async function upgradeRoom( ); await cli.sendStateEvent(parent.roomId, EventType.SpaceChild, {}, room.roomId); - progress.updateSpacesProgress++; + progress.updateSpacesProgress!++; progressCallback?.(progress); } } catch (e) { diff --git a/src/utils/ShieldUtils.ts b/src/utils/ShieldUtils.ts index 6bf57801be7..089cf3feeb7 100644 --- a/src/utils/ShieldUtils.ts +++ b/src/utils/ShieldUtils.ts @@ -51,7 +51,7 @@ export async function shieldStatusForRoom(client: MatrixClient, room: Room): Pro !inDMMap && // Don't alarm for self in DMs with other users members.length !== 2) || // Don't alarm for self in 1:1 chats with other users members.length === 1; // Do alarm for self if we're alone in a room - const targets = includeUser ? [...verified, client.getUserId()] : verified; + const targets = includeUser ? [...verified, client.getUserId()!] : verified; for (const userId of targets) { const devices = client.getStoredDevicesForUser(userId); const anyDeviceNotVerified = devices.some(({ deviceId }) => { diff --git a/src/utils/StorageManager.ts b/src/utils/StorageManager.ts index 5bb8861311a..6cab834855a 100644 --- a/src/utils/StorageManager.ts +++ b/src/utils/StorageManager.ts @@ -181,7 +181,7 @@ export function setCryptoInitialised(cryptoInited: boolean): void { /* Simple wrapper functions around IndexedDB. */ -let idb: IDBDatabase = null; +let idb: IDBDatabase | null = null; async function idbInit(): Promise { if (!indexedDB) { @@ -206,7 +206,7 @@ export async function idbLoad(table: string, key: string | string[]): Promise { - const txn = idb.transaction([table], "readonly"); + const txn = idb!.transaction([table], "readonly"); txn.onerror = reject; const objectStore = txn.objectStore(table); @@ -223,7 +223,7 @@ export async function idbSave(table: string, key: string | string[], data: any): await idbInit(); } return new Promise((resolve, reject) => { - const txn = idb.transaction([table], "readwrite"); + const txn = idb!.transaction([table], "readwrite"); txn.onerror = reject; const objectStore = txn.objectStore(table); @@ -240,7 +240,7 @@ export async function idbDelete(table: string, key: string | string[]): Promise< await idbInit(); } return new Promise((resolve, reject) => { - const txn = idb.transaction([table], "readwrite"); + const txn = idb!.transaction([table], "readwrite"); txn.onerror = reject; const objectStore = txn.objectStore(table); diff --git a/src/utils/Timer.ts b/src/utils/Timer.ts index 1038aed4f94..975a498e962 100644 --- a/src/utils/Timer.ts +++ b/src/utils/Timer.ts @@ -26,8 +26,8 @@ Once a timer is finished or aborted, it can't be started again a new one through `clone()` or `cloneIfRun()`. */ export default class Timer { - private timerHandle: number; - private startTs: number; + private timerHandle?: number; + private startTs?: number; private promise: Promise; private resolve: () => void; private reject: (Error) => void; @@ -37,19 +37,19 @@ export default class Timer { } private setNotStarted(): void { - this.timerHandle = null; - this.startTs = null; + this.timerHandle = undefined; + this.startTs = undefined; this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }).finally(() => { - this.timerHandle = null; + this.timerHandle = undefined; }); } private onTimeout = (): void => { const now = Date.now(); - const elapsed = now - this.startTs; + const elapsed = now - this.startTs!; if (elapsed >= this.timeout) { this.resolve(); this.setNotStarted(); @@ -124,6 +124,6 @@ export default class Timer { } public isRunning(): boolean { - return this.timerHandle !== null; + return this.timerHandle !== undefined; } } diff --git a/src/utils/UrlUtils.ts b/src/utils/UrlUtils.ts index bc185b20567..9c4b81ad3e3 100644 --- a/src/utils/UrlUtils.ts +++ b/src/utils/UrlUtils.ts @@ -31,7 +31,7 @@ export function abbreviateUrl(u: string): string { if (parsedUrl.path === "/") { // we ignore query / hash parts: these aren't relevant for IS server URLs - return parsedUrl.host; + return parsedUrl.host || ""; } return u; diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts index 09cc1de3b9d..d5436c601c0 100644 --- a/src/utils/arrays.ts +++ b/src/utils/arrays.ts @@ -266,7 +266,7 @@ export class ArrayUtil { const obj = this.a.reduce((rv: Map, val: T) => { const k = fn(val); if (!rv.has(k)) rv.set(k, []); - rv.get(k).push(val); + rv.get(k)!.push(val); return rv; }, new Map()); return new GroupedArray(obj); @@ -299,7 +299,7 @@ export class GroupedArray { const a: T[] = []; for (const k of keyOrder) { if (!this.val.has(k)) continue; - a.push(...this.val.get(k)); + a.push(...this.val.get(k)!); } return new ArrayUtil(a); } diff --git a/src/utils/leave-behaviour.ts b/src/utils/leave-behaviour.ts index deaa9f92ae5..11047c5fb79 100644 --- a/src/utils/leave-behaviour.ts +++ b/src/utils/leave-behaviour.ts @@ -39,7 +39,7 @@ import { SdkContextClass } from "../contexts/SDKContext"; export async function leaveRoomBehaviour(roomId: string, retry = true, spinner = true): Promise { let spinnerModal: IHandle; if (spinner) { - spinnerModal = Modal.createDialog(Spinner, null, "mx_Dialog_spinner"); + spinnerModal = Modal.createDialog(Spinner, undefined, "mx_Dialog_spinner"); } const cli = MatrixClientPeg.get(); diff --git a/src/utils/permalinks/Permalinks.ts b/src/utils/permalinks/Permalinks.ts index e5ed070c037..eb72a873622 100644 --- a/src/utils/permalinks/Permalinks.ts +++ b/src/utils/permalinks/Permalinks.ts @@ -79,14 +79,13 @@ const ANY_REGEX = /.*/; // the list and magically have the link work. export class RoomPermalinkCreator { - private room: Room; private roomId: string; - private highestPlUserId: string; - private populationMap: { [serverName: string]: number }; - private bannedHostsRegexps: RegExp[]; - private allowedHostsRegexps: RegExp[]; - private _serverCandidates: string[]; - private started: boolean; + private highestPlUserId: string | null = null; + private populationMap: { [serverName: string]: number } | null = null; + private bannedHostsRegexps: RegExp[] | null = null; + private allowedHostsRegexps: RegExp[] | null = null; + private _serverCandidates: string[] | null = null; + private started = false; // We support being given a roomId as a fallback in the event the `room` object // doesn't exist or is not healthy for us to rely on. For example, loading a @@ -94,15 +93,8 @@ export class RoomPermalinkCreator { // Some of the tests done by this class are relatively expensive, so normally // throttled to not happen on every update. Pass false as the shouldThrottle // param to disable this behaviour, eg. for tests. - public constructor(room: Room, roomId: string | null = null, shouldThrottle = true) { - this.room = room; - this.roomId = room ? room.roomId : roomId; - this.highestPlUserId = null; - this.populationMap = null; - this.bannedHostsRegexps = null; - this.allowedHostsRegexps = null; - this._serverCandidates = null; - this.started = false; + public constructor(private room: Room, roomId: string | null = null, shouldThrottle = true) { + this.roomId = room ? room.roomId : roomId!; if (!this.roomId) { throw new Error("Failed to resolve a roomId for the permalink creator to use"); @@ -316,7 +308,7 @@ export function isPermalinkHost(host: string): boolean { * @param {string} entity The entity to transform. * @returns {string|null} The transformed permalink or null if unable. */ -export function tryTransformEntityToPermalink(entity: string): string { +export function tryTransformEntityToPermalink(entity: string): string | null { if (!entity) return null; // Check to see if it is a bare entity for starters @@ -391,7 +383,7 @@ export function tryTransformPermalinkToLocalHref(permalink: string): string { return permalink; } -export function getPrimaryPermalinkEntity(permalink: string): string { +export function getPrimaryPermalinkEntity(permalink: string): string | null { try { let permalinkParts = parsePermalink(permalink); @@ -425,7 +417,7 @@ function getPermalinkConstructor(): PermalinkConstructor { return new MatrixToPermalinkConstructor(); } -export function parsePermalink(fullUrl: string): PermalinkParts { +export function parsePermalink(fullUrl: string): PermalinkParts | null { try { const elementPrefix = SdkConfig.get("permalink_prefix"); if (decodeURIComponent(fullUrl).startsWith(matrixtoBaseUrl)) { diff --git a/src/utils/space.tsx b/src/utils/space.tsx index fe062a080c4..fc183a3a7d8 100644 --- a/src/utils/space.tsx +++ b/src/utils/space.tsx @@ -41,7 +41,7 @@ import { OpenAddExistingToSpaceDialogPayload } from "../dispatcher/payloads/Open import { SdkContextClass } from "../contexts/SDKContext"; export const shouldShowSpaceSettings = (space: Room): boolean => { - const userId = space.client.getUserId(); + const userId = space.client.getUserId()!; return ( space.getMyMembership() === "join" && (space.currentState.maySendStateEvent(EventType.RoomAvatar, userId) || @@ -88,7 +88,7 @@ export const showCreateNewRoom = async (space: Room, type?: RoomType): Promise - ((space?.getMyMembership() === "join" && space.canInvite(space.client.getUserId())) || + ((space?.getMyMembership() === "join" && space.canInvite(space.client.getUserId()!)) || space.getJoinRule() === JoinRule.Public) && shouldShowComponent(UIComponent.InviteUsers); @@ -149,7 +149,7 @@ export const bulkSpaceBehaviour = async ( children: Room[], fn: (room: Room) => Promise, ): Promise => { - const modal = Modal.createDialog(Spinner, null, "mx_Dialog_spinner"); + const modal = Modal.createDialog(Spinner, undefined, "mx_Dialog_spinner"); try { for (const room of children) { await fn(room); diff --git a/src/utils/tooltipify.tsx b/src/utils/tooltipify.tsx index 3cf6ec50a04..e3280f7fe25 100644 --- a/src/utils/tooltipify.tsx +++ b/src/utils/tooltipify.tsx @@ -47,9 +47,9 @@ export function tooltipifyLinks(rootNodes: ArrayLike, ignoredNodes: Ele if ( node.tagName === "A" && node.getAttribute("href") && - node.getAttribute("href") !== node.textContent.trim() + node.getAttribute("href") !== node.textContent?.trim() ) { - let href = node.getAttribute("href"); + let href = node.getAttribute("href")!; try { href = new URL(href, window.location.href).toString(); } catch (e) { From f1a08cd572b7d2d38810792fa34c2b974343a1da Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Fri, 3 Feb 2023 16:36:37 +0100 Subject: [PATCH 77/97] Gitter sunsetting: Use findPredecessor in EventTileFactory (#10075) --- cypress/e2e/timeline/timeline.spec.ts | 2 +- src/events/EventTileFactory.tsx | 19 ++- .../structures/MessagePanel-test.tsx | 11 +- test/events/EventTileFactory-test.ts | 130 +++++++++++++++++- 4 files changed, 153 insertions(+), 9 deletions(-) diff --git a/cypress/e2e/timeline/timeline.spec.ts b/cypress/e2e/timeline/timeline.spec.ts index f4a861ab7cd..bef1cd0393c 100644 --- a/cypress/e2e/timeline/timeline.spec.ts +++ b/cypress/e2e/timeline/timeline.spec.ts @@ -250,7 +250,7 @@ describe("Timeline", () => { 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") + cy.get(".mx_EventTile:not(:first-child) .mx_ViewSourceEvent") .should("exist") .realHover() .within(() => { diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx index 9c28678e7dc..95e41395ac3 100644 --- a/src/events/EventTileFactory.tsx +++ b/src/events/EventTileFactory.tsx @@ -22,6 +22,7 @@ import { M_POLL_START } from "matrix-js-sdk/src/@types/polls"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { GroupCallIntent } from "matrix-js-sdk/src/webrtc/groupCall"; +import SettingsStore from "../settings/SettingsStore"; import EditorStateTransfer from "../utils/EditorStateTransfer"; import { RoomPermalinkCreator } from "../utils/permalinks/Permalinks"; import LegacyCallEventGrouper from "../components/structures/LegacyCallEventGrouper"; @@ -91,6 +92,7 @@ const HiddenEventFactory: Factory = (ref, props) => ; export const JSONEventFactory: Factory = (ref, props) => ; +export const RoomCreateEventFactory: Factory = (ref, props) => ; const EVENT_TILE_TYPES = new Map([ [EventType.RoomMessage, MessageEventFactory], // note that verification requests are handled in pickFactory() @@ -105,7 +107,7 @@ const EVENT_TILE_TYPES = new Map([ const STATE_EVENT_TILE_TYPES = new Map([ [EventType.RoomEncryption, (ref, props) => ], [EventType.RoomCanonicalAlias, TextualEventFactory], - [EventType.RoomCreate, (_ref, props) => ], + [EventType.RoomCreate, RoomCreateEventFactory], [EventType.RoomMember, TextualEventFactory], [EventType.RoomName, TextualEventFactory], [EventType.RoomAvatar, (ref, props) => ], @@ -213,6 +215,14 @@ export function pickFactory( } } + if (evType === EventType.RoomCreate) { + const dynamicPredecessorsEnabled = SettingsStore.getValue("feature_dynamic_room_predecessors"); + const predecessor = cli.getRoom(mxEvent.getRoomId())?.findPredecessor(dynamicPredecessorsEnabled); + if (!predecessor) { + return noEventFactoryFactory(); + } + } + // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) if (evType === "im.vector.modular.widgets") { let type = mxEvent.getContent()["type"]; @@ -415,12 +425,15 @@ export function haveRendererForEvent(mxEvent: MatrixEvent, showHiddenEvents: boo // No tile for replacement events since they update the original tile if (mxEvent.isRelation(RelationType.Replace)) return false; - const handler = pickFactory(mxEvent, MatrixClientPeg.get(), showHiddenEvents); + const cli = MatrixClientPeg.get(); + const handler = pickFactory(mxEvent, cli, showHiddenEvents); if (!handler) return false; if (handler === TextualEventFactory) { return hasText(mxEvent, showHiddenEvents); } else if (handler === STATE_EVENT_TILE_TYPES.get(EventType.RoomCreate)) { - return Boolean(mxEvent.getContent()["predecessor"]); + const dynamicPredecessorsEnabled = SettingsStore.getValue("feature_dynamic_room_predecessors"); + const predecessor = cli.getRoom(mxEvent.getRoomId())?.findPredecessor(dynamicPredecessorsEnabled); + return Boolean(predecessor); } else if ( ElementCall.CALL_EVENT_TYPE.names.some((eventType) => handler === STATE_EVENT_TILE_TYPES.get(eventType)) ) { diff --git a/test/components/structures/MessagePanel-test.tsx b/test/components/structures/MessagePanel-test.tsx index d3659ae302a..e05dc038fdd 100644 --- a/test/components/structures/MessagePanel-test.tsx +++ b/test/components/structures/MessagePanel-test.tsx @@ -37,6 +37,7 @@ import { } from "../../test-utils"; import ResizeNotifier from "../../../src/utils/ResizeNotifier"; import { IRoomState } from "../../../src/components/structures/RoomView"; +import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; jest.mock("../../../src/utils/beacon", () => ({ useBeacon: jest.fn(), @@ -58,6 +59,7 @@ describe("MessagePanel", function () { getRoom: jest.fn(), getClientWellKnown: jest.fn().mockReturnValue({}), }); + jest.spyOn(MatrixClientPeg, "get").mockReturnValue(client); const room = new Room(roomId, client, userId); @@ -464,11 +466,12 @@ describe("MessagePanel", function () { it("should collapse creation events", function () { const events = mkCreationEvents(); - TestUtilsMatrix.upsertRoomStateEvents(room, events); - const { container } = render(getComponent({ events })); - const createEvent = events.find((event) => event.getType() === "m.room.create"); const encryptionEvent = events.find((event) => event.getType() === "m.room.encryption"); + client.getRoom.mockImplementation((id) => (id === createEvent!.getRoomId() ? room : null)); + TestUtilsMatrix.upsertRoomStateEvents(room, events); + + const { container } = render(getComponent({ events })); // we expect that // - the room creation event, the room encryption event, and Alice inviting Bob, @@ -508,6 +511,8 @@ describe("MessagePanel", function () { it("should hide read-marker at the end of creation event summary", function () { const events = mkCreationEvents(); + const createEvent = events.find((event) => event.getType() === "m.room.create"); + client.getRoom.mockImplementation((id) => (id === createEvent!.getRoomId() ? room : null)); TestUtilsMatrix.upsertRoomStateEvents(room, events); const { container } = render( diff --git a/test/events/EventTileFactory-test.ts b/test/events/EventTileFactory-test.ts index 911abb71b4c..b54bedfb17c 100644 --- a/test/events/EventTileFactory-test.ts +++ b/test/events/EventTileFactory-test.ts @@ -18,8 +18,10 @@ import { JSONEventFactory, MessageEventFactory, pickFactory, + RoomCreateEventFactory, TextualEventFactory, } from "../../src/events/EventTileFactory"; +import SettingsStore from "../../src/settings/SettingsStore"; import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoState } from "../../src/voice-broadcast"; import { createTestClient, mkEvent } from "../test-utils"; import { mkVoiceBroadcastInfoStateEvent } from "../voice-broadcast/utils/test-utils"; @@ -27,23 +29,64 @@ import { mkVoiceBroadcastInfoStateEvent } from "../voice-broadcast/utils/test-ut const roomId = "!room:example.com"; describe("pickFactory", () => { + let client: MatrixClient; + let room: Room; + + let createEventWithPredecessor: MatrixEvent; + let createEventWithoutPredecessor: MatrixEvent; + let dynamicPredecessorEvent: MatrixEvent; + let voiceBroadcastStartedEvent: MatrixEvent; let voiceBroadcastStoppedEvent: MatrixEvent; let voiceBroadcastChunkEvent: MatrixEvent; let utdEvent: MatrixEvent; let utdBroadcastChunkEvent: MatrixEvent; let audioMessageEvent: MatrixEvent; - let client: MatrixClient; beforeAll(() => { client = createTestClient(); - const room = new Room(roomId, client, client.getSafeUserId()); + room = new Room(roomId, client, client.getSafeUserId()); mocked(client.getRoom).mockImplementation((getRoomId: string): Room | null => { if (getRoomId === room.roomId) return room; return null; }); + createEventWithoutPredecessor = mkEvent({ + event: true, + type: EventType.RoomCreate, + user: client.getUserId()!, + room: roomId, + content: { + creator: client.getUserId()!, + room_version: "9", + }, + }); + createEventWithPredecessor = mkEvent({ + event: true, + type: EventType.RoomCreate, + user: client.getUserId()!, + room: roomId, + content: { + creator: client.getUserId()!, + room_version: "9", + predecessor: { + room_id: "roomid1", + event_id: null, + }, + }, + }); + dynamicPredecessorEvent = mkEvent({ + event: true, + type: EventType.RoomPredecessor, + user: client.getUserId()!, + room: roomId, + skey: "", + content: { + predecessor_room_id: "roomid2", + last_known_event_id: null, + }, + }); voiceBroadcastStartedEvent = mkVoiceBroadcastInfoStateEvent( roomId, VoiceBroadcastInfoState.Started, @@ -117,6 +160,15 @@ describe("pickFactory", () => { expect(pickFactory(voiceBroadcastChunkEvent, client, true)).toBe(JSONEventFactory); }); + it("should return a JSONEventFactory for a room create event without predecessor", () => { + room.currentState.events.set( + EventType.RoomCreate, + new Map([[createEventWithoutPredecessor.getStateKey()!, createEventWithoutPredecessor]]), + ); + room.currentState.events.set(EventType.RoomPredecessor, new Map()); + expect(pickFactory(createEventWithoutPredecessor, client, true)).toBe(JSONEventFactory); + }); + it("should return a TextualEventFactory for a voice broadcast stopped event", () => { expect(pickFactory(voiceBroadcastStoppedEvent, client, true)).toBe(TextualEventFactory); }); @@ -131,6 +183,80 @@ describe("pickFactory", () => { }); describe("when not showing hidden events", () => { + describe("without dynamic predecessor support", () => { + beforeEach(() => { + jest.spyOn(SettingsStore, "getValue").mockReset(); + }); + + it("should return undefined for a room without predecessor", () => { + room.currentState.events.set( + EventType.RoomCreate, + new Map([[createEventWithoutPredecessor.getStateKey()!, createEventWithoutPredecessor]]), + ); + room.currentState.events.set(EventType.RoomPredecessor, new Map()); + expect(pickFactory(createEventWithoutPredecessor, client, false)).toBeUndefined(); + }); + + it("should return a RoomCreateFactory for a room with fixed predecessor", () => { + room.currentState.events.set( + EventType.RoomCreate, + new Map([[createEventWithPredecessor.getStateKey()!, createEventWithPredecessor]]), + ); + room.currentState.events.set(EventType.RoomPredecessor, new Map()); + expect(pickFactory(createEventWithPredecessor, client, false)).toBe(RoomCreateEventFactory); + }); + + it("should return undefined for a room with dynamic predecessor", () => { + room.currentState.events.set( + EventType.RoomCreate, + new Map([[createEventWithoutPredecessor.getStateKey()!, createEventWithoutPredecessor]]), + ); + room.currentState.events.set( + EventType.RoomPredecessor, + new Map([[dynamicPredecessorEvent.getStateKey()!, dynamicPredecessorEvent]]), + ); + expect(pickFactory(createEventWithoutPredecessor, client, false)).toBeUndefined(); + }); + }); + + describe("with dynamic predecessor support", () => { + beforeEach(() => { + jest.spyOn(SettingsStore, "getValue") + .mockReset() + .mockImplementation((settingName) => settingName === "feature_dynamic_room_predecessors"); + }); + + it("should return undefined for a room without predecessor", () => { + room.currentState.events.set( + EventType.RoomCreate, + new Map([[createEventWithoutPredecessor.getStateKey()!, createEventWithoutPredecessor]]), + ); + room.currentState.events.set(EventType.RoomPredecessor, new Map()); + expect(pickFactory(createEventWithoutPredecessor, client, false)).toBeUndefined(); + }); + + it("should return a RoomCreateFactory for a room with fixed predecessor", () => { + room.currentState.events.set( + EventType.RoomCreate, + new Map([[createEventWithPredecessor.getStateKey()!, createEventWithPredecessor]]), + ); + room.currentState.events.set(EventType.RoomPredecessor, new Map()); + expect(pickFactory(createEventWithPredecessor, client, false)).toBe(RoomCreateEventFactory); + }); + + it("should return a RoomCreateFactory for a room with dynamic predecessor", () => { + room.currentState.events.set( + EventType.RoomCreate, + new Map([[createEventWithoutPredecessor.getStateKey()!, createEventWithoutPredecessor]]), + ); + room.currentState.events.set( + EventType.RoomPredecessor, + new Map([[dynamicPredecessorEvent.getStateKey()!, dynamicPredecessorEvent]]), + ); + expect(pickFactory(createEventWithoutPredecessor, client, false)).toBe(RoomCreateEventFactory); + }); + }); + it("should return undefined for a voice broadcast event", () => { expect(pickFactory(voiceBroadcastChunkEvent, client, false)).toBeUndefined(); }); From 2b66cfc25f916da382250c1020ff652f56e39059 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Fri, 3 Feb 2023 17:43:02 +0100 Subject: [PATCH 78/97] Open message in editing mode when keyboard up is pressed (RTE) (#10079) Move to previous message when arrow up is pressed in the main composer (RTE) --- .../views/rooms/MessageComposer.tsx | 1 + .../rooms/wysiwyg_composer/ComposerContext.ts | 2 + .../wysiwyg_composer/SendWysiwygComposer.tsx | 5 +- .../hooks/useInitialContent.ts | 4 +- .../hooks/useInputEventProcessor.ts | 36 +- .../rooms/wysiwyg_composer/utils/event.ts | 13 + .../rooms/wysiwyg_composer/utils/selection.ts | 10 +- .../EditWysiwygComposer-test.tsx | 329 +-------------- .../SendWysiwygComposer-test.tsx | 19 +- .../components/WysiwygComposer-test.tsx | 384 +++++++++++++++++- .../views/rooms/wysiwyg_composer/utils.ts | 49 +++ 11 files changed, 487 insertions(+), 365 deletions(-) create mode 100644 test/components/views/rooms/wysiwyg_composer/utils.ts diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 1e8276ba8f4..1515c408d7f 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -489,6 +489,7 @@ export class MessageComposer extends React.Component { e2eStatus={this.props.e2eStatus} menuPosition={menuPosition} placeholder={this.renderPlaceholderText()} + eventRelation={this.props.relation} /> ); } else { diff --git a/src/components/views/rooms/wysiwyg_composer/ComposerContext.ts b/src/components/views/rooms/wysiwyg_composer/ComposerContext.ts index 1de070216c0..19daf8fde8d 100644 --- a/src/components/views/rooms/wysiwyg_composer/ComposerContext.ts +++ b/src/components/views/rooms/wysiwyg_composer/ComposerContext.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { createContext, useContext } from "react"; +import { IEventRelation } from "matrix-js-sdk/src/matrix"; import { SubSelection } from "./types"; import EditorStateTransfer from "../../../../utils/EditorStateTransfer"; @@ -29,6 +30,7 @@ export function getDefaultContextValue(defaultValue?: Partial(getDefaultContextValue()); diff --git a/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx index 2691df80dd1..d12432481db 100644 --- a/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/SendWysiwygComposer.tsx @@ -15,6 +15,7 @@ limitations under the License. */ import React, { ForwardedRef, forwardRef, MutableRefObject, useRef } from "react"; +import { IEventRelation } from "matrix-js-sdk/src/models/event"; import { useWysiwygSendActionHandler } from "./hooks/useWysiwygSendActionHandler"; import { WysiwygComposer } from "./components/WysiwygComposer"; @@ -48,6 +49,7 @@ interface SendWysiwygComposerProps { onChange: (content: string) => void; onSend: () => void; menuPosition: MenuProps; + eventRelation?: IEventRelation; } // Default needed for React.lazy @@ -55,10 +57,11 @@ export default function SendWysiwygComposer({ isRichTextEnabled, e2eStatus, menuPosition, + eventRelation, ...props }: SendWysiwygComposerProps): JSX.Element { const Composer = isRichTextEnabled ? WysiwygComposer : PlainTextComposer; - const defaultContextValue = useRef(getDefaultContextValue()); + const defaultContextValue = useRef(getDefaultContextValue({ eventRelation })); return ( diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useInitialContent.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useInitialContent.ts index 7ec4c5a3136..f4612a097e1 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useInitialContent.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useInitialContent.ts @@ -33,7 +33,7 @@ function getFormattedContent(editorStateTransfer: EditorStateTransfer): string { ); } -function parseEditorStateTransfer( +export function parseEditorStateTransfer( editorStateTransfer: EditorStateTransfer, room: Room, mxClient: MatrixClient, @@ -64,7 +64,7 @@ function parseEditorStateTransfer( // this.saveStoredEditorState(); } -export function useInitialContent(editorStateTransfer: EditorStateTransfer): string { +export function useInitialContent(editorStateTransfer: EditorStateTransfer): string | undefined { const roomContext = useRoomContext(); const mxClient = useMatrixClientContext(); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts index 405539fc709..def7d74bc01 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts @@ -30,7 +30,7 @@ import { ComposerContextState, useComposerContext } from "../ComposerContext"; import EditorStateTransfer from "../../../../../utils/EditorStateTransfer"; import { useMatrixClientContext } from "../../../../../contexts/MatrixClientContext"; import { isCaretAtEnd, isCaretAtStart } from "../utils/selection"; -import { getEventsFromEditorStateTransfer } from "../utils/event"; +import { getEventsFromEditorStateTransfer, getEventsFromRoom } from "../utils/event"; import { endEditing } from "../utils/editing"; export function useInputEventProcessor( @@ -87,7 +87,8 @@ function handleKeyboardEvent( mxClient: MatrixClient, ): KeyboardEvent | null { const { editorStateTransfer } = composerContext; - const isEditorModified = initialContent !== composer.content(); + const isEditing = Boolean(editorStateTransfer); + const isEditorModified = isEditing ? initialContent !== composer.content() : composer.content().length !== 0; const action = getKeyBindingsManager().getMessageComposerAction(event); switch (action) { @@ -95,14 +96,21 @@ function handleKeyboardEvent( send(); return null; case KeyBindingAction.EditPrevMessage: { - // If not in edition // Or if the caret is not at the beginning of the editor // Or the editor is modified - if (!editorStateTransfer || !isCaretAtStart(editor) || isEditorModified) { + if (!isCaretAtStart(editor) || isEditorModified) { break; } - const isDispatched = dispatchEditEvent(event, false, editorStateTransfer, roomContext, mxClient); + const isDispatched = dispatchEditEvent( + event, + false, + editorStateTransfer, + composerContext, + roomContext, + mxClient, + ); + if (isDispatched) { return null; } @@ -117,7 +125,14 @@ function handleKeyboardEvent( break; } - const isDispatched = dispatchEditEvent(event, true, editorStateTransfer, roomContext, mxClient); + const isDispatched = dispatchEditEvent( + event, + true, + editorStateTransfer, + composerContext, + roomContext, + mxClient, + ); if (!isDispatched) { endEditing(roomContext); event.preventDefault(); @@ -134,11 +149,14 @@ function handleKeyboardEvent( function dispatchEditEvent( event: KeyboardEvent, isForward: boolean, - editorStateTransfer: EditorStateTransfer, + editorStateTransfer: EditorStateTransfer | undefined, + composerContext: ComposerContextState, roomContext: IRoomState, mxClient: MatrixClient, ): boolean { - const foundEvents = getEventsFromEditorStateTransfer(editorStateTransfer, roomContext, mxClient); + const foundEvents = editorStateTransfer + ? getEventsFromEditorStateTransfer(editorStateTransfer, roomContext, mxClient) + : getEventsFromRoom(composerContext, roomContext); if (!foundEvents) { return false; } @@ -146,7 +164,7 @@ function dispatchEditEvent( const newEvent = findEditableEvent({ events: foundEvents, isForward, - fromEventId: editorStateTransfer.getEvent().getId(), + fromEventId: editorStateTransfer?.getEvent().getId(), }); if (newEvent) { dis.dispatch({ diff --git a/src/components/views/rooms/wysiwyg_composer/utils/event.ts b/src/components/views/rooms/wysiwyg_composer/utils/event.ts index 4d65497faca..2220b7d37a6 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/event.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/event.ts @@ -15,9 +15,11 @@ limitations under the License. */ import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread"; import EditorStateTransfer from "../../../../../utils/EditorStateTransfer"; import { IRoomState } from "../../../../structures/RoomView"; +import { ComposerContextState } from "../ComposerContext"; // From EditMessageComposer private get events(): MatrixEvent[] export function getEventsFromEditorStateTransfer( @@ -44,3 +46,14 @@ export function getEventsFromEditorStateTransfer( const isInThread = Boolean(editorStateTransfer.getEvent().getThread()); return liveTimelineEvents.concat(isInThread ? [] : pendingEvents); } + +// From SendMessageComposer private onKeyDown = (event: KeyboardEvent): void +export function getEventsFromRoom( + composerContext: ComposerContextState, + roomContext: IRoomState, +): MatrixEvent[] | undefined { + const isReplyingToThread = composerContext.eventRelation?.key === THREAD_RELATION_TYPE.name; + return roomContext.liveTimeline + ?.getEvents() + .concat(isReplyingToThread ? [] : roomContext.room?.getPendingEvents() || []); +} diff --git a/src/components/views/rooms/wysiwyg_composer/utils/selection.ts b/src/components/views/rooms/wysiwyg_composer/utils/selection.ts index 4ed64154e53..4af4b00c95e 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/selection.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/selection.ts @@ -44,15 +44,21 @@ export function isCaretAtStart(editor: HTMLElement): boolean { const selection = document.getSelection(); // No selection or the caret is not at the beginning of the selected element - if (!selection || selection.anchorOffset !== 0) { + if (!selection) { return false; } + // When we are pressing keyboard up in an empty main composer, the selection is on the editor with an anchorOffset at O or 1 (yes, this is strange) + const isOnFirstElement = selection.anchorNode === editor && selection.anchorOffset <= 1; + if (isOnFirstElement) { + return true; + } + // In case of nested html elements (list, code blocks), we are going through all the first child let child = editor.firstChild; do { if (child === selection.anchorNode) { - return true; + return selection.anchorOffset === 0; } } while ((child = child?.firstChild || null)); diff --git a/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx index c2c7052e054..7b7b87e8be9 100644 --- a/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/EditWysiwygComposer-test.tsx @@ -17,21 +17,12 @@ limitations under the License. import "@testing-library/jest-dom"; import React from "react"; import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; -import { EventTimeline, MatrixEvent } from "matrix-js-sdk/src/matrix"; import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; import RoomContext from "../../../../../src/contexts/RoomContext"; import defaultDispatcher from "../../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../../src/dispatcher/actions"; -import { IRoomState } from "../../../../../src/components/structures/RoomView"; -import { - createTestClient, - flushPromises, - getRoomContext, - mkEvent, - mkStubRoom, - mockPlatformPeg, -} from "../../../../test-utils"; +import { flushPromises, mkEvent } from "../../../../test-utils"; import { EditWysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer"; import EditorStateTransfer from "../../../../../src/utils/EditorStateTransfer"; import { Emoji } from "../../../../../src/components/views/rooms/wysiwyg_composer/components/Emoji"; @@ -40,43 +31,13 @@ import dis from "../../../../../src/dispatcher/dispatcher"; import { ComposerInsertPayload, ComposerType } from "../../../../../src/dispatcher/payloads/ComposerInsertPayload"; import { ActionPayload } from "../../../../../src/dispatcher/payloads"; import * as EmojiButton from "../../../../../src/components/views/rooms/EmojiButton"; -import { setSelection } from "../../../../../src/components/views/rooms/wysiwyg_composer/utils/selection"; -import * as EventUtils from "../../../../../src/utils/EventUtils"; -import { SubSelection } from "../../../../../src/components/views/rooms/wysiwyg_composer/types"; +import { createMocks } from "./utils"; describe("EditWysiwygComposer", () => { afterEach(() => { jest.resetAllMocks(); }); - function createMocks(eventContent = "Replying to this new content") { - const mockClient = createTestClient(); - const mockEvent = mkEvent({ - type: "m.room.message", - room: "myfakeroom", - user: "myfakeuser", - content: { - msgtype: "m.text", - body: "Replying to this", - format: "org.matrix.custom.html", - formatted_body: eventContent, - }, - event: true, - }); - const mockRoom = mkStubRoom("myfakeroom", "myfakeroom", mockClient) as any; - mockRoom.findEventById = jest.fn((eventId) => { - return eventId === mockEvent.getId() ? mockEvent : null; - }); - - const defaultRoomContext: IRoomState = getRoomContext(mockRoom, { - liveTimeline: { getEvents: (): MatrixEvent[] => [] } as unknown as EventTimeline, - }); - - const editorStateTransfer = new EditorStateTransfer(mockEvent); - - return { defaultRoomContext, editorStateTransfer, mockClient, mockEvent }; - } - const { editorStateTransfer, defaultRoomContext, mockClient, mockEvent } = createMocks(); const customRender = ( @@ -342,290 +303,4 @@ describe("EditWysiwygComposer", () => { await waitFor(() => expect(screen.getByRole("textbox")).toHaveTextContent(/🦫/)); dis.unregister(dispatcherRef); }); - - describe("Keyboard navigation", () => { - const setup = async ( - editorState = editorStateTransfer, - client = createTestClient(), - roomContext = defaultRoomContext, - ) => { - const spyDispatcher = jest.spyOn(defaultDispatcher, "dispatch"); - customRender(false, editorState, client, roomContext); - await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true")); - return { textbox: screen.getByRole("textbox"), spyDispatcher }; - }; - - beforeEach(() => { - mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) }); - jest.spyOn(EventUtils, "findEditableEvent").mockReturnValue(mockEvent); - }); - - function select(selection: SubSelection) { - return act(async () => { - await setSelection(selection); - // the event is not automatically fired by jest - document.dispatchEvent(new CustomEvent("selectionchange")); - }); - } - - describe("Moving up", () => { - it("Should not moving when caret is not at beginning of the text", async () => { - // When - const { textbox, spyDispatcher } = await setup(); - const textNode = textbox.firstChild; - await select({ - anchorNode: textNode, - anchorOffset: 1, - focusNode: textNode, - focusOffset: 2, - isForward: true, - }); - - fireEvent.keyDown(textbox, { - key: "ArrowUp", - }); - - // Then - expect(spyDispatcher).toBeCalledTimes(0); - }); - - it("Should not moving when the content has changed", async () => { - // When - const { textbox, spyDispatcher } = await setup(); - fireEvent.input(textbox, { - data: "word", - inputType: "insertText", - }); - const textNode = textbox.firstChild; - await select({ - anchorNode: textNode, - anchorOffset: 0, - focusNode: textNode, - focusOffset: 0, - isForward: true, - }); - - fireEvent.keyDown(textbox, { - key: "ArrowUp", - }); - - // Then - expect(spyDispatcher).toBeCalledTimes(0); - }); - - it("Should moving up", async () => { - // When - const { textbox, spyDispatcher } = await setup(); - const textNode = textbox.firstChild; - await select({ - anchorNode: textNode, - anchorOffset: 0, - focusNode: textNode, - focusOffset: 0, - isForward: true, - }); - - fireEvent.keyDown(textbox, { - key: "ArrowUp", - }); - - // Wait for event dispatch to happen - await act(async () => { - await flushPromises(); - }); - - // Then - await waitFor(() => - expect(spyDispatcher).toBeCalledWith({ - action: Action.EditEvent, - event: mockEvent, - timelineRenderingType: defaultRoomContext.timelineRenderingType, - }), - ); - }); - - it("Should moving up in list", async () => { - // When - const { mockEvent, defaultRoomContext, mockClient, editorStateTransfer } = createMocks( - "
    • Content
    • Other Content
    ", - ); - jest.spyOn(EventUtils, "findEditableEvent").mockReturnValue(mockEvent); - const { textbox, spyDispatcher } = await setup(editorStateTransfer, mockClient, defaultRoomContext); - - const textNode = textbox.firstChild; - await select({ - anchorNode: textNode, - anchorOffset: 0, - focusNode: textNode, - focusOffset: 0, - isForward: true, - }); - - fireEvent.keyDown(textbox, { - key: "ArrowUp", - }); - - // Wait for event dispatch to happen - await act(async () => { - await flushPromises(); - }); - - // Then - expect(spyDispatcher).toBeCalledWith({ - action: Action.EditEvent, - event: mockEvent, - timelineRenderingType: defaultRoomContext.timelineRenderingType, - }); - }); - }); - - describe("Moving down", () => { - it("Should not moving when caret is not at the end of the text", async () => { - // When - const { textbox, spyDispatcher } = await setup(); - const brNode = textbox.lastChild; - await select({ - anchorNode: brNode, - anchorOffset: 0, - focusNode: brNode, - focusOffset: 0, - isForward: true, - }); - - fireEvent.keyDown(textbox, { - key: "ArrowDown", - }); - - // Then - expect(spyDispatcher).toBeCalledTimes(0); - }); - - it("Should not moving when the content has changed", async () => { - // When - const { textbox, spyDispatcher } = await setup(); - fireEvent.input(textbox, { - data: "word", - inputType: "insertText", - }); - const brNode = textbox.lastChild; - await select({ - anchorNode: brNode, - anchorOffset: 0, - focusNode: brNode, - focusOffset: 0, - isForward: true, - }); - - fireEvent.keyDown(textbox, { - key: "ArrowDown", - }); - - // Then - expect(spyDispatcher).toBeCalledTimes(0); - }); - - it("Should moving down", async () => { - // When - const { textbox, spyDispatcher } = await setup(); - // Skipping the BR tag - const textNode = textbox.childNodes[textbox.childNodes.length - 2]; - const { length } = textNode.textContent || ""; - await select({ - anchorNode: textNode, - anchorOffset: length, - focusNode: textNode, - focusOffset: length, - isForward: true, - }); - - fireEvent.keyDown(textbox, { - key: "ArrowDown", - }); - - // Wait for event dispatch to happen - await act(async () => { - await flushPromises(); - }); - - // Then - await waitFor(() => - expect(spyDispatcher).toBeCalledWith({ - action: Action.EditEvent, - event: mockEvent, - timelineRenderingType: defaultRoomContext.timelineRenderingType, - }), - ); - }); - - it("Should moving down in list", async () => { - // When - const { mockEvent, defaultRoomContext, mockClient, editorStateTransfer } = createMocks( - "
    • Content
    • Other Content
    ", - ); - jest.spyOn(EventUtils, "findEditableEvent").mockReturnValue(mockEvent); - const { textbox, spyDispatcher } = await setup(editorStateTransfer, mockClient, defaultRoomContext); - - // Skipping the BR tag and get the text node inside the last LI tag - const textNode = textbox.childNodes[textbox.childNodes.length - 2].lastChild?.lastChild || textbox; - const { length } = textNode.textContent || ""; - await select({ - anchorNode: textNode, - anchorOffset: length, - focusNode: textNode, - focusOffset: length, - isForward: true, - }); - - fireEvent.keyDown(textbox, { - key: "ArrowDown", - }); - - // Wait for event dispatch to happen - await act(async () => { - await flushPromises(); - }); - - // Then - expect(spyDispatcher).toBeCalledWith({ - action: Action.EditEvent, - event: mockEvent, - timelineRenderingType: defaultRoomContext.timelineRenderingType, - }); - }); - - it("Should close editing", async () => { - // When - jest.spyOn(EventUtils, "findEditableEvent").mockReturnValue(undefined); - const { textbox, spyDispatcher } = await setup(); - // Skipping the BR tag - const textNode = textbox.childNodes[textbox.childNodes.length - 2]; - const { length } = textNode.textContent || ""; - await select({ - anchorNode: textNode, - anchorOffset: length, - focusNode: textNode, - focusOffset: length, - isForward: true, - }); - - fireEvent.keyDown(textbox, { - key: "ArrowDown", - }); - - // Wait for event dispatch to happen - await act(async () => { - await flushPromises(); - }); - - // Then - await waitFor(() => - expect(spyDispatcher).toBeCalledWith({ - action: Action.EditEvent, - event: null, - timelineRenderingType: defaultRoomContext.timelineRenderingType, - }), - ); - }); - }); - }); }); diff --git a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx index aa39de7a573..4dcf8d504ee 100644 --- a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx @@ -22,12 +22,12 @@ import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext import RoomContext from "../../../../../src/contexts/RoomContext"; import defaultDispatcher from "../../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../../src/dispatcher/actions"; -import { IRoomState } from "../../../../../src/components/structures/RoomView"; -import { createTestClient, flushPromises, getRoomContext, mkEvent, mkStubRoom } from "../../../../test-utils"; +import { flushPromises } from "../../../../test-utils"; import { SendWysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer/"; import { aboveLeftOf } from "../../../../../src/components/structures/ContextMenu"; import { ComposerInsertPayload, ComposerType } from "../../../../../src/dispatcher/payloads/ComposerInsertPayload"; import { setSelection } from "../../../../../src/components/views/rooms/wysiwyg_composer/utils/selection"; +import { createMocks } from "./utils"; jest.mock("../../../../../src/components/views/rooms/EmojiButton", () => ({ EmojiButton: ({ addEmoji }: { addEmoji: (emoji: string) => void }) => { @@ -44,20 +44,7 @@ describe("SendWysiwygComposer", () => { jest.resetAllMocks(); }); - const mockClient = createTestClient(); - const mockEvent = mkEvent({ - type: "m.room.message", - room: "myfakeroom", - user: "myfakeuser", - content: { msgtype: "m.text", body: "Replying to this" }, - event: true, - }); - const mockRoom = mkStubRoom("myfakeroom", "myfakeroom", mockClient) as any; - mockRoom.findEventById = jest.fn((eventId) => { - return eventId === mockEvent.getId() ? mockEvent : null; - }); - - const defaultRoomContext: IRoomState = getRoomContext(mockRoom, {}); + const { defaultRoomContext, mockClient } = createMocks(); const registerId = defaultDispatcher.register((payload) => { switch (payload.action) { diff --git a/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx index 4d485b5a3fa..5a41488be45 100644 --- a/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx @@ -16,25 +16,38 @@ limitations under the License. import "@testing-library/jest-dom"; import React from "react"; -import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { WysiwygComposer } from "../../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer"; import SettingsStore from "../../../../../../src/settings/SettingsStore"; -import { mockPlatformPeg } from "../../../../../test-utils"; +import { createTestClient, flushPromises, mockPlatformPeg } from "../../../../../test-utils"; +import defaultDispatcher from "../../../../../../src/dispatcher/dispatcher"; +import * as EventUtils from "../../../../../../src/utils/EventUtils"; +import { Action } from "../../../../../../src/dispatcher/actions"; +import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext"; +import RoomContext from "../../../../../../src/contexts/RoomContext"; +import { + ComposerContext, + getDefaultContextValue, +} from "../../../../../../src/components/views/rooms/wysiwyg_composer/ComposerContext"; +import { createMocks } from "../utils"; +import EditorStateTransfer from "../../../../../../src/utils/EditorStateTransfer"; +import { SubSelection } from "../../../../../../src/components/views/rooms/wysiwyg_composer/types"; +import { setSelection } from "../../../../../../src/components/views/rooms/wysiwyg_composer/utils/selection"; +import { parseEditorStateTransfer } from "../../../../../../src/components/views/rooms/wysiwyg_composer/hooks/useInitialContent"; describe("WysiwygComposer", () => { - const customRender = ( - onChange = (_content: string) => void 0, - onSend = () => void 0, - disabled = false, - initialContent?: string, - ) => { + const customRender = (onChange = jest.fn(), onSend = jest.fn(), disabled = false, initialContent?: string) => { return render( , ); }; + afterEach(() => { + jest.resetAllMocks(); + }); + it("Should have contentEditable at false when disabled", () => { // When customRender(jest.fn(), jest.fn(), true); @@ -191,4 +204,359 @@ describe("WysiwygComposer", () => { await waitFor(() => expect(onSend).toBeCalledTimes(1)); }); }); + + describe("Keyboard navigation", () => { + const { mockClient, defaultRoomContext, mockEvent, editorStateTransfer } = createMocks(); + + const customRender = ( + client = mockClient, + roomContext = defaultRoomContext, + _editorStateTransfer?: EditorStateTransfer, + ) => { + return render( + + + + + + + , + ); + }; + + afterEach(() => { + jest.resetAllMocks(); + }); + + const setup = async ( + editorState?: EditorStateTransfer, + client = createTestClient(), + roomContext = defaultRoomContext, + ) => { + const spyDispatcher = jest.spyOn(defaultDispatcher, "dispatch"); + customRender(client, roomContext, editorState); + await waitFor(() => expect(screen.getByRole("textbox")).toHaveAttribute("contentEditable", "true")); + return { textbox: screen.getByRole("textbox"), spyDispatcher }; + }; + + beforeEach(() => { + mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) }); + jest.spyOn(EventUtils, "findEditableEvent").mockReturnValue(mockEvent); + }); + + describe("In message creation", () => { + it("Should not moving when the composer is filled", async () => { + // When + const { textbox, spyDispatcher } = await setup(); + fireEvent.input(textbox, { + data: "word", + inputType: "insertText", + }); + + // Move at the beginning of the composer + fireEvent.keyDown(textbox, { + key: "ArrowUp", + }); + + // Then + expect(spyDispatcher).toBeCalledTimes(0); + }); + + it("Should moving when the composer is empty", async () => { + // When + const { textbox, spyDispatcher } = await setup(); + + fireEvent.keyDown(textbox, { + key: "ArrowUp", + }); + + // Then + expect(spyDispatcher).toBeCalledWith({ + action: Action.EditEvent, + event: mockEvent, + timelineRenderingType: defaultRoomContext.timelineRenderingType, + }); + }); + }); + + describe("In message editing", () => { + function select(selection: SubSelection) { + return act(async () => { + await setSelection(selection); + // the event is not automatically fired by jest + document.dispatchEvent(new CustomEvent("selectionchange")); + }); + } + + describe("Moving up", () => { + it("Should not moving when caret is not at beginning of the text", async () => { + // When + const { textbox, spyDispatcher } = await setup(editorStateTransfer); + const textNode = textbox.firstChild; + await select({ + anchorNode: textNode, + anchorOffset: 1, + focusNode: textNode, + focusOffset: 2, + isForward: true, + }); + + fireEvent.keyDown(textbox, { + key: "ArrowUp", + }); + + // Then + expect(spyDispatcher).toBeCalledTimes(0); + }); + + it("Should not moving when the content has changed", async () => { + // When + const { textbox, spyDispatcher } = await setup(editorStateTransfer); + fireEvent.input(textbox, { + data: "word", + inputType: "insertText", + }); + const textNode = textbox.firstChild; + await select({ + anchorNode: textNode, + anchorOffset: 0, + focusNode: textNode, + focusOffset: 0, + isForward: true, + }); + + fireEvent.keyDown(textbox, { + key: "ArrowUp", + }); + + // Then + expect(spyDispatcher).toBeCalledTimes(0); + }); + + it("Should moving up", async () => { + // When + const { textbox, spyDispatcher } = await setup(editorStateTransfer); + const textNode = textbox.firstChild; + await select({ + anchorNode: textNode, + anchorOffset: 0, + focusNode: textNode, + focusOffset: 0, + isForward: true, + }); + + fireEvent.keyDown(textbox, { + key: "ArrowUp", + }); + + // Wait for event dispatch to happen + await act(async () => { + await flushPromises(); + }); + + // Then + await waitFor(() => + expect(spyDispatcher).toBeCalledWith({ + action: Action.EditEvent, + event: mockEvent, + timelineRenderingType: defaultRoomContext.timelineRenderingType, + }), + ); + }); + + it("Should moving up in list", async () => { + // When + const { mockEvent, defaultRoomContext, mockClient, editorStateTransfer } = createMocks( + "
    • Content
    • Other Content
    ", + ); + jest.spyOn(EventUtils, "findEditableEvent").mockReturnValue(mockEvent); + const { textbox, spyDispatcher } = await setup(editorStateTransfer, mockClient, defaultRoomContext); + + const textNode = textbox.firstChild; + await select({ + anchorNode: textNode, + anchorOffset: 0, + focusNode: textNode, + focusOffset: 0, + isForward: true, + }); + + fireEvent.keyDown(textbox, { + key: "ArrowUp", + }); + + // Wait for event dispatch to happen + await act(async () => { + await flushPromises(); + }); + + // Then + expect(spyDispatcher).toBeCalledWith({ + action: Action.EditEvent, + event: mockEvent, + timelineRenderingType: defaultRoomContext.timelineRenderingType, + }); + }); + }); + + describe("Moving down", () => { + it("Should not moving when caret is not at the end of the text", async () => { + // When + const { textbox, spyDispatcher } = await setup(editorStateTransfer); + const brNode = textbox.lastChild; + await select({ + anchorNode: brNode, + anchorOffset: 0, + focusNode: brNode, + focusOffset: 0, + isForward: true, + }); + + fireEvent.keyDown(textbox, { + key: "ArrowDown", + }); + + // Then + expect(spyDispatcher).toBeCalledTimes(0); + }); + + it("Should not moving when the content has changed", async () => { + // When + const { textbox, spyDispatcher } = await setup(editorStateTransfer); + fireEvent.input(textbox, { + data: "word", + inputType: "insertText", + }); + const brNode = textbox.lastChild; + await select({ + anchorNode: brNode, + anchorOffset: 0, + focusNode: brNode, + focusOffset: 0, + isForward: true, + }); + + fireEvent.keyDown(textbox, { + key: "ArrowDown", + }); + + // Then + expect(spyDispatcher).toBeCalledTimes(0); + }); + + it("Should moving down", async () => { + // When + const { textbox, spyDispatcher } = await setup(editorStateTransfer); + // Skipping the BR tag + const textNode = textbox.childNodes[textbox.childNodes.length - 2]; + const { length } = textNode.textContent || ""; + await select({ + anchorNode: textNode, + anchorOffset: length, + focusNode: textNode, + focusOffset: length, + isForward: true, + }); + + fireEvent.keyDown(textbox, { + key: "ArrowDown", + }); + + // Wait for event dispatch to happen + await act(async () => { + await flushPromises(); + }); + + // Then + await waitFor(() => + expect(spyDispatcher).toBeCalledWith({ + action: Action.EditEvent, + event: mockEvent, + timelineRenderingType: defaultRoomContext.timelineRenderingType, + }), + ); + }); + + it("Should moving down in list", async () => { + // When + const { mockEvent, defaultRoomContext, mockClient, editorStateTransfer } = createMocks( + "
    • Content
    • Other Content
    ", + ); + jest.spyOn(EventUtils, "findEditableEvent").mockReturnValue(mockEvent); + const { textbox, spyDispatcher } = await setup(editorStateTransfer, mockClient, defaultRoomContext); + + // Skipping the BR tag and get the text node inside the last LI tag + const textNode = textbox.childNodes[textbox.childNodes.length - 2].lastChild?.lastChild || textbox; + const { length } = textNode.textContent || ""; + await select({ + anchorNode: textNode, + anchorOffset: length, + focusNode: textNode, + focusOffset: length, + isForward: true, + }); + + fireEvent.keyDown(textbox, { + key: "ArrowDown", + }); + + // Wait for event dispatch to happen + await act(async () => { + await flushPromises(); + }); + + // Then + expect(spyDispatcher).toBeCalledWith({ + action: Action.EditEvent, + event: mockEvent, + timelineRenderingType: defaultRoomContext.timelineRenderingType, + }); + }); + + it("Should close editing", async () => { + // When + jest.spyOn(EventUtils, "findEditableEvent").mockReturnValue(undefined); + const { textbox, spyDispatcher } = await setup(editorStateTransfer); + // Skipping the BR tag + const textNode = textbox.childNodes[textbox.childNodes.length - 2]; + const { length } = textNode.textContent || ""; + await select({ + anchorNode: textNode, + anchorOffset: length, + focusNode: textNode, + focusOffset: length, + isForward: true, + }); + + fireEvent.keyDown(textbox, { + key: "ArrowDown", + }); + + // Wait for event dispatch to happen + await act(async () => { + await flushPromises(); + }); + + // Then + await waitFor(() => + expect(spyDispatcher).toBeCalledWith({ + action: Action.EditEvent, + event: null, + timelineRenderingType: defaultRoomContext.timelineRenderingType, + }), + ); + }); + }); + }); + }); }); diff --git a/test/components/views/rooms/wysiwyg_composer/utils.ts b/test/components/views/rooms/wysiwyg_composer/utils.ts new file mode 100644 index 00000000000..0eb99b251db --- /dev/null +++ b/test/components/views/rooms/wysiwyg_composer/utils.ts @@ -0,0 +1,49 @@ +/* +Copyright 2023 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, MatrixEvent } from "matrix-js-sdk/src/matrix"; + +import { createTestClient, getRoomContext, mkEvent, mkStubRoom } from "../../../../test-utils"; +import { IRoomState } from "../../../../../src/components/structures/RoomView"; +import EditorStateTransfer from "../../../../../src/utils/EditorStateTransfer"; + +export function createMocks(eventContent = "Replying to this new content") { + const mockClient = createTestClient(); + const mockEvent = mkEvent({ + type: "m.room.message", + room: "myfakeroom", + user: "myfakeuser", + content: { + msgtype: "m.text", + body: "Replying to this", + format: "org.matrix.custom.html", + formatted_body: eventContent, + }, + event: true, + }); + const mockRoom = mkStubRoom("myfakeroom", "myfakeroom", mockClient) as any; + mockRoom.findEventById = jest.fn((eventId) => { + return eventId === mockEvent.getId() ? mockEvent : null; + }); + + const defaultRoomContext: IRoomState = getRoomContext(mockRoom, { + liveTimeline: { getEvents: (): MatrixEvent[] => [] } as unknown as EventTimeline, + }); + + const editorStateTransfer = new EditorStateTransfer(mockEvent); + + return { defaultRoomContext, editorStateTransfer, mockClient, mockEvent }; +} From a756b33fe912ea9a4349e8d5f098c2afe3a60afd Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 3 Feb 2023 16:58:52 +0000 Subject: [PATCH 79/97] Rename RoomCreate -> RoomPredecessorTile (#10047) --- ...RoomCreate.tsx => RoomPredecessorTile.tsx} | 7 +++--- src/events/EventTileFactory.tsx | 4 ++-- ...-test.tsx => RoomPredecessorTile-test.tsx} | 24 +++++++++---------- ...snap => RoomPredecessorTile-test.tsx.snap} | 2 +- 4 files changed, 19 insertions(+), 18 deletions(-) rename src/components/views/messages/{RoomCreate.tsx => RoomPredecessorTile.tsx} (92%) rename test/components/views/messages/{RoomCreate-test.tsx => RoomPredecessorTile-test.tsx} (91%) rename test/components/views/messages/__snapshots__/{RoomCreate-test.tsx.snap => RoomPredecessorTile-test.tsx.snap} (88%) diff --git a/src/components/views/messages/RoomCreate.tsx b/src/components/views/messages/RoomPredecessorTile.tsx similarity index 92% rename from src/components/views/messages/RoomCreate.tsx rename to src/components/views/messages/RoomPredecessorTile.tsx index ffeb10f3efe..5c47cda56fe 100644 --- a/src/components/views/messages/RoomCreate.tsx +++ b/src/components/views/messages/RoomPredecessorTile.tsx @@ -40,7 +40,7 @@ interface IProps { * A message tile showing that this room was created as an upgrade of a previous * room. */ -export const RoomCreate: React.FC = ({ mxEvent, timestamp }) => { +export const RoomPredecessorTile: React.FC = ({ mxEvent, timestamp }) => { const msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors"); // Note: we ask the room for its predecessor here, instead of directly using @@ -74,13 +74,14 @@ export const RoomCreate: React.FC = ({ mxEvent, timestamp }) => { if (!roomContext.room || roomContext.room.roomId !== mxEvent.getRoomId()) { logger.warn( - "RoomCreate unexpectedly used outside of the context of the room containing this m.room.create event.", + "RoomPredecessorTile unexpectedly used outside of the context of the" + + "room containing this m.room.create event.", ); return <>; } if (!predecessor) { - logger.warn("RoomCreate unexpectedly used in a room with no predecessor."); + logger.warn("RoomPredecessorTile unexpectedly used in a room with no predecessor."); return
    ; } diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx index 95e41395ac3..7f1a31518ad 100644 --- a/src/events/EventTileFactory.tsx +++ b/src/events/EventTileFactory.tsx @@ -34,7 +34,7 @@ import LegacyCallEvent from "../components/views/messages/LegacyCallEvent"; import { CallEvent } from "../components/views/messages/CallEvent"; import TextualEvent from "../components/views/messages/TextualEvent"; import EncryptionEvent from "../components/views/messages/EncryptionEvent"; -import { RoomCreate } from "../components/views/messages/RoomCreate"; +import { RoomPredecessorTile } from "../components/views/messages/RoomPredecessorTile"; import RoomAvatarEvent from "../components/views/messages/RoomAvatarEvent"; import { WIDGET_LAYOUT_EVENT_TYPE } from "../stores/widgets/WidgetLayoutStore"; import { ALL_RULE_TYPES } from "../mjolnir/BanList"; @@ -92,7 +92,7 @@ const HiddenEventFactory: Factory = (ref, props) => ; export const JSONEventFactory: Factory = (ref, props) => ; -export const RoomCreateEventFactory: Factory = (ref, props) => ; +export const RoomCreateEventFactory: Factory = (_ref, props) => ; const EVENT_TILE_TYPES = new Map([ [EventType.RoomMessage, MessageEventFactory], // note that verification requests are handled in pickFactory() diff --git a/test/components/views/messages/RoomCreate-test.tsx b/test/components/views/messages/RoomPredecessorTile-test.tsx similarity index 91% rename from test/components/views/messages/RoomCreate-test.tsx rename to test/components/views/messages/RoomPredecessorTile-test.tsx index cd5afa7ee2e..61bf8e82c5a 100644 --- a/test/components/views/messages/RoomCreate-test.tsx +++ b/test/components/views/messages/RoomPredecessorTile-test.tsx @@ -22,7 +22,7 @@ import { EventType, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import dis from "../../../../src/dispatcher/dispatcher"; import SettingsStore from "../../../../src/settings/SettingsStore"; -import { RoomCreate } from "../../../../src/components/views/messages/RoomCreate"; +import { RoomPredecessorTile } from "../../../../src/components/views/messages/RoomPredecessorTile"; import { stubClient, upsertRoomStateEvents } from "../../../test-utils/test-utils"; import { Action } from "../../../../src/dispatcher/actions"; import RoomContext from "../../../../src/contexts/RoomContext"; @@ -31,7 +31,7 @@ import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; jest.mock("../../../../src/dispatcher/dispatcher"); -describe("", () => { +describe("", () => { const userId = "@alice:server.org"; const roomId = "!room:server.org"; const createEvent = new MatrixEvent({ @@ -97,21 +97,21 @@ describe("", () => { jest.spyOn(SettingsStore, "setValue").mockRestore(); }); - function renderRoomCreate(room: Room) { + function renderTile(room: Room) { return render( - + , ); } it("Renders as expected", () => { - const roomCreate = renderRoomCreate(roomJustCreate); + const roomCreate = renderTile(roomJustCreate); expect(roomCreate.asFragment()).toMatchSnapshot(); }); it("Links to the old version of the room", () => { - renderRoomCreate(roomJustCreate); + renderTile(roomJustCreate); expect(screen.getByText("Click here to see older messages.")).toHaveAttribute( "href", "https://matrix.to/#/old_room_id/tombstone_event_id", @@ -119,12 +119,12 @@ describe("", () => { }); it("Shows an empty div if there is no predecessor", () => { - renderRoomCreate(roomNoPredecessors); + renderTile(roomNoPredecessors); expect(screen.queryByText("Click here to see older messages.", { exact: false })).toBeNull(); }); it("Opens the old room on click", async () => { - renderRoomCreate(roomJustCreate); + renderTile(roomJustCreate); const link = screen.getByText("Click here to see older messages."); await act(() => userEvent.click(link)); @@ -142,7 +142,7 @@ describe("", () => { }); it("Ignores m.predecessor if labs flag is off", () => { - renderRoomCreate(roomCreateAndPredecessor); + renderTile(roomCreateAndPredecessor); expect(screen.getByText("Click here to see older messages.")).toHaveAttribute( "href", "https://matrix.to/#/old_room_id/tombstone_event_id", @@ -161,7 +161,7 @@ describe("", () => { }); it("Uses the create event if there is no m.predecessor", () => { - renderRoomCreate(roomJustCreate); + renderTile(roomJustCreate); expect(screen.getByText("Click here to see older messages.")).toHaveAttribute( "href", "https://matrix.to/#/old_room_id/tombstone_event_id", @@ -169,7 +169,7 @@ describe("", () => { }); it("Uses m.predecessor when it's there", () => { - renderRoomCreate(roomCreateAndPredecessor); + renderTile(roomCreateAndPredecessor); expect(screen.getByText("Click here to see older messages.")).toHaveAttribute( "href", "https://matrix.to/#/old_room_id_from_predecessor", @@ -177,7 +177,7 @@ describe("", () => { }); it("Links to the event in the room if event ID is provided", () => { - renderRoomCreate(roomCreateAndPredecessorWithEventId); + renderTile(roomCreateAndPredecessorWithEventId); expect(screen.getByText("Click here to see older messages.")).toHaveAttribute( "href", "https://matrix.to/#/old_room_id_from_predecessor/tombstone_event_id_from_predecessor", diff --git a/test/components/views/messages/__snapshots__/RoomCreate-test.tsx.snap b/test/components/views/messages/__snapshots__/RoomPredecessorTile-test.tsx.snap similarity index 88% rename from test/components/views/messages/__snapshots__/RoomCreate-test.tsx.snap rename to test/components/views/messages/__snapshots__/RoomPredecessorTile-test.tsx.snap index 97c1cee66f6..5e692de3fd5 100644 --- a/test/components/views/messages/__snapshots__/RoomCreate-test.tsx.snap +++ b/test/components/views/messages/__snapshots__/RoomPredecessorTile-test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` Renders as expected 1`] = ` +exports[` Renders as expected 1`] = `
    Date: Sun, 5 Feb 2023 21:31:16 -0500 Subject: [PATCH 80/97] Indicate unread rooms in document.title Signed-off-by: Tim Tills --- src/components/structures/MatrixChat.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index cc07f28e4a1..7fdda93eb61 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -139,6 +139,7 @@ import GenericToast from "../views/toasts/GenericToast"; import RovingSpotlightDialog, { Filter } from "../views/dialogs/spotlight/SpotlightDialog"; import { findDMForUser } from "../../utils/dm/findDMForUser"; import { Linkify } from "../../HtmlUtils"; +import { NotificationColor } from "../../stores/notifications/NotificationColor"; // legacy export export { default as Views } from "../../Views"; @@ -1961,6 +1962,8 @@ export default class MatrixChat extends React.PureComponent { } if (numUnreadRooms > 0) { this.subTitleStatus += `[${numUnreadRooms}]`; + } else if (notificationState.color >= NotificationColor.Bold) { + this.subTitleStatus += `*`; } this.setPageSubtitle(); From d4349bb3610eb3e3e63a1ed04ed0483f7a7b43b1 Mon Sep 17 00:00:00 2001 From: Gustavo Santos <53129852+gefgu@users.noreply.github.com> Date: Mon, 6 Feb 2023 07:50:06 -0300 Subject: [PATCH 81/97] Add border to 'reject' button on room preview card (#9205) * Add border to 'reject' button on room preview card Signed-off-by: gefgu * feat: use correct kind --------- Signed-off-by: gefgu Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomPreviewCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomPreviewCard.tsx b/src/components/views/rooms/RoomPreviewCard.tsx index 4c0a016f85d..2714bec93ab 100644 --- a/src/components/views/rooms/RoomPreviewCard.tsx +++ b/src/components/views/rooms/RoomPreviewCard.tsx @@ -116,7 +116,7 @@ const RoomPreviewCard: FC = ({ room, onJoinButtonClicked, onRejectButton joinButtons = ( <> { setBusy(true); onRejectButtonClicked(); From 5ba8ecabb52e7f35cae01b1a356da8d8a3f29594 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Date: Mon, 6 Feb 2023 10:50:34 +0000 Subject: [PATCH 82/97] Element-R: fix rageshages (#10081) quick hacks to get rageshakes working in element R Fixes https://github.com/vector-im/element-web/issues/24430 --- src/rageshake/submit-rageshake.ts | 3 ++- src/sentry.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts index 1024caadf0b..09f9ae60376 100644 --- a/src/rageshake/submit-rageshake.ts +++ b/src/rageshake/submit-rageshake.ts @@ -84,7 +84,8 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true): Promise
    { - if (!client.isCryptoEnabled()) { + // TODO: make this work with rust crypto + if (!client.isCryptoEnabled() || !client.crypto) { return {}; } const keys = [`ed25519:${client.getDeviceEd25519Key()}`]; From 39fe72e53ad6aa7cc723595d5d0d3662329ee785 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Mon, 6 Feb 2023 13:15:20 +0100 Subject: [PATCH 83/97] Fix broadcast pip seekbar (#10072) --- src/components/structures/PipContainer.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/components/structures/PipContainer.tsx b/src/components/structures/PipContainer.tsx index 416458e6ff6..7bbbb1c5682 100644 --- a/src/components/structures/PipContainer.tsx +++ b/src/components/structures/PipContainer.tsx @@ -258,17 +258,16 @@ class PipContainerInner extends React.Component { } private createVoiceBroadcastPlaybackPipContent(voiceBroadcastPlayback: VoiceBroadcastPlayback): CreatePipChildren { - if (this.state.viewedRoomId === voiceBroadcastPlayback.infoEvent.getRoomId()) { - return ({ onStartMoving }) => ( -
    - -
    + const content = + this.state.viewedRoomId === voiceBroadcastPlayback.infoEvent.getRoomId() ? ( + + ) : ( + ); - } return ({ onStartMoving }) => ( -
    - +
    + {content}
    ); } From 885d5098ab24e6e56590a012cac537417976e87d Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Tue, 7 Feb 2023 08:45:13 +0100 Subject: [PATCH 84/97] Fix flaky test crypto/decryption-failure.spec.ts "Decryption Failure Bar" (#10092) --- cypress/e2e/crypto/decryption-failure.spec.ts | 13 +++++++------ cypress/support/bot.ts | 2 ++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/cypress/e2e/crypto/decryption-failure.spec.ts b/cypress/e2e/crypto/decryption-failure.spec.ts index 5f0a9056ad6..b9e3265b767 100644 --- a/cypress/e2e/crypto/decryption-failure.spec.ts +++ b/cypress/e2e/crypto/decryption-failure.spec.ts @@ -52,6 +52,8 @@ const handleVerificationRequest = (request: VerificationRequest): Chainable { }) .then(() => { cy.botSendMessage(bot, roomId, "test"); - cy.wait(5000); - cy.get(".mx_DecryptionFailureBar .mx_DecryptionFailureBar_message_headline").should( - "have.text", + cy.contains( + ".mx_DecryptionFailureBar .mx_DecryptionFailureBar_message_headline", "Verify this device to access all messages", ); @@ -124,6 +125,7 @@ describe("Decryption Failure Bar", () => { const verificationRequestPromise = waitForVerificationRequest(otherDevice); cy.get(".mx_CompleteSecurity_actionRow .mx_AccessibleButton").click(); + cy.contains("To proceed, please accept the verification request on your other device."); cy.wrap(verificationRequestPromise).then((verificationRequest: VerificationRequest) => { cy.wrap(verificationRequest.accept()); handleVerificationRequest(verificationRequest).then((emojis) => { @@ -170,9 +172,8 @@ describe("Decryption Failure Bar", () => { ); cy.botSendMessage(bot, roomId, "test"); - cy.wait(5000); - cy.get(".mx_DecryptionFailureBar .mx_DecryptionFailureBar_message_headline").should( - "have.text", + cy.contains( + ".mx_DecryptionFailureBar .mx_DecryptionFailureBar_message_headline", "Reset your keys to prevent future decryption errors", ); diff --git a/cypress/support/bot.ts b/cypress/support/bot.ts index 9cb5e472deb..2799927916e 100644 --- a/cypress/support/bot.ts +++ b/cypress/support/bot.ts @@ -163,6 +163,8 @@ function setupBotClient( } }) .then(() => cli), + // extra timeout, as this sometimes takes a while + { timeout: 30_000 }, ); }); } From 5ac014ff294df515c0a533e70dcc7501358e93ea Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Tue, 7 Feb 2023 08:09:44 +0000 Subject: [PATCH 85/97] Add a whitespace character after 'broadcast?' (#10097) Signed-off-by: Suguru Hirahara --- src/i18n/strings/en_EN.json | 2 +- src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c1aeeab2de5..a2fdf6870fd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -662,7 +662,7 @@ "Unable to decrypt voice broadcast": "Unable to decrypt voice broadcast", "Unable to play this voice broadcast": "Unable to play this voice broadcast", "Stop live broadcasting?": "Stop live broadcasting?", - "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.", + "Are you sure you want to stop your live broadcast? This will end the broadcast and the full recording will be available in the room.": "Are you sure you want to stop your live broadcast? This will end the broadcast and the full recording will be available in the room.", "Yes, stop broadcast": "Yes, stop broadcast", "Listen to live broadcast?": "Listen to live broadcast?", "If you start listening to this live broadcast, your current live broadcast recording will be ended.": "If you start listening to this live broadcast, your current live broadcast recording will be ended.", diff --git a/src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx b/src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx index 1b2bde2b405..71f4ccf1f1b 100644 --- a/src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx +++ b/src/voice-broadcast/hooks/useVoiceBroadcastRecording.tsx @@ -36,7 +36,7 @@ const showStopBroadcastingDialog = async (): Promise => { description: (

    {_t( - "Are you sure you want to stop your live broadcast?" + + "Are you sure you want to stop your live broadcast? " + "This will end the broadcast and the full recording will be available in the room.", )}

    From 4648fa3c8c080f881a7b54e03d85c3deaea8bac7 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Tue, 7 Feb 2023 10:14:28 +0100 Subject: [PATCH 86/97] Add PiP move threshold (#10040) (#10033) --- .../models/VoiceBroadcastPlayback.ts | 4 + .../models/VoiceBroadcastPlayback-test.tsx | 285 ++++++++++-------- 2 files changed, 170 insertions(+), 119 deletions(-) diff --git a/src/voice-broadcast/models/VoiceBroadcastPlayback.ts b/src/voice-broadcast/models/VoiceBroadcastPlayback.ts index 0a5442cb62c..cb45d9f29af 100644 --- a/src/voice-broadcast/models/VoiceBroadcastPlayback.ts +++ b/src/voice-broadcast/models/VoiceBroadcastPlayback.ts @@ -396,7 +396,11 @@ export class VoiceBroadcastPlayback } if (!this.playbacks.has(eventId)) { + // set to buffering while loading the chunk data + const currentState = this.getState(); + this.setState(VoiceBroadcastPlaybackState.Buffering); await this.loadPlayback(event); + this.setState(currentState); } const playback = this.playbacks.get(eventId); diff --git a/test/voice-broadcast/models/VoiceBroadcastPlayback-test.tsx b/test/voice-broadcast/models/VoiceBroadcastPlayback-test.tsx index e7f4c8afcc1..9f59ba6369e 100644 --- a/test/voice-broadcast/models/VoiceBroadcastPlayback-test.tsx +++ b/test/voice-broadcast/models/VoiceBroadcastPlayback-test.tsx @@ -18,6 +18,7 @@ import { mocked } from "jest-mock"; import { screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { MatrixClient, MatrixEvent, MatrixEventEvent, Room } from "matrix-js-sdk/src/matrix"; +import { defer } from "matrix-js-sdk/src/utils"; import { Playback, PlaybackState } from "../../../src/audio/Playback"; import { PlaybackManager } from "../../../src/audio/PlaybackManager"; @@ -31,9 +32,10 @@ import { VoiceBroadcastPlaybackState, VoiceBroadcastRecording, } from "../../../src/voice-broadcast"; -import { filterConsole, flushPromises, stubClient } from "../../test-utils"; +import { filterConsole, flushPromises, flushPromisesWithFakeTimers, stubClient } from "../../test-utils"; import { createTestPlayback } from "../../test-utils/audio"; import { mkVoiceBroadcastChunkEvent, mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils"; +import { LazyValue } from "../../../src/utils/LazyValue"; jest.mock("../../../src/utils/MediaEventHelper", () => ({ MediaEventHelper: jest.fn(), @@ -49,6 +51,7 @@ describe("VoiceBroadcastPlayback", () => { let playback: VoiceBroadcastPlayback; let onStateChanged: (state: VoiceBroadcastPlaybackState) => void; let chunk1Event: MatrixEvent; + let deplayedChunk1Event: MatrixEvent; let chunk2Event: MatrixEvent; let chunk2BEvent: MatrixEvent; let chunk3Event: MatrixEvent; @@ -58,6 +61,7 @@ describe("VoiceBroadcastPlayback", () => { const chunk1Data = new ArrayBuffer(2); const chunk2Data = new ArrayBuffer(3); const chunk3Data = new ArrayBuffer(3); + let delayedChunk1Helper: MediaEventHelper; let chunk1Helper: MediaEventHelper; let chunk2Helper: MediaEventHelper; let chunk3Helper: MediaEventHelper; @@ -97,8 +101,8 @@ describe("VoiceBroadcastPlayback", () => { }; const startPlayback = () => { - beforeEach(async () => { - await playback.start(); + beforeEach(() => { + playback.start(); }); }; @@ -127,11 +131,36 @@ describe("VoiceBroadcastPlayback", () => { }; }; + const mkDeplayedChunkHelper = (data: ArrayBuffer): MediaEventHelper => { + const deferred = defer>(); + + setTimeout(() => { + deferred.resolve({ + // @ts-ignore + arrayBuffer: jest.fn().mockResolvedValue(data), + }); + }, 7500); + + return { + sourceBlob: { + cachedValue: new Blob(), + done: false, + // @ts-ignore + value: deferred.promise, + }, + }; + }; + + const simulateFirstChunkArrived = async (): Promise => { + jest.advanceTimersByTime(10000); + await flushPromisesWithFakeTimers(); + }; + const mkInfoEvent = (state: VoiceBroadcastInfoState) => { return mkVoiceBroadcastInfoStateEvent(roomId, state, userId, deviceId); }; - const mkPlayback = async () => { + const mkPlayback = async (fakeTimers = false): Promise => { const playback = new VoiceBroadcastPlayback( infoEvent, client, @@ -140,7 +169,7 @@ describe("VoiceBroadcastPlayback", () => { jest.spyOn(playback, "removeAllListeners"); jest.spyOn(playback, "destroy"); playback.on(VoiceBroadcastPlaybackEvent.StateChanged, onStateChanged); - await flushPromises(); + fakeTimers ? await flushPromisesWithFakeTimers() : await flushPromises(); return playback; }; @@ -152,6 +181,7 @@ describe("VoiceBroadcastPlayback", () => { const createChunkEvents = () => { chunk1Event = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk1Length, 1); + deplayedChunk1Event = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk1Length, 1); chunk2Event = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk2Length, 2); chunk2Event.setTxnId("tx-id-1"); chunk2BEvent = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk2Length, 2); @@ -159,6 +189,7 @@ describe("VoiceBroadcastPlayback", () => { chunk3Event = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk3Length, 3); chunk1Helper = mkChunkHelper(chunk1Data); + delayedChunk1Helper = mkDeplayedChunkHelper(chunk1Data); chunk2Helper = mkChunkHelper(chunk2Data); chunk3Helper = mkChunkHelper(chunk3Data); @@ -181,6 +212,7 @@ describe("VoiceBroadcastPlayback", () => { mocked(MediaEventHelper).mockImplementation((event: MatrixEvent): any => { if (event === chunk1Event) return chunk1Helper; + if (event === deplayedChunk1Event) return delayedChunk1Helper; if (event === chunk2Event) return chunk2Helper; if (event === chunk3Event) return chunk3Helper; }); @@ -488,11 +520,17 @@ describe("VoiceBroadcastPlayback", () => { describe("when there is a stopped voice broadcast", () => { beforeEach(async () => { + jest.useFakeTimers(); infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Stopped); createChunkEvents(); - setUpChunkEvents([chunk2Event, chunk1Event, chunk3Event]); - room.addLiveEvents([infoEvent, chunk1Event, chunk2Event, chunk3Event]); - playback = await mkPlayback(); + // use delayed first chunk here to simulate loading time + setUpChunkEvents([chunk2Event, deplayedChunk1Event, chunk3Event]); + room.addLiveEvents([infoEvent, deplayedChunk1Event, chunk2Event, chunk3Event]); + playback = await mkPlayback(true); + }); + + afterEach(() => { + jest.useRealTimers(); }); it("should expose the info event", () => { @@ -504,166 +542,174 @@ describe("VoiceBroadcastPlayback", () => { describe("and calling start", () => { startPlayback(); - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing); + itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Buffering); - it("should play the chunks beginning with the first one", () => { - // assert that the first chunk is being played - expect(chunk1Playback.play).toHaveBeenCalled(); - expect(chunk2Playback.play).not.toHaveBeenCalled(); - }); - - describe("and calling start again", () => { - it("should not play the first chunk a second time", () => { - expect(chunk1Playback.play).toHaveBeenCalledTimes(1); + describe("and the first chunk data has been loaded", () => { + beforeEach(async () => { + await simulateFirstChunkArrived(); }); - }); - describe("and the chunk playback progresses", () => { - beforeEach(() => { - chunk1Playback.clockInfo.liveData.update([11]); - }); + itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing); - it("should update the time", () => { - expect(playback.timeSeconds).toBe(11); - expect(playback.timeLeftSeconds).toBe(2); + it("should play the chunks beginning with the first one", () => { + // assert that the first chunk is being played + expect(chunk1Playback.play).toHaveBeenCalled(); + expect(chunk2Playback.play).not.toHaveBeenCalled(); }); - }); - - describe("and the chunk playback progresses across the actual time", () => { - // This can be the case if the meta data is out of sync with the actual audio data. - beforeEach(() => { - chunk1Playback.clockInfo.liveData.update([15]); + describe("and calling start again", () => { + it("should not play the first chunk a second time", () => { + expect(chunk1Playback.play).toHaveBeenCalledTimes(1); + }); }); - it("should update the time", () => { - expect(playback.timeSeconds).toBe(15); - expect(playback.timeLeftSeconds).toBe(0); + describe("and the chunk playback progresses", () => { + beforeEach(() => { + chunk1Playback.clockInfo.liveData.update([11]); + }); + + it("should update the time", () => { + expect(playback.timeSeconds).toBe(11); + }); }); - }); - describe("and skipping to the middle of the second chunk", () => { - const middleOfSecondChunk = (chunk1Length + chunk2Length / 2) / 1000; + describe("and the chunk playback progresses across the actual time", () => { + // This can be the case if the meta data is out of sync with the actual audio data. - beforeEach(async () => { - await playback.skipTo(middleOfSecondChunk); - }); + beforeEach(() => { + chunk1Playback.clockInfo.liveData.update([15]); + }); - it("should play the second chunk", () => { - expect(chunk1Playback.stop).toHaveBeenCalled(); - expect(chunk1Playback.destroy).toHaveBeenCalled(); - expect(chunk2Playback.play).toHaveBeenCalled(); + it("should update the time", () => { + expect(playback.timeSeconds).toBe(15); + expect(playback.timeLeftSeconds).toBe(0); + }); }); - it("should update the time", () => { - expect(playback.timeSeconds).toBe(middleOfSecondChunk); - }); + describe("and skipping to the middle of the second chunk", () => { + const middleOfSecondChunk = (chunk1Length + chunk2Length / 2) / 1000; - describe("and skipping to the start", () => { beforeEach(async () => { - await playback.skipTo(0); + await playback.skipTo(middleOfSecondChunk); }); - it("should play the first chunk", () => { - expect(chunk2Playback.stop).toHaveBeenCalled(); - expect(chunk2Playback.destroy).toHaveBeenCalled(); - expect(chunk1Playback.play).toHaveBeenCalled(); + it("should play the second chunk", () => { + expect(chunk1Playback.stop).toHaveBeenCalled(); + expect(chunk1Playback.destroy).toHaveBeenCalled(); + expect(chunk2Playback.play).toHaveBeenCalled(); }); it("should update the time", () => { - expect(playback.timeSeconds).toBe(0); + expect(playback.timeSeconds).toBe(middleOfSecondChunk); }); - }); - }); - describe("and skipping multiple times", () => { - beforeEach(async () => { - return Promise.all([ - playback.skipTo(middleOfSecondChunk), - playback.skipTo(middleOfThirdChunk), - playback.skipTo(0), - ]); + describe("and skipping to the start", () => { + beforeEach(async () => { + await playback.skipTo(0); + }); + + it("should play the first chunk", () => { + expect(chunk2Playback.stop).toHaveBeenCalled(); + expect(chunk2Playback.destroy).toHaveBeenCalled(); + expect(chunk1Playback.play).toHaveBeenCalled(); + }); + + it("should update the time", () => { + expect(playback.timeSeconds).toBe(0); + }); + }); }); - it("should only skip to the first and last position", () => { - expect(chunk1Playback.stop).toHaveBeenCalled(); - expect(chunk1Playback.destroy).toHaveBeenCalled(); - expect(chunk2Playback.play).toHaveBeenCalled(); + describe("and skipping multiple times", () => { + beforeEach(async () => { + return Promise.all([ + playback.skipTo(middleOfSecondChunk), + playback.skipTo(middleOfThirdChunk), + playback.skipTo(0), + ]); + }); - expect(chunk3Playback.play).not.toHaveBeenCalled(); + it("should only skip to the first and last position", () => { + expect(chunk1Playback.stop).toHaveBeenCalled(); + expect(chunk1Playback.destroy).toHaveBeenCalled(); + expect(chunk2Playback.play).toHaveBeenCalled(); - expect(chunk2Playback.stop).toHaveBeenCalled(); - expect(chunk2Playback.destroy).toHaveBeenCalled(); - expect(chunk1Playback.play).toHaveBeenCalled(); - }); - }); + expect(chunk3Playback.play).not.toHaveBeenCalled(); - describe("and the first chunk ends", () => { - beforeEach(() => { - chunk1Playback.emit(PlaybackState.Stopped); + expect(chunk2Playback.stop).toHaveBeenCalled(); + expect(chunk2Playback.destroy).toHaveBeenCalled(); + expect(chunk1Playback.play).toHaveBeenCalled(); + }); }); - it("should play until the end", () => { - // assert first chunk was unloaded - expect(chunk1Playback.destroy).toHaveBeenCalled(); + describe("and the first chunk ends", () => { + beforeEach(() => { + chunk1Playback.emit(PlaybackState.Stopped); + }); - // assert that the second chunk is being played - expect(chunk2Playback.play).toHaveBeenCalled(); + it("should play until the end", () => { + // assert first chunk was unloaded + expect(chunk1Playback.destroy).toHaveBeenCalled(); - // simulate end of second and third chunk - chunk2Playback.emit(PlaybackState.Stopped); - chunk3Playback.emit(PlaybackState.Stopped); + // assert that the second chunk is being played + expect(chunk2Playback.play).toHaveBeenCalled(); - // assert that the entire playback is now in stopped state - expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped); - }); - }); - - describe("and calling pause", () => { - pausePlayback(); - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused); - itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Paused); - }); + // simulate end of second and third chunk + chunk2Playback.emit(PlaybackState.Stopped); + chunk3Playback.emit(PlaybackState.Stopped); - describe("and calling stop", () => { - stopPlayback(); - itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped); + // assert that the entire playback is now in stopped state + expect(playback.getState()).toBe(VoiceBroadcastPlaybackState.Stopped); + }); + }); - it("should stop the playback", () => { - expect(chunk1Playback.stop).toHaveBeenCalled(); + describe("and calling pause", () => { + pausePlayback(); + itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Paused); + itShouldEmitAStateChangedEvent(VoiceBroadcastPlaybackState.Paused); }); - describe("and skipping to somewhere in the middle of the first chunk", () => { - beforeEach(async () => { - mocked(chunk1Playback.play).mockClear(); - await playback.skipTo(1); + describe("and calling stop", () => { + stopPlayback(); + itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Stopped); + + it("should stop the playback", () => { + expect(chunk1Playback.stop).toHaveBeenCalled(); }); - it("should not start the playback", () => { - expect(chunk1Playback.play).not.toHaveBeenCalled(); + describe("and skipping to somewhere in the middle of the first chunk", () => { + beforeEach(async () => { + mocked(chunk1Playback.play).mockClear(); + await playback.skipTo(1); + }); + + it("should not start the playback", () => { + expect(chunk1Playback.play).not.toHaveBeenCalled(); + }); }); }); - }); - describe("and calling destroy", () => { - beforeEach(() => { - playback.destroy(); - }); + describe("and calling destroy", () => { + beforeEach(() => { + playback.destroy(); + }); - it("should call removeAllListeners", () => { - expect(playback.removeAllListeners).toHaveBeenCalled(); - }); + it("should call removeAllListeners", () => { + expect(playback.removeAllListeners).toHaveBeenCalled(); + }); - it("should call destroy on the playbacks", () => { - expect(chunk1Playback.destroy).toHaveBeenCalled(); - expect(chunk2Playback.destroy).toHaveBeenCalled(); + it("should call destroy on the playbacks", () => { + expect(chunk1Playback.destroy).toHaveBeenCalled(); + expect(chunk2Playback.destroy).toHaveBeenCalled(); + }); }); }); }); describe("and calling toggle for the first time", () => { beforeEach(async () => { - await playback.toggle(); + playback.toggle(); + await simulateFirstChunkArrived(); }); itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing); @@ -693,7 +739,8 @@ describe("VoiceBroadcastPlayback", () => { describe("and calling toggle", () => { beforeEach(async () => { mocked(onStateChanged).mockReset(); - await playback.toggle(); + playback.toggle(); + await simulateFirstChunkArrived(); }); itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing); From 30cc55515e57df74980043fb2b319941979e7a72 Mon Sep 17 00:00:00 2001 From: Arnabdaz <96580571+Arnabdaz@users.noreply.github.com> Date: Tue, 7 Feb 2023 15:07:52 +0530 Subject: [PATCH 87/97] Fix scrollbar colliding with checkbox in add to space section (#10093) Fixes https://github.com/vector-im/element-web/issues/23189 fixes https://github.com/vector-im/element-web/issues/23189 --- res/css/views/dialogs/_AddExistingToSpaceDialog.pcss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.pcss b/res/css/views/dialogs/_AddExistingToSpaceDialog.pcss index cda642f610f..9a6372a5adb 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.pcss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.pcss @@ -38,6 +38,8 @@ limitations under the License. } .mx_AddExistingToSpace_section { + margin-right: 12px; // provides space for scrollbar so that checkbox and scrollbar do not collide + &:not(:first-child) { margin-top: 24px; } From 35d222bac6dc7d7467467830df777ce23e7b4ab5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 7 Feb 2023 10:08:10 +0000 Subject: [PATCH 88/97] Add @typescript-eslint/no-base-to-string (#10091) --- .eslintrc.js | 9 +++++++++ package.json | 2 +- src/components/views/dialogs/ServerOfflineDialog.tsx | 3 ++- src/components/views/settings/ProfileSettings.tsx | 6 +++++- src/rageshake/submit-rageshake.ts | 2 +- src/stores/widgets/StopGapWidget.ts | 2 +- src/utils/FileUtils.ts | 2 +- src/utils/Whenable.ts | 4 ++-- src/utils/exportUtils/HtmlExport.tsx | 4 ++-- yarn.lock | 8 ++++---- 10 files changed, 28 insertions(+), 14 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index a65f20893b5..7c2ebb96df5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,9 @@ module.exports = { plugins: ["matrix-org"], extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react", "plugin:matrix-org/a11y"], + parserOptions: { + project: ["./tsconfig.json"], + }, env: { browser: true, node: true, @@ -168,6 +171,12 @@ module.exports = { "@typescript-eslint/explicit-member-accessibility": "off", }, }, + { + files: ["cypress/**/*.ts"], + parserOptions: { + project: ["./cypress/tsconfig.json"], + }, + }, ], settings: { react: { diff --git a/package.json b/package.json index fe618fc40b4..f71915058ee 100644 --- a/package.json +++ b/package.json @@ -190,7 +190,7 @@ "eslint-plugin-deprecate": "^0.7.0", "eslint-plugin-import": "^2.25.4", "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-matrix-org": "0.9.0", + "eslint-plugin-matrix-org": "0.10.0", "eslint-plugin-react": "^7.28.0", "eslint-plugin-react-hooks": "^4.3.0", "eslint-plugin-unicorn": "^45.0.0", diff --git a/src/components/views/dialogs/ServerOfflineDialog.tsx b/src/components/views/dialogs/ServerOfflineDialog.tsx index bacb4257aea..b4b199661d9 100644 --- a/src/components/views/dialogs/ServerOfflineDialog.tsx +++ b/src/components/views/dialogs/ServerOfflineDialog.tsx @@ -48,7 +48,8 @@ export default class ServerOfflineDialog extends React.PureComponent { private renderTimeline(): React.ReactElement[] { return EchoStore.instance.contexts.map((c, i) => { if (!c.firstFailedTime) return null; // not useful - if (!(c instanceof RoomEchoContext)) throw new Error("Cannot render unknown context: " + c); + if (!(c instanceof RoomEchoContext)) + throw new Error("Cannot render unknown context: " + c.constructor.name); const header = (
    diff --git a/src/components/views/settings/ProfileSettings.tsx b/src/components/views/settings/ProfileSettings.tsx index 05a44b8e4fc..7953c3e9653 100644 --- a/src/components/views/settings/ProfileSettings.tsx +++ b/src/components/views/settings/ProfileSettings.tsx @@ -185,6 +185,10 @@ export default class ProfileSettings extends React.Component<{}, IState> { withDisplayName: true, }); + // False negative result from no-base-to-string rule, doesn't seem to account for Symbol.toStringTag + // eslint-disable-next-line @typescript-eslint/no-base-to-string + const avatarUrl = this.state.avatarUrl?.toString(); + return ( {

    { reader.readAsArrayBuffer(value as Blob); }); } else { - metadata += `${key} = ${value}\n`; + metadata += `${key} = ${value as string}\n`; } } tape.append("issue.txt", metadata); diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index 5fe190179ec..1604e49778f 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -389,7 +389,7 @@ export class StopGapWidget extends EventEmitter { // Now open the integration manager // TODO: Spec this interaction. const data = ev.detail.data; - const integType = data?.integType; + const integType = data?.integType as string; const integId = data?.integId; // noinspection JSIgnoredPromiseFromCall diff --git a/src/utils/FileUtils.ts b/src/utils/FileUtils.ts index b9cd9a79d33..aa12f790b04 100644 --- a/src/utils/FileUtils.ts +++ b/src/utils/FileUtils.ts @@ -69,7 +69,7 @@ export function presentableTextForFile( // it since it is "ugly", users generally aren't aware what it // means and the type of the attachment can usually be inferred // from the file extension. - text += " (" + filesize(content.info.size) + ")"; + text += " (" + filesize(content.info.size) + ")"; } return text; } diff --git a/src/utils/Whenable.ts b/src/utils/Whenable.ts index 8cb50a91a6d..2e154fca65d 100644 --- a/src/utils/Whenable.ts +++ b/src/utils/Whenable.ts @@ -19,7 +19,7 @@ import { logger } from "matrix-js-sdk/src/logger"; import { IDestroyable } from "./IDestroyable"; import { arrayFastClone } from "./arrays"; -export type WhenFn = (w: Whenable) => void; +export type WhenFn = (w: Whenable) => void; /** * Whenables are a cheap way to have Observable patterns mixed with typical @@ -27,7 +27,7 @@ export type WhenFn = (w: Whenable) => void; * are intended to be used when a condition will be met multiple times and * the consumer needs to know *when* that happens. */ -export abstract class Whenable implements IDestroyable { +export abstract class Whenable implements IDestroyable { private listeners: { condition: T | null; fn: WhenFn }[] = []; /** diff --git a/src/utils/exportUtils/HtmlExport.tsx b/src/utils/exportUtils/HtmlExport.tsx index e915d180250..e2bc5604325 100644 --- a/src/utils/exportUtils/HtmlExport.tsx +++ b/src/utils/exportUtils/HtmlExport.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ReactNode } from "react"; +import React from "react"; import ReactDOM from "react-dom"; import { Room } from "matrix-js-sdk/src/models/room"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; @@ -65,7 +65,7 @@ export default class HTMLExporter extends Exporter { this.threadsEnabled = SettingsStore.getValue("feature_threadenabled"); } - protected async getRoomAvatar(): Promise { + protected async getRoomAvatar(): Promise { let blob: Blob | undefined = undefined; const avatarUrl = Avatar.avatarUrlForRoom(this.room, 32, 32, "crop"); const avatarPath = "room.png"; diff --git a/yarn.lock b/yarn.lock index b548d9f9136..aa88e900a0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4226,10 +4226,10 @@ eslint-plugin-jsx-a11y@^6.5.1: minimatch "^3.1.2" semver "^6.3.0" -eslint-plugin-matrix-org@0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-matrix-org/-/eslint-plugin-matrix-org-0.9.0.tgz#b2a5186052ddbfa7dc9878779bafa5d68681c7b4" - integrity sha512-+j6JuMnFH421Z2vOxc+0YMt5Su5vD76RSatviy3zHBaZpgd+sOeAWoCLBHD5E7mMz5oKae3Y3wewCt9LRzq2Nw== +eslint-plugin-matrix-org@0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-matrix-org/-/eslint-plugin-matrix-org-0.10.0.tgz#8d0998641a4d276343cae2abf253a01bb4d4cc60" + integrity sha512-L7ail0x1yUlF006kn4mHc+OT8/aYZI++i852YXPHxCbM1EY7jeg/fYAQ8tCx5+x08LyqXeS7inAVSL784m0C6Q== eslint-plugin-react-hooks@^4.3.0: version "4.6.0" From 54a6ce589f2bc1a7aafadf94ff34e1ebc4123a12 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 7 Feb 2023 10:09:46 +0000 Subject: [PATCH 89/97] Fix wrongly grouping 3pid invites into a single repeated transition (#10087) --- .../views/elements/EventListSummary.tsx | 35 ++++++------ .../views/elements/EventListSummary-test.tsx | 55 ++++++++++++++++++- 2 files changed, 70 insertions(+), 20 deletions(-) diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx index 4436b2d9bf7..60288fb2f56 100644 --- a/src/components/views/elements/EventListSummary.tsx +++ b/src/components/views/elements/EventListSummary.tsx @@ -507,39 +507,36 @@ export default class EventListSummary extends React.Component { eventsToRender.forEach((e, index) => { const type = e.getType(); - let userId = e.getSender(); - if (type === EventType.RoomMember) { - userId = e.getStateKey(); + let userKey = e.getSender()!; + if (type === EventType.RoomThirdPartyInvite) { + userKey = e.getContent().display_name; + } else if (type === EventType.RoomMember) { + userKey = e.getStateKey(); } else if (e.isRedacted()) { - userId = e.getUnsigned()?.redacted_because?.sender; + userKey = e.getUnsigned()?.redacted_because?.sender; } // Initialise a user's events - if (!userEvents[userId]) { - userEvents[userId] = []; + if (!userEvents[userKey]) { + userEvents[userKey] = []; } - let displayName = userId; - if (type === EventType.RoomThirdPartyInvite) { - displayName = e.getContent().display_name; - if (e.sender) { - latestUserAvatarMember.set(userId, e.sender); - } - } else if (e.isRedacted()) { - const sender = this.context?.room.getMember(userId); + let displayName = userKey; + if (e.isRedacted()) { + const sender = this.context?.room?.getMember(userKey); if (sender) { displayName = sender.name; - latestUserAvatarMember.set(userId, sender); + latestUserAvatarMember.set(userKey, sender); } } else if (e.target && TARGET_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) { displayName = e.target.name; - latestUserAvatarMember.set(userId, e.target); - } else if (e.sender) { + latestUserAvatarMember.set(userKey, e.target); + } else if (e.sender && type !== EventType.RoomThirdPartyInvite) { displayName = e.sender.name; - latestUserAvatarMember.set(userId, e.sender); + latestUserAvatarMember.set(userKey, e.sender); } - userEvents[userId].push({ + userEvents[userKey].push({ mxEvent: e, displayName, index: index, diff --git a/test/components/views/elements/EventListSummary-test.tsx b/test/components/views/elements/EventListSummary-test.tsx index ebf799e3f22..6f8c5fd7c8f 100644 --- a/test/components/views/elements/EventListSummary-test.tsx +++ b/test/components/views/elements/EventListSummary-test.tsx @@ -21,6 +21,7 @@ import { MatrixEvent, RoomMember } from "matrix-js-sdk/src/matrix"; import { getMockClientWithEventEmitter, + mkEvent, mkMembership, mockClientMethodsUser, unmockClientPeg, @@ -100,7 +101,7 @@ describe("EventListSummary", function () { // is created by replacing the first "$" in userIdTemplate with `i` for // `i = 0 .. n`. const generateEventsForUsers = (userIdTemplate, n, events) => { - let eventsForUsers = []; + let eventsForUsers: MatrixEvent[] = []; let userId = ""; for (let i = 0; i < n; i++) { userId = userIdTemplate.replace("$", i); @@ -656,4 +657,56 @@ describe("EventListSummary", function () { expect(summaryText).toBe("user_0, user_1 and 18 others joined"); }); + + it("should not blindly group 3pid invites and treat them as distinct users instead", () => { + const events = [ + mkEvent({ + event: true, + skey: "randomstring1", + user: "@user1:server", + type: "m.room.third_party_invite", + content: { + display_name: "n...@d...", + key_validity_url: "https://blah", + public_key: "public_key", + }, + }), + mkEvent({ + event: true, + skey: "randomstring2", + user: "@user1:server", + type: "m.room.third_party_invite", + content: { + display_name: "n...@d...", + key_validity_url: "https://blah", + public_key: "public_key", + }, + }), + mkEvent({ + event: true, + skey: "randomstring3", + user: "@user1:server", + type: "m.room.third_party_invite", + content: { + display_name: "d...@w...", + key_validity_url: "https://blah", + public_key: "public_key", + }, + }), + ]; + + const props = { + events: events, + children: generateTiles(events), + summaryLength: 2, + avatarsMaxLength: 5, + threshold: 3, + }; + + const wrapper = renderComponent(props); + const summary = wrapper.find(".mx_GenericEventListSummary_summary"); + const summaryText = summary.text(); + + expect(summaryText).toBe("n...@d... was invited 2 times, d...@w... was invited"); + }); }); From da2471ffbd85e8ed5d42cfe4a169c87b8f9b0a49 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 7 Feb 2023 11:14:19 +0000 Subject: [PATCH 90/97] Fix a crash when removing persistent widgets (updated) (#10099) * Fix a crash when removing persistent widgets When a persistent widget is removed, multiple calls to updateShowWidgetInPip happen in succession as each of the widget stores emit updates. But by depending on this.state.persistentWidgetId at the time of the call rather than passing an update function to setState, this had the effect that the removal of the widget could be reverted in the component's state, and so it could end up passing the ID of a removed widget to WidgetPip. * Re-public updateShowWidgetInPip so we don't change the interface --------- Co-authored-by: Robin Townsend --- src/components/structures/PipContainer.tsx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/components/structures/PipContainer.tsx b/src/components/structures/PipContainer.tsx index 7bbbb1c5682..0697f4a6da6 100644 --- a/src/components/structures/PipContainer.tsx +++ b/src/components/structures/PipContainer.tsx @@ -192,10 +192,7 @@ class PipContainerInner extends React.Component { }; private onWidgetPersistence = (): void => { - this.updateShowWidgetInPip( - ActiveWidgetStore.instance.getPersistentWidgetId(), - ActiveWidgetStore.instance.getPersistentRoomId(), - ); + this.updateShowWidgetInPip(); }; private onWidgetDockChanges = (): void => { @@ -234,11 +231,10 @@ class PipContainerInner extends React.Component { } }; - // Accepts a persistentWidgetId to be able to skip awaiting the setState for persistentWidgetId - public updateShowWidgetInPip( - persistentWidgetId = this.state.persistentWidgetId, - persistentRoomId = this.state.persistentRoomId, - ): void { + public updateShowWidgetInPip(): void { + const persistentWidgetId = ActiveWidgetStore.instance.getPersistentWidgetId(); + const persistentRoomId = ActiveWidgetStore.instance.getPersistentRoomId(); + let fromAnotherRoom = false; let notDocked = false; // Sanity check the room - the widget may have been destroyed between render cycles, and From 76c4341cae02205c7dd96353da7e42c3a1c1c294 Mon Sep 17 00:00:00 2001 From: Element Translate Bot Date: Tue, 7 Feb 2023 12:46:22 +0100 Subject: [PATCH 91/97] Translations update from Weblate (#10100) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (German) Currently translated at 100.0% (3712 of 3712 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (Arabic) Currently translated at 36.6% (1360 of 3712 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ar/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3712 of 3712 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% (3712 of 3712 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% (3712 of 3712 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 100.0% (3712 of 3712 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% (3712 of 3712 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ * Translated using Weblate (French) Currently translated at 100.0% (3712 of 3712 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/ * Translated using Weblate (Czech) Currently translated at 100.0% (3712 of 3712 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% (3712 of 3712 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/hu/ * Translated using Weblate (Japanese) Currently translated at 91.8% (3410 of 3712 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Finnish) Currently translated at 92.5% (3434 of 3712 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fi/ * Translated using Weblate (Italian) Currently translated at 100.0% (3712 of 3712 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/it/ * Translated using Weblate (Japanese) Currently translated at 93.6% (3478 of 3712 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Japanese) Currently translated at 93.8% (3484 of 3712 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Icelandic) Currently translated at 88.3% (3278 of 3712 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/is/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 77.4% (2876 of 3712 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/pt_BR/ * Translated using Weblate (Japanese) Currently translated at 94.4% (3506 of 3712 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Icelandic) Currently translated at 89.9% (3340 of 3712 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/is/ * Translated using Weblate (Japanese) Currently translated at 94.6% (3514 of 3712 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (German) Currently translated at 100.0% (3713 of 3713 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (Czech) Currently translated at 100.0% (3713 of 3713 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 100.0% (3715 of 3715 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% (3715 of 3715 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Japanese) Currently translated at 95.9% (3564 of 3715 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Czech) Currently translated at 100.0% (3715 of 3715 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% (3715 of 3715 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sk/ * Translated using Weblate (French) Currently translated at 100.0% (3715 of 3715 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.9% (3713 of 3715 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% (3715 of 3715 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/it/ * Translated using Weblate (Japanese) Currently translated at 96.5% (3587 of 3715 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Japanese) Currently translated at 97.0% (3604 of 3715 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (3715 of 3715 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/id/ * Translated using Weblate (Japanese) Currently translated at 97.0% (3606 of 3715 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Japanese) Currently translated at 97.0% (3606 of 3715 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Estonian) Currently translated at 100.0% (3715 of 3715 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ * Translated using Weblate (Japanese) Currently translated at 97.1% (3610 of 3715 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Czech) Currently translated at 100.0% (3717 of 3717 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 100.0% (3717 of 3717 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 100.0% (3719 of 3719 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (Slovak) Currently translated at 99.9% (3716 of 3719 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% (3718 of 3719 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ * Translated using Weblate (English (United States)) Currently translated at 12.2% (454 of 3719 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/en_US/ * Translated using Weblate (French) Currently translated at 100.0% (3719 of 3719 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/ * Translated using Weblate (Swedish) Currently translated at 100.0% (3719 of 3719 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sv/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3719 of 3719 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Japanese) Currently translated at 97.1% (3614 of 3719 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (3719 of 3719 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% (3719 of 3719 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sk/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (3719 of 3719 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ * Translated using Weblate (Japanese) Currently translated at 97.4% (3626 of 3719 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Czech) Currently translated at 100.0% (3719 of 3719 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ * Translated using Weblate (Japanese) Currently translated at 97.4% (3626 of 3719 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Japanese) Currently translated at 97.4% (3626 of 3719 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (German) Currently translated at 100.0% (3721 of 3721 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% (3721 of 3721 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% (3721 of 3721 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% (3721 of 3721 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/hu/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (3721 of 3721 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/id/ * Translated using Weblate (Albanian) Currently translated at 99.6% (3707 of 3721 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sq/ * Translated using Weblate (Slovak) Currently translated at 100.0% (3721 of 3721 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 100.0% (3723 of 3723 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.8% (3719 of 3723 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% (3723 of 3723 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% (3723 of 3723 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% (3723 of 3723 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 99.9% (3722 of 3723 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/it/ * Translated using Weblate (Japanese) Currently translated at 97.4% (3628 of 3723 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Estonian) Currently translated at 99.9% (3722 of 3723 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ * Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ * Translated using Weblate (German) Currently translated at 100.0% (3721 of 3721 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% (3721 of 3721 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/ * Translated using Weblate (Japanese) Currently translated at 97.6% (3635 of 3721 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Slovak) Currently translated at 100.0% (3721 of 3721 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sk/ * Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ * Translated using Weblate (Czech) Currently translated at 100.0% (3721 of 3721 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 100.0% (3722 of 3722 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (3722 of 3722 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/id/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3722 of 3722 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Estonian) Currently translated at 100.0% (3722 of 3722 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ * Translated using Weblate (Estonian) Currently translated at 100.0% (3722 of 3722 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ * Translated using Weblate (Japanese) Currently translated at 97.6% (3635 of 3722 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Japanese) Currently translated at 97.7% (3639 of 3722 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Slovak) Currently translated at 100.0% (3722 of 3722 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 100.0% (3724 of 3724 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% (3724 of 3724 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% (3724 of 3724 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% (3724 of 3724 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Japanese) Currently translated at 97.7% (3639 of 3724 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (3724 of 3724 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% (3724 of 3724 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ * Translated using Weblate (Japanese) Currently translated at 97.7% (3639 of 3724 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Japanese) Currently translated at 97.7% (3639 of 3724 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Estonian) Currently translated at 99.9% (3723 of 3724 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ * Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ * Translated using Weblate (German) Currently translated at 100.0% (3724 of 3724 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (Japanese) Currently translated at 97.6% (3638 of 3724 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Czech) Currently translated at 100.0% (3724 of 3724 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 99.9% (3721 of 3724 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% (3722 of 3724 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (3724 of 3724 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/id/ * Translated using Weblate (Japanese) Currently translated at 97.6% (3638 of 3724 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3724 of 3724 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Japanese) Currently translated at 97.6% (3638 of 3724 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Slovak) Currently translated at 100.0% (3724 of 3724 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 100.0% (3725 of 3725 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% (3725 of 3725 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% (3725 of 3725 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ * Translated using Weblate (Japanese) Currently translated at 97.6% (3638 of 3725 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (3725 of 3725 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% (3725 of 3725 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ * Translated using Weblate (Italian) Currently translated at 100.0% (3725 of 3725 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/it/ * Translated using Weblate (Japanese) Currently translated at 97.6% (3638 of 3725 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Czech) Currently translated at 100.0% (3725 of 3725 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 99.7% (3738 of 3748 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (Czech) Currently translated at 100.0% (3748 of 3748 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% (3748 of 3748 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 100.0% (3748 of 3748 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 100.0% (3748 of 3748 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 97.4% (3653 of 3748 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/es/ * Translated using Weblate (French) Currently translated at 100.0% (3748 of 3748 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% (3748 of 3748 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% (3748 of 3748 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% (3748 of 3748 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/it/ * Translated using Weblate (Japanese) Currently translated at 97.0% (3638 of 3748 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (3748 of 3748 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 100.0% (3748 of 3748 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ * Translated using Weblate (Japanese) Currently translated at 97.0% (3638 of 3748 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Swedish) Currently translated at 100.0% (3748 of 3748 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sv/ * Translated using Weblate (Japanese) Currently translated at 97.0% (3638 of 3748 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Albanian) Currently translated at 99.5% (3731 of 3748 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sq/ * Translated using Weblate (Japanese) Currently translated at 97.0% (3638 of 3748 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ * Translated using Weblate (German) Currently translated at 100.0% (3748 of 3748 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 100.0% (3748 of 3748 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ * Translated using Weblate (Japanese) Currently translated at 97.0% (3637 of 3748 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Slovak) Currently translated at 100.0% (3748 of 3748 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sk/ * Translated using Weblate (Japanese) Currently translated at 97.0% (3639 of 3748 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Ukrainian) Currently translated at 100.0% (3748 of 3748 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ * Translated using Weblate (Japanese) Currently translated at 97.4% (3654 of 3748 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ * Translated using Weblate (Japanese) Currently translated at 97.5% (3657 of 3748 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ --------- Co-authored-by: Weblate Co-authored-by: Vri Co-authored-by: Ali-x98 Co-authored-by: Ihor Hordiichuk Co-authored-by: Linerly Co-authored-by: Jozef Gaal Co-authored-by: Priit Jõerüüt Co-authored-by: Jeff Huang Co-authored-by: Glandos Co-authored-by: waclaw66 Co-authored-by: Balázs Meskó Co-authored-by: Suguru Hirahara Co-authored-by: Jiri Grönroos Co-authored-by: random Co-authored-by: Sveinn í Felli Co-authored-by: Thiago Blake Co-authored-by: Szimszon Co-authored-by: Aleksa Sarai Co-authored-by: theamazing0 Co-authored-by: LinAGKar Co-authored-by: Suguru Hirahara Co-authored-by: Besnik Bleta Co-authored-by: Clément Hampaï Co-authored-by: Levi Co-authored-by: Jairo Llopis Co-authored-by: Michael Weimann --- src/i18n/strings/cs.json | 51 ++- src/i18n/strings/de_DE.json | 48 ++- src/i18n/strings/el.json | 1 - src/i18n/strings/en_US.json | 5 +- src/i18n/strings/eo.json | 3 - src/i18n/strings/es.json | 5 +- src/i18n/strings/et.json | 53 ++- src/i18n/strings/fa.json | 1 - src/i18n/strings/fi.json | 2 - src/i18n/strings/fr.json | 47 ++- src/i18n/strings/gl.json | 2 - src/i18n/strings/he.json | 1 - src/i18n/strings/hu.json | 17 +- src/i18n/strings/id.json | 45 ++- src/i18n/strings/is.json | 2 - src/i18n/strings/it.json | 45 ++- src/i18n/strings/ja.json | 604 +++++++++++++++++++++------------- src/i18n/strings/lo.json | 2 - src/i18n/strings/nl.json | 3 - src/i18n/strings/pt_BR.json | 1 - src/i18n/strings/ru.json | 3 - src/i18n/strings/sk.json | 46 ++- src/i18n/strings/sq.json | 43 ++- src/i18n/strings/sv.json | 49 ++- src/i18n/strings/uk.json | 46 ++- src/i18n/strings/zh_Hans.json | 3 - src/i18n/strings/zh_Hant.json | 45 ++- 27 files changed, 844 insertions(+), 329 deletions(-) diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index 215c451dbbd..93a7313a826 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -3204,11 +3204,9 @@ "%(count)s participants|other": "%(count)s účastníků", "New video room": "Nová video místnost", "New room": "Nová místnost", - "Give feedback": "Poskytnout zpětnou vazbu", "%(featureName)s Beta feedback": "Zpětná vazba beta funkce %(featureName)s", "Beta feature. Click to learn more.": "Beta funkce. Kliknutím získáte další informace.", "Beta feature": "Beta funkce", - "Threads are a beta feature": "Vlákna jsou beta funkcí", "Threads help keep your conversations on-topic and easy to track.": "Vlákna pomáhají udržovat konverzace k tématu a snadno je sledovat.", "Threads help keep conversations on-topic and easy to track. Learn more.": "Vlákna pomáhají udržovat konverzace k tématu a snadno je sledovat. Další informace.", "Keep discussions organised with threads.": "Diskuse udržovat organizované pomocí vláken.", @@ -3532,7 +3530,6 @@ "Italic": "Kurzíva", "Notifications silenced": "Oznámení ztlumena", "Yes, stop broadcast": "Ano, zastavit vysílání", - "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Opravdu chcete ukončit živé vysílání? Tím se vysílání ukončí a v místnosti bude k dispozici celý záznam.", "Stop live broadcasting?": "Ukončit živé vysílání?", "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Hlasové vysílání už nahrává někdo jiný. Počkejte, až jeho hlasové vysílání skončí, a spusťte nové.", "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "Nemáte potřebná oprávnění ke spuštění hlasového vysílání v této místnosti. Obraťte se na správce místnosti, aby vám zvýšil oprávnění.", @@ -3641,7 +3638,6 @@ "Right panel stays open": "Pravý panel zůstane otevřený", "Currently experimental.": "V současnosti experimentální.", "New ways to ignore people": "Nové způsoby ignorování lidí", - "Use rich text instead of Markdown in the message composer. Plain text mode coming soon.": "V editoru zpráv používat formátovaný text namísto Markdownu. Brzy bude k dispozici režim prostého textu.", "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.": "V místnostech, které podporují moderování, můžete pomocí tlačítka \"Nahlásit\" nahlásit zneužití moderátorům místnosti.", "Report to moderators": "Nahlásit moderátorům", "You have unverified sessions": "Máte neověřené relace", @@ -3661,19 +3657,19 @@ "%(senderName)s ended a voice broadcast": "%(senderName)s ukončil(a) hlasové vysílání", "You ended a voice broadcast": "Ukončili jste hlasové vysílání", "Threaded messages": "Zprávy ve vláknech", - "Unable to decrypt message": "Nelze dešifrovat zprávu", + "Unable to decrypt message": "Nepodařilo se dešifrovat zprávu", "This message could not be decrypted": "Tuto zprávu se nepodařilo dešifrovat", "Resend key requests": "Opětovně odeslat žádosti o klíč", "Unfortunately, there are no other verified devices to request decryption keys from. Signing in and verifying other devices may help avoid this situation in the future.": "Bohužel neexistují žádná další ověřená zařízení, ze kterých by si bylo možné vyžádat dešifrovací klíče. Přihlášení a ověření dalších zařízení může pomoci této situaci v budoucnu předejít.", "Some messages could not be decrypted": "Některé zprávy nebylo možné dešifrovat", "View your device list": "Zobrazit seznam vašich zařízení", - "This device is requesting decryption keys from your other devices. Opening one of your other devices may speed this up.": "Toto zařízení si vyžádá dešifrovací klíče z ostatních zařízení. Otevření některého z vašich dalších zařízení to může urychlit.", + "This device is requesting decryption keys from your other devices. Opening one of your other devices may speed this up.": "Toto zařízení požaduje dešifrovací klíče z vašich ostatních zařízení. Otevření některého z vašich dalších zařízení to může urychlit.", "Open another device to load encrypted messages": "Otevřete jiné zařízení pro načtení zašifrovaných zpráv", "You will not be able to access old undecryptable messages, but resetting your keys will allow you to receive new messages.": "Ke starým nedešifrovatelným zprávám nebudete mít přístup, ale resetování klíčů vám umožní přijímat nové zprávy.", "Reset your keys to prevent future decryption errors": "Resetujte své klíče, abyste předešli budoucím chybám při dešifrování", "This device was unable to decrypt some messages because it has not been verified yet.": "Toto zařízení nebylo schopno dešifrovat některé zprávy, protože dosud nebylo ověřeno.", "Verify this device to access all messages": "Ověřte toto zařízení pro přístup ke všem zprávám", - "Please wait as we try to decrypt your messages. This may take a few moments.": "Počkejte prosím, než se pokusíme vaše zprávy dešifrovat. Může to chvíli trvat.", + "Please wait as we try to decrypt your messages. This may take a few moments.": "Počkejte prosím, zatímco se snažíme dešifrovat vaše zprávy. Může to chvíli trvat.", "Decrypting messages...": "Dešifrování zpráv...", "%(senderName)s ended a voice broadcast": "%(senderName)s ukončil(a) hlasové vysílání", "You ended a voice broadcast": "Ukončili jste hlasové vysílání", @@ -3710,5 +3706,44 @@ "Decrypted source unavailable": "Dešifrovaný zdroj není dostupný", "Connection error - Recording paused": "Chyba připojení - nahrávání pozastaveno", "%(senderName)s started a voice broadcast": "%(senderName)s zahájil(a) hlasové vysílání", - "%(displayName)s (%(matrixId)s)": "%(displayName)s (%(matrixId)s)" + "%(displayName)s (%(matrixId)s)": "%(displayName)s (%(matrixId)s)", + "Unable to play this voice broadcast": "Nelze přehrát toto hlasové vysílání", + "Registration token": "Registrační token", + "Enter a registration token provided by the homeserver administrator.": "Zadejte registrační token poskytnutý správcem domovského serveru.", + "Manage account": "Spravovat účet", + "Your account details are managed separately at %(hostname)s.": "Údaje o vašem účtu jsou spravovány samostatně na adrese %(hostname)s.", + "Enable MSC3946 (to support late-arriving room archives)": "Povolit MSC3946 (podpora pozdních archivů místností)", + "Dynamic room predecessors": "Předchůdci dynamické místnosti", + "All messages and invites from this user will be hidden. Are you sure you want to ignore them?": "Všechny zprávy a pozvánky od tohoto uživatele budou skryty. Opravdu je chcete ignorovat?", + "Ignore %(user)s": "Ignorovat %(user)s", + "Indent decrease": "Zmenšit odsazení", + "Indent increase": "Zvětšit odsazení", + "View a list of polls in a room. (Under active development)": "Zobrazit seznam hlasování v místnosti. (V aktivním vývoji)", + "Polls history": "Historie hlasování", + "Unable to decrypt voice broadcast": "Nelze dešifrovat hlasové vysílání", + "Use rich text instead of Markdown in the message composer.": "V editoru zpráv použít formátovaný text namísto Markdown.", + "There are no polls in this room": "V této místnosti nejsou žádná hlasování", + "Thread Id: ": "Id vlákna: ", + "Threads timeline": "Časová osa vláken", + "Sender: ": "Odesílatel: ", + "Type: ": "Typ: ", + "ID: ": "ID: ", + "Last event:": "Poslední událost:", + "No receipt found": "Žádné potvrzení o přečtení", + "User read up to: ": "Uživatel přečetl až: ", + "Dot: ": "Tečka: ", + "Highlight: ": "Nejdůležitější: ", + "Total: ": "Celkem: ", + "Main timeline": "Hlavní časová osa", + "not encrypted 🚨": "nešifrovaná 🚨", + "encrypted ✅": "šifrovaná ✅", + "Room is ": "Místnost je ", + "Notification state is": "Stav oznámení je", + ", count:": ", počet:", + "Room unread status: ": "Stav nepřečtené místnosti: ", + "Room status": "Stav místnosti", + "Notifications debug": "Ladění oznámení", + "unknown": "neznámé", + "Red": "Červená", + "Grey": "Šedá" } diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 87e0951831b..967faa1fc1b 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -1298,7 +1298,7 @@ "Accepting…": "Annehmen…", "Start Verification": "Verifizierung starten", "Messages in this room are end-to-end encrypted.": "Nachrichten in diesem Raum sind Ende-zu-Ende verschlüsselt.", - "Your messages are secured and only you and the recipient have the unique keys to unlock them.": "Diese Nachrichten sind verschlüsselt und nur du und der Empfänger könnt sie lesen.", + "Your messages are secured and only you and the recipient have the unique keys to unlock them.": "Diese Nachricht ist verschlüsselt. Nur Sie und der Empfänger haben den Schlüssel, um die Nachricht zu entschlüsseln.", "In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.": "Nachrichten in verschlüsselten Räumen können nur von dir und vom Empfänger gelesen werden.", "Verify User": "Nutzer verifizieren", "For extra security, verify this user by checking a one-time code on both of your devices.": "Für zusätzliche Sicherheit, verifiziere diesen Nutzer, durch Vergleichen eines Einmal-Codes auf euren beiden Geräten.", @@ -3246,8 +3246,6 @@ "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "Du wurdest von allen Geräten abgemeldet und erhältst keine Push-Benachrichtigungen mehr. Um Benachrichtigungen wieder zu aktivieren, melde dich auf jedem Gerät erneut an.", "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.": "Wenn du dein Passwort zurücksetzt, werden all deine anderen Geräte abgemeldet. Wenn auf diesen Ende-zu-Ende-Schlüssel gespeichert sind, kann der Verlauf deiner verschlüsselten Unterhaltungen verloren gehen.", "Event ID: %(eventId)s": "Event-ID: %(eventId)s", - "Give feedback": "Rückmeldung geben", - "Threads are a beta feature": "Threads sind eine Betafunktion", "Threads help keep your conversations on-topic and easy to track.": "Threads helfen dabei, dass deine Konversationen beim Thema und leicht nachverfolgbar bleiben.", "Your message wasn't sent because this homeserver has been blocked by its administrator. Please contact your service administrator to continue using the service.": "Deine Nachricht wurde nicht gesendet, weil dieser Heim-Server von dessen Administration gesperrt wurde. Bitte kontaktiere deine Dienstadministration, um den Dienst weiterzunutzen.", "Video rooms": "Videoräume", @@ -3531,7 +3529,6 @@ "Italic": "Kursiv", "Underline": "Unterstrichen", "Notifications silenced": "Benachrichtigungen stummgeschaltet", - "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Möchtest du die Übertragung wirklich beenden? Dies wird die Übertragung beenden und die vollständige Aufnahme im Raum bereitstellen.", "Yes, stop broadcast": "Ja, Übertragung beenden", "Stop live broadcasting?": "Live-Übertragung beenden?", "Sign in with QR code": "Mit QR-Code anmelden", @@ -3627,7 +3624,6 @@ "What's next for %(brand)s? Labs are the best way to get things early, test out new features and help shape them before they actually launch.": "Was passiert als nächstes in %(brand)s? Das Labor ist deine erste Anlaufstelle, um Funktionen früh zu erhalten, zu testen und mitzugestalten, bevor sie tatsächlich veröffentlicht werden.", "Upcoming features": "Zukünftige Funktionen", "Low bandwidth mode": "Modus für geringe Bandbreite", - "Use rich text instead of Markdown in the message composer. Plain text mode coming soon.": "Nutze direkte Formatierungen statt Markdown im Eingabefeld. Einen Klartextmodus gibt’s auch bald.", "Rich text editor": "Textverarbeitungs-Editor", "WARNING: ": "WARNUNG: ", "Requires compatible homeserver.": "Benötigt kompatiblen Heim-Server.", @@ -3710,5 +3706,45 @@ "Decrypted source unavailable": "Entschlüsselte Quelle nicht verfügbar", "Connection error - Recording paused": "Verbindungsfehler − Aufnahme pausiert", "%(senderName)s started a voice broadcast": "%(senderName)s begann eine Sprachübertragung", - "%(displayName)s (%(matrixId)s)": "%(displayName)s (%(matrixId)s)" + "%(displayName)s (%(matrixId)s)": "%(displayName)s (%(matrixId)s)", + "Unable to play this voice broadcast": "Wiedergabe der Sprachübertragung nicht möglich", + "Registration token": "Registrierungstoken", + "Enter a registration token provided by the homeserver administrator.": "Gib einen von deiner Home-Server-Administration zur Verfügung gestellten Registrierungstoken ein.", + "Manage account": "Konto verwalten", + "Your account details are managed separately at %(hostname)s.": "Deine Kontodaten werden separat auf %(hostname)s verwaltet.", + "Dynamic room predecessors": "Veränderbare Raumvorgänger", + "Enable MSC3946 (to support late-arriving room archives)": "MSC3946 aktivieren (zur Verknüpfung von Raumarchiven nach der Raumerstellung)", + "All messages and invites from this user will be hidden. Are you sure you want to ignore them?": "Alle Nachrichten und Einladungen der Person werden verborgen. Bist du sicher, dass du sie ignorieren möchtest?", + "Ignore %(user)s": "%(user)s ignorieren", + "Indent decrease": "Einrückung verringern", + "Indent increase": "Einrückung erhöhen", + "Unable to decrypt voice broadcast": "Entschlüsseln der Sprachübertragung nicht möglich", + "View a list of polls in a room. (Under active development)": "Betrachte Raumumfragen in Listenform. (In aktiver Entwicklung)", + "Polls history": "Umfrageverlauf", + "Use rich text instead of Markdown in the message composer.": "Verwende Textverarbeitung (Rich-Text) statt Markdown im Eingabefeld.", + "There are no polls in this room": "In diesem Raum gibt es keine Umfragen", + "Thread Id: ": "Thread-ID: ", + "Threads timeline": "Thread-Verlauf", + "Type: ": "Typ: ", + "ID: ": "ID: ", + "Last event:": "Neuestes Ereignis:", + "Total: ": "Insgesamt: ", + "Main timeline": "Hauptverlauf", + "not encrypted 🚨": "nicht verschlüsselt 🚨", + "encrypted ✅": "verschlüsselt ✅", + "Room status": "Raumstatus", + "unknown": "unbekannt", + "Red": "Rot", + "Grey": "Grau", + "Sender: ": "Absender: ", + "No receipt found": "Keine Bestätigung gefunden", + "User read up to: ": "Der Benutzer hat gelesen bis: ", + "Dot: ": "Punkt: ", + "Highlight: ": "Höhepunkt: ", + "Room is ": "Der Raum ist ", + "Notification state is": "Der Benachrichtigung-Status ist", + ", count:": ", Anzahl:", + "Room unread status: ": "Ungelesenstatus des Raumes: ", + "Notifications debug": "Debug-Modus für Benachrichtigungen", + "Are you sure you want to stop your live broadcast? This will end the broadcast and the full recording will be available in the room.": "Möchtest du deine Übertragung wirklich beenden? Dies wird die Übertragung abschließen und die vollständige Aufnahme im Raum bereitstellen." } diff --git a/src/i18n/strings/el.json b/src/i18n/strings/el.json index 89d45fe1047..46bc0854dce 100644 --- a/src/i18n/strings/el.json +++ b/src/i18n/strings/el.json @@ -3198,7 +3198,6 @@ "Close dialog or context menu": "Κλείσιμο διαλόγου ή μενού περιβάλλοντος", "Threads help keep conversations on-topic and easy to track. Learn more.": "Τα νήματα βοηθούν στην καλύτερη οργάνωση των συζητήσεων και στην εύκολη παρακολούθηση. Μάθετε περισσότερα.", "Keep discussions organised with threads.": "Διατηρήστε τις συζητήσεις οργανωμένες σε νήματα.", - "Threads are a beta feature": "Τα νήματα είναι μια δοκιμαστική δυνατότητα", "Close sidebar": "Κλείσιμο πλαϊνής γραμμής", "View List": "Προβολή Λίστας", "View list": "Προβολή λίστας", diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index 23bf4779c46..4c6f3a899f3 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -451,5 +451,8 @@ "Answered Elsewhere": "Answered Elsewhere", "The call could not be established": "The call could not be established", "The user you called is busy.": "The user you called is busy.", - "User Busy": "User Busy" + "User Busy": "User Busy", + "%(seconds)ss left": "%(seconds)ss left", + "%(minutes)sm %(seconds)ss left": "%(minutes)sm %(seconds)ss left", + "%(hours)sh %(minutes)sm %(seconds)ss left": "%(hours)sh %(minutes)sm %(seconds)ss left" } diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index 3c7a2209a87..0022a17d5db 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -2876,7 +2876,6 @@ "Unverified sessions": "Nekontrolitaj salutaĵoj", "From the beginning": "De la komenco", "Current Timeline": "Nuna historio", - "Use rich text instead of Markdown in the message composer. Plain text mode coming soon.": "Uzu riĉan tekston anstataŭ Markdown en la mesaĝkomponilo. Plata teksta reĝimo baldaŭ venos.", "Plain Text": "Plata Teksto", "Show HTML representation of room topics": "Montru HTML-prezenton de ĉambrotemoj", "Creating HTML...": "Kreante HTML...", @@ -2888,7 +2887,6 @@ "Media omitted - file size limit exceeded": "Amaskomunikilaro preterlasis - dosiero tro granda", "Select from the options below to export chats from your timeline": "Elektu el la subaj elektoj por eksporti babilojn el via historio", "Public rooms": "Publikajn ĉambrojn", - "Give feedback": "Doni komentojn", "Results not as expected? Please give feedback.": "Rezultoj ne kiel atenditaj? Bonvolu doni komentojn.", "Show details": "Montri detalojn", "Hide details": "Kaŝi detalojn", @@ -2897,7 +2895,6 @@ "Push notifications": "Puŝaj sciigoj", "IP address": "IP-adreso", "Browser": "Retumilo", - "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Ĉu vi certas, ke vi volas fini la elsendon? Ĉi tio finos la transdonon kaj provizos la plenan registradon en la ĉambro.", "Add privileged users": "Aldoni rajtigitan uzanton", "Number of messages": "Nombro da mesaĝoj", "Number of messages can only be a number between %(min)s and %(max)s": "Nombro da mesaĝoj povas esti nur nombro inter %(min)s kaj %(max)s", diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index 63b5b8c5ecf..8b8dfd25a89 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -454,7 +454,7 @@ "%(items)s and %(count)s others|other": "%(items)s y otros %(count)s", "%(items)s and %(count)s others|one": "%(items)s y otro más", "collapse": "encoger", - "expand": "despregar", + "expand": "desplegar", "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "No se pudo cargar el evento al que se respondió, bien porque no existe o no tiene permiso para verlo.", "In reply to ": "Respondiendo a ", "And %(count)s more...|other": "Y %(count)s más…", @@ -3164,8 +3164,6 @@ "Send custom room account data event": "Enviar evento personalizado de cuenta de la sala", "Send custom timeline event": "Enviar evento personalizado de historial de mensajes", "Help us identify issues and improve %(analyticsOwner)s by sharing anonymous usage data. To understand how people use multiple devices, we'll generate a random identifier, shared by your devices.": "Ayúdanos a identificar problemas y a mejorar %(analyticsOwner)s. Comparte datos anónimos sobre cómo usas la aplicación para que entendamos mejor cómo usa la gente varios dispositivos. Generaremos un identificador aleatorio que usarán todos tus dispositivos.", - "Give feedback": "Danos tu opinión", - "Threads are a beta feature": "Los hilos son una funcionalidad beta", "Create room": "Crear sala", "Create a video room": "Crear una sala de vídeo", "Create video room": "Crear sala de vídeo", @@ -3587,7 +3585,6 @@ "Right panel stays open": "El panel derecho se mantiene abierto", "Currently experimental.": "Actualmente en fase experimental.", "New ways to ignore people": "Nuevas maneras de ignorar a otras personas", - "Use rich text instead of Markdown in the message composer. Plain text mode coming soon.": "Usar el editor de texto enriquecido en lugar de Markdown en la barra de escritura. El modo de texto plano estará disponible próximamente.", "Rich text editor": "Editor de texto enriquecido", "Threaded messages": "Hilos de mensajes", "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.": "En las salas que sean compatible con la moderación, el botón de «Denunciar» avisará a los moderadores de la sala.", diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index 93d3b573e57..90c636677dd 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -2701,7 +2701,7 @@ "Verify with Security Key or Phrase": "Verifitseeri turvavõtme või turvafraasiga", "Skip verification for now": "Jäta verifitseerimine praegu vahele", "Really reset verification keys?": "Kas tõesti kustutame kõik verifitseerimisvõtmed?", - "Create poll": "Koosta üks küsitlus", + "Create poll": "Loo selline küsitlus", "Space Autocomplete": "Kogukonnakeskuste dünaamiline otsing", "Updating spaces... (%(progress)s out of %(count)s)|one": "Uuendan kogukonnakeskust...", "Updating spaces... (%(progress)s out of %(count)s)|other": "Uuendan kogukonnakeskuseid... (%(progress)s / %(count)s)", @@ -2786,7 +2786,7 @@ "Write something...": "Kirjuta midagi...", "Question or topic": "Küsimus või teema", "What is your poll question or topic?": "Mis on küsitluse teema?", - "Create Poll": "Koosta üks küsitlus", + "Create Poll": "Loo selline küsitlus", "You do not have permission to start polls in this room.": "Sul ei ole õigusi küsitluste korraldamiseks siin jututoas.", "Copy link to thread": "Kopeeri jutulõnga link", "Thread options": "Jutulõnga valikud", @@ -3058,8 +3058,8 @@ "Show current avatar and name for users in message history": "Sõnumite ajaloos leiduvate kasutajate puhul näita kehtivat tunnuspilti ning nime", "Results will be visible when the poll is ended": "Tulemused on näha siis, kui küsitlus on lõppenud", "Poll type": "Küsitluse tüüp", - "Open poll": "Pooleliolev küsitlus", - "Closed poll": "Lõppenud küsitlus", + "Open poll": "Avatud valikutega küsitlus", + "Closed poll": "Suletud valikutega küsitlus", "Voters see results as soon as they have voted": "Osalejad näevad tulemusi kohe peale oma valiku tegemist", "Results are only revealed when you end the poll": "Tulemused on näha vaid siis, kui küsitlus in lõppenud", "Search Dialog": "Otsinguvaade", @@ -3204,8 +3204,6 @@ "%(count)s participants|other": "%(count)s oselejat", "New video room": "Uus videotuba", "New room": "Uus jututuba", - "Give feedback": "Jaga tagasisidet", - "Threads are a beta feature": "Jutulõngad on beetajärgus funktsionaalsus", "Threads help keep your conversations on-topic and easy to track.": "Jutulõngad aitavad hoida vestlused teemakohastena ning mugavalt loetavatena.", "%(featureName)s Beta feedback": "%(featureName)s beetaversiooni tagasiside", "Beta feature. Click to learn more.": "Beetafunktsionaalsus. Lisateabe lugemiseks klõpsi siin.", @@ -3557,7 +3555,6 @@ "You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.": "Sa saad kasutada seda seadet mõne muu seadme logimiseks Matrix'i võrku QR-koodi alusel. Selleks skaneeri võrgust väljalogitud seadmega seda QR-koodi.", "Allow a QR code to be shown in session manager to sign in another device (requires compatible homeserver)": "Teise seadme sisselogimiseks luba QR-koodi kuvamine sessioonihalduris (eeldab, et koduserver sellist võimalust toetab)", "Yes, stop broadcast": "Jah, lõpeta", - "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Kas sa oled kindel, et soovid otseeetri lõpetada? Sellega ringhäälingukõne salvestamine lõppeb ja salvestis on kättesaadav kõigile jututoas.", "Stop live broadcasting?": "Kas lõpetame otseeetri?", "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Keegi juba salvestab ringhäälingukõnet. Uue ringhäälingukõne salvestamiseks palun oota, kuni see teine ringhäälingukõne on lõppenud.", "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "Sul pole piisavalt õigusi selles jututoas ringhäälingukõne algatamiseks. Õiguste lisamiseks palun võta ühendust jututoa haldajaga.", @@ -3628,7 +3625,6 @@ "Right panel stays open": "Parem paan jääb avatuks", "Currently experimental.": "Parasjagu katsejärgus.", "New ways to ignore people": "Uued võimalused osalejate eiramiseks", - "Use rich text instead of Markdown in the message composer. Plain text mode coming soon.": "Sõnumi kirjutamisel võid tavalise Markdown-vormingu asemel kasutada kujundatud teksti. Lihtteksti kasutamise võimalus lisandub varsti.", "Rich text editor": "Kujundatud teksti toimeti", "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.": "Kui jututoas on modereerimine kasutusel, siis nupust „Teata sisust“ avaneva vormi abil saad jututoa reegleid rikkuvast sisust teatada moderaatoritele.", "Report to moderators": "Teata moderaatoritele", @@ -3710,5 +3706,44 @@ "Decrypted source unavailable": "Dekrüptitud lähteandmed pole saadaval", "Connection error - Recording paused": "Viga võrguühenduses - salvestamine on peatatud", "%(senderName)s started a voice broadcast": "%(senderName)s alustas ringhäälingukõnet", - "%(displayName)s (%(matrixId)s)": "%(displayName)s (%(matrixId)s)" + "%(displayName)s (%(matrixId)s)": "%(displayName)s (%(matrixId)s)", + "Registration token": "Registreerimise tunnuskood", + "Enter a registration token provided by the homeserver administrator.": "Sisesta koduserveri haldaja poolt antud tunnuskood.", + "Unable to play this voice broadcast": "Selle ringhäälingukõne esitamine ei õnnestu", + "Enable MSC3946 (to support late-arriving room archives)": "Võta kasutusele MSC3946 (jututoa ajaloo aeglane laadimine)", + "Your account details are managed separately at %(hostname)s.": "Sinu kasutajakonto lisateave on hallatav siin serveris - %(hostname)s.", + "Manage account": "Halda kasutajakontot", + "Indent decrease": "Vähenda taandrida", + "Indent increase": "Suurenda taandrida", + "All messages and invites from this user will be hidden. Are you sure you want to ignore them?": "Kõik selle kasutaja sõnumid ja kutsed saava olema peidetud. Kas sa oled kindel, et soovid teda eirata?", + "Ignore %(user)s": "Eira kasutajat %(user)s", + "Dynamic room predecessors": "Jututoa dünaamilised eellased", + "Unable to decrypt voice broadcast": "Ringhäälingukõne dekrüptimine ei õnnestu", + "Polls history": "Küsitluste ajalugu", + "View a list of polls in a room. (Under active development)": "Vaata jututoas leiduvaid küsitlusi (funktsionaalsus on aktiivsel arendamisel)", + "Thread Id: ": "Jutulõnga tunnus: ", + "Threads timeline": "Jutulõngade ajajoon", + "Sender: ": "Saatja: ", + "Type: ": "Tüüp: ", + "ID: ": "ID: ", + "Last event:": "Viimane sündmus:", + "No receipt found": "Lugemisteatist ei leidu", + "User read up to: ": "Kasutaja on lugenud kuni: ", + "Dot: ": "Punkt: ", + "Highlight: ": "Esiletõstetud: ", + "Total: ": "Kokku: ", + "Main timeline": "Peamine ajajoon", + "not encrypted 🚨": "krüptimata 🚨", + "encrypted ✅": "krüptitud ✅", + "Room is ": "Jututuba on ", + "Notification state is": "Teavituste olek on", + ", count:": ", kokku:", + "Room unread status: ": "Jututoa lugemata sõnumite olek: ", + "Room status": "Jututoa sõnumite olek", + "There are no polls in this room": "Selles jututoas pole küsitlusi", + "Notifications debug": "Teavituste silumine", + "Use rich text instead of Markdown in the message composer.": "Sõnumite kirjutamisel kasuta Markdown'i asemel täisfunktsionaalset küljendust.", + "unknown": "teadmata", + "Red": "Punane", + "Grey": "Hall" } diff --git a/src/i18n/strings/fa.json b/src/i18n/strings/fa.json index 4a81f5c5103..eeac8b9c606 100644 --- a/src/i18n/strings/fa.json +++ b/src/i18n/strings/fa.json @@ -2582,7 +2582,6 @@ "resume voice broadcast": "بازگشت به صدای جمعی", "play voice broadcast": "پخش صدای جمعی", "Yes, stop broadcast": "بله، توقف ارسال جمعی", - "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "آیا میخواهید ارسال جمعی زنده متوقف شود؟ ارسال جمعی متوقف شده و کل صدای ضبط شده اتاق در دسترس خواهد بود.", "Stop live broadcasting?": "آیا ارسال جمعی زنده متوقف شود؟", "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "شخص دیگری در حال ضبط صدا برای ارسال جمعی است. برای ارسال صدای جمعی باید منتظر بمانید تا کار ایشان به پایان برسد.", "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "شما دسترسی لازم برای ارسال صدای جمعی در این اتاق را ندارید. لطفا با مدیر اتاق تماس بگیرید.", diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index 0b2ef3e28cb..fd5a6ff7680 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -2795,7 +2795,6 @@ "Device verified": "Laite vahvistettu", "Verify this device": "Vahvista tämä laite", "Unable to verify this device": "Tätä laitetta ei voitu vahvistaa", - "Give feedback": "Anna palautetta", "Failed to load list of rooms.": "Huoneluettelon lataaminen epäonnistui.", "Joined": "Liitytty", "Joining": "Liitytään", @@ -2982,7 +2981,6 @@ "%(senderName)s removed %(targetName)s": "%(senderName)s poisti %(targetName)s", "%(senderName)s removed %(targetName)s: %(reason)s": "%(senderName)s poisti %(targetName)s: %(reason)s", "Jump to the given date in the timeline": "Siirry annetulle päivälle aikajanalla", - "Threads are a beta feature": "Ketjut ovat beetaominaisuus", "Keep discussions organised with threads": "Pidä keskustelut järjestyksessä ketjuissa", "Show all threads": "Näytä kaikki ketjut", "Shows all threads you've participated in": "Näyttää kaikki ketjut, joissa olet ollut osallinen", diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index a33b28bed25..17221e65dde 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -2174,7 +2174,7 @@ "Recently visited rooms": "Salons visités récemment", "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.": "Sauvegardez vos clés de chiffrement et les données de votre compte au cas où vous perdiez l’accès à vos sessions. Vos clés seront sécurisés avec une Clé de Sécurité unique.", "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Mettre en cache localement et de manière sécurisée les messages chiffrés pour qu’ils apparaissent dans les résultats de recherche, en utilisant %(size)s pour stocker les messages de %(rooms)s salons.", - "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Mettre en cache localement et de manière sécurisée les messages chiffrés pour qu’ils apparaissent dans les résultats de recherche, en utilisant %(size)s pour stocker les messages de %(rooms)s salons.", + "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Mettre en cache localement et de manière sécurisée les messages chiffrés pour qu’ils apparaissent dans les résultats de recherche. Actuellement %(size)s sont utilisé pour stocker les messages de %(rooms)s salons.", "Channel: ": "Canal : ", "Workspace: ": "Espace de travail : ", "Dial pad": "Pavé de numérotation", @@ -3164,8 +3164,6 @@ "%(value)sm": "%(value)sm", "%(value)sh": "%(value)sh", "%(value)sd": "%(value)sd", - "Give feedback": "Faire un commentaire", - "Threads are a beta feature": "Les fils de discussion sont une fonctionnalité bêta", "Threads help keep your conversations on-topic and easy to track.": "Les fils de discussion vous permettent de recentrer vos conversations et de les rendre facile à suivre.", "An error occurred while stopping your live location, please try again": "Une erreur s’est produite en arrêtant le partage de votre position, veuillez réessayer", "Create room": "Créer un salon", @@ -3557,7 +3555,6 @@ "Browser": "Navigateur", "Notifications silenced": "Notifications silencieuses", "Yes, stop broadcast": "Oui, arrêter la diffusion", - "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Êtes-vous sûr de vouloir arrêter votre diffusion en direct ? Cela terminera la diffusion et l’enregistrement complet sera disponible dans le salon.", "Stop live broadcasting?": "Arrêter la diffusion en direct ?", "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Une autre personne est déjà en train de réaliser une diffusion audio. Attendez que sa diffusion audio soit terminée pour en démarrer une nouvelle.", "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "Vous n’avez pas les permissions requises pour démarrer une nouvelle diffusion audio dans ce salon. Contactez un administrateur du salon pour mettre-à-jour vos permissions.", @@ -3640,7 +3637,6 @@ "Right panel stays open": "Le panneau de droite reste ouvert", "Currently experimental.": "Actuellement expérimental.", "New ways to ignore people": "Nouvelles manières d’ignorer des gens", - "Use rich text instead of Markdown in the message composer. Plain text mode coming soon.": "Utilise le texte formaté au lieu de Markdown dans le compositeur de message. Le mode texte brut arrive bientôt.", "Rich text editor": "Éditeur de texte formaté", "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.": "Dans les salons prenant en charge la modération, le bouton « Signaler » vous permet de signaler des abus aux modérateurs du salon.", "Report to moderators": "Signaler aux modérateurs", @@ -3710,5 +3706,44 @@ "Decrypted source unavailable": "Source déchiffrée non disponible", "Connection error - Recording paused": "Erreur de connexion – Enregistrement en pause", "%(senderName)s started a voice broadcast": "%(senderName)s a démarré une diffusion audio", - "%(displayName)s (%(matrixId)s)": "%(displayName)s (%(matrixId)s)" + "%(displayName)s (%(matrixId)s)": "%(displayName)s (%(matrixId)s)", + "Registration token": "Jeton d’enregistrement", + "Enter a registration token provided by the homeserver administrator.": "Saisissez un jeton d’enregistrement fourni par l’administrateur du serveur d’accueil.", + "Unable to play this voice broadcast": "Impossible de lire cette diffusion audio", + "Manage account": "Gérer le compte", + "Your account details are managed separately at %(hostname)s.": "Les détails de votre compte sont gérés séparément sur %(hostname)s.", + "Enable MSC3946 (to support late-arriving room archives)": "Active MSC3946 (pour prendre en charge les archives de salon après création)", + "Dynamic room predecessors": "Prédécesseurs de salon dynamique", + "All messages and invites from this user will be hidden. Are you sure you want to ignore them?": "Tous les messages et invitations de cette utilisateur seront cachés. Êtes-vous sûr de vouloir les ignorer ?", + "Ignore %(user)s": "Ignorer %(user)s", + "Indent decrease": "Réduire l’indentation", + "Indent increase": "Augmenter l’indentation", + "View a list of polls in a room. (Under active development)": "Voir une liste des sondages présents dans un salon. (En cours de développement)", + "Polls history": "Historique des sondages", + "Unable to decrypt voice broadcast": "Impossible de décrypter la diffusion audio", + "There are no polls in this room": "Il n’y a aucun sondage dans ce salon", + "Use rich text instead of Markdown in the message composer.": "Utilise le texte formaté au lieu de Markdown dans le compositeur de message.", + "Thread Id: ": "Id du fil de discussion : ", + "Threads timeline": "Historique des fils de discussion", + "Sender: ": "Expéditeur : ", + "Type: ": "Type : ", + "ID: ": "ID : ", + "Last event:": "Dernier évènement :", + "No receipt found": "Aucun accusé disponible", + "User read up to: ": "L’utilisateur a lu jusqu’à : ", + "Dot: ": "Point : ", + "Highlight: ": "Mentions : ", + "Total: ": "Total : ", + "Main timeline": "Historique principal", + "not encrypted 🚨": "non chiffré 🚨", + "encrypted ✅": "chiffré ✅", + "Room is ": "Le salon est ", + "Notification state is": "L’état de notification est", + ", count:": ", total :", + "Room unread status: ": "Statut non-lu du salon : ", + "Room status": "Statut du salon", + "Notifications debug": "Débogage des notifications", + "unknown": "inconnu", + "Red": "Rouge", + "Grey": "Gris" } diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 0439679e1d8..eb25013ba40 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -3172,8 +3172,6 @@ "User is already invited to the space": "A usuaria xa está convidada ao espazo", "You do not have permission to invite people to this space.": "Non tes permiso para convidar persoas a este espazo.", "Failed to invite users to %(roomName)s": "Fallou o convite das usuarias para %(roomName)s", - "Give feedback": "Informar e dar opinión", - "Threads are a beta feature": "Os fíos son unha ferramenta beta", "Threads help keep your conversations on-topic and easy to track.": "Os fíos axúdanche a manter as conversas no tema e facilitan o seguimento.", "An error occurred while stopping your live location, please try again": "Algo fallou ao deter a túa localización en directo, inténtao outra vez", "Create room": "Crear sala", diff --git a/src/i18n/strings/he.json b/src/i18n/strings/he.json index 5826ada9ffd..5a48950adec 100644 --- a/src/i18n/strings/he.json +++ b/src/i18n/strings/he.json @@ -2458,7 +2458,6 @@ "We're creating a room with %(names)s": "יצרנו חדר עם %(names)s", "Jump to the given date in the timeline": "קיפצו לתאריך הנתון בציר הזמן", "Thread": "שרשורים", - "Threads are a beta feature": "שרשורים הם תכונה ניסיונית", "Keep discussions organised with threads": "שימרו על דיונים מאורגנים בשרשורים", "Tip: Use “%(replyInThread)s” when hovering over a message.": "טיפ: השתמש ב-\"%(replyInThread)s\" כשאתם מרחפים מעל הודעה.", "Threads help keep your conversations on-topic and easy to track.": "שרשורים עוזרים לשמור על השיחות שלכם בנושא וקל למעקב.", diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index d1d1417f99e..eacefb709bc 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -3180,8 +3180,6 @@ "Disinvite from space": "Meghívó visszavonása a térről", "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to the person who invited you.": "A meghívó ellenőrzésekor az alábbi hibát kaptuk: %(errcode)s. Ezt az információt megpróbálhatja eljuttatni a szoba gazdájának.", "Joining …": "Belépés…", - "Give feedback": "Visszajelzés küldése", - "Threads are a beta feature": "Az üzenetszálak béta funkció", "Tip: Use “%(replyInThread)s” when hovering over a message.": "Tipp: Használja a „%(replyInThread)s” lehetőséget a szöveg fölé navigálva.", "Threads help keep your conversations on-topic and easy to track.": "Az üzenetszálak segítenek a különböző témájú beszélgetések figyelemmel kísérésében.", "An error occurred while stopping your live location, please try again": "Élő pozíció megosztás befejezése közben hiba történt, kérjük próbálja újra", @@ -3563,7 +3561,6 @@ "You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.": "Ennek az eszköznek a felhasználásával és a QR kóddal beléptethet egy másik eszközt. Be kell olvasni a QR kódot azon az eszközön ami még nincs belépve.", "Allow a QR code to be shown in session manager to sign in another device (requires compatible homeserver)": "QR kód megjelenítésének engedélyezése a munkamenet kezelőben, hogy másik eszközzel bejelentkezhessen vele (kompatibilis matrix szervert igényel)", "play voice broadcast": "hang közvetítés lejátszása", - "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Biztos, hogy befejezi az élő közvetítést? Ez befejezi a közvetítést és a felvétel az egész szoba számára elérhető lesz.", "Are you sure you want to sign out of %(count)s sessions?|one": "Biztos, hogy ki szeretne lépni %(count)s munkamenetből?", "Are you sure you want to sign out of %(count)s sessions?|other": "Biztos, hogy ki szeretne lépni %(count)s munkamenetből?", "Show formatting": "Formázás megjelenítése", @@ -3618,7 +3615,6 @@ "For best security and privacy, it is recommended to use Matrix clients that support encryption.": "A biztonság és adatbiztonság érdekében javasolt olyan Matrix klienst használni ami támogatja a titkosítást.", "You won't be able to participate in rooms where encryption is enabled when using this session.": "Ezzel a munkamenettel olyan szobákban ahol a titkosítás be van kapcsolva nem tud részt venni.", "Feeling experimental? Try out our latest ideas in development. These features are not finalised; they may be unstable, may change, or may be dropped altogether. Learn more.": "Kísérletező kedvében van? Próbálja ki a legújabb fejlesztési ötleteinket. Ezek nem befejezettek; lehetnek instabilak, változhatnak vagy el is tűnhetnek. Tudjon meg többet.", - "Use rich text instead of Markdown in the message composer. Plain text mode coming soon.": "Szövegszerkesztő használata a Markdown helyett az üzenetek írásakor. Nemsokára érkezik az egyszerű szöveges mód.", "Rich text editor": "Szövegszerkesztő használata", "Sign in instead": "Bejelentkezés inkább", "Re-enter email address": "E-mail cím megadása újból", @@ -3710,5 +3706,16 @@ "Decrypted source unavailable": "A visszafejtett forrás nem érhető el", "%(displayName)s (%(matrixId)s)": "%(displayName)s (%(matrixId)s)", "Connection error - Recording paused": "Kapcsolódási hiba – Felvétel szüneteltetve", - "%(senderName)s started a voice broadcast": "%(senderName)s hangos közvetítést indított" + "%(senderName)s started a voice broadcast": "%(senderName)s hangos közvetítést indított", + "Unable to play this voice broadcast": "A hang közvetítés nem játszható le", + "Registration token": "Regisztrációs kulcs", + "Enter a registration token provided by the homeserver administrator.": "Adja meg a regisztrációs kulcsot amit a szolgáltató szerver adminisztrátora adott meg.", + "All messages and invites from this user will be hidden. Are you sure you want to ignore them?": "Minden üzenet és meghívó ettől a felhasználótól rejtve marad. Biztos, hogy figyelmen kívül hagyja?", + "Ignore %(user)s": "%(user)s figyelmen kívül hagyása", + "Manage account": "Fiók kezelése", + "Your account details are managed separately at %(hostname)s.": "A fiók adatok külön vannak kezelve itt: %(hostname)s.", + "Enable MSC3946 (to support late-arriving room archives)": "MSC3946 engedélyezése (a későn érkező szoba archívum támogatáshoz)", + "Dynamic room predecessors": "Dinamikus szoba előfutára", + "Indent decrease": "Behúzás csökkentés", + "Indent increase": "Behúzás növelés" } diff --git a/src/i18n/strings/id.json b/src/i18n/strings/id.json index 81e4cac574e..3e60ed77246 100644 --- a/src/i18n/strings/id.json +++ b/src/i18n/strings/id.json @@ -3210,8 +3210,6 @@ "Beta feature": "Fitur beta", "Threads help keep conversations on-topic and easy to track. Learn more.": "Utasan membantu membuat percakapan sesuai topik dan mudah untuk dilacak. Pelajari lebih lanjut.", "Keep discussions organised with threads.": "Buat diskusi tetap teratur dengan utasan.", - "Give feedback": "Berikan masukan", - "Threads are a beta feature": "Utasan adalah fitur beta", "sends hearts": "mengirim hati", "Sends the given message with hearts": "Kirim pesan dengan hati", "Confirm signing out these devices|one": "Konfirmasi mengeluarkan perangkat ini", @@ -3558,7 +3556,6 @@ "Allow a QR code to be shown in session manager to sign in another device (requires compatible homeserver)": "Perbolehkan sebuah kode QR untuk ditampilkan dalam pengelola sesi untuk masuk ke perangkat lain (membutuhkan homeserver yang kompatibel)", "play voice broadcast": "mainkan siaran suara", "Yes, stop broadcast": "Iya, hentikan siaran", - "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Apakah Anda yakin ingin menghentikan siaran langsung Anda? Ini akan mengakhiri siaran dan rekaman lengkap akan tersedia di ruangan.", "Stop live broadcasting?": "Hentikan siaran langsung?", "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Ada orang lain yang saat ini merekam sebuah siaran suara. Tunggu siaran suaranya berakhir untuk memulai yang baru.", "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "Anda tidak memiliki izin untuk memulai sebuah siaran suara di ruangan ini. Hubungi sebuah administrator ruangan untuk meningkatkan izin Anda.", @@ -3640,7 +3637,6 @@ "Right panel stays open": "Panel kanan tetap buka", "Currently experimental.": "Saat ini masih dalam uji coba.", "New ways to ignore people": "Cara baru mengabaikan orang", - "Use rich text instead of Markdown in the message composer. Plain text mode coming soon.": "Gunakan teks kaya daripada Markdown dalam komposer pesan. Mode teks biasa akan datang.", "Rich text editor": "Editor teks kaya", "Report to moderators": "Laporkan ke moderator", "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.": "Dalam ruangan yang mendukung moderasi, tombol “Laporkan” memungkinkan Anda untuk melaporkan penyalahgunaan ke moderator ruangan.", @@ -3710,5 +3706,44 @@ "Decrypted source unavailable": "Sumber terdekripsi tidak tersedia", "Connection error - Recording paused": "Kesalahan koneksi - Perekaman dijeda", "%(senderName)s started a voice broadcast": "%(senderName)s memulai sebuah siaran suara", - "%(displayName)s (%(matrixId)s)": "%(displayName)s (%(matrixId)s)" + "%(displayName)s (%(matrixId)s)": "%(displayName)s (%(matrixId)s)", + "Registration token": "Token pendaftaran", + "Enter a registration token provided by the homeserver administrator.": "Masukkan token pendaftaran yang disediakan oleh administrator homeserver.", + "Unable to play this voice broadcast": "Tidak dapat memutar siaran suara ini", + "Manage account": "Kelola akun", + "Your account details are managed separately at %(hostname)s.": "Detail akun Anda dikelola secara terpisah di %(hostname)s.", + "Enable MSC3946 (to support late-arriving room archives)": "Aktifkan MSC3946 (untuk mendukung arsip ruangan yang datang terlambat)", + "Dynamic room predecessors": "Pendahulu ruang dinamis", + "All messages and invites from this user will be hidden. Are you sure you want to ignore them?": "Semua pesan dan undangan dari pengguna ini akan disembunyikan. Apakah Anda yakin ingin mengabaikan?", + "Ignore %(user)s": "Abaikan %(user)s", + "Indent increase": "Tambahkan indentasi", + "Indent decrease": "Kurangi indentasi", + "Unable to decrypt voice broadcast": "Tidak dapat mendekripsi siaran suara", + "View a list of polls in a room. (Under active development)": "Tampilkan peminkam. (Dalam pengemabgan aktif)", + "Polls history": "Riwatar pegunungan next2 i1 ak suara", + "Use rich text instead of Markdown in the message composer.": "Menggunakan teks kaya daripada Markdown dalam komposer pesan.", + "There are no polls in this room": "Tidak ada pemungutan suara di ruangan ini", + "Thread Id: ": "ID utasan: ", + "Threads timeline": "Lini masa utasan", + "Sender: ": "Pengirim: ", + "Type: ": "Jenis: ", + "ID: ": "ID: ", + "Last event:": "Peristiwa terakhir:", + "No receipt found": "Tidak ada laporan yang ditemukan", + "User read up to: ": "Pembacaan pengguna sampai: ", + "Dot: ": "Titik: ", + "Highlight: ": "Sorotan: ", + "Total: ": "Jumlah: ", + "Main timeline": "Lini masa utama", + "not encrypted 🚨": "tidak terenkripsi 🚨", + "encrypted ✅": "terenkripsi ✅", + "Room is ": "Ruangan ", + "Notification state is": "Keadaan notifikasi", + ", count:": ", jumlah:", + "Room unread status: ": "Keadaan ruangan belum dibaca: ", + "Room status": "Keadaan ruangan", + "Notifications debug": "Pengawakutuan notifikasi", + "unknown": "tidak diketahui", + "Red": "Merah", + "Grey": "Abu-Abu" } diff --git a/src/i18n/strings/is.json b/src/i18n/strings/is.json index 66432663aed..ea3a64f85ba 100644 --- a/src/i18n/strings/is.json +++ b/src/i18n/strings/is.json @@ -2986,7 +2986,6 @@ "Enter fullscreen": "Fara í fullskjásstillingu", "Show spaces": "Sýna svæði", "Failed to set direct message tag": "Ekki tókst að stilla merki um bein skilaboð", - "Give feedback": "Gefðu umsögn", "Check your email to continue": "Skoðaðu tölvupóstinn þinn til að halda áfram", "Stop and close": "Hætta og loka", "Show rooms": "Sýna spjallrásir", @@ -3180,7 +3179,6 @@ "resume voice broadcast": "halda áfram með talútsendingu", "Listen to live broadcast?": "Hlusta á beina útsendingu?", "Yes, stop broadcast": "Já, stöðva útsendingu", - "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Ertu viss um að þú viljir stöðva þessa beinu útsendingu? Þetta mun stöðva útsendinguna og full skráning hennar verður tiltæk á spjallrásinni.", "Stop live broadcasting?": "Stöðva beina útsendingu?", "%(senderName)s ended a voice broadcast": "%(senderName)s endaði talútsendingu", "You ended a voice broadcast": "Þú endaðir talútsendingu", diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index b919eb12155..c6f6c111fff 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -3205,8 +3205,6 @@ "New video room": "Nuova stanza video", "New room": "Nuova stanza", "Threads help keep conversations on-topic and easy to track. Learn more.": "Le conversazioni aiutano a tenere le discussioni in tema e rintracciabili. Maggiori info.", - "Give feedback": "Lascia feedback", - "Threads are a beta feature": "Le conversazioni sono una funzionalità beta", "Threads help keep your conversations on-topic and easy to track.": "Le conversazioni ti aiutano a tenere le tue discussioni in tema e rintracciabili.", "%(featureName)s Beta feedback": "Feedback %(featureName)s beta", "Beta feature. Click to learn more.": "Funzionalità beta. Clicca per maggiori informazioni.", @@ -3532,7 +3530,6 @@ "pause voice broadcast": "sospendi trasmissione vocale", "Notifications silenced": "Notifiche silenziose", "Yes, stop broadcast": "Sì, ferma la trasmissione", - "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Vuoi davvero fermare la tua trasmissione in diretta? Verrà terminata la trasmissione e la registrazione completa sarà disponibile nella stanza.", "Stop live broadcasting?": "Fermare la trasmissione in diretta?", "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "Non hai l'autorizzazione necessaria per iniziare una trasmissione vocale in questa stanza. Contatta un amministratore della stanza per aggiornare le tue autorizzazioni.", "You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.": "Stai già registrando una trasmissione vocale. Termina quella in corso per iniziarne una nuova.", @@ -3636,7 +3633,6 @@ "Right panel stays open": "Il pannello destro resta aperto", "Currently experimental.": "Al momento è sperimentale.", "New ways to ignore people": "Nuovi modi di ignorare le persone", - "Use rich text instead of Markdown in the message composer. Plain text mode coming soon.": "Usa il formato rich text invece del markdown nella scrittura dei messaggi. La modalità in testo semplice è in arrivo.", "Rich text editor": "Editor in rich text", "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.": "Nelle stanze che supportano la moderazione, il pulsante \"Segnala\" ti permetterà di segnalare abusi ai moderatori della stanza.", "Report to moderators": "Segnala ai moderatori", @@ -3710,5 +3706,44 @@ "Bulleted list": "Elenco puntato", "Connection error - Recording paused": "Errore di connessione - Registrazione in pausa", "%(senderName)s started a voice broadcast": "%(senderName)s ha iniziato una trasmissione vocale", - "%(displayName)s (%(matrixId)s)": "%(displayName)s (%(matrixId)s)" + "%(displayName)s (%(matrixId)s)": "%(displayName)s (%(matrixId)s)", + "Registration token": "Token di registrazione", + "Enter a registration token provided by the homeserver administrator.": "Inserisci un token di registrazione fornito dall'amministratore dell'homeserver.", + "Unable to play this voice broadcast": "Impossibile avviare questa trasmissione vocale", + "All messages and invites from this user will be hidden. Are you sure you want to ignore them?": "Tutti i messaggi e gli inviti da questo utente verranno nascosti. Vuoi davvero ignorarli?", + "Ignore %(user)s": "Ignora %(user)s", + "Indent decrease": "Diminuzione indentazione", + "Indent increase": "Aumento indentazione", + "Manage account": "Gestisci account", + "Your account details are managed separately at %(hostname)s.": "I dettagli del tuo account sono gestiti separatamente su %(hostname)s.", + "Enable MSC3946 (to support late-arriving room archives)": "Attiva MSC3946 (per supportare archivi della stanza arrivati in ritardo)", + "There are no polls in this room": "Non ci sono sondaggi in questa stanza", + "Dynamic room predecessors": "Predecessori della stanza dinamici", + "View a list of polls in a room. (Under active development)": "Vedi una lista di sondaggi in una stanza. (In sviluppo attivo)", + "Polls history": "Cronologia sondaggi", + "Use rich text instead of Markdown in the message composer.": "Usa il rich text invece del Markdown nel compositore di messaggi.", + "Unable to decrypt voice broadcast": "Impossibile decifrare la trasmissione vocale", + "Thread Id: ": "ID conversazione: ", + "Threads timeline": "Linea temporale conversazioni", + "Sender: ": "Mittente: ", + "Type: ": "Tipo: ", + "ID: ": "ID: ", + "Last event:": "Ultimo evento:", + "No receipt found": "Nessuna ricevuta trovata", + "User read up to: ": "L'utente ha letto fino: ", + "Dot: ": "Punto: ", + "Highlight: ": "Evidenziazione: ", + "Total: ": "Totale: ", + "Main timeline": "Linea temporale principale", + "not encrypted 🚨": "non crittografata 🚨", + "encrypted ✅": "crittografata ✅", + "Room is ": "La stanza è ", + "Notification state is": "Lo stato di notifica è", + ", count:": ", conteggio:", + "Room unread status: ": "Stato non letto della stanza: ", + "Room status": "Stato della stanza", + "Notifications debug": "Debug notifiche", + "unknown": "sconosciuto", + "Red": "Rosso", + "Grey": "Grigio" } diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json index aa317793697..fa90aab3213 100644 --- a/src/i18n/strings/ja.json +++ b/src/i18n/strings/ja.json @@ -5,7 +5,7 @@ "Current password": "現在のパスワード", "Favourite": "お気に入り", "Favourites": "お気に入り", - "Invited": "招待中", + "Invited": "招待済", "Low priority": "低優先度", "Mute": "ミュート", "Notifications": "通知", @@ -19,13 +19,13 @@ "Always show message timestamps": "発言時刻を常に表示", "Filter room members": "ルームのメンバーを絞り込む", "Show timestamps in 12 hour format (e.g. 2:30pm)": "発言時刻を12時間形式で表示(例:2:30午後)", - "Upload avatar": "アイコン画像を変更", + "Upload avatar": "アバターをアップロード", "Add": "追加", "No Microphones detected": "マイクが見つかりません", "No Webcams detected": "カメラが見つかりません", "Microphone": "マイク", "Camera": "カメラ", - "Are you sure?": "本当によろしいですか?", + "Are you sure?": "よろしいですか?", "OK": "OK", "Operation failed": "操作に失敗しました", "Dismiss": "閉じる", @@ -43,23 +43,23 @@ "Unnamed room": "名前のないルーム", "This email address is already in use": "このメールアドレスは既に使用されています", "This phone number is already in use": "この電話番号は既に使用されています", - "Failed to verify email address: make sure you clicked the link in the email": "メールアドレスの認証に失敗しました。メール中のリンクをクリックしたか、確認してください", + "Failed to verify email address: make sure you clicked the link in the email": "メールアドレスの認証に失敗しました。電子メール内のリンクを開いたことを確認してください", "Analytics": "分析", "Thursday": "木曜日", "Messages in one-to-one chats": "1対1のチャットでのメッセージ", "All Rooms": "全てのルーム", "You cannot delete this message. (%(code)s)": "この発言を削除できません。(%(code)s)", "Send": "送信", - "All messages": "全ての発言", + "All messages": "全てのメッセージ", "Sunday": "日曜日", "Today": "今日", "Monday": "月曜日", "Messages in group chats": "グループチャットでのメッセージ", "Friday": "金曜日", "Yesterday": "昨日", - "Messages sent by bot": "ボットから送信されたメッセージ", + "Messages sent by bot": "ボットによるメッセージ", "Low Priority": "低優先度", - "Collecting logs": "ログの収集", + "Collecting logs": "ログを収集しています", "No update available.": "更新はありません。", "Collecting app version information": "アプリのバージョン情報を収集", "Changelog": "変更履歴", @@ -76,7 +76,7 @@ "When I'm invited to a room": "ルームに招待されたとき", "Resend": "再送信", "Messages containing my display name": "自身の表示名を含むメッセージ", - "Notification targets": "通知先", + "Notification targets": "通知対象", "Update": "更新", "Failed to send logs: ": "ログの送信に失敗しました: ", "Unavailable": "使用できません", @@ -85,7 +85,7 @@ "Noisy": "音量大", "View Source": "ソースコードを表示", "Back": "戻る", - "Event sent!": "イベントが送信されました!", + "Event sent!": "イベントを送信しました!", "Preparing to send logs": "ログを送信する準備をしています", "Reject": "拒否", "Toolbox": "ツールボックス", @@ -144,26 +144,26 @@ "Admin": "管理者", "Failed to invite": "招待できませんでした", "You need to be logged in.": "ログインする必要があります。", - "You need to be able to invite users to do that.": "それをするためにユーザーを招待できる必要があります。", + "You need to be able to invite users to do that.": "それを行うにはユーザーを招待する権限が必要です。", "Unable to create widget.": "ウィジェットを作成できません。", "Missing roomId.": "roomIdがありません。", "Failed to send request.": "リクエストの送信に失敗しました。", "This room is not recognised.": "このルームは認識されません。", "Power level must be positive integer.": "権限レベルは正の整数でなければなりません。", "You are not in this room.": "このルームのメンバーではありません。", - "You do not have permission to do that in this room.": "あなたはこのルームでそれを行う権限を持っていません。", + "You do not have permission to do that in this room.": "このルームでそれを行う権限がありません。", "Missing room_id in request": "リクエストにroom_idがありません", "Room %(roomId)s not visible": "ルーム %(roomId)s は見えません", "Missing user_id in request": "リクエストにuser_idがありません", "Usage": "用法", "Changes your display nickname": "表示されるニックネームを変更", - "Invites user with given id to current room": "指定されたIDを持つユーザーを現在のルームに招待", + "Invites user with given id to current room": "指定したIDのユーザーを現在のルームに招待", "Leave room": "ルームから退出", - "Bans user with given id": "指定されたIDでユーザーをブロック", - "Ignores a user, hiding their messages from you": "ユーザーを無視し、自分からのメッセージを隠す", + "Bans user with given id": "指定したIDのユーザーをブロック", + "Ignores a user, hiding their messages from you": "ユーザーを無視し、そのメッセージを非表示に設定", "Ignored user": "無視しているユーザー", "You are now ignoring %(userId)s": "%(userId)sを無視しています", - "Stops ignoring a user, showing their messages going forward": "ユーザーの無視を止めて、メッセージを表示", + "Stops ignoring a user, showing their messages going forward": "ユーザーの無視を解除し、以後のメッセージを表示", "Unignored user": "無視していないユーザー", "You are no longer ignoring %(userId)s": "あなたは%(userId)sを無視していません", "Define the power level of a user": "ユーザーの権限レベルを規定", @@ -195,8 +195,8 @@ "Failure to create room": "ルームの作成に失敗しました", "Server may be unavailable, overloaded, or you hit a bug.": "サーバーが使用できないか、オーバーロードしているか、または不具合が発生した可能性があります。", "Unnamed Room": "名前のないルーム", - "This homeserver has hit its Monthly Active User limit.": "このホームサーバーは、月間アクティブユーザー制限を超えています。", - "This homeserver has exceeded one of its resource limits.": "このホームサーバーは、リソース制限の1つを超えています。", + "This homeserver has hit its Monthly Active User limit.": "このホームサーバーは月間アクティブユーザー数の上限に達しました 。", + "This homeserver has exceeded one of its resource limits.": "このホームサーバーはリソースの上限に達しました。", "Please contact your service administrator to continue using the service.": "サービスを引き続き使用するには、サービス管理者にお問い合わせください。", "Unable to connect to Homeserver. Retrying...": "ホームサーバーに接続できません。再試行しています…", "Your browser does not support the required cryptography extensions": "お使いのブラウザーは、必要な暗号化拡張機能をサポートしていません", @@ -216,14 +216,14 @@ "Incorrect verification code": "認証コードが誤っています", "Submit": "提出", "Phone": "電話", - "No display name": "表示名なし", + "No display name": "表示名がありません", "New passwords don't match": "新しいパスワードが一致しません", "Passwords can't be empty": "パスワードを空にすることはできません", "Warning!": "警告!", "Export E2E room keys": "ルームのエンドツーエンド暗号鍵をエクスポート", "Do you want to set an email address?": "メールアドレスを設定しますか?", "Password": "パスワード", - "Confirm password": "確認のパスワード", + "Confirm password": "パスワードを確認", "Authentication": "認証", "Failed to set display name": "表示名の設定に失敗しました", "Off": "オフ", @@ -234,13 +234,13 @@ "Call Failed": "呼び出しに失敗しました", "Automatically replace plain text Emoji": "自動的にプレーンテキスト絵文字を置き換える", "Demote yourself?": "自身を降格しますか?", - "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "自分自身を降格しようとしています。この変更を元に戻すことはできません。ルーム内の最後の特権ユーザーである場合、特権を取り戻すことはできません。", + "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "あなたは自分自身を降格させようとしています。この変更は取り消せません。あなたがルームの中で最後の特権ユーザーである場合、特権を再取得することはできなくなります。", "Demote": "降格する", "Failed to mute user": "ユーザーのミュートに失敗しました", "Failed to change power level": "権限レベルの変更に失敗しました", - "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "このユーザーにあなたと同じ権限レベルを与えようとしています。この変更を元に戻すことはできません。", + "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "このユーザーにあなたと同じ権限レベルを与えようとしています。この変更は取り消せません。", "Ignore": "無視", - "Jump to read receipt": "既読へジャンプ", + "Jump to read receipt": "既読通知へ移動", "Invite": "招待", "Share Link to User": "ユーザーへのリンクを共有", "Unmute": "ミュート解除", @@ -252,10 +252,10 @@ "Hangup": "電話を切る", "Voice call": "音声通話", "Video call": "ビデオ通話", - "Send an encrypted reply…": "暗号化された返信を送信…", + "Send an encrypted reply…": "暗号化された返信を送る…", "Send an encrypted message…": "暗号化されたメッセージを送信…", "This room has been replaced and is no longer active.": "このルームは置き換えられており、アクティブではありません。", - "The conversation continues here.": "こちらから継続中の会話を確認する。", + "The conversation continues here.": "こちらから継続中の会話を確認。", "You do not have permission to post to this room": "このルームに投稿する権限がありません", "Server error": "サーバーエラー", "Server unavailable, overloaded, or something else went wrong.": "サーバーが使用できないか、オーバーロードしているか、または問題が発生しました。", @@ -292,12 +292,12 @@ "Muted Users": "ミュートされたユーザー", "Banned users": "ブロックされたユーザー", "This room is not accessible by remote Matrix servers": "このルームはリモートのMatrixサーバーからアクセスできません", - "Publish this room to the public in %(domain)s's room directory?": "%(domain)sのルームディレクトリにこのルームを公開しますか?", - "Who can read history?": "誰が履歴を読むことができますか?", + "Publish this room to the public in %(domain)s's room directory?": "%(domain)sのルームディレクトリーにこのルームを公開しますか?", + "Who can read history?": "履歴を閲覧できる人は?", "Members only (since the point in time of selecting this option)": "メンバーのみ(この設定を選択した時点から)", "Members only (since they were invited)": "メンバーのみ(招待を送った時点から)", "Members only (since they joined)": "メンバーのみ(参加した時点から)", - "Permissions": "アクセス許可", + "Permissions": "権限", "Advanced": "詳細", "Only room administrators will see this warning": "この警告はルームの管理者にのみ表示されます", "You don't currently have any stickerpacks enabled": "現在、使用可能なステッカーパックはありません", @@ -328,7 +328,7 @@ "Copied!": "コピーしました!", "Failed to copy": "コピーに失敗しました", "Add an Integration": "統合を追加", - "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "サードパーティーのサイトに移動して、%(integrationsUrl)sで使用するためにアカウントを認証できるようになります。続行しますか?", + "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "%(integrationsUrl)sで使用するアカウントを認証するため、外部サイトに移動します。続行してよろしいですか?", "Please review and accept the policies of this homeserver:": "このホームサーバーの運営方針を確認し、同意してください:", "Token incorrect": "誤ったトークン", "A text message has been sent to %(msisdn)s": "テキストメッセージが%(msisdn)sに送信されました", @@ -403,7 +403,7 @@ "Create": "作成", "Unknown error": "不明なエラー", "Incorrect password": "間違ったパスワード", - "Deactivate Account": "アカウントを無効にする", + "Deactivate Account": "アカウントを無効化", "An error has occurred.": "エラーが発生しました。", "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が使用されていました。このバージョンでは、遅延ロードは無効です。ローカルキャッシュはこれらの2つの設定の間で互換性がないので、%(brand)sはアカウントを再同期する必要があります。", "If the other version of %(brand)s is still open in another tab, please close it as using %(brand)s on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "他のバージョンの%(brand)sが別のタブで開いている場合は、それを閉じてください。同じホスト上で、遅延ロードを有効と無効の両方に設定して%(brand)sを使用すると、問題が発生します。", @@ -425,7 +425,7 @@ "Sign out": "サインアウト", "Clear Storage and Sign Out": "ストレージのクリアとサインアウト", "Send Logs": "ログを送信", - "Refresh": "リフレッシュ", + "Refresh": "再読み込み", "Unable to restore session": "セッションを復元できません", "We encountered an error trying to restore your previous session.": "以前のセッションを復元する際にエラーが発生しました。", "If you have previously used a more recent version of %(brand)s, your session may be incompatible with this version. Close this window and return to the more recent version.": "以前に%(brand)sの最新バージョンを使用していた場合、セッションはこのバージョンと互換性がない可能性があります。このウィンドウを閉じて、最新のバージョンに戻ってください。", @@ -433,7 +433,7 @@ "Invalid Email Address": "無効なメールアドレス", "This doesn't appear to be a valid email address": "メールアドレスの形式が正しくありません", "Verification Pending": "認証の保留中", - "Please check your email and click on the link it contains. Once this is done, click continue.": "電子メールを確認して、本文中のURLをクリックしてください。完了したら「続行する」をクリックしてください。", + "Please check your email and click on the link it contains. Once this is done, click continue.": "電子メールを確認して、本文中のURLをクリックしてください。完了したら「続行」をクリックしてください。", "Unable to add email address": "メールアドレスを追加できません", "Unable to verify email address.": "メールアドレスを確認できません。", "This will allow you to reset your password and receive notifications.": "これにより、パスワードをリセットして通知を受け取ることができます。", @@ -443,12 +443,12 @@ "Share User": "ユーザーを共有", "Share Room Message": "ルームのメッセージを共有", "Link to selected message": "選択したメッセージにリンク", - "Reject invitation": "招待を拒否", - "Are you sure you want to reject the invitation?": "招待を拒否しますか?", + "Reject invitation": "招待を辞退", + "Are you sure you want to reject the invitation?": "招待を辞退してよろしいですか?", "Name": "名前", "You must register to use this functionality": "この機能を使用するには登録する必要があります", "You must join the room to see its files": "ルームのファイルを表示するには、ルームに参加する必要があります", - "Description": "説明", + "Description": "詳細", "Failed to reject invitation": "招待を拒否できませんでした", "This room is not public. You will not be able to rejoin without an invite.": "このルームは公開されていません。再度参加するには、招待が必要です。", "Are you sure you want to leave the room '%(roomName)s'?": "このルーム「%(roomName)s」から退出してよろしいですか?", @@ -463,8 +463,8 @@ "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "%(brand)sの古いバージョンのデータを検出しました。これにより、古いバージョンではエンドツーエンドの暗号化が機能しなくなります。古いバージョンを使用している間に最近交換されたエンドツーエンドの暗号化されたメッセージは、このバージョンでは復号化できません。また、このバージョンで交換されたメッセージが失敗することもあります。問題が発生した場合は、ログアウトして再度ログインしてください。メッセージ履歴を保持するには、鍵をエクスポートして再インポートしてください。", "Logout": "ログアウト", "You can't send any messages until you review and agree to our terms and conditions.": "利用規約 を確認して同意するまでは、いかなるメッセージも送信できません。", - "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.": "このホームサーバーが月間アクティブユーザー制限を超えたため、メッセージは送信されませんでした。サービスを引き続き使用するには、サービス管理者にお問い合わせください。", - "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.": "このホームサーバーがリソース制限を超えたため、メッセージは送信されませんでした。サービスを引き続き使用するには、サービス管理者にお問い合わせください。", + "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.": "このホームサーバーが月間アクティブユーザー制限を超えたため、メッセージを送信できませんでした。サービスを引き続き使用するには、サービスの管理者にお問い合わせください。", + "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.": "このホームサーバーがリソース制限を超えたため、メッセージを送信できませんでした。サービスを引き続き使用するには、サービスの管理者にお問い合わせください。", "Connectivity to the server has been lost.": "サーバーとの接続が失われました。", "Sent messages will be stored until your connection has returned.": "送信されたメッセージは、接続が復旧するまで保存されます。", "You seem to be uploading files, are you sure you want to quit?": "ファイルをアップロードしているようですが、中止しますか?", @@ -475,7 +475,7 @@ "Room": "ルーム", "Failed to reject invite": "招待を拒否できませんでした", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "このルームのタイムラインに特定のポイントをロードしようとしましたが、問題のメッセージを見る権限がありません。", - "Tried to load a specific point in this room's timeline, but was unable to find it.": "このルームのタイムラインに特定のポイントをロードしようとしましたが、それを見つけることができませんでした。", + "Tried to load a specific point in this room's timeline, but was unable to find it.": "このルームのタイムラインに特定のポイントを読み込もうとしましたが、見つけられませんでした。", "Failed to load timeline position": "タイムラインの位置を読み込めませんでした", "Uploading %(filename)s and %(count)s others|other": "%(filename)sと他%(count)s件をアップロードしています", "Uploading %(filename)s and %(count)s others|zero": "%(filename)sをアップロードしています", @@ -505,7 +505,7 @@ "New passwords must match each other.": "新しいパスワードは互いに一致する必要があります。", "Return to login screen": "ログイン画面に戻る", "Please contact your service administrator to continue using this service.": "このサービスを続行するには、サービス管理者にお問い合わせください。", - "Incorrect username and/or password.": "不正なユーザー名またはパスワード。", + "Incorrect username and/or password.": "ユーザー名とパスワードの一方あるいは両方が正しくありません。", "Please note you are logging into the %(hs)s server, not matrix.org.": "matrix.orgではなく、%(hs)sのサーバーにログインしていることに注意してください。", "This homeserver doesn't offer any login flows which are supported by this client.": "このホームサーバーは、このクライアントでサポートされているログインフローを提供していません。", "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "HTTPS URLがブラウザーバーにある場合、HTTP経由でホームサーバーに接続できません。HTTPSを使用するか安全でないスクリプトを有効にしてください。", @@ -532,7 +532,7 @@ "Import": "インポート", "Failed to remove tag %(tagName)s from room": "ルームからタグ %(tagName)s を削除できませんでした", "Failed to add tag %(tagName)s to room": "ルームにタグ %(tagName)s を追加できませんでした", - "Unignore": "無視をやめる", + "Unignore": "無視を解除", "Unable to load! Check your network connectivity and try again.": "ロードできません!ネットワーク通信を確認して、もう一度やり直してください。", "You do not have permission to invite people to this room.": "このルームにユーザーを招待する権限がありません。", "Unknown server error": "不明なサーバーエラー", @@ -556,34 +556,34 @@ "Sets the room name": "ルーム名を設定", "Change room name": "ルーム名の変更", "Room Name": "ルーム名", - "Add Email Address": "メールアドレスの追加", - "Add Phone Number": "電話番号の追加", + "Add Email Address": "メールアドレスを追加", + "Add Phone Number": "電話番号を追加", "Call failed due to misconfigured server": "サーバーの誤設定により呼び出し失敗", "Try using turn.matrix.org": "turn.matrix.orgを試してみる", "The file '%(fileName)s' failed to upload.": "ファイル '%(fileName)s' のアップロードに失敗しました.", "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "ファイル '%(fileName)s' はこのホームサーバーのアップロードのサイズ上限を超えています", - "The server does not support the room version specified.": "このサーバーは指定されたルームバージョンに対応していません。", + "The server does not support the room version specified.": "このサーバーは指定されたルームのバージョンに対応していません。", "Identity server has no terms of service": "IDサーバーには利用規約がありません", "Messages": "メッセージ", "Actions": "アクション", "Other": "その他", "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "プレーンテキストメッセージの前に ¯\\_(ツ)_/¯ を付ける", "Sends a message as plain text, without interpreting it as markdown": "メッセージをマークダウンと解釈せずプレーンテキストとして送信", - "Upgrades a room to a new version": "ルームを新しいバージョンへアップグレード", + "Upgrades a room to a new version": "ルームを新しいバージョンにアップグレード", "You do not have the required permissions to use this command.": "このコマンドを実行するのに必要な権限がありません。", - "Changes your display nickname in the current room only": "表示されるニックネームをこのルームに関してのみ変更", + "Changes your display nickname in the current room only": "このルームでのみ表示名を変更", "Changes the avatar of the current room": "現在のルームのアバターを変更", - "Changes your avatar in this current room only": "アバターをこのルームに関してのみ変更", - "Changes your avatar in all rooms": "全てのルームに対するアバターを変更", + "Changes your avatar in this current room only": "このルームでのみアバターを変更", + "Changes your avatar in all rooms": "全てのルームでアバターを変更", "Gets or sets the room topic": "ルームのトピック情報を取得または設定", "This room has no topic.": "このルームはトピックを持ちません。", "Use an identity server": "IDサーバーを使用", - "Unbans user with given ID": "与えられたIDを持つユーザーのブロックを解除", + "Unbans user with given ID": "指定したIDのユーザーのブロックを解除", "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.": "このルームのウィジェットを変更できません。", - "Sends the given message coloured as a rainbow": "与えられたメッセージを虹色にして送信", - "Sends the given emote coloured as a rainbow": "与えられたエモートを虹色で送信", + "Sends the given message coloured as a rainbow": "指定したメッセージを虹色で送信", + "Sends the given emote coloured as a rainbow": "指定したエモートを虹色で送信", "Displays list of commands with usages and descriptions": "使い方と説明付きのコマンド一覧を表示", "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)sがこのルームをアップグレードしました。", "%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)sがこのルームを「リンクを知っている人全員」に公開しました。", @@ -635,16 +635,16 @@ "Set a new custom sound": "カスタム音を設定", "Browse": "参照", "Roles & Permissions": "役割と権限", - "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "誰が履歴を読み取れるかに関する変更は、今後送信されるメッセージにのみ適用されます。既に存在する履歴の表示は変更されません。", + "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "履歴の閲覧権限に関する変更は、今後、このルームで表示されるメッセージにのみ適用されます。既存の履歴の見え方には影響しません。", "Encryption": "暗号化", - "Once enabled, encryption cannot be disabled.": "暗号化は一度有効にすると、二度と無効にできません。", + "Once enabled, encryption cannot be disabled.": "いったん有効にすると、暗号化を無効にすることはできません。", "Encrypted": "暗号化", "Email Address": "メールアドレス", "Main address": "メインアドレス", "Join": "参加", "Create a private room": "非公開のルームを作成", "Topic (optional)": "トピック(任意)", - "Hide advanced": "高度な設定を非表示", + "Hide advanced": "高度な設定を非表示にする", "Show advanced": "高度な設定を表示", "Room Settings - %(roomName)s": "ルームの設定 - %(roomName)s", "Enable room encryption": "ルームの暗号化を有効にする", @@ -687,7 +687,7 @@ "Versions": "バージョン", "Voice & Video": "音声とビデオ", "Remove recent messages": "最近のメッセージを削除", - "%(creator)s created and configured the room.": "%(creator)sがルームを作成して設定しました。", + "%(creator)s created and configured the room.": "%(creator)sがルームを作成し設定しました。", "Add room": "ルームを追加", "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "本当によろしいですか? もし鍵が正常にバックアップされていない場合、暗号化されたメッセージにアクセスできなくなります。", "not stored": "保存されていません", @@ -696,7 +696,7 @@ "Start using Key Backup": "鍵のバックアップを使用開始", "Edited at %(date)s. Click to view edits.": "%(date)sに編集済。クリックして変更履歴を表示。", "edited": "編集済", - "I don't want my encrypted messages": "暗号化されたメッセージは必要ありません", + "I don't want my encrypted messages": "暗号化されたメッセージは不要です", "Manually export keys": "手動で鍵をエクスポート", "You'll lose access to your encrypted messages": "暗号化されたメッセージにアクセスできなくなります", "You'll upgrade this room from to .": "このルームをからにアップグレードします。", @@ -706,14 +706,14 @@ "Download": "ダウンロード", "Print it and store it somewhere safe": "印刷して安全な場所に保管", "Save it on a USB key or backup drive": "USBメモリーやバックアップ用のドライブに保存", - "Copy it to your personal cloud storage": "個人用クラウドストレージにコピー", + "Copy it to your personal cloud storage": "個人用のクラウドストレージにコピー", "Display Name": "表示名", "Profile picture": "プロフィール画像", "Encryption enabled": "暗号化が有効です", - "Encryption not enabled": "暗号化が無効です", + "Encryption not enabled": "暗号化が有効になっていません", "The encryption used by this room isn't supported.": "このルームで使用されている暗号化はサポートされていません。", - "Cross-signing public keys:": "クロス署名公開鍵:", - "Cross-signing private keys:": "クロス署名秘密鍵:", + "Cross-signing public keys:": "クロス署名の公開鍵:", + "Cross-signing private keys:": "クロス署名の秘密鍵:", "Clear cache and reload": "キャッシュを削除して再読み込み", "Session ID:": "セッションID:", "Session key:": "セッションキー:", @@ -725,9 +725,9 @@ "Encrypted messages in one-to-one chats": "1対1のチャットでの暗号化されたメッセージ", "Encrypted messages in group chats": "グループチャットでの暗号化されたメッセージ", "Upload": "アップロード", - "Enable desktop notifications for this session": "このセッションではデスクトップ通知を有効にする", + "Enable desktop notifications for this session": "このセッションでデスクトップ通知を有効にする", "Email addresses": "メールアドレス", - "This room is end-to-end encrypted": "このルームはエンドツーエンド暗号化されています", + "This room is end-to-end encrypted": "このルームはエンドツーエンドで暗号化されています", "Encrypted by an unverified session": "未認証のセッションによる暗号化", "Close preview": "プレビューを閉じる", "Direct Messages": "ダイレクトメッセージ", @@ -738,7 +738,7 @@ "Destroy cross-signing keys?": "クロス署名鍵を破棄してよろしいですか?", "Clear cross-signing keys": "クロス署名鍵を削除", "Clear all data in this session?": "このセッションの全てのデータを削除してよろしいですか?", - "Clear all data": "全てのデータを削除", + "Clear all data": "全てのデータを消去", "Create a public room": "公開ルームを作成", "Message edits": "メッセージの編集履歴", "Report Content to Your Homeserver Administrator": "あなたのホームサーバーの管理者にコンテンツを報告", @@ -749,7 +749,7 @@ "To continue you need to accept the terms of this service.": "続行するには、このサービスの利用規約に同意する必要があります。", "Report Content": "コンテンツを報告", "Bold": "太字", - "Italics": "イタリック体", + "Italics": "斜字体", "React": "リアクション", "Quick Reactions": "一般的なリアクション", "Keyboard Shortcuts": "キーボードショートカット", @@ -764,13 +764,13 @@ "Unrecognised command: %(commandText)s": "認識されていないコマンド:%(commandText)s", "Send as message": "メッセージとして送信", "Confirm": "確認", - "Enable audible notifications for this session": "このセッションでは音声通知を有効にする", + "Enable audible notifications for this session": "このセッションで音声通知を有効にする", "Enable encryption?": "暗号化を有効にしますか?", - "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.": "一度有効にしたルームの暗号化は無効にすることはできません。暗号化されたルームで送信されたメッセージは、サーバーからは見ることができず、そのルームのメンバーだけが見ることができます。暗号化を有効にすると、多くのボットやブリッジが正常に動作しなくなる場合があります。暗号化についての詳細はこちらをご覧ください。", + "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.": "一度有効にしたルームの暗号化は無効にすることはできません。暗号化されたルームで送信されたメッセージは、サーバーからは閲覧できず、そのルームのメンバーだけが閲覧できます。暗号化を有効にすると、多くのボットやブリッジが正常に動作しなくなる可能性があります。暗号化についての詳細はこちらをご覧ください。", "Enter username": "ユーザー名を入力", "Email (optional)": "メールアドレス(任意)", "Phone (optional)": "電話番号(任意)", - "Verify this session": "このセッションの認証", + "Verify this session": "このセッションを認証", "Encryption upgrade available": "暗号化のアップグレードが利用できます", "Not Trusted": "信頼されていません", "Done": "戻る", @@ -789,11 +789,11 @@ "Security": "セキュリティー", "Welcome to %(appName)s": "%(appName)sにようこそ", "Send a Direct Message": "ダイレクトメッセージを送信", - "Explore Public Rooms": "公開ルームを探索", + "Explore Public Rooms": "公開ルームを探す", "Create a Group Chat": "グループチャットを作成", "Go Back": "戻る", - "Messages in this room are end-to-end encrypted.": "このルームでのメッセージはエンドツーエンド暗号化されます。", - "Messages in this room are not end-to-end encrypted.": "このルームでのメッセージはエンドツーエンド暗号化されません。", + "Messages in this room are end-to-end encrypted.": "このルームのメッセージはエンドツーエンドで暗号化されています。", + "Messages in this room are not end-to-end encrypted.": "このルームのメッセージはエンドツーエンドで暗号化されていません。", "You signed in to a new session without verifying it:": "あなたのこのセッションはまだ認証されていません:", "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)sの(%(userId)s)は未認証のセッションにサインインしました:", "Recent Conversations": "最近会話したユーザー", @@ -814,7 +814,7 @@ "Secret storage public key:": "機密ストレージの公開鍵:", "in account data": "アカウントデータ内", "Homeserver feature support:": "ホームサーバーの対応状況:", - "exists": "対応している", + "exists": "対応", "Unable to load session list": "セッション一覧を読み込めません", "Manage": "管理", "Custom theme URL": "カスタムテーマURL", @@ -834,9 +834,9 @@ "Error changing power level": "権限レベルを変更する際のエラー", "Frequently Used": "使用頻度の高いリアクション", "Smileys & People": "表情と人々", - "Animals & Nature": "動物と植物", + "Animals & Nature": "動物と自然", "Food & Drink": "食べ物と飲み物", - "Activities": "アクティビティ", + "Activities": "アクティビティー", "Travel & Places": "旅行と場所", "Objects": "物体", "Symbols": "シンボル", @@ -849,7 +849,7 @@ "Room ID": "ルームID", "More options": "他のオプション", "Manually verify all remote sessions": "全てのリモートセッションを手動で認証", - "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "クロス署名された端末を信頼せず、信頼済としてマークするためにユーザーが使用する各セッションを個別に認証します。", + "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "クロス署名された端末を信頼せず、ユーザーが使用する各セッションを個別に認証し、信頼済に設定。", "This session is backing up your keys. ": "このセッションは鍵をバックアップしています。 ", "Show all": "全て表示", "Message deleted": "メッセージが削除されました", @@ -875,11 +875,11 @@ "Document": "ドキュメント", "Appearance": "外観", "Other users may not trust it": "他のユーザーはこのセッションを信頼しない可能性があります", - "Show a placeholder for removed messages": "削除されたメッセージの場所にプレースホルダーを表示", - "Prompt before sending invites to potentially invalid matrix IDs": "不正かもしれないMatrix IDに招待を送信する前に確認を表示", + "Show a placeholder for removed messages": "削除されたメッセージに関する通知を表示", + "Prompt before sending invites to potentially invalid matrix IDs": "不正の可能性があるMatrix IDに招待を送信する前に確認画面を表示", "Show shortcuts to recently viewed rooms above the room list": "最近表示したルームのショートカットをルームリストの上に表示", - "Show previews/thumbnails for images": "画像のプレビュー/サムネイルを表示", - "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "あなたのアカウントではクロス署名の認証情報がシークレットストレージに保存されていますが、このセッションでは信頼されていません。", + "Show previews/thumbnails for images": "画像のプレビューまたはサムネイルを表示", + "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "あなたのアカウントではクロス署名の認証情報が機密ストレージに保存されていますが、このセッションでは信頼されていません。", "This session is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "このセッションでは鍵をバックアップしていませんが、復元に使用したり、今後鍵を追加したりできるバックアップがあります。", "Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.": "サインアウトする前に、このセッションにだけある鍵を失わないよう、セッションを鍵のバックアップに接続しましょう。", "Connect this session to Key Backup": "このセッションを鍵のバックアップに接続", @@ -895,7 +895,7 @@ "Single Sign On": "シングルサインオン", "Light": "ライト", "Dark": "ダーク", - "Font size": "フォントサイズ", + "Font size": "フォントの大きさ", "Use custom size": "独自のサイズを使用", "Use a system font": "システムフォントを使用", "System font name": "システムフォントの名前", @@ -925,7 +925,7 @@ "Do not use an identity server": "IDサーバーを使用しない", "Composer": "入力欄", "Sort by": "並び替え", - "List options": "一覧の設定", + "List options": "オプションの一覧を表示", "Use Single Sign On to continue": "シングルサインオンを使用して続行", "Accept to continue:": "に同意して続行:", "Always show the window menu bar": "常にウィンドウメニューバーを表示", @@ -973,7 +973,7 @@ "Widgets": "ウィジェット", "Cross-signing is ready for use.": "クロス署名の使用準備が完了しました。", "Secure Backup": "セキュアバックアップ", - "Set up Secure Backup": "セキュアバックアップの設定", + "Set up Secure Backup": "セキュアバックアップを設定", "Go back": "戻る", "Everyone in this room is verified": "このルーム内の全員を認証済", "Verify all users in a room to ensure it's secure.": "このルーム内の全てのユーザーが安全であることを確認しました。", @@ -984,7 +984,7 @@ "Reject & Ignore user": "拒否した上で、このユーザーを無視", " invited you": "があなたを招待しています", "Do you want to join %(roomName)s?": "%(roomName)sに参加しますか?", - "Start chatting": "チャットを開始", + "Start chatting": "会話を開始", " wants to chat": "がチャット開始を求めています", "Do you want to chat with %(user)s?": "%(user)sとのチャットを開始しますか?", "Use the Desktop app to search encrypted messages": "デスクトップアプリを使用すると暗号化されたメッセージを検索できます", @@ -1016,8 +1016,8 @@ "%(displayName)s created this room.": "%(displayName)sがこのルームを作成しました。", "You created this room.": "このルームを作成しました。", "%(creator)s created this DM.": "%(creator)sがこのダイレクトメッセージを作成しました。", - "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "ここでのメッセージはエンドツーエンド暗号化されます。%(displayName)sのアバターをタップすると、プロフィールから認証を行うことができます。", - "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "このルームでのメッセージはエンドツーエンド暗号化されます。参加者のアバターをタップすると、プロフィールから参加者を認証することができます。", + "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "ここでのメッセージはエンドツーエンドで暗号化されます。%(displayName)sのアバターをタップすると、プロフィールから認証を行うことができます。", + "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "このルームでのメッセージはエンドツーエンドで暗号化されます。参加者のアバターをタップすると、プロフィールから参加者を認証することができます。", "Use default": "既定の設定を使用", "e.g. my-room": "例:my-room", "Room address": "ルームのアドレス", @@ -1027,7 +1027,7 @@ "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "検索結果の表示用に、暗号化されたメッセージをローカルに安全にキャッシュしています。現在、%(rooms)s件のルームのメッセージの保存に%(size)sを使用しています。", "Mentions & Keywords": "メンションとキーワード", "Security Key": "セキュリティーキー", - "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "IDサーバーの使用は任意です。IDサーバーを使用しない場合、あなたは他のユーザーから発見されず、メールアドレスや電話番号で他のユーザーを招待することもできません。", + "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "IDサーバーの使用は任意です。IDサーバーを使用しない場合、他のユーザーによって見つけられず、また、メールアドレスや電話で他のユーザーを招待することもできません。", "Integrations not allowed": "インテグレーションは許可されていません", "Integrations are disabled": "インテグレーションが無効になっています", "Manage integrations": "インテグレーションの管理", @@ -1041,11 +1041,11 @@ "Master private key:": "マスター秘密鍵:", "Add a photo, so people can easily spot your room.": "写真を追加して、あなたのルームを目立たせましょう。", "Add a photo so people know it's you.": "写真を追加して、あなただとわかるようにしましょう。", - "Only the two of you are in this conversation, unless either of you invites anyone to join.": "あなたか宛先が誰かを招待しない限りは、この会話は2人だけのものです。", + "Only the two of you are in this conversation, unless either of you invites anyone to join.": "あなたか相手が誰かを招待しない限りは、この会話に参加しているのはあなたたちだけです。", "Password is allowed, but unsafe": "パスワードの要件は満たしていますが、安全ではありません", "Nice, strong password!": "素晴らしい、強固なパスワードです!", "Enter password": "パスワードを入力してください", - "Forgot password?": "パスワードをお忘れですか?", + "Forgot password?": "パスワードを忘れましたか?", "Enter email address": "メールアドレスを入力", "Enter phone number (required on this homeserver)": "電話番号を入力(このホームサーバーでは必須)", "Enter phone number": "電話番号を入力", @@ -1054,7 +1054,7 @@ "New version available. Update now.": "新しいバージョンが利用可能です。今すぐ更新", "Sign In": "サインイン", "Create Account": "アカウントを作成", - "Explore rooms": "ルームを探索", + "Explore rooms": "ルームを探す", "Please view existing bugs on Github first. No match? Start a new one.": "まず、Githubで既知の不具合を確認してください。また掲載されていない新しい不具合を発見した場合は報告してください。", "Report a bug": "不具合の報告", "Update %(brand)s": "%(brand)sの更新", @@ -1068,7 +1068,7 @@ "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "ルームの代替アドレスを更新する際にエラーが発生しました。サーバーで許可されていないか、一時的な障害が発生した可能性があります。", "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "ルームのメインアドレスを更新する際にエラーが発生しました。サーバーで許可されていないか、一時的な障害が発生した可能性があります。", "Error updating main address": "メインアドレスを更新する際のエラー", - "Mark all as read": "全て既読としてマーク", + "Mark all as read": "全て既読にする", "Invited by %(sender)s": "%(sender)sからの招待", "Revoke invite": "招待を取り消す", "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "招待を取り消すことができませんでした。サーバーで一時的な問題が発生しているか、招待を取り消すための十分な権限がない可能性があります。", @@ -1085,8 +1085,8 @@ "%(count)s unread messages including mentions.|other": "メンションを含む未読メッセージ%(count)s件。", "Jump to first invite.": "最初の招待にジャンプします。", "Jump to first unread room.": "未読のある最初のルームにジャンプします。", - "A-Z": "A-Z", - "Activity": "活発さ", + "A-Z": "アルファベット順", + "Activity": "アクティビティー順", "Show previews of messages": "メッセージのプレビューを表示", "Show rooms with unread messages first": "未読メッセージのあるルームを最初に表示", "You're previewing %(roomName)s. Want to join it?": "ルーム %(roomName)s のプレビューです。参加しますか?", @@ -1103,15 +1103,15 @@ "Reason: %(reason)s": "理由:%(reason)s", "Sign Up": "サインアップ", "Join the conversation with an account": "アカウントで会話に参加", - "Rejecting invite …": "招待を拒否する…", - "Explore public rooms": "公開ルームを探索", - "Discovery options will appear once you have added a phone number above.": "上で電話番号を追加すると、ディスカバリーのオプションが表示されます。", + "Rejecting invite …": "招待を拒否しています…", + "Explore public rooms": "公開ルームを探す", + "Discovery options will appear once you have added a phone number above.": "上で電話番号を追加すると、発見可能に設定する電話番号を選択できるようになります。", "Verification code": "認証コード", "Please enter verification code sent via text.": "テキストで送信された確認コードを入力してください。", "Unable to verify phone number.": "電話番号を認証できません。", "Unable to share phone number": "電話番号を共有できません", "Unable to revoke sharing for phone number": "電話番号の共有を取り消せません", - "Discovery options will appear once you have added an email above.": "上でメールアドレスを追加すると、ディスカバリーのオプションが表示されます。", + "Discovery options will appear once you have added an email above.": "上でメールアドレスを追加すると、発見可能に設定するメールアドレスを選択できるようになります。", "Share": "共有", "Revoke": "取り消す", "Complete": "完了", @@ -1162,7 +1162,7 @@ "Please verify the room ID or address and try again.": "ルームのIDやアドレスを確認して、もう一度お試しください。", "Error subscribing to list": "リストを購読する際のエラー", "Something went wrong. Please try again or view your console for hints.": "問題が発生しました。もう一度試すか、コンソールで手がかりを確認してください。", - "Error adding ignored user/server": "無視したユーザー/サーバーを追加する際のエラー", + "Error adding ignored user/server": "無視したユーザーまたはサーバーを追加する際のエラー", "Ignored/Blocked": "無視/ブロック", "Chat with %(brand)s Bot": "%(brand)sボットとチャット", "For help with using %(brand)s, click here or start a chat with our bot using the button below.": "%(brand)sの使用についてサポートが必要な場合は、こちらをクリックするか、下のボタンを使用してボットとチャットを開始してください。", @@ -1175,10 +1175,10 @@ "Use between %(min)s pt and %(max)s pt": "%(min)s~%(max)s(pt)の間の数字を指定", "Custom font size can only be between %(min)s pt and %(max)s pt": "カスタムフォントのサイズは%(min)s~%(max)s(単位:point)の間で指定できます", "Size must be a number": "サイズには数値を指定してください", - "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "IDサーバーから切断すると、他のユーザーから発見されたり、メールアドレスや電話番号で他のユーザーを招待したりできなくなります。", - "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "現在、IDサーバーを使用していません。あなたの知っている連絡先を発見したり、その連絡先から発見されるようにするには、以下にIDサーバーを追加してください。", - "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "連絡先の検出にではなく他のIDサーバーを使いたい場合は以下に指定してください。", - "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "現在を使用して、連絡先を検出可能にしています。以下でIDサーバーを変更できます。", + "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "IDサーバーとの接続を解除すると、他のユーザーによって見つけられなくなり、また、メールアドレスや電話で他のユーザーを招待することもできなくなります。", + "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "現在、IDサーバーを使用していません。連絡先を見つけたり、連絡先から見つけてもらったりするには、以下にIDサーバーを追加してください。", + "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "連絡先の検出にではなく他のIDサーバーを使いたい場合は、以下に指定してください。", + "You are currently using to discover and be discoverable by existing contacts you know. You can change your identity server below.": "現在を使用して、自分の連絡先を見つけたり、連絡先から見つけてもらったりできるようにしています。以下でIDサーバーを変更できます。", "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "切断する前に、IDサーバーからメールアドレスと電話番号を削除することを推奨します。", "You are still sharing your personal data on the identity server .": "まだIDサーバー 個人データを共有しています。", "Disconnect anyway": "切断", @@ -1221,8 +1221,8 @@ "Pin": "ピン", "Folder": "フォルダー", "Headphones": "ヘッドホン", - "Anchor": "錨", - "Bell": "鐘", + "Anchor": "いかり", + "Bell": "ベル", "Trumpet": "トランペット", "Guitar": "ギター", "Ball": "ボール", @@ -1230,14 +1230,14 @@ "Rocket": "ロケット", "Aeroplane": "飛行機", "Bicycle": "自転車", - "Train": "列車", + "Train": "電車", "Flag": "旗", - "Telephone": "電話", - "Hammer": "ハンマー", + "Telephone": "電話機", + "Hammer": "金槌", "Key": "鍵", "Lock": "錠前", - "Scissors": "鋏", - "Paperclip": "紙ばさみ", + "Scissors": "はさみ", + "Paperclip": "クリップ", "Pencil": "鉛筆", "Book": "本", "Light bulb": "電球", @@ -1245,53 +1245,53 @@ "Clock": "時計", "Hourglass": "砂時計", "Umbrella": "傘", - "Thumbs up": "サムズアップ", + "Thumbs up": "いいね", "Santa": "サンタ", "Spanner": "スパナ", - "Glasses": "眼鏡", + "Glasses": "めがね", "Hat": "帽子", "Robot": "ロボット", - "Smiley": "笑顔", + "Smiley": "スマイル", "Heart": "ハート", "Cake": "ケーキ", "Pizza": "ピザ", - "Corn": "トウモロコシ", - "Strawberry": "苺", - "Apple": "林檎", + "Corn": "とうもろこし", + "Strawberry": "いちご", + "Apple": "リンゴ", "Banana": "バナナ", "Fire": "炎", "Cloud": "雲", "Moon": "月", - "Globe": "金魚鉢", - "Mushroom": "茸", + "Globe": "地球", + "Mushroom": "きのこ", "Cactus": "サボテン", "Tree": "木", "Flower": "花", - "Butterfly": "蝶", - "Octopus": "蛸", + "Butterfly": "ちょうちょ", + "Octopus": "たこ", "Fish": "魚", "Turtle": "亀", "Penguin": "ペンギン", - "Rooster": "鶏", + "Rooster": "ニワトリ", "Panda": "パンダ", - "Rabbit": "兎", - "Elephant": "象", - "Pig": "豚", - "Unicorn": "一角獣", + "Rabbit": "うさぎ", + "Elephant": "ゾウ", + "Pig": "ブタ", + "Unicorn": "ユニコーン", "Horse": "馬", "Lion": "ライオン", "Cat": "猫", "Dog": "犬", - "To be secure, do this in person or use a trusted way to communicate.": "安全を確保するため、対面で行うか、または信頼できる方法で認証してください。", - "They don't match": "異なる絵文字です", - "They match": "同じ絵文字です", + "To be secure, do this in person or use a trusted way to communicate.": "セキュリティーを高めるために、対面で行うか、または他の信頼できる通信手段を利用しましょう。", + "They don't match": "一致していません", + "They match": "一致しています", "Cancelling…": "キャンセルしています…", "Unable to find a supported verification method.": "どの認証方法にも対応していません。", "Verify this user by confirming the following number appears on their screen.": "このユーザーを認証するため、両方の画面に同じ番号が表示されていることを確認してください。", "The user must be unbanned before they can be invited.": "招待する前にユーザーのブロックを解除する必要があります。", "Unrecognised address": "認識されないアドレス", "Error leaving room": "ルームを出る際のエラー", - "Unexpected server error trying to leave the room": "ルームを退出する際に予期しないサーバーエラーが発生しました", + "Unexpected server error trying to leave the room": "ルームから退出する際に予期しないサーバーエラーが発生しました", "Unexpected error resolving identity server configuration": "IDサーバーの設定の解釈中に予期しないエラーが発生しました", "Unexpected error resolving homeserver configuration": "ホームサーバーの設定の解釈中に予期しないエラーが発生しました", "You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "ログインできますが、IDサーバーがオンラインに戻るまで一部の機能を使用できません。この警告が引き続き表示される場合は、構成を確認するか、サーバー管理者に連絡してください。", @@ -1469,9 +1469,9 @@ "Room %(name)s": "ルーム %(name)s", "Code block": "コードブロック", "Strikethrough": "取り消し線", - "The authenticity of this encrypted message can't be guaranteed on this device.": "この暗号化されたメッセージの信頼性はこの端末では保証できません。", + "The authenticity of this encrypted message can't be guaranteed on this device.": "この暗号化されたメッセージの真正性はこの端末では保証できません。", "Mod": "モデレーター", - "Edit message": "メッセージの編集", + "Edit message": "メッセージを編集", "Someone is using an unknown session": "誰かが不明なセッションを使用しています", "You have verified this user. This user has verified all of their sessions.": "このユーザーを認証しました。このユーザーは全てのセッションを認証しました。", "You have not verified this user.": "あなたはこのユーザーを認証していません。", @@ -1698,12 +1698,12 @@ "Compare a unique set of emoji if you don't have a camera on either device": "両方の端末でQRコードをキャプチャできない場合、絵文字の比較を選んでください", "Compare unique emoji": "絵文字の並びを比較", "Scan this unique code": "ユニークなコードをスキャン", - "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "ユーザー間でエンドツーエンド暗号化されたメッセージです。第三者が解読することはできません。", - "Verified!": "認証されました!", + "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "このユーザーとのメッセージはエンドツーエンドで暗号化されており、第三者が解読することはできません。", + "Verified!": "認証しました!", "The other party cancelled the verification.": "相手が認証をキャンセルしました。", "Unknown caller": "不明な発信者", "Dial pad": "ダイヤルパッド", - "There was an error looking up the phone number": "電話番号を見つける際にエラーが発生しました", + "There was an error looking up the phone number": "電話番号を検索する際にエラーが発生しました", "Unable to look up phone number": "電話番号が見つかりません", "%(name)s on hold": "%(name)sが保留中", "Return to call": "通話に戻る", @@ -1744,22 +1744,22 @@ "Waiting for answer": "応答を待っています", "%(senderName)s started a call": "%(senderName)sが通話を開始しました", "You started a call": "通話を開始しました", - "Call ended": "通話終了", + "Call ended": "通話が終了しました", "%(senderName)s ended the call": "%(senderName)sが通話を終了しました", "You ended the call": "通話を終了しました", "Call in progress": "通話中", "%(senderName)s joined the call": "%(senderName)sが通話に参加しました", "You joined the call": "通話に参加しました", "Guest": "ゲスト", - "New login. Was this you?": "新しいログインがありました。これはあなたですか?", + "New login. Was this you?": "新しいログインです。ログインしましたか?", "Safeguard against losing access to encrypted messages & data": "暗号化されたメッセージとデータへのアクセスが失われるのを防ぎましょう", "Ok": "OK", "Contact your server admin.": "サーバー管理者に問い合わせてください。", - "Your homeserver has exceeded one of its resource limits.": "ホームサーバーはリソースの上限に達しました。", + "Your homeserver has exceeded one of its resource limits.": "あなたのホームサーバーはリソースの上限に達しました。", "Your homeserver has exceeded its user limit.": "あなたのホームサーバーはユーザー数の上限に達しました。", "Use app": "アプリを使用", "Use app for a better experience": "より良い体験のためにアプリケーションを使用", - "Enable": "有効", + "Enable": "有効にする", "Enable desktop notifications": "デスクトップ通知を有効にする", "Don't miss a reply": "返信をお見逃しなく", "No": "いいえ", @@ -1773,7 +1773,7 @@ "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "マイクを利用できなかったため通話に失敗しました。マイクが接続されて正しく設定されているか確認してください。", "Unable to access microphone": "マイクを利用できません", "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "公開サーバー turn.matrix.org を使用することもできますが、信頼性は高くなく、またサーバーとIPアドレスが共有されます。これは設定からも管理できます。", - "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "通話が機能するよう、ホームサーバー(%(homeserverDomain)s)の管理者にTURNサーバーの設定を尋ねてください。", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "安定した通話のために、ホームサーバー(%(homeserverDomain)s)の管理者にTURNサーバーの設定を依頼してください。", "The call was answered on another device.": "他の端末で通話に応答しました。", "Answered Elsewhere": "他端末で応答しました", "The call could not be established": "通話を確立できませんでした", @@ -1820,7 +1820,7 @@ "Remove recent messages by %(user)s": "%(user)sからの最近のメッセージを削除", "Try scrolling up in the timeline to see if there are any earlier ones.": "タイムラインを上にスクロールして、以前のものがあるかどうかを確認してください。", "No recent messages by %(user)s found": "%(user)sからの最近のメッセージが見つかりません", - "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the space it will be impossible to regain privileges.": "自分自身を降格しようとしています。この変更を元に戻すことはできません。スペース内の最後の特権ユーザーである場合、特権を取り戻すことはできません。", + "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the space it will be impossible to regain privileges.": "あなたは自分自身を降格させようとしています。この変更は取り消せません。あなたがスペースの中で最後の特権ユーザーである場合、特権を再取得することはできなくなります。", "Not encrypted": "暗号化されていません", "Edit widgets, bridges & bots": "ウィジェット、ブリッジ、ボットを編集", "Set my room layout for everyone": "このルームのレイアウトを参加者全体に設定", @@ -1845,16 +1845,16 @@ "Sending your message...": "メッセージを送信しています…", "Space options": "スペースのオプション", "Leave space": "スペースから退出", - "Invite people": "人々を招待", + "Invite people": "連絡先を招待", "Share your public space": "公開スペースを共有", "Share invite link": "招待リンクを共有", "Click to copy": "クリックでコピー", "Creating...": "作成しています…", "Your private space": "あなたの非公開のスペース", "Your public space": "あなたの公開スペース", - "Invite only, best for yourself or teams": "招待者のみ参加可能、個人やチーム向け", + "Invite only, best for yourself or teams": "招待者のみ参加可能。個人やチーム向け", "Private": "非公開", - "Open space for anyone, best for communities": "誰もが利用できるオープンスペース、コミュニティー向け", + "Open space for anyone, best for communities": "誰でも参加できる公開スペース。コミュニティー向け", "Public": "公開", "Create a space": "スペースを作成", "Delete": "削除", @@ -1875,10 +1875,10 @@ "Welcome to ": "にようこそ", "Invite to just this room": "このルームに招待", "Invite to %(spaceName)s": "%(spaceName)sに招待", - "A private space for you and your teammates": "あなたとチームメイトの非公開のスペース", + "A private space for you and your teammates": "自分とチームメイトの非公開のスペース", "Me and my teammates": "自分とチームメイト", "Just me": "自分専用", - "Make sure the right people have access to %(name)s": "必要な人が%(name)sにアクセスできるようにしましょう", + "Make sure the right people have access to %(name)s": "正しい参加者が%(name)sにアクセスできるようにしましょう。", "Who are you working with?": "誰と使いますか?", "Beta": "ベータ版", "Check your devices": "端末を確認", @@ -1896,8 +1896,8 @@ "Adding rooms... (%(progress)s out of %(count)s)|one": "ルームを追加しています…", "Adding rooms... (%(progress)s out of %(count)s)|other": "ルームを追加しています…(%(progress)s/%(count)s)", "Skip for now": "スキップ", - "What do you want to organise?": "どれを追加しますか?", - "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "ルームや会話を追加できます。これはあなた専用のスペースで、他の人からは見えません。後からルームや会話を追加することもできます。", + "What do you want to organise?": "何を追加しますか?", + "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "ルームや会話を追加できます。これはあなた専用のスペースで、他の人からは見えません。後から追加することもできます。", "Support": "サポート", "You can change these anytime.": "ここで入力した情報はいつでも編集できます。", "Add some details to help people recognise it.": "情報を入力してください。", @@ -1906,7 +1906,7 @@ "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "インテグレーションマネージャーは設定データを受け取り、ユーザーの代わりにウィジェットの変更や、ルームへの招待の送信、権限レベルの設定を行うことができます。", "Use an integration manager to manage bots, widgets, and sticker packs.": "インテグレーションマネージャーを使用すると、ボット、ウィジェット、ステッカーパックを管理できます。", "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "インテグレーションマネージャー(%(serverName)s) を使用すると、ボット、ウィジェット、ステッカーパックを管理できます。", - "Identity server": "認証サーバー", + "Identity server": "IDサーバー", "Identity server (%(server)s)": "IDサーバー(%(server)s)", "Could not connect to identity server": "IDサーバーに接続できませんでした", "Not a valid identity server (status code %(code)s)": "有効なIDサーバーではありません(ステータスコード %(code)s)", @@ -1915,7 +1915,7 @@ "Failed to save space settings.": "スペースの設定を保存できませんでした。", "Transfer Failed": "転送に失敗しました", "Unable to transfer call": "通話を転送できませんでした", - "All rooms you're in will appear in Home.": "ホームに、あなたが参加している全てのルームが表示されます。", + "All rooms you're in will appear in Home.": "あなたが参加している全てのルームがホームに表示されます。", "Show all rooms in Home": "ホームに全てのルームを表示", "Images, GIFs and videos": "画像・GIF・動画", "Displaying time": "表示時刻", @@ -1933,7 +1933,7 @@ "Space members": "スペースのメンバー", "Upgrade required": "アップグレードが必要", "Only invited people can join.": "招待された人のみ参加できます。", - "Private (invite only)": "非公開(招待者のみ)", + "Private (invite only)": "非公開(招待者のみ参加可能)", "Decide who can join %(roomName)s.": "%(roomName)sに参加できる人を設定してください。", "%(senderName)s invited %(targetName)s": "%(senderName)sが%(targetName)sを招待しました", "Verify your identity to access encrypted messages and prove your identity to others.": "暗号化されたメッセージにアクセスするには、本人確認が必要です。", @@ -1941,7 +1941,7 @@ "Are you sure you want to sign out?": "サインアウトしてよろしいですか?", "Upgrade to %(hostSignupBrand)s": "%(hostSignupBrand)sにアップグレード", "Rooms and spaces": "ルームとスペース", - "Add a space to a space you manage.": "管理しているスペースに、スペースを追加します。", + "Add a space to a space you manage.": "新しいスペースを、あなたが管理するスペースに追加。", "Add space": "スペースを追加", "This room is suggested as a good one to join": "このルームは、参加を推奨するルームとしておすすめされています", "Mark as suggested": "おすすめに追加", @@ -1949,9 +1949,9 @@ "Suggested": "おすすめ", "Removing...": "削除しています…", "Joined": "参加済", - "Give feedback.": "フィードバックを送信する。", + "Give feedback.": "フィードバックを送信。", "Thank you for trying Spaces. Your feedback will help inform the next versions.": "スペースをお試しいただきありがとうございます。いただいたフィードバックは、次のバージョンの参考にさせていただきます。", - "Spaces feedback": "スペースに関するフィードバック", + "Spaces feedback": "スペースについてのフィードバック", "Spaces are a new feature.": "スペースは新しい機能です。", "To join a space you'll need an invite.": "スペースに参加するには招待が必要です。", "Sign out %(count)s selected devices|one": "%(count)s個の端末からサインアウト", @@ -1966,7 +1966,7 @@ "Group all your favourite rooms and people in one place.": "お気に入りのルームと連絡先を表示します。", "Show all your rooms in Home, even if they're in a space.": "他のスペースに存在するルームを含めて、全てのルームをホームに表示します。", "Home is useful for getting an overview of everything.": "ホームは全体を把握するのに便利です。", - "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.": "スペースは、ルームや連絡先をまとめる方法です。いくつかの構築済スペースと、参加済のスペースを使用できます。", "Spaces to show": "表示するスペース", "Sidebar": "サイドバー", "Show all rooms": "全てのルームを表示", @@ -1988,7 +1988,7 @@ "Select from the options below to export chats from your timeline": "以下のオプションを選択して、チャットをエクスポートできます", "Export Chat": "チャットをエクスポート", "Export chat": "チャットをエクスポート", - "View source": "ソースを表示", + "View source": "ソースコードを表示", "Failed to send": "送信に失敗しました", "You can't see earlier messages": "以前のメッセージは表示できません", "Encrypted messages before this point are unavailable.": "これ以前の暗号化されたメッセージは利用できません。", @@ -2007,7 +2007,7 @@ "Preparing to download logs": "ログのダウンロードを準備しています", "User Busy": "ユーザーは通話中です", "The user you called is busy.": "呼び出したユーザーは通話中です。", - "Hide stickers": "ステッカーを隠す", + "Hide stickers": "ステッカーを表示しない", "Send voice message": "音声メッセージを送信", "You do not have permission to start polls in this room.": "このルームでアンケートを開始する権限がありません。", "Voice Message": "音声メッセージ", @@ -2093,7 +2093,7 @@ "Spaces you're in": "参加しているスペース", "Command Help": "コマンドヘルプ", "Link to room": "ルームへのリンク", - "This groups your chats with members of this space. Turning this off will hide those chats from your view of %(spaceName)s.": "このスペースのメンバーとの会話をグループ化します。無効にすると、それらの会話は%(spaceName)sの表示画面に表示されなくなります。", + "This groups your chats with members of this space. Turning this off will hide those chats from your view of %(spaceName)s.": "このスペースのメンバーとの会話をまとめます。無効にすると、それらの会話は%(spaceName)sの表示画面に表示されなくなります。", "Space home": "スペースのホーム", "Go to Home View": "ホームに移動", "Previous autocomplete suggestion": "前の自動補完の候補", @@ -2129,7 +2129,7 @@ "Navigate to next message to edit": "次のメッセージに移動して編集", "Toggle Italics": "斜字体を切り替える", "Toggle Bold": "太字を切り替える", - "Toggle Quote": "引用を切り替える", + "Toggle Quote": "引用の表示を切り替える", "Toggle webcam on/off": "Webカメラのオン/オフを切り替える", "Toggle right panel": "右のパネルの表示を切り替える", "Toggle space panel": "スペースのパネルを切り替える", @@ -2180,7 +2180,7 @@ "Message": "メッセージ", "Shows all threads from current room": "現在のルームのスレッドを全て表示", "All threads": "全てのスレッド", - "Keep discussions organised with threads": "スレッドでディスカッションを整理して管理", + "Keep discussions organised with threads": "スレッド機能を使って、会話をまとめましょう", "Show:": "表示:", "Shows all threads you've participated in": "参加している全スレッドを表示", "My threads": "自分のスレッド", @@ -2230,7 +2230,7 @@ "Successfully restored %(sessionCount)s keys": "%(sessionCount)s個の鍵が復元されました", "Keys restored": "鍵が復元されました", "Backup could not be decrypted with this Security Key: please verify that you entered the correct Security Key.": "このセキュリティーキーではバックアップを復号化できませんでした。正しいセキュリティーキーを入力したことを確認してください。", - "Security Key mismatch": "セキュリティーキーの不一致", + "Security Key mismatch": "セキュリティーキーが一致しません", "%(completed)s of %(total)s keys restored": "%(total)s個のうち%(completed)s個の鍵が復元されました", "Fetching keys from server...": "鍵をサーバーから取得しています…", "Restoring keys from backup": "バックアップから鍵を復元", @@ -2245,8 +2245,8 @@ "%(senderName)s pinned a message to this room. See all pinned messages.": "%(senderName)sがメッセージをこのルームに固定しました。全ての固定されたメッセージを表示。", "Someone already has that username. Try another or if it is you, sign in below.": "そのユーザー名は既に使用されています。別のユーザー名を試すか、あなたのユーザー名なら、以下でサインインしてください。", "Sign in with SSO": "シングルサインオンでサインイン", - "Use email or phone to optionally be discoverable by existing contacts.": "メールアドレスまたは電話番号で知人に見つけてもらえるようにできます(任意)。", - "Use email to optionally be discoverable by existing contacts.": "メールアドレスで知人に見つけてもらえるようにできます(任意)。", + "Use email or phone to optionally be discoverable by existing contacts.": "メールアドレスまたは電話番号で連絡先に見つけてもらえるようにできます。", + "Use email to optionally be discoverable by existing contacts.": "メールアドレスで連絡先に見つけてもらえるようにできます。", "Add an email to be able to reset your password.": "アカウント復旧用のメールアドレスを設定します。", "About homeservers": "ホームサーバーについて(英語)", "Use your preferred Matrix homeserver if you have one, or host your own.": "好みのホームサーバーがあるか、自分でホームサーバーを運営している場合は、そちらをお使いください。", @@ -2256,7 +2256,7 @@ "Decide where your account is hosted": "アカウントを管理する場所を決めましょう", "Host account on": "アカウントを以下のホームサーバーでホスト", "Continue with %(provider)s": "%(provider)sで続行", - "Join millions for free on the largest public server": "最大のパブリックサーバーで、数百万人に無料で参加", + "Join millions for free on the largest public server": "最大の公開サーバーで、数百万人に無料で参加", "Already have an account? Sign in here": "既にアカウントを持っていますか?ここからサインインしてください", "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).": "新しいアカウント(%(newAccountId)s)が登録されましたが、あなたは別のアカウント(%(loggedInUserId)s)でログインしています。", "Continue with previous account": "以前のアカウントで続行", @@ -2295,11 +2295,11 @@ "Force complete": "強制的に自動補完", "Activate selected button": "選択したボタンを有効にする", "Close dialog or context menu": "ダイアログかコンテクストメニューを閉じる", - "Enter the name of a new server you want to explore.": "探索したい新しいサーバーの名前を入力してください。", + "Enter the name of a new server you want to explore.": "探したい新しいサーバーの名前を入力してください。", "Upgrade public room": "公開ルームをアップグレード", "Public room": "公開ルーム", "Upgrade private room": "非公開のルームをアップグレード", - "Private room (invite only)": "非公開のルーム(招待者のみ)", + "Private room (invite only)": "非公開のルーム(招待者のみ参加可能)", "Can't find this server or its room list": "サーバーまたはそのルーム一覧が見つかりません", "Join public room": "公開ルームに参加", "You can change this at any time from room settings.": "これはルームの設定で後からいつでも変更できます。", @@ -2392,7 +2392,7 @@ "Upload %(count)s other files|other": "あと%(count)s個のファイルをアップロード", "This file is too large to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "アップロードしようとしているファイルのサイズが大きすぎます。最大のサイズは%(limit)sですが、ファイルのサイズは%(sizeOfThisFile)sです。", "Approve": "同意", - "Away": "退席中", + "Away": "離席中", "Categories": "カテゴリー", "Space": "スペース", "Disable": "無効にする", @@ -2419,7 +2419,7 @@ "Signature upload success": "署名のアップロードに成功しました", "Cancelled signature upload": "署名のアップロードをキャンセルしました", "This address does not point at this room": "このアドレスはこのルームを指していません", - "You do not have permissions to add spaces to this space": "スペースをこのスペースに追加する権限がありません", + "You do not have permissions to add spaces to this space": "このスペースに別のスペースを追加する権限がありません", "Open in OpenStreetMap": "OpenStreetMapで開く", "Please enter a name for the room": "ルームの名称を入力してください", "The following users may not exist": "次のユーザーは存在しない可能性があります", @@ -2567,7 +2567,7 @@ "Space information": "スペースの情報", "Retry all": "全て再試行", "You can select all or individual messages to retry or delete": "全てのメッセージ、あるいは個別のメッセージを選択して、再送を試みるか削除することができます", - "Some of your messages have not been sent": "メッセージが送信されませんでした", + "Some of your messages have not been sent": "いくつかのメッセージが送信されませんでした", "Mentions only": "メンションのみ", "Forget": "消去", "Not a valid Security Key": "正しいセッションキーではありません", @@ -2592,7 +2592,7 @@ "Clear personal data": "個人データを消去", "Starting backup...": "バックアップを開始しています…", "This homeserver does not support login using email address.": "このホームサーバーではメールアドレスによるログインをサポートしていません。", - "Your password has been reset.": "パスワードがリセットされました。", + "Your password has been reset.": "パスワードをリセットしました。", "Couldn't load page": "ページを読み込めませんでした", "Confirm the emoji below are displayed on both devices, in the same order:": "以下の絵文字が、両方の端末で、同じ順番で表示されているかどうか確認してください:", "Show sidebar": "サイドバーを表示", @@ -2601,7 +2601,7 @@ "We couldn't create your DM.": "ダイレクトメッセージを作成できませんでした。", "Confirm to continue": "確認して続行", "Failed to find the following users": "次のユーザーの発見に失敗しました", - "Privacy Policy": "個人情報保護方針", + "Privacy Policy": "プライバシーポリシー", "Cookie Policy": "Cookieの使用方針", "Sorry, the poll did not end. Please try again.": "アンケートを終了できませんでした。もう一度やり直してください。", "Are you sure you want to stop exporting your data? If you do, you'll need to start over.": "データのエクスポートを停止してよろしいですか?改めてやり直す必要があります。", @@ -2653,13 +2653,13 @@ "Fetched %(count)s events so far|one": "%(count)s個のイベントを取得しました", "Fetched %(count)s events so far|other": "%(count)s個のイベントを取得しました", "Processing event %(number)s out of %(total)s": "%(total)s個のうち%(number)s個のイベントを処理しています", - "Error fetching file": "ファイルを取得する際のエラー", + "Error fetching file": "ファイルの取得中にエラーが発生しました", "Exported %(count)s events in %(seconds)s seconds|one": "%(count)s個のイベントを%(seconds)s秒でエクスポートしました", "Exported %(count)s events in %(seconds)s seconds|other": "%(count)s個のイベントを%(seconds)s秒でエクスポートしました", "Fetched %(count)s events in %(seconds)ss|other": "%(count)s個のイベントを%(seconds)s秒で取得しました", "That's fine": "問題ありません", "Your new device is now verified. Other users will see it as trusted.": "端末が認証されました。他のユーザーには「信頼済」として表示されます。", - "Your new device is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "端末が認証されました。暗号化されたメッセージにアクセスすることができます。また、端末は他のユーザーに「信頼済」として表示されます。", + "Your new device is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "新しい端末が認証されました。端末は暗号化されたメッセージにアクセスすることができます。また、端末は他のユーザーに「信頼済」として表示されます。", "Verify with Security Key": "セキュリティーキーで認証", "Verify with Security Key or Phrase": "セキュリティーキーあるいはセキュリティーフレーズで認証", "A new Security Phrase and key for Secure Messages have been detected.": "新しいセキュリティーフレーズと、セキュアメッセージの鍵が検出されました。", @@ -2682,7 +2682,7 @@ "<%(count)s spaces>|other": "<%(count)s個のスペース>", "Create key backup": "鍵のバックアップを作成", "My current location": "自分の現在の位置情報", - "My live location": "自分の現在の位置情報(ライブ)", + "My live location": "自分の位置情報(ライブ)", "What location type do you want to share?": "どのような種類の位置情報を共有したいですか?", "Match system": "システムに合致", "We couldn't send your location": "位置情報を送信できませんでした", @@ -2718,8 +2718,8 @@ "Write something...": "記入してください…", "Based on %(count)s votes|one": "%(count)s個の投票に基づく途中結果", "Based on %(count)s votes|other": "%(count)s個の投票に基づく途中結果", - "Final result based on %(count)s votes|one": "%(count)s個の投票に基づく最終結果", - "Final result based on %(count)s votes|other": "%(count)s個の投票に基づく最終結果", + "Final result based on %(count)s votes|one": "合計%(count)s票の投票に基づく最終結果", + "Final result based on %(count)s votes|other": "合計%(count)s票の投票に基づく最終結果", "Sorry, you can't edit a poll after votes have been cast.": "投票があったアンケートは編集できません。", "%(oneUser)ssent %(count)s hidden messages|one": "%(oneUser)sが1件の非表示のメッセージを送信しました", "%(oneUser)ssent %(count)s hidden messages|other": "%(oneUser)sが%(count)s件の非表示のメッセージを送信しました", @@ -2920,8 +2920,8 @@ "Message search initialisation failed, check your settings for more information": "メッセージの検索の初期化に失敗しました。設定から詳細を確認してください", "Any of the following data may be shared:": "以下のデータが共有される可能性があります:", "%(reactors)s reacted with %(content)s": "%(reactors)sは%(content)sでリアクションしました", - "%(count)s votes cast. Vote to see the results|one": "%(count)s個の投票がありました。投票すると結果を表示します", - "%(count)s votes cast. Vote to see the results|other": "%(count)s個の投票がありました。投票すると結果を表示します", + "%(count)s votes cast. Vote to see the results|one": "合計%(count)s票。投票すると結果を確認できます", + "%(count)s votes cast. Vote to see the results|other": "合計%(count)s票。投票すると結果を確認できます", "Some encryption parameters have been changed.": "暗号化のパラメーターのいくつかが変更されました。", "The call is in an unknown state!": "通話の状態が不明です!", "Verify this device by completing one of the following:": "以下のいずれかでこの端末を認証してください:", @@ -2974,7 +2974,7 @@ "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.": "注意:メールアドレスを追加せずパスワードを忘れた場合、永久にアカウントにアクセスできなくなる可能性があります。", "Some characters not allowed": "使用できない文字が含まれています", "Send a sticker": "ステッカーを送信", - "Spaces are a new way to group rooms and people. What kind of Space do you want to create? You can change this later.": "スペースは、ルームや連絡先をグループ化する新しい方法です。どんなグループを作りますか?これは後から変更できます。", + "Spaces are a new way to group rooms and people. What kind of Space do you want to create? You can change this later.": "スペースは、ルームや連絡先をまとめる新しい方法です。どんなグループを作りますか?これは後から変更できます。", "Collapse quotes": "引用を折りたたむ", "Expand quotes": "引用を展開", "Click": "クリック", @@ -2987,7 +2987,7 @@ "This version of %(brand)s does not support viewing some encrypted files": "この%(brand)sのバージョンは、暗号化されたファイルの表示をサポートしていません", "This version of %(brand)s does not support searching encrypted messages": "この%(brand)sのバージョンは、暗号化されたメッセージの検索をサポートしていません", "Their device couldn't start the camera or microphone": "相手の端末はカメラ、もしくはマイクを使用できませんでした", - "We don't record or profile any account data": "私たちは、アカウントのいかなるデータも記録したり分析したりしません", + "We don't record or profile any account data": "私たちは、アカウントのいかなるデータも記録したり分析したりすることはありません", "Use an identity server to invite by email. Manage in Settings.": "IDサーバーを使うと、メールアドレスで招待できます。設定画面で管理できます。", "Use an identity server to invite by email. Use the default (%(defaultIdentityServerName)s) or manage in Settings.": "IDサーバーを使うと、メールアドレスで招待できます。既定(%(defaultIdentityServerName)s)のサーバーを使うか、設定画面で管理できます。", "Adding spaces has moved.": "スペースの追加機能は移動しました。", @@ -3010,7 +3010,7 @@ "Can't create a thread from an event with an existing relation": "既存の関係のあるイベントからスレッドを作成することはできません", "Remove them from everything I'm able to": "自分に可能な範囲で、全てのものから追放", "Remove them from specific things I'm able to": "自分に可能な範囲で、特定のものから追放", - "Make sure the right people have access. You can invite more later.": "必要な人がアクセスできるようにしましょう。後から追加で招待できます。", + "Make sure the right people have access. You can invite more later.": "正しい参加者がアクセスできるようにしましょう。後から追加で招待できます。", "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の新しいバージョンへと戻る必要があります", "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "このセッションのデータの消去は取り消せません。鍵がバックアップされていない限り、暗号化されたメッセージを読むことはできなくなります。", "Including %(commaSeparatedMembers)s": "%(commaSeparatedMembers)sを含む", @@ -3022,7 +3022,7 @@ "Ban them from specific things I'm able to": "自分に可能な範囲で、特定のものからブロック", "Ban them from everything I'm able to": "自分に可能な範囲で、全てのものからブロック", "If you've submitted a bug via GitHub, debug logs can help us track down the problem. ": "もしGitHubで不具合を報告した場合は、デバッグログが問題の解決に役立ちます。 ", - "Do you want to enable threads anyway?": "スレッドを有効にしますか?", + "Do you want to enable threads anyway?": "スレッド機能を有効にしますか?", "Yes, enable": "有効にする", "Live location error": "位置情報(ライブ)のエラー", "sends hearts": "ハートを送信", @@ -3039,7 +3039,7 @@ "%(value)sh": "%(value)s時", "%(value)sd": "%(value)s日", "Beta feature": "ベータ版の機能", - "Live location enabled": "位置情報が有効です", + "Live location enabled": "位置情報(ライブ)が有効です", "Jump to the given date in the timeline": "タイムラインの指定した日に移動", "Unban from space": "スペースからのブロックを解除", "Ban from space": "スペースからブロック", @@ -3047,8 +3047,6 @@ "Ban from room": "ルームからブロック", "Copy link": "リンクをコピー", "%(featureName)s Beta feedback": "%(featureName)sのベータ版のフィードバック", - "Threads are a beta feature": "スレッドはベータ版の機能です", - "Give feedback": "フィードバックを送信", "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "警告:あなたの個人情報(暗号化の鍵を含む)が、このセッションに保存されています。このセッションの使用を終了するか、他のアカウントにログインしたい場合は、そのデータを消去してください。", "The user's homeserver does not support the version of the space.": "ユーザーのホームサーバーは、このバージョンのスペースをサポートしていません。", "User may or may not exist": "ユーザーが存在するか不明です", @@ -3057,7 +3055,7 @@ "User is already in the space": "ユーザーは既にスペースに入っています", "User is already invited to the room": "ユーザーは既にルームに招待されています", "User is already invited to the space": "ユーザーは既にスペースに招待されています", - "You do not have permission to invite people to this space.": "ユーザーをこのスペースに招待する権限がありません。", + "You do not have permission to invite people to this space.": "このスペースにユーザーを招待する権限がありません。", "Failed to invite users to %(roomName)s": "ユーザーを%(roomName)sに招待するのに失敗しました", "%(value)ss": "%(value)s秒", "You can still join here.": "参加できます。", @@ -3074,12 +3072,12 @@ "Create room": "ルームを作成", "Threads help keep your conversations on-topic and easy to track.": "スレッド機能を使うと、会話のテーマを維持したり、会話を簡単に追跡したりすることができます。", "Keep discussions organised with threads.": "スレッド機能を使って、会話をまとめましょう。", - "Threads help keep conversations on-topic and easy to track. Learn more.": "スレッド機能を使うと、会話のテーマを維持したり、会話を簡単に追跡したりすることができます。詳しく知る。", + "Threads help keep conversations on-topic and easy to track. Learn more.": "スレッド機能を使うと、会話のテーマを維持したり、会話を簡単に追跡したりすることができます。詳細を確認。", "Beta feature. Click to learn more.": "ベータ版の機能です。クリックすると詳細を表示します。", "Partial Support for Threads": "スレッド機能の部分的サポート", - "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "ホームサーバーがサポートしていないため、スレッド機能は不安定かもしれません。スレッドのメッセージは安定して表示されないおそれがあります。詳しく知る。", + "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "ホームサーバーがサポートしていないため、スレッド機能は不安定かもしれません。スレッドのメッセージが安定して表示されないおそれがあります。詳細を表示。", "Confirm this user's session by comparing the following with their User Settings:": "ユーザー設定画面で以下を比較し、このユーザーのセッションを承認してください:", - "Confirm by comparing the following with the User Settings in your other session:": "以下をあなたの別のセッションのユーザー設定画面で比較し、承認してください:", + "Confirm by comparing the following with the User Settings in your other session:": "他のセッションのユーザー設定で、以下を比較して承認してください:", "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s. The administrators will NOT be able to read the encrypted content of this room.": "不法なコンテンツの投稿が行われ、モデレーターによる適切な管理がなされていない。\nこのルームを%(homeserver)sの管理者に報告します。ただし、このルームの暗号化されたコンテンツを、管理者に読み取ることはできません。", "This user is spamming the room with ads, links to ads or to propaganda.\nThis will be reported to the room moderators.": "ユーザーが広告や、広告・プロパガンダへのリンクのスパムを行っている。\nこのユーザーをルームのモデレーターに報告します。", "What this user is writing is wrong.\nThis will be reported to the room moderators.": "ユーザーの投稿内容が正確でない。\nこのユーザーをルームのモデレーターに報告します。", @@ -3097,7 +3095,7 @@ "Send custom account data event": "アカウントのデータイベントを送信", "Doesn't look like valid JSON.": "正しいJSONではありません。", "Failed to send event!": "イベントの送信に失敗しました!", - "Send custom state event": "カスタムステートイベントを送信", + "Send custom state event": "カスタムのステートイベントを送信", "Failed to load.": "読み込みに失敗しました。", "Client Versions": "クライアントのバージョン", "Server Versions": "サーバーのバージョン", @@ -3116,7 +3114,7 @@ "Forget this space": "このスペースの履歴を消去", "You were banned by %(memberName)s": "%(memberName)sによりブロックされました", "Something went wrong with your invite.": "招待に問題が発生しました。", - "This invite was sent to %(email)s which is not associated with your account": "この招待は、あなたのアカウントに関連付けられていない%(email)sに送信されました", + "This invite was sent to %(email)s which is not associated with your account": "この招待は、アカウントに関連付けられていないメールアドレス %(email)s に送られました", "Try again later, or ask a room or space admin to check if you have access.": "後でもう一度やり直すか、ルームまたはスペースの管理者に、アクセス権の有無を確認してください。", "Live location ended": "位置情報(ライブ)が終了しました", "Disinvite from space": "スペースへの招待を取り消す", @@ -3128,13 +3126,13 @@ "View list": "一覧を表示", "View List": "一覧を表示", "Mute microphone": "マイクをミュート", - "Unmute microphone": "ミュート解除", + "Unmute microphone": "マイクのミュートを解除", "Turn off camera": "カメラを無効にする", "Turn on camera": "カメラを有効にする", "%(user1)s and %(user2)s": "%(user1)sと%(user2)s", "Video call started in %(roomName)s. (not supported by this browser)": "ビデオ通話が%(roomName)sで開始しました。(このブラウザーではサポートされていません)", "Video call started in %(roomName)s.": "ビデオ通話が%(roomName)sで開始しました。", - "You need to be able to kick users to do that.": "それをするためにユーザーをキックできる必要があります。", + "You need to be able to kick users to do that.": "それを行うにはユーザーをキックする権限が必要です。", "Empty room (was %(oldName)s)": "空のルーム(以前の名前は%(oldName)s)", "Inviting %(user)s and %(count)s others|one": "%(user)sと1人を招待しています", "Inviting %(user)s and %(count)s others|other": "%(user)sと%(count)s人を招待しています", @@ -3154,15 +3152,15 @@ "Remember my selection for this widget": "このウィジェットに関する選択を記憶", "Unable to load commit detail: %(msg)s": "コミットの詳細を読み込めません:%(msg)s", "Capabilities": "能力", - "Toggle Code Block": "コードブロックを切り替える", + "Toggle Code Block": "コードブロックの表示を切り替える", "Toggle Link": "リンクを切り替える", "New group call experience": "グループ通話の新しい経験", "Element Call video rooms": "Element Callのビデオ通話ルーム", "Send read receipts": "開封確認メッセージを送信", - "Explore public spaces in the new search dialog": "新しい検索ダイアログで公開スペースを探索", + "Explore public spaces in the new search dialog": "新しい検索ダイアログで公開スペースを探す", "Yes, the chat timeline is displayed alongside the video.": "はい、会話のタイムラインが動画と並んで表示されます。", "Can I use text chat alongside the video call?": "テキストによる会話も行えますか?", - "Use the “+” button in the room section of the left panel.": "左のパネルにあるルームのセクションの「+」ボタンで作成できます。", + "Use the “+” button in the room section of the left panel.": "左のパネルにあるルームのセクションの「+」ボタンで作成できます。", "How can I create a video room?": "ビデオ通話ルームの作成方法", "Video rooms are always-on VoIP channels embedded within a room in %(brand)s.": "ビデオ通話ルームは、%(brand)sのルームに埋め込まれている、VoIPが常時有効のチャンネルです。", "A new way to chat over voice and video in %(brand)s.": "%(brand)sで音声と動画により会話する新しい方法です。", @@ -3170,14 +3168,14 @@ "You were disconnected from the call. (Error: %(message)s)": "通話から切断されました。(エラー:%(message)s)", "Connection lost": "接続が切断されました", "Video": "動画", - "Room info": "ルーム情報", - "Receive push notifications on this session.": "このセッションでプッシュ通知を受信します。", + "Room info": "ルームの情報", + "Receive push notifications on this session.": "このセッションでプッシュ通知を受信。", "Push notifications": "プッシュ通知", "Sign out of this session": "このセッションからサインアウト", - "Last activity": "最後のアクティビティ", - "Other sessions": "他のセッション", + "Last activity": "直近のアクティビティー", + "Other sessions": "その他のセッション", "Current session": "現在のセッション", - "Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.": "新しいセッションマネージャーは、全セッションを見やすくし、遠隔からプッシュ通知を切り替えるなど、セッションを管理しやすくします。", + "Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.": "新しいセッションマネージャーを使用すると、全てのセッションが見えやすくなり、遠隔からプッシュ通知を切り替えるなど、セッションをより容易に管理できるようになります。", "New session manager": "新しいセッションマネージャー", "Use new session manager": "新しいセッションマネージャーを使用", "Video room": "ビデオ通話ルーム", @@ -3199,8 +3197,8 @@ "Copy invite link": "招待リンクをコピー", "Show spaces": "スペースを表示", "Show rooms": "ルームを表示", - "Interactively verify by emoji": "絵文字によるインタラクティブ認証", - "Manually verify by text": "テキストによる手動認証", + "Interactively verify by emoji": "絵文字によるインタラクティブな認証", + "Manually verify by text": "テキストを使って手動で認証", "Checking...": "確認中...", "Modal Widget": "モーダルウィジェット", "You will no longer be able to log in": "ログインできなくなります", @@ -3208,8 +3206,8 @@ "Help": "ヘルプ", "Minimise": "最小化", "Underline": "下線", - "Italic": "イタリック", - "Joining…": "参加中…", + "Italic": "斜字体", + "Joining…": "参加しています…", "Show Labs settings": "ラボ設定を表示", "Private room": "非公開ルーム", "Video call (Jitsi)": "ビデオ通話(Jitsi)", @@ -3220,10 +3218,10 @@ "All": "全て", "Verified session": "認証済のセッション", "IP address": "IPアドレス", - "Browser": "ブラウザ", + "Browser": "ブラウザー", "Version": "バージョン", "Click the button below to confirm your identity.": "本人確認のため下のボタンをクリックしてください。", - "Ignore user": "ユーザーを無視する", + "Ignore user": "ユーザーを無視", "Proxy URL (optional)": "プロキシーURL(任意)", "Proxy URL": "プロキシーURL", "%(count)s Members|other": "%(count)s人の参加者", @@ -3242,22 +3240,22 @@ "The request was cancelled.": "リクエストはキャンセルされました。", "An unexpected error occurred.": "予期しないエラーが起こりました。", "Devices connected": "接続中の端末", - "Check that the code below matches with your other device:": "下のコードが他の端末と一致するか確認:", - "Connecting...": "接続中...", + "Check that the code below matches with your other device:": "以下のコードが他の端末と一致していることを確認してください:", + "Connecting...": "接続しています…", "Use lowercase letters, numbers, dashes and underscores only": "小文字、数字、ダッシュ、アンダースコアのみを使ってください", "Your server does not support showing space hierarchies.": "あなたのサーバーはスペースの階層表示をサポートしていません。", - "That e-mail address or phone number is already in use.": "そのメールアドレスまたは電話番号はすでに使われています。", + "That e-mail address or phone number is already in use.": "そのメールアドレスまたは電話番号は既に使われています。", "Great! This Security Phrase looks strong enough.": "すばらしい! このセキュリティーフレーズは十分に強力なようです。", "%(downloadButton)s or %(copyButton)s": "%(downloadButton)sまたは%(copyButton)s", - "Voice broadcast": "音声ブロードキャスト", + "Voice broadcast": "音声配信", "Live": "ライブ", - "pause voice broadcast": "音声ブロードキャストを一時停止", - "resume voice broadcast": "音声ブロードキャストを再開", - "play voice broadcast": "音声ブロードキャストを再生", - "Yes, stop broadcast": "はい、ブロードキャストを停止します", - "Stop live broadcasting?": "ライブブロードキャストを停止しますか?", - "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "他の人がすでに音声ブロードキャストを録音しています。新しく始めるにはその音声ブロードキャストが終わるのを待ってください。", - "Can't start a new voice broadcast": "新しい音声ブロードキャストを開始できません", + "pause voice broadcast": "音声配信を一時停止", + "resume voice broadcast": "音声配信を再開", + "play voice broadcast": "音声配信を再生", + "Yes, stop broadcast": "はい、配信を停止します", + "Stop live broadcasting?": "ライブ配信を停止しますか?", + "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "他の人が既に音声配信を録音しています。新しく始めるには音声配信が終わるまで待機してください。", + "Can't start a new voice broadcast": "新しい音声配信を開始できません", "%(minutes)sm %(seconds)ss left": "残り%(minutes)s分%(seconds)s秒", "%(hours)sh %(minutes)sm %(seconds)ss left": "残り%(hours)s時間 %(minutes)s分%(seconds)s秒", "Exit fullscreen": "フルスクリーンを解除", @@ -3274,18 +3272,18 @@ "Filter devices": "端末を絞り込む", "You made it!": "できました!", "Find and invite your friends": "友達を見つけて招待する", - "Sorry — this call is currently full": "すみませんーこの通話は現在満員です", + "Sorry — this call is currently full": "すみません ― この通話は現在満員です", "Enable hardware acceleration": "ハードウェアアクセラレーションを有効にする", "Allow Peer-to-Peer for 1:1 calls": "1対1通話でP2Pを使用する", "Enter fullscreen": "フルスクリーンにする", "Error downloading image": "画像をダウンロードする際のエラー", "Unable to show image due to error": "エラーにより画像を表示できません", - "Share your activity and status with others.": "アクティビティやステータスを他の人と共有します。", + "Share your activity and status with others.": "アクティビティーやステータスを他の人と共有します。", "Presence": "プレゼンス(ステータス表示)", "Reset event store?": "イベントストアをリセットしますか?", "Your firewall or anti-virus is blocking the request.": "ファイアーウォールまたはアンチウイルスソフトがリクエストをブロックしています。", "We're creating a room with %(names)s": "%(names)sという名前のルームを作成中", - "Enable notifications for this account": "このアカウントでは通知を有効にする", + "Enable notifications for this account": "このアカウントで通知を有効にする", "Welcome to %(brand)s": "%(brand)sにようこそ", "Find your co-workers": "同僚を見つける", "Start your first chat": "最初のチャットを始める", @@ -3304,20 +3302,20 @@ "Show shortcut to welcome checklist above the room list": "ルームリストの上に最初に設定すべき項目リストへのショートカットを表示", "Notifications silenced": "無音で通知", "Sound on": "サウンド再生", - "You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.": "すでに音声ブロードキャストを録音中です。新しく始めるには今の音声ブロードキャストを終了してください。", + "You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.": "既に音声配信を録音しています。新しく始めるには今の音声配信を終了してください。", "Close sidebar": "サイドバーを閉じる", "You are sharing your live location": "位置情報(ライブ)を共有中です", "Stop and close": "停止して閉じる", "Session details": "セッションの詳細", "Operating system": "オペレーティングシステム", - "Model": "モデル", + "Model": "形式", "Device": "端末", "URL": "URL", "Application": "アプリケーション", - "Renaming sessions": "セッション名を変更", + "Renaming sessions": "セッション名を変更しています", "Call type": "通話の種類", "You do not have sufficient permissions to change this.": "これを変更するのに必要な権限を持っていません。", - "Great, that'll help people know it's you": "素晴らしい、他の人があなただと気づく助けになるでしょう", + "Great, that'll help people know it's you": "すばらしい、他の人があなただと気づく助けになるでしょう", "Show HTML representation of room topics": "ルームのトピックをHTML形式で表示", "Reset bearing to north": "北向きにリセット", "Saved Items": "保存済み項目", @@ -3329,7 +3327,7 @@ "Seen by %(count)s people|one": "%(count)s人が閲覧済", "Seen by %(count)s people|other": "%(count)s人が閲覧済", "%(members)s and %(last)s": "%(members)sと%(last)s", - "Hide formatting": "フォーマットを非表示", + "Hide formatting": "フォーマットを表示しない", "Show formatting": "フォーマットを表示", "Updated %(humanizedUpdateTime)s": "%(humanizedUpdateTime)sに更新", "Joining the beta will reload %(brand)s.": "ベータ版に参加すると%(brand)sをリロードします。", @@ -3356,7 +3354,7 @@ "Edit topic": "トピックを編集", "Un-maximise": "最大化をやめる", "%(displayName)s's live location": "%(displayName)sの位置情報(ライブ)", - "You need to have the right permissions in order to share locations in this room.": "このルームで位置情報を共有するには適切な権限を持っていることが必要です。", + "You need to have the right permissions in order to share locations in this room.": "このルームでの位置情報の共有には適切な権限が必要です。", "To view, please enable video rooms in Labs first": "表示するには、まずラボのビデオ通話ルームを有効にしてください", "Are you sure you're at the right place?": "本当に問題ない場所にいますか?", "Unknown session type": "セッションタイプ不明", @@ -3364,15 +3362,15 @@ "Mobile session": "モバイル端末セッション", "Desktop session": "デスクトップセッション", "Unverified": "未認証", - "Verified": "認証済み", - "Can’t start a call": "呼び出しを開始できません", + "Verified": "認証済", + "Can’t start a call": "通話を開始できません", "Failed to read events": "イベント受信に失敗しました", "Failed to send event": "イベント送信に失敗しました", "Show details": "詳細を表示", "Hide details": "詳細を非表示にする", - "Security recommendations": "セキュリティーに関する推奨", + "Security recommendations": "セキュリティーに関する勧告", "Unverified session": "未認証のセッション", - "Community ownership": "コミュニティーの所有者", + "Community ownership": "コミュニティーを所有", "Text": "テキスト", "Link": "リンク", "Freedom": "自由", @@ -3389,7 +3387,7 @@ "Connection": "接続", "Voice processing": "音声を処理しています", "Automatically adjust the microphone volume": "マイクの音量を自動的に調節", - "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.": "最高のセキュリティーを達成するには、セッションを認証し、不明なセッションや利用していないセッションからサインアウトしてください。", + "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.": "セキュリティーを最大限に高めるには、セッションを認証し、不明なセッションや利用していないセッションからサインアウトしてください。", "Are you sure you want to sign out of %(count)s sessions?|one": "%(count)s個のセッションからサインアウトしてよろしいですか?", "Are you sure you want to sign out of %(count)s sessions?|other": "%(count)s個のセッションからサインアウトしてよろしいですか?", "Bulk options": "一括オプション", @@ -3399,12 +3397,11 @@ "Yes, end my recording": "はい、録音を終了してください", "If you start listening to this live broadcast, your current live broadcast recording will be ended.": "このライブ配信の視聴を開始すると、現在のライブ配信の録音は終了します。", "Listen to live broadcast?": "ライブ配信を視聴しますか?", - "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "ライブ配信を終了してよろしいですか?配信を終了し、録音をこのルームで利用できるよう設定します。", "%(senderName)s ended a voice broadcast": "%(senderName)sが音声配信を終了しました", "You ended a voice broadcast": "音声配信を終了しました", "%(senderName)s ended a voice broadcast": "%(senderName)sが音声配信を終了しました", "You ended a voice broadcast": "音声配信を終了しました", - "You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call.": "ライブ配信の録音中のため通話を開始できません。通話を開始するにはライブ配信を終了してください。", + "You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call.": "ライブ配信を録音しているため、通話を開始できません。通話を開始するには、ライブ配信を終了してください。", "Unfortunately we're unable to start a recording right now. Please try again later.": "録音を開始できません。後でもう一度やり直してください。", "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "このルームで音声配信を開始する権限がありません。ルームの管理者に連絡して権限の付与を依頼してください。", "%(senderName)s started a voice broadcast": "%(senderName)sが音声配信を開始しました", @@ -3440,7 +3437,6 @@ "Hide notification dot (only display counters badges)": "通知のドットを非表示にする(カウンターのバッジのみを表示)", "Rust cryptography implementation": "Rustによる暗号の実装", "Right panel stays open": "右のパネルを開いたままにする", - "Use rich text instead of Markdown in the message composer. Plain text mode coming soon.": "メッセージの入力欄でマークダウンの代わりにリッチテキストを使用。プレーンテキストモードは近日公開。", "Toggle attribution": "属性を切り替える", "Buffering…": "バッファリングしています…", "Go live": "ライブ配信", @@ -3519,5 +3515,153 @@ "No one will be able to reuse your username (MXID), including you: this username will remain unavailable": "あなたのユーザー名(MXID)は、あなた自身を含めて誰も再利用することができなくなります", "You will leave all rooms and DMs that you are in": "全てのルームとダイレクトメッセージから退出します", "Confirm that you would like to deactivate your account. If you proceed:": "アカウントを非アクティブ化したいことを確認してください。継続すると、", - "You will not be able to reactivate your account": "アカウントを再開できなくなります" + "You will not be able to reactivate your account": "アカウントを再開できなくなります", + "Completing set up of your new device": "新しい端末の設定を完了しています", + "Waiting for device to sign in": "端末のサインインを待機しています", + "The request was declined on the other device.": "リクエストはもう一方の端末で拒否されました。", + "When you sign out, these keys will be deleted from this device, which means you won't be able to read encrypted messages unless you have the keys for them on your other devices, or backed them up to the server.": "サインアウトすると、これらの鍵はこの端末から削除され、他の端末に使用できる鍵がなかったり、サーバーにバックアップしたりしているのでない限り、暗号化されたメッセージを読むことができなくなります。", + "%(brand)s encountered an error during upload of:": "以下のアップロードの際にエラーが発生しました:", + "The following users might not exist or are invalid, and cannot be invited: %(csvNames)s": "以下のユーザーは存在しないか不正であるため、招待できません:%(csvNames)s", + "A call can only be transferred to a single user.": "通話は一人のユーザーにしか転送できません。", + "We couldn't invite those users. Please check the users you want to invite and try again.": "ユーザーを招待できませんでした。招待したいユーザーを確認して、もう一度試してください。", + "Open room": "ルームを開く", + "Settings explorer": "設定を調査", + "Explore account data": "アカウントデータを調査", + "Explore room account data": "ルームのアカウントデータを調査", + "Explore room state": "ルームの状態を調査", + "Send custom timeline event": "カスタムタイムラインイベントを送信", + "Hide my messages from new joiners": "自分のメッセージを新しい参加者に表示しない", + "Messages in this chat will be end-to-end encrypted.": "このチャットのメッセージはエンドツーエンドで暗号化されます。", + "You don't have permission to share locations": "位置情報の共有に必要な権限がありません", + "Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.": "未認証のセッションは、認証情報でログインされていますが、クロス認証は行われていないセッションです。", + "You should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account.": "これらのセッションは、アカウントの不正使用を示している可能性があるため、注意して確認してください。", + "This means that you have all the keys needed to unlock your encrypted messages and confirm to other users that you trust this session.": "認証済のセッションには、暗号化されたメッセージを復号化する際に使用する全ての鍵が備わっています。また、他のユーザーに対しては、あなたがこのセッションを信頼していることが表示されます。", + "Verified sessions are anywhere you are using this account after entering your passphrase or confirming your identity with another verified session.": "認証済のセッションは、パスフレーズの入力、または他の認証済のセッションで本人確認を行ったセッションです。", + "Removing inactive sessions improves security and performance, and makes it easier for you to identify if a new session is suspicious.": "使用していないセッションを削除すると、セキュリティーとパフォーマンスが改善されます。また、新しいセッションが疑わしい場合に、より容易に特定できるようになります。", + "Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.": "非アクティブなセッションは、しばらく使用されていませんが、暗号鍵を受信しているセッションです。", + "Feeling experimental? Try out our latest ideas in development. These features are not finalised; they may be unstable, may change, or may be dropped altogether. Learn more.": "実験に参加したいですか?開発中のアイディアを試してください。これらの機能は完成していません。不安定な可能性や変更される可能性、また、開発が中止される可能性もあります。詳細を確認。", + "Upcoming features": "今後の機能", + "What's next for %(brand)s? Labs are the best way to get things early, test out new features and help shape them before they actually launch.": "%(brand)sのラボでは、最新の機能をいち早く使用したり、テストしたりできるほか、機能が実際にリリースされる前の改善作業を支援することができます。", + "Have greater visibility and control over all your sessions.": "改善したセッションの管理画面を使用します。", + "Verify your current session for enhanced secure messaging.": "より安全なメッセージのやりとりのために、現在のセッションを認証しましょう。", + "Your current session is ready for secure messaging.": "現在のセッションは安全なメッセージのやりとりに対応しています。", + "Inactive for %(inactiveAgeDays)s+ days": "%(inactiveAgeDays)s日以上使用されていません", + "Inactive sessions": "非アクティブなセッション", + "For best security, sign out from any session that you don't recognize or use anymore.": "セキュリティーを最大限に高めるには、不明なセッションや利用していないセッションからサインアウトしてください。", + "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.": "セッションを認証すると、より安全なメッセージのやりとりが可能になります。見覚えのない、または使用していないセッションがあれば、サインアウトしましょう。", + "Improve your account security by following these recommendations.": "以下の勧告に従い、アカウントのセキュリティーを改善しましょう。", + "Turn off to disable notifications on all your devices and sessions": "オフにすると、全ての端末とセッションで通知が無効になります", + "Enable notifications for this device": "この端末で通知を有効にする", + "Deactivating your account is a permanent action — be careful!": "アカウントを無効化すると取り消せません。ご注意ください!", + "Start messages with /plain to send without markdown and /md to send with.": "/plainを先頭に置くとマークダウンを無効にしてメッセージを送信し、/mdを先頭に置くとマークダウンを有効にしてメッセージを送信。", + "Verify your email to continue": "続行するには電子メールを認証してください", + "Did not receive it?": "受け取りませんでしたか?", + "Verification link email resent!": "認証リンクの電子メールを再送しました!", + "Wrong email address?": "メールアドレスが正しくないですか?", + "Re-enter email address": "メールアドレスを再入力", + "For best security and privacy, it is recommended to use Matrix clients that support encryption.": "セキュリティーとプライバシー保護の観点から、暗号化をサポートしているMatrixのクライアントの使用を推奨します。", + "Sign out of %(count)s sessions|other": "%(count)s件のセッションからサインアウト", + "Sign out of %(count)s sessions|one": "%(count)s件のセッションからサインアウト", + "Unable to play this voice broadcast": "この音声配信を再生できません", + "Registration token": "登録用トークン", + "Enter a registration token provided by the homeserver administrator.": "ホームサーバーの管理者から提供された登録用トークンを入力してください。", + "The homeserver doesn't support signing in another device.": "ホームサーバーは他の端末でのサインインをサポートしていません。", + "Select 'Scan QR code'": "「QRコードをスキャン」を選択してください", + "Scan the QR code below with your device that's signed out.": "サインアウトした端末で以下のQRコードをスキャンしてください。", + "We were unable to start a chat with the other user.": "他のユーザーをチャットを開始できませんでした。", + "Error starting verification": "認証を開始する際にエラーが発生しました", + "You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.": "この端末を使い、QRコードをスキャンして新しい端末でサインインできます。この端末に表示されるQRコードを、サインインしていない端末でスキャンしてください。", + "Inactive for %(inactiveAgeDays)s days or longer": "%(inactiveAgeDays)s日以上使用されていません", + "Inactive": "非アクティブ", + "Not ready for secure messaging": "安全なメッセージのやりとりの準備ができていません", + "Ready for secure messaging": "安全なメッセージのやりとりの準備ができました", + "No verified sessions found.": "認証済のセッションはありません。", + "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore.": "使用していない古いセッション(%(inactiveAgeDays)s日以上使用されていません)からサインアウトすることを検討してください。", + "Debug logs contain application usage data including your username, the IDs or aliases of the rooms you have visited, which UI elements you last interacted with, and the usernames of other users. They do not contain messages.": "デバッグログは、ユーザー名、訪問済のルームのIDやエイリアス、最後に使用したユーザーインターフェース上の要素、他のユーザーのユーザー名などを含むアプリケーションの使用状況データを含みます。メッセージは含まれません。", + "You will not receive push notifications on other devices until you sign back in to them.": "再びサインインするまで、他の端末でプッシュ通知は送信されません。", + "If you want to retain access to your chat history in encrypted rooms you should first export your room keys and re-import them afterwards.": "暗号化されたルームの会話の履歴へのアクセスを維持したい場合は、ルームの鍵をエクスポートし、後で再インポートしてください。", + "Other users in direct messages and rooms that you join are able to view a full list of your sessions.": "あなたが参加するダイレクトメッセージとルームの他のユーザーは、あなたのセッションの一覧を閲覧できます。", + "Please be aware that session names are also visible to people you communicate with.": "セッション名は連絡先にも表示されます。", + "This session is ready for secure messaging.": "このセッションは安全なメッセージのやりとりの準備ができています。", + "No inactive sessions found.": "使用していないセッションはありません。", + "No unverified sessions found.": "未認証のセッションはありません。", + "It’s what you’re here for, so lets get to it": "早速初めましょう", + "Don’t miss a thing by taking %(brand)s with you": "%(brand)sを持ち歩いて最新の情報を見逃さないようにしましょう", + "Get stuff done by finding your teammates": "同僚を見つけて仕事を片付けましょう", + "Temporary implementation. Locations persist in room history.": "一時的な実装。位置情報がルームの履歴に残ります。", + "Complete these to get the most out of %(brand)s": "これらを完了し、%(brand)sを最大限に利用しましょう", + "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は連絡を取るのに最適な手段です。", + "Decrypted source unavailable": "復号化したソースコードが利用できません", + "Thread root ID: %(threadRootId)s": "スレッドのルートID:%(threadRootId)s", + "Reset your password": "パスワードを再設定", + "Sign out of all devices": "全ての端末からサインアウト", + "Confirm new password": "新しいパスワードを確認", + "Currently removing messages in %(count)s rooms|one": "現在%(count)s個のルームのメッセージを削除しています", + "Currently removing messages in %(count)s rooms|other": "現在%(count)s個のルームのメッセージを削除しています", + "Verify or sign out from this session for best security and reliability.": "セキュリティーと安定性の観点から、このセッションを認証するかサインアウトしてください。", + "Review and approve the sign in": "サインインを確認して承認", + "By approving access for this device, it will have full access to your account.": "この端末へのアクセスを許可すると、あなたのアカウントに完全にアクセスできるようになります。", + "The other device isn't signed in.": "もう一方の端末はサインインしていません。", + "The other device is already signed in.": "もう一方のデバイスは既にサインインしています。", + "%(homeserver)s will send you a verification link to let you reset your password.": "%(homeserver)sが、パスワードの再設定用の認証リンクを送信します。", + "Follow the instructions sent to %(email)s": "%(email)sに送信される指示に従ってください", + "Enter your email to reset password": "パスワードを再設定するには、あなたの電子メールを入力してください", + "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.": "セキュアメッセージリカバリーを設定しない場合、ログアウトまたは他のセッションを使用した際に、暗号化したメッセージの履歴を復元することができなくなります。", + "Set up Secure Message Recovery": "セキュアメッセージリカバリーを設定", + "Your old messages will still be visible to people who received them, just like emails you sent in the past. Would you like to hide your sent messages from people who join rooms in the future?": "あなたの古いメッセージは、それを受信した人には表示され続けます。これは電子メールの場合と同様です。あなたが送信したメッセージを今後のルームの参加者に表示しないようにしますか?", + "Please note: this is a labs feature using a temporary implementation. This means you will not be able to delete your location history, and advanced users will be able to see your location history even after you stop sharing your live location with this room.": "注意:これは一時的な実装による試験機能です。位置情報の履歴を削除することはできません。高度なユーザーは、あなたがこのルームで位置情報(ライブ)の共有を停止した後でも、あなたの位置情報の履歴を閲覧することができます。", + "Upgrading this room requires closing down the current instance of the room and creating a new room in its place. To give room members the best possible experience, we will:": "このルームをアップグレードするには、現在のルームを閉鎖し、新しくルームを作成する必要があります。ルームの参加者のため、アップグレードの際に以下を行います。", + "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.": "このホームサーバーでパスワードをリセットすると、あなたの端末は全てサインアウトします。端末に保存されているメッセージの暗号鍵が削除され、暗号化された会話の履歴を読むことができなくなります。", + "If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "実行していたこと、ルームID、ユーザーIDなど、問題を分析するのに役立つ追加情報があれば、それをここに含めてください。", + "Changing your password on this homeserver will cause all of your other devices to be signed out. This will delete the message encryption keys stored on them, and may make encrypted chat history unreadable.": "このホームサーバーでパスワードを変更すると、あなたの端末は全てサインアウトします。端末に保存されているメッセージの暗号鍵が削除され、暗号化された会話の履歴を読むことができなくなります。", + "Reminder: Your browser is unsupported, so your experience may be unpredictable.": "注意:ブラウザーはサポートされていません。期待通りに動作しない可能性があります。", + "Invalid identity server discovery response": "IDサーバーのディスカバリー(発見)に関する不正な応答です", + "Show: %(instance)s rooms (%(server)s)": "表示:%(instance)s ルーム(%(server)s)", + "Manage account": "アカウントを管理", + "Signing out your devices will delete the message encryption keys stored on them, making encrypted chat history unreadable.": "端末からサインアウトすると、暗号化の鍵が削除され、暗号化された会話の履歴を読むことができなくなります。", + "If you want to retain access to your chat history in encrypted rooms, set up Key Backup or export your message keys from one of your other devices before proceeding.": "暗号化されたルームの会話の履歴を保ちたい場合は、続行する前に、鍵のバックアップを設定するか、他の端末からメッセージの鍵をエクスポートしてください。", + "Verifying this device will mark it as trusted, and users who have verified with you will trust this device.": "この端末を認証すると信頼済に設定されます。あなたを認証したユーザーはこの端末を信頼することができるようになります。", + "You can't disable this later. The room will be encrypted but the embedded call will not.": "これを後で無効にすることはできません。ルームは暗号化されますが、埋め込まれる通話は暗号化されません。", + "Message pending moderation: %(reason)s": "メッセージはモデレートの保留中です:%(reason)s", + "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to the person who invited you.": "招待の検証を試みる際にエラー(%(errcode)s)が発生しました。あなたを招待した人にこの情報を渡してみてください。", + "Developer command: Discards the current outbound group session and sets up new Olm sessions": "管理者コマンド:現在のアウトバウンドグループセッションを破棄して、新しいOlmセッションを設定", + "Currently, %(count)s spaces have access|one": "現在1個のスペースがアクセスできます", + "You can also ask your homeserver admin to upgrade the server to change this behaviour.": "この動作を変更したい場合は、あなたのホームサーバーの管理者にサーバーをアップグレードするように依頼してください。", + "Uncheck if you also want to remove system messages on this user (e.g. membership change, profile change…)": "このユーザーに関するシステムメッセージ(メンバーシップの変更、プロフィールの変更など)も削除したい場合は、チェックを外してください", + "You are about to remove %(count)s messages by %(user)s. This will remove them permanently for everyone in the conversation. Do you wish to continue?|one": "%(user)sによる%(count)s件のメッセージを削除しようとしています。これは会話に参加している全員からメッセージを永久に削除します。続行してよろしいですか?", + "You are about to remove %(count)s messages by %(user)s. This will remove them permanently for everyone in the conversation. Do you wish to continue?|other": "%(user)sによる%(count)s件のメッセージを削除しようとしています。これは会話に参加している全員からメッセージを永久に削除します。続行してよろしいですか?", + "This provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here.": "セッションの一覧から、相手はあなたとやり取りしていることを確かめることができます。なお、あなたがここに入力するセッション名は相手に対して表示されます。", + "Your message wasn't sent because this homeserver has been blocked by its administrator. Please contact your service administrator to continue using the service.": "このホームサーバーが管理者によりブロックされているため、メッセージを送信できませんでした。サービスを引き続き使用するには、サービスの管理者にお問い合わせください。", + "You may want to try a different search or check for typos.": "別のキーワードで検索するか、キーワードが正しいか確認してください。", + "Too many attempts in a short time. Wait some time before trying again.": "再試行の数が多すぎます。少し待ってから再度試してください。", + "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "すべての端末からログアウトしているため、プッシュ通知を受け取れません。通知を再び有効にするには、各端末でサインインしてください。", + "Invalid homeserver discovery response": "ホームサーバーのディスカバリー(発見)に関する不正な応答です", + "Failed to get autodiscovery configuration from server": "自動発見の設定をサーバーから取得できませんでした", + "Unable to query for supported registration methods.": "サポートしている登録方法を照会できません。", + "Sign in instead": "サインイン", + "We need to know it’s you before resetting your password.\n Click the link in the email we just sent to %(email)s": "パスワードを再設定する前に本人確認を行います。\n %(email)sに送信した電子メールにあるリンクをクリックしてください。", + "Ignore %(user)s": "%(user)sを無視", + "Indent decrease": "インデントを減らす", + "Indent increase": "インデントを増やす", + "Join the room to participate": "ルームに参加", + "Threads timeline": "スレッドのタイムライン", + "Are you sure you want to stop your live broadcast? This will end the broadcast and the full recording will be available in the room.": "ライブ配信を終了してよろしいですか?配信を終了し、録音をこのルームで利用できるよう設定します。", + "Consult first": "初めに相談", + "Enable 'Manage Integrations' in Settings to do this.": "これを行うには設定から「インテグレーションを管理」を有効にしてください。", + "Notifications debug": "通知のデバッグ", + "Help us identify issues and improve %(analyticsOwner)s by sharing anonymous usage data. To understand how people use multiple devices, we'll generate a random identifier, shared by your devices.": "%(analyticsOwner)sの改善と課題抽出のために、匿名の使用状況データの送信をお願いします。複数の端末での使用を分析するために、あなたの全端末共通のランダムな識別子を生成します。", + "We'll help you get connected.": "みんなと繋がる手助けをいたします。", + "Who will you chat to the most?": "誰と最もよく会話しますか?", + "Share for %(duration)s": "%(duration)sの間共有", + "All messages and invites from this user will be hidden. Are you sure you want to ignore them?": "このユーザーのメッセージと招待を非表示にします。無視してよろしいですか?", + "Send your first message to invite to chat": "最初のメッセージを送信すると、を会話に招待", + "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "%(transferTarget)sと相談しています。%(transferee)sに転送", + "Polls history": "アンケートの履歴", + "unknown": "不明", + "Red": "赤色", + "Grey": "灰色", + "Unable to decrypt voice broadcast": "音声配信を復号化できません", + "Sender: ": "送信者: ", + "Room status": "ルームの状態", + "There are no polls in this room": "このルームにアンケートはありません" } diff --git a/src/i18n/strings/lo.json b/src/i18n/strings/lo.json index ccb486f7a80..8b810db1172 100644 --- a/src/i18n/strings/lo.json +++ b/src/i18n/strings/lo.json @@ -962,8 +962,6 @@ "Tried to load a specific point in this room's timeline, but was unable to find it.": "ພະຍາຍາມໂຫຼດຈຸດສະເພາະໃນທາມລາຍຂອງຫ້ອງນີ້, ແຕ່ບໍ່ສາມາດຊອກຫາມັນໄດ້.", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "ພະຍາຍາມໂຫຼດຈຸດສະເພາະຢູ່ໃນທາມລາຍຂອງຫ້ອງນີ້, ແຕ່ທ່ານບໍ່ໄດ້ຮັບອະນຸຍາດໃຫ້ເບິ່ງຂໍ້ຄວາມທີ່ເປັນຄໍາຖາມ.", "Thread": "ກະທູ້", - "Give feedback": "ໃຫ້ຄໍາຄິດເຫັນ", - "Threads are a beta feature": "ກະທູ້ແມ່ນຄຸນສົມບັດຂອງເບຕ້າ", "Keep discussions organised with threads": "ຮັກສາການສົນທະນາທີ່ມີການຈັດລະບຽບ", "Tip: Use “%(replyInThread)s” when hovering over a message.": "ເຄັດລັບ: ໃຊ້ “%(replyInThread)s” ເມື່ອເລື່ອນໃສ່ຂໍ້ຄວາມ.", "Threads help keep your conversations on-topic and easy to track.": "ກະທູ້ຊ່ວຍໃຫ້ການສົນທະນາຂອງທ່ານຢູ່ໃນຫົວຂໍ້ ແລະ ງ່າຍຕໍ່ການຕິດຕາມ.", diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 41ec6182707..a48c3c2c043 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -3201,8 +3201,6 @@ "Toggle Link": "Koppeling wisselen", "Accessibility": "Toegankelijkheid", "Event ID: %(eventId)s": "Gebeurtenis ID: %(eventId)s", - "Give feedback": "Feedback geven", - "Threads are a beta feature": "Threads zijn een bètafunctie", "Threads help keep your conversations on-topic and easy to track.": "Threads helpen jou gesprekken on-topic te houden en gemakkelijk bij te houden.", "Reply to an ongoing thread or use “%(replyInThread)s” when hovering over a message to start a new one.": "Reageer op een lopende thread of gebruik \"%(replyInThread)s\" wanneer je de muisaanwijzer op een bericht plaatst om een nieuwe te starten.", "We'll create rooms for each of them.": "We zullen kamers voor elk van hen maken.", @@ -3567,7 +3565,6 @@ "resume voice broadcast": "hervat spraakuitzending", "play voice broadcast": "spraakuitzending afspelen", "Yes, stop broadcast": "Ja, stop uitzending", - "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Weet u zeker dat u de live uitzending wilt stoppen? Hiermee wordt de uitzending beëindigd en is de volledige opname beschikbaar in de kamer.", "Stop live broadcasting?": "Live uitzending stoppen?", "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Iemand anders neemt al een spraakuitzending op. Wacht tot de spraakuitzending is afgelopen om een nieuwe te starten.", "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "U heeft niet de vereiste rechten om een spraakuitzending in deze kamer te starten. Neem contact op met een kamer beheerder om uw machtiging aan te passen.", diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json index 425e78f5ae2..f3c3af3b7bc 100644 --- a/src/i18n/strings/pt_BR.json +++ b/src/i18n/strings/pt_BR.json @@ -2846,7 +2846,6 @@ "Fetching events...": "Buscando eventos...", "Starting export process...": "Iniciando processo de exportação...", "Yes, stop broadcast": "Sim, interromper a transmissão", - "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Tem certeza de que deseja interromper sua transmissão ao vivo? Isso encerrará a transmissão e a gravação completa estará disponível na sala.", "Stop live broadcasting?": "Parar a transmissão ao vivo?", "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Outra pessoa já está gravando uma transmissão de voz. Aguarde o término da transmissão de voz para iniciar uma nova.", "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "Você não tem as permissões necessárias para iniciar uma transmissão de voz nesta sala. Entre em contato com um administrador de sala para atualizar suas permissões.", diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 21e0662e0d9..c3ba599de15 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -3192,7 +3192,6 @@ "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": "Обсуждения — бета-функция", "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": "Не получили? Отправить его повторно", @@ -3335,7 +3334,6 @@ "Please note: this is a labs feature using a temporary implementation. This means you will not be able to delete your location history, and advanced users will be able to see your location history even after you stop sharing your live location with this room.": "Обратите внимание: это временная реализация функции. Это означает, что вы не сможете удалить свою историю местоположений, а опытные пользователи смогут просмотреть вашу историю местоположений даже после того, как вы перестанете делиться своим местоположением в этой комнате.", "View live location": "Посмотреть трансляцию местоположения", "Live location enabled": "Трансляция местоположения включена", - "Give feedback": "Оставить отзыв", "If you can't find the room you're looking for, ask for an invite or create a new room.": "Если не можете найти нужную комнату, просто попросите пригласить вас или создайте новую комнату.", "Send custom timeline event": "Отправить пользовательское событие ленты сообщений", "No verification requests found": "Запросов проверки не найдено", @@ -3503,7 +3501,6 @@ "resume voice broadcast": "продолжить голосовую трансляцию", "play voice broadcast": "проиграть голосовую трансляцию", "Yes, stop broadcast": "Да, остановить трансляцию", - "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Вы уверены в том, что хотите остановить голосовую трансляцию? Это закончит трансляцию и полная запись станет доступной в комнате.", "Stop live broadcasting?": "Закончить голосовую трансляцию?", "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Кто-то уже записывает голосовую трансляцию. Ждите окончания их голосовой трансляции, чтобы начать новую.", "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "У вас нет необходимых разрешений, чтобы начать голосовую трансляцию в этой комнате. Свяжитесь с администратором комнаты для получения разрешений.", diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index aee9e29e985..82850b7233d 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -3203,8 +3203,6 @@ "%(count)s participants|other": "%(count)s účastníkov", "New video room": "Nová video miestnosť", "New room": "Nová miestnosť", - "Give feedback": "Poskytnúť spätnú väzbu", - "Threads are a beta feature": "Vlákna sú beta funkciou", "Threads help keep your conversations on-topic and easy to track.": "Vlákna pomáhajú udržiavať konverzácie v téme a uľahčujú ich sledovanie.", "%(featureName)s Beta feedback": "%(featureName)s Beta spätná väzba", "Beta feature. Click to learn more.": "Beta funkcia. Kliknutím sa dozviete viac.", @@ -3557,7 +3555,6 @@ "Browser": "Prehliadač", "Allow a QR code to be shown in session manager to sign in another device (requires compatible homeserver)": "Umožniť zobrazenie QR kódu v správcovi relácií na prihlásenie do iného zariadenia (vyžaduje kompatibilný domovský server)", "Yes, stop broadcast": "Áno, zastaviť vysielanie", - "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Určite chcete zastaviť vaše vysielanie naživo? Tým sa vysielanie ukončí a v miestnosti bude k dispozícii celý záznam.", "Stop live broadcasting?": "Zastaviť vysielanie naživo?", "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Niekto iný už nahráva hlasové vysielanie. Počkajte, kým sa skončí jeho hlasové vysielanie, a potom spustite nové.", "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "Nemáte požadované oprávnenia na spustenie hlasového vysielania v tejto miestnosti. Obráťte sa na správcu miestnosti, aby vám rozšíril oprávnenia.", @@ -3639,7 +3636,6 @@ "Right panel stays open": "Pravý panel zostáva otvorený", "Currently experimental.": "V súčasnosti experimentálne.", "New ways to ignore people": "Nové spôsoby ignorovania ľudí", - "Use rich text instead of Markdown in the message composer. Plain text mode coming soon.": "Používajte rozšírený režim textu v správach. Obyčajný text už čoskoro.", "Rich text editor": "Rozšírený textový editor", "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.": "V miestnostiach, ktoré podporujú moderovanie, môžete pomocou tlačidla \"Nahlásiť\" nahlásiť porušovanie pravidiel moderátorom miestnosti.", "Report to moderators": "Nahlásiť moderátorom", @@ -3710,5 +3706,45 @@ "Decrypted source unavailable": "Dešifrovaný zdroj nie je dostupný", "Connection error - Recording paused": "Chyba pripojenia - nahrávanie pozastavené", "%(senderName)s started a voice broadcast": "%(senderName)s začal/a hlasové vysielanie", - "%(displayName)s (%(matrixId)s)": "%(displayName)s (%(matrixId)s)" + "%(displayName)s (%(matrixId)s)": "%(displayName)s (%(matrixId)s)", + "Registration token": "Registračný token", + "Enter a registration token provided by the homeserver administrator.": "Zadajte registračný token poskytnutý správcom domovského servera.", + "Unable to play this voice broadcast": "Toto hlasové vysielanie nie je možné prehrať", + "Dynamic room predecessors": "Predchodcovia dynamickej miestnosti", + "Manage account": "Spravovať účet", + "Your account details are managed separately at %(hostname)s.": "Údaje o vašom účte sú spravované samostatne na adrese %(hostname)s.", + "Enable MSC3946 (to support late-arriving room archives)": "Zapnúť MSC3946 (na podporu neskorých archívov miestností)", + "All messages and invites from this user will be hidden. Are you sure you want to ignore them?": "Všetky správy a pozvánky od tohto používateľa budú skryté. Ste si istí, že ich chcete ignorovať?", + "Ignore %(user)s": "Ignorovať %(user)s", + "Indent decrease": "Zmenšenie odsadenia", + "Indent increase": "Zväčšenie odsadenia", + "Unable to decrypt voice broadcast": "Hlasové vysielanie sa nedá dešifrovať", + "View a list of polls in a room. (Under active development)": "Zobraziť zoznam ankiet v miestnosti. (V štádiu aktívneho vývoja)", + "Polls history": "História ankety", + "Use rich text instead of Markdown in the message composer.": "Použiť rozšírený text namiesto Markdown v správach.", + "No receipt found": "Nenašlo sa žiadne potvrdenie", + "User read up to: ": "Používateľ sa dočítal až do: ", + "Dot: ": "Bodka: ", + "Room unread status: ": "Stav neprečítaných v miestnosti: ", + "Thread Id: ": "ID vlákna: ", + "Threads timeline": "Časová os vlákien", + "Sender: ": "Odosielateľ: ", + "ID: ": "ID: ", + "Type: ": "Typ: ", + "Last event:": "Posledná udalosť:", + "Highlight: ": "Zvýraznené: ", + "Total: ": "Celkom: ", + "Main timeline": "Hlavná časová os", + "not encrypted 🚨": "nezašifrovaná 🚨", + "encrypted ✅": "zašifrovaná ✅", + "Room is ": "Miestnosť je ", + "Notification state is": "Stav oznámenia je", + ", count:": ", počet:", + "Room status": "Stav miestnosti", + "There are no polls in this room": "V tejto miestnosti nie sú žiadne ankety", + "Notifications debug": "Ladenie oznámení", + "unknown": "neznáme", + "Red": "Červená", + "Grey": "Sivá", + "Are you sure you want to stop your live broadcast? This will end the broadcast and the full recording will be available in the room.": "Určite chcete zastaviť vysielanie naživo? Tým sa vysielanie ukončí a v miestnosti bude k dispozícii celý záznam." } diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 3f829d56c57..2ea45af3fd9 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -3199,8 +3199,6 @@ "%(count)s participants|other": "%(count)s pjesëmarrës", "New video room": "Dhomë e re me video", "New room": "Dhomë e re", - "Give feedback": "Jepni përshtypjet", - "Threads are a beta feature": "Rrjedhat janë një veçori beta", "Threads help keep your conversations on-topic and easy to track.": "Rrjedhat ndihmojnë që të mbahen bisedat tuaja brenda temës dhe të ndiqen kollaj.", "%(featureName)s Beta feedback": "Përshtypje për %(featureName)s Beta", "Beta feature. Click to learn more.": "Veçori në version beta. Klikoni për të mësuar më tepër.", @@ -3566,7 +3564,6 @@ "Show shortcut to welcome checklist above the room list": "Shhkurtoren e listës së hapave të mirëseardhjes shfaqe mbi listën e dhomave", "Allow a QR code to be shown in session manager to sign in another device (requires compatible homeserver)": "Lejoni shfaqjen e një kodi QR në përgjegjës sesioni, për hyrje në një pajisje tjetër (lyp shërbyes Home të përputhshëm)", "Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.": "Përgjegjësi ynë i ri i sesioneve furnizon dukshmëri më të mirë të krejt sesioneve tuaja dhe kontroll më të fortë mbi ta, përfshi aftësinë për aktivizim/çaktivizim së largëti të njoftimeve push.", - "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Jeni i sigurt se doni të ndalet transmetimi juaj i drejtpërdrejtë? Kjo do të përfundojë transmetimin dhe regjistrimi i plotë do të jetë i passhëm te dhoma.", "We need to know it’s you before resetting your password.\n Click the link in the email we just sent to %(email)s": "Duhet të dimë se jeni ju, përpara ricaktimit të fjalëkalimt.\n Klikoni lidhjen te email-i që sapo ju dërguam te %(email)s", "Verify your email to continue": "Që të vazhdohet, verifikoni email-in tuaj", "%(homeserver)s will send you a verification link to let you reset your password.": "%(homeserver)s do t’ju dërgojë një lidhje verifikimi, që t’ju lejojë të ricaktoni fjalëkalimin tuaj.", @@ -3629,7 +3626,6 @@ "Right panel stays open": "Paneli i djathtë mbetet i hapur", "Currently experimental.": "Aktualisht eksperimental.", "New ways to ignore people": "Rrugë të reja për të shpërfillur njerëz", - "Use rich text instead of Markdown in the message composer. Plain text mode coming soon.": "Përdorni te hartuesi i mesazheve tekst të pasur, në vend se Markdown. Së shpejti vjen mënyra tekst i thjeshtë.", "Rich text editor": "Përpunues teksti të pasur", "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.": "Në dhoma që mbulojnë moderimin, butoni “Raportojeni” do t’ju lejojë t’u raportoni abuzim moderatorëve të dhomës.", "Report to moderators": "Raportojeni te moderatorët", @@ -3689,7 +3685,7 @@ "Yes, end my recording": "Po, përfundoje regjistrimin tim", "If you start listening to this live broadcast, your current live broadcast recording will be ended.": "Nëse filloni të dëgjoni te ky transmetim i drejtpërdrejtë, regjistrimi juaj i tanishëm i një transmetimi të drejtpërdrejtë do të përfundojë.", "Listen to live broadcast?": "Të dëgjohet te transmetimi i drejtpërdrejtë?", - "You can't start a voice message as you are currently recording a live broadcast. Please end your live broadcast in order to start recording a voice message.": "S’mund të niset mesazh zanor, ngaqë aktualisht po regjistroni një transmetim të drejtpërdrejtë. Ju lutemi, përfundoni transmetimin e drejtpërdrejtë, që të mund të nisni regjistrimin e një mesazhi zanor", + "You can't start a voice message as you are currently recording a live broadcast. Please end your live broadcast in order to start recording a voice message.": "S’mund të niset mesazh zanor, ngaqë aktualisht po incizoni një transmetim të drejtpërdrejtë. Ju lutemi, përfundoni transmetimin e drejtpërdrejtë, që të mund të nisni incizimin e një mesazhi zanor.", "Can't start voice message": "S’niset dot mesazh zanor", "Edit link": "Përpunoni lidhje", "Numbered list": "Listë e numërtuar", @@ -3697,5 +3693,40 @@ "Connection error - Recording paused": "Gabim lidhjeje - Regjistrimi u ndal", "Unfortunately we're unable to start a recording right now. Please try again later.": "Mjerisht, s’qemë në gjendje të nisnim tani një regjistrim. Ju lutemi, riprovoni më vonë.", "Connection error": "Gabim lidhjeje", - "%(senderName)s started a voice broadcast": "%(senderName)s nisi një transmetim zanor" + "%(senderName)s started a voice broadcast": "%(senderName)s nisi një transmetim zanor", + "Decrypted source unavailable": "Burim i shfshehtëzuar jo i passhëm", + "Registration token": "Token regjistrimi", + "Enter a registration token provided by the homeserver administrator.": "Jepni një token regjistrimi dhënë nga përgjegjësi i shërbyesit Home.", + "All messages and invites from this user will be hidden. Are you sure you want to ignore them?": "Krejt mesazhet dhe ftesat prej këtij përdoruesi do të fshihen. Jeni i sigurt se doni të shpërfillet?", + "Ignore %(user)s": "Shpërfille %(user)s", + "Manage account": "Administroni llogari", + "Your account details are managed separately at %(hostname)s.": "Hollësitë e llogarisë tuaj administrohen ndarazi te %(hostname)s.", + "Unable to play this voice broadcast": "S’arrihet të luhet ky transmetim zanor", + "Threads timeline": "Rrjedhë kohore rrjedhash", + "Sender: ": "Dërgues: ", + "Type: ": "Lloj: ", + "ID: ": "ID: ", + "Last event:": "Veprimtaria e fundit", + "No receipt found": "S’u gjet dëftesë", + "Highlight: ": "Theksoje: ", + "Total: ": "Gjithsej: ", + "Main timeline": "Rrjedhë kohore kryesore", + "not encrypted 🚨": "jo i fshehtëzuar 🚨", + "encrypted ✅": "fshehtëzuar ✅", + "Room is ": "Dhoma është ", + "Notification state is": "Gjendje njoftimesh është", + ", count:": ", numër:", + "Room status": "Gjendje dhome", + "There are no polls in this room": "S’ka pyetësorë në këtë dhomë", + "Notifications debug": "Diagnostikim njoftimesh", + "%(displayName)s (%(matrixId)s)": "%(displayName)s (%(matrixId)s)", + "Indent decrease": "Zvogëlim shmangieje kryeradhe", + "Indent increase": "Rritje shmangieje kryeradhe", + "View a list of polls in a room. (Under active development)": "Shihni një listë pyetësorësh në një dhomë. (Nën zhvillim aktiv)", + "Polls history": "Historik pyetësorësh", + "Use rich text instead of Markdown in the message composer.": "Përdor tekst të pasur, në vend se Markdown, te hartuesi i mesazheve.", + "unknown": "e panjohur", + "Red": "E kuqe", + "Grey": "Gri", + "Unable to decrypt voice broadcast": "S’arrihet të shfshehtëzohet transmetim zanor" } diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index 5d13c5f2852..a54256b46e3 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -3167,8 +3167,6 @@ "Toggle Link": "Växla länk av/på", "Toggle Code Block": "Växla kodblock av/på", "Event ID: %(eventId)s": "Händelse-ID: %(eventId)s", - "Give feedback": "Ge återkoppling", - "Threads are a beta feature": "Trådar är en beta-funktion", "Tip: Use “%(replyInThread)s” when hovering over a message.": "Tips: Välj \"%(replyInThread)s\" när du håller över ett meddelande.", "Threads help keep your conversations on-topic and easy to track.": "Trådar underlättar för att hålla konversationer till ämnet och gör dem lättare att följa.", "Reply to an ongoing thread or use “%(replyInThread)s” when hovering over a message to start a new one.": "Svara i en pågående tråd eller använd \"%(replyInThread)s\" när du håller över ett meddelande för att starta en ny tråd.", @@ -3393,7 +3391,6 @@ "resume voice broadcast": "återuppta röstsändning", "play voice broadcast": "spela röstsändning", "Yes, stop broadcast": "Ja, avsluta sändning", - "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Är du säker på att du vill avsluta din livesändning? Det här kommer att avsluta sändningen och den fulla inspelningen kommer att vara tillgänglig i rummet.", "Allow a QR code to be shown in session manager to sign in another device (requires compatible homeserver)": "Låt en QR-kod visas i sessionshanteraren för att logga in en annan enhet (kräver en kompatibel hemserver)", "Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.": "Vår nya sessionshanterare ger dig bättre insyn i alla dina sessioner, och större kontroll över dem, inklusive förmågan att växla pushnotiser på håll.", "Have greater visibility and control over all your sessions.": "Ha bättre insyn och kontroll över alla dina sessioner.", @@ -3488,7 +3485,6 @@ "Right panel stays open": "Högerpanelen hålls öppen", "Currently experimental.": "För närvarande experimentellt.", "New ways to ignore people": "Nya sätt att ignorera personer", - "Use rich text instead of Markdown in the message composer. Plain text mode coming soon.": "Använd rik text istället för Markdown i meddelanderedigeraren. Vanligt textläge kommer snart.", "Rich text editor": "Riktextredigerare", "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.": "I rum som stöder moderering så låter \"Rapportera\"-knappen dig rapportera trakasseri till rumsmoderatorer.", "Report to moderators": "Rapportera till moderatorer", @@ -3706,5 +3702,48 @@ "Failed to read events": "Misslyckades att läsa händelser", "Failed to send event": "Misslyckades att skicka händelse", "Numbered list": "Numrerad lista", - "Bulleted list": "Punktlista" + "Bulleted list": "Punktlista", + "Decrypted source unavailable": "Avkrypterad källa otillgänglig", + "Registration token": "Registreringstoken", + "Enter a registration token provided by the homeserver administrator.": "Ange en registreringstoken försedd av hemserveradministratören.", + "%(displayName)s (%(matrixId)s)": "%(displayName)s (%(matrixId)s)", + "Manage account": "Hantera konto", + "Your account details are managed separately at %(hostname)s.": "Dina rumsdetaljer hanteras separat av %(hostname)s.", + "Enable MSC3946 (to support late-arriving room archives)": "Aktivera MSC3946 (för att stöda sen-ankomna rumsarkiv)", + "Dynamic room predecessors": "Dynamiska rumsföregångare", + "Connection error - Recording paused": "Anslutningsfel - Inspelning pausad", + "Unable to play this voice broadcast": "Kan inte spela den här röstsändningen", + "%(senderName)s started a voice broadcast": "%(senderName)s startade en röstsändning", + "Thread Id: ": "Tråd-ID: ", + "Threads timeline": "Trådtidslinje", + "Sender: ": "Avsändare: ", + "Type: ": "Typ: ", + "ID: ": "ID: ", + "Last event:": "Senaste händelsen:", + "No receipt found": "Inga kvitton hittade", + "User read up to: ": "Användaren har läst fram till: ", + "Dot: ": "Punkt: ", + "Highlight: ": "Markering: ", + "Total: ": "Totalt: ", + "Main timeline": "Huvudtidslinje", + "not encrypted 🚨": "inte krypterad 🚨", + "encrypted ✅": "krypterad ✅", + "Room is ": "Rum är ", + "Notification state is": "Aviseringsstatus är", + ", count:": ", antal:", + "Room unread status: ": "Rummets oläststatus: ", + "Room status": "Rumsstatus", + "There are no polls in this room": "Det finns inga omröstningar i det här rummet", + "Notifications debug": "Aviseringsfelsökning", + "All messages and invites from this user will be hidden. Are you sure you want to ignore them?": "Alla meddelanden och inbjudningar från den här användaren kommer att döljas. Är du säker på att du vill ignorera denne?", + "Ignore %(user)s": "Ignorera %(user)s", + "Indent decrease": "Minska indrag", + "Indent increase": "Öka indrad", + "View a list of polls in a room. (Under active development)": "Visa lista över omröstningar i ett rum. (Under aktiv utveckling)", + "Polls history": "Omröstningshistorik", + "Use rich text instead of Markdown in the message composer.": "Använd rik text istället för Markdown i meddelanderedigeraren.", + "unknown": "okänd", + "Red": "Röd", + "Grey": "Grå", + "Unable to decrypt voice broadcast": "Kunde inte kryptera röstsändning" } diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json index 26ea9bf6483..60bcf9ad593 100644 --- a/src/i18n/strings/uk.json +++ b/src/i18n/strings/uk.json @@ -3204,8 +3204,6 @@ "%(count)s participants|other": "%(count)s учасників", "New video room": "Нова відеокімната", "New room": "Нова кімната", - "Give feedback": "Залиште відгук", - "Threads are a beta feature": "Гілки — бета-функція", "Threads help keep your conversations on-topic and easy to track.": "Гілки допомагають підтримувати розмови за темою та за ними легко стежити.", "%(featureName)s Beta feedback": "%(featureName)s — відгук про бетаверсію", "Beta feature. Click to learn more.": "Бетафункція. Натисніть, щоб дізнатися більше.", @@ -3535,7 +3533,6 @@ "Browser": "Браузер", "Allow a QR code to be shown in session manager to sign in another device (requires compatible homeserver)": "Дозволити показ QR-коду в менеджері сеансів для входу на іншому пристрої (потрібен сумісний домашній сервер)", "Yes, stop broadcast": "Так, припинити трансляцію", - "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Ви впевнені, що хочете припинити голосову трансляцію? На цьому трансляція завершиться, і повний запис буде доступний у кімнаті.", "Stop live broadcasting?": "Припинити голосову трансляцію?", "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Хтось інший вже записує голосову трансляцію. Зачекайте, поки запис завершиться, щоб розпочати новий.", "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "Ви не маєте необхідних дозволів для початку голосової трансляції в цю кімнату. Зверніться до адміністратора кімнати, щоб оновити ваші дозволи.", @@ -3640,7 +3637,6 @@ "Right panel stays open": "Права панель залишається відкритою", "Currently experimental.": "Наразі експериментально.", "New ways to ignore people": "Нові способи нехтувати людей", - "Use rich text instead of Markdown in the message composer. Plain text mode coming soon.": "Використовуйте розширений текст замість Markdown у редакторі повідомлень. Режим звичайного тексту з'явиться незабаром.", "Rich text editor": "Розширений текстовий редактор", "Report to moderators": "Поскаржитись модераторам", "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.": "У кімнатах, які підтримують модерацію, кнопка «Поскаржитися» дає змогу повідомити про зловживання модераторам кімнати.", @@ -3710,5 +3706,45 @@ "Decrypted source unavailable": "Розшифроване джерело недоступне", "Connection error - Recording paused": "Помилка з'єднання - Запис призупинено", "%(senderName)s started a voice broadcast": "%(senderName)s розпочинає голосову трансляцію", - "%(displayName)s (%(matrixId)s)": "%(displayName)s (%(matrixId)s)" + "%(displayName)s (%(matrixId)s)": "%(displayName)s (%(matrixId)s)", + "Registration token": "Токен реєстрації", + "Enter a registration token provided by the homeserver administrator.": "Введіть реєстраційний токен, наданий адміністратором домашнього сервера.", + "Unable to play this voice broadcast": "Неможливо відтворити цю голосову трансляцію", + "Manage account": "Керувати обліковим записом", + "Your account details are managed separately at %(hostname)s.": "Ваші дані облікового запису керуються окремо за адресою %(hostname)s.", + "Enable MSC3946 (to support late-arriving room archives)": "Увімкнути MSC3946 (для підтримки архівів пізніх кімнат)", + "Dynamic room predecessors": "Попередники динамічної кімнати", + "All messages and invites from this user will be hidden. Are you sure you want to ignore them?": "Усі повідомлення та запрошення від цього користувача будуть приховані. Ви впевнені, що хочете їх нехтувати?", + "Ignore %(user)s": "Нехтувати %(user)s", + "Indent decrease": "Зменшення відступу", + "Indent increase": "Збільшення відступу", + "Unable to decrypt voice broadcast": "Невдалося розшифрувати голосову трансляцію", + "View a list of polls in a room. (Under active development)": "Перегляд списку опитувань у кімнаті (в активній розробці)", + "Polls history": "Історія опитувань", + "Use rich text instead of Markdown in the message composer.": "Використовувати розширений текст замість розмітки в редакторі повідомлень.", + "Thread Id: ": "Id стрічки: ", + "Threads timeline": "Стрічка гілок", + "Sender: ": "Відправник: ", + "Type: ": "Тип: ", + "ID: ": "ID: ", + "Last event:": "Остання подія:", + "No receipt found": "Підтвердження не знайдено", + "User read up to: ": "Користувач прочитав до: ", + "Dot: ": "Крапка: ", + "Highlight: ": "Виділене: ", + "Total: ": "Загалом: ", + "Main timeline": "Основна стрічка", + "not encrypted 🚨": "не зашифрована 🚨", + "encrypted ✅": "зашифрована ✅", + "Room is ": "Кімната ", + "Notification state is": "Стан сповіщення", + ", count:": ", кількість:", + "Room unread status: ": "Статус непрочитаного в кімнаті: ", + "Room status": "Статус кімнати", + "There are no polls in this room": "У цій кімнаті немає опитувань", + "Notifications debug": "Сповіщення зневадження", + "unknown": "невідомо", + "Red": "Черврний", + "Grey": "Сірий", + "Are you sure you want to stop your live broadcast? This will end the broadcast and the full recording will be available in the room.": "Ви впевнені, що хочете припинити пряму трансляцію? Це призведе до завершення трансляції, а повний запис буде доступний у кімнаті." } diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json index a5e0f1c19b1..170f86de7c8 100644 --- a/src/i18n/strings/zh_Hans.json +++ b/src/i18n/strings/zh_Hans.json @@ -3108,7 +3108,6 @@ "Threads help keep your conversations on-topic and easy to track.": "消息列帮助保持你的对话切题并易于追踪。", "Reply to an ongoing thread or use “%(replyInThread)s” when hovering over a message to start a new one.": "回复进行中的消息列或当悬停在消息上时使用%(replyInThread)s来发起新的消息列。", "Can't create a thread from an event with an existing relation": "无法从既有关系的事件创建消息列", - "Threads are a beta feature": "消息列是beta功能", "Joining the beta will reload %(brand)s.": "加入beta会重载%(brand)s。", "Leaving the beta will reload %(brand)s.": "离开beta会重载%(brand)s。", "This is a beta feature": "这是beta功能", @@ -3464,7 +3463,6 @@ "resume voice broadcast": "恢复语音广播", "play voice broadcast": "播放语音广播", "Yes, stop broadcast": "是的,停止广播", - "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "你确定要停止你的直播吗?这将结束直播,房间里将有完整的录音。", "Stop live broadcasting?": "停止直播吗?", "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "别人已经在录制语音广播了。等到他们的语音广播结束后再开始新的广播。", "Upcoming features": "即将到来的功能", @@ -3513,7 +3511,6 @@ "Right panel stays open": "右侧面板保持打开状态", "Currently experimental.": "目前是实验性的。", "New ways to ignore people": "忽略他人的新方式", - "Use rich text instead of Markdown in the message composer. Plain text mode coming soon.": "在消息撰写器中使用富文本而不是Markdown。纯文本模式即将到来。", "Join %(brand)s calls": "加入%(brand)s呼叫", "Start %(brand)s calls": "开始%(brand)s呼叫", "Automatically adjust the microphone volume": "自动调整话筒音量", diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 5901988af0a..4ea7b8a3962 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -3197,8 +3197,6 @@ "You do not have permission to invite people to this space.": "您無權邀請他人加入此空間。", "Failed to invite users to %(roomName)s": "未能邀請使用者加入 %(roomName)s", "An error occurred while stopping your live location, please try again": "停止您的即時位置時發生錯誤,請再試一次", - "Give feedback": "給予回饋", - "Threads are a beta feature": "討論串是測試版功能", "Threads help keep your conversations on-topic and easy to track.": "討論串可讓您的對話不離題且易於追蹤。", "Create room": "建立聊天室", "Create video room": "建立視訊聊天室", @@ -3557,7 +3555,6 @@ "Browser": "瀏覽器", "Allow a QR code to be shown in session manager to sign in another device (requires compatible homeserver)": "允許在工作階段管理程式中顯示 QR code 以在另一台裝置上登入(需要相容的家伺服器)", "Yes, stop broadcast": "是的,停止廣播", - "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "您確定您要停止您的即時廣播嗎?這將會結束廣播,聊天室內將會提供完整的錄製內容。", "Stop live broadcasting?": "停止即時廣播?", "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "其他人已在錄製語音廣播。等待他們的語音廣播結束以開始新的語音廣播。", "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "您沒有在此聊天室內開始語音廣播所需的權限。請聯絡聊天室管理員升級您的權限。", @@ -3639,7 +3636,6 @@ "Right panel stays open": "右側面板維持開啟狀態", "Currently experimental.": "目前為實驗性。", "New ways to ignore people": "忽略人們的新方式", - "Use rich text instead of Markdown in the message composer. Plain text mode coming soon.": "在訊息編輯器中使用格式化文字而非 Markdown。純文字模式即將到來。", "Rich text editor": "格式化文字編輯器", "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.": "在支援審核的聊天室中,「回報」按鈕讓您可以回報濫用行為給聊天室管理員。", "Report to moderators": "回報給管理員", @@ -3710,5 +3706,44 @@ "Bulleted list": "項目符號清單", "Connection error - Recording paused": "連線錯誤 - 紀錄已暫停", "%(senderName)s started a voice broadcast": "%(senderName)s 開始了語音廣播", - "%(displayName)s (%(matrixId)s)": "%(displayName)s (%(matrixId)s)" + "%(displayName)s (%(matrixId)s)": "%(displayName)s (%(matrixId)s)", + "Registration token": "註冊權杖", + "Enter a registration token provided by the homeserver administrator.": "輸入由家伺服器管理員提供的註冊權杖。", + "Manage account": "管理帳號", + "Your account details are managed separately at %(hostname)s.": "您的帳號詳細資訊在 %(hostname)s 中單獨管理。", + "Enable MSC3946 (to support late-arriving room archives)": "啟用 MSC3946(為了支援遲到聊天室存檔)", + "Dynamic room predecessors": "動態聊天室前身", + "Unable to play this voice broadcast": "無法播放此音訊廣播", + "All messages and invites from this user will be hidden. Are you sure you want to ignore them?": "來自該使用者的所有訊息與邀請都將被隱藏。您確定要忽略它們嗎?", + "Ignore %(user)s": "忽略 %(user)s", + "Indent decrease": "減少縮排", + "Indent increase": "增加縮排", + "View a list of polls in a room. (Under active development)": "檢視聊天室中的投票清單。(積極開發中)", + "Polls history": "投票歷史紀錄", + "Unable to decrypt voice broadcast": "無法解密語音廣播", + "There are no polls in this room": "此聊天室中沒有投票", + "Use rich text instead of Markdown in the message composer.": "在訊息編輯器中使用格式化文字而非 Markdown。", + "Thread Id: ": "討論串 ID: ", + "Threads timeline": "討論串時間軸", + "Sender: ": "傳送者: ", + "Type: ": "類型: ", + "ID: ": "ID: ", + "Last event:": "最後活動:", + "No receipt found": "找不到回條", + "User read up to: ": "使用者讀取至: ", + "Dot: ": "點: ", + "Highlight: ": "突顯: ", + "Total: ": "總共: ", + "Main timeline": "主時間軸", + "not encrypted 🚨": "未加密 🚨", + "encrypted ✅": "已加密 ✅", + "Room is ": "聊天室為 ", + "Notification state is": "通知狀態為", + ", count:": ", 計數:", + "Room unread status: ": "聊天室未讀狀態: ", + "Room status": "聊天室狀態", + "Notifications debug": "通知除錯", + "unknown": "未知", + "Red": "紅", + "Grey": "灰" } From 3a460f3c1de6d6355ae9a0d9c74df11eefad5021 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 7 Feb 2023 12:07:29 +0000 Subject: [PATCH 92/97] Upgrade matrix-js-sdk to 23.3.0-rc.1 --- package.json | 2 +- yarn.lock | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index f71915058ee..730c0c672a5 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "maplibre-gl": "^2.0.0", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "0.0.1", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "23.3.0-rc.1", "matrix-widget-api": "^1.1.1", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index aa88e900a0d..103921ffd0f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1584,10 +1584,10 @@ resolved "https://registry.yarnpkg.com/@matrix-org/analytics-events/-/analytics-events-0.4.0.tgz#ec75400ac41272cd2d47774da433437aba789bcf" integrity sha512-tLeak2pYiyxn431o7EdOJGwRjj2DApybOLZ0YtFMuhv87pBQhc3lz+IKCJ2c8NwV6e7kf4YNaTdXeygBvFPGFw== -"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.2": - version "0.1.0-alpha.2" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.2.tgz#a09d0fea858e817da971a3c9f904632ef7b49eb6" - integrity sha512-oVkBCh9YP7H9i4gAoQbZzswniczfo/aIptNa4dxRi4Ff9lSvUCFv6Hvzi7C+90c0/PWZLXjIDTIAWZYmwyd2fA== +"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.3": + version "0.1.0-alpha.4" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.4.tgz#1b20294e0354c3dcc9c7dc810d883198a4042f04" + integrity sha512-mdaDKrw3P5ZVCpq0ioW0pV6ihviDEbS8ZH36kpt9stLKHwwDSopPogE6CkQhi0B1jn1yBUtOYi32mBV/zcOR7g== "@matrix-org/matrix-wysiwyg@^0.23.0": version "0.23.0" @@ -6496,12 +6496,13 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": - version "23.2.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/1c26dc02339c6b7c67b030b1701248a2d68c24c0" +matrix-js-sdk@23.3.0-rc.1: + version "23.3.0-rc.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-23.3.0-rc.1.tgz#eb78f6b10864d5f6f8edb2738795e79e9aee9e02" + integrity sha512-EX7yW49Z8+u1+zb7CaDBx0i6eLBQuQZ0Owh51vOpf81G+j60sEPZ76uwsz2rLe3bD0BkCFjLPKN4Dm+RTKpUXQ== dependencies: "@babel/runtime" "^7.12.5" - "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.2" + "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.3" another-json "^0.2.0" bs58 "^5.0.0" content-type "^1.0.4" From b2ea19957c3572b3b0f459cc141d064efbc971f2 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 7 Feb 2023 12:11:57 +0000 Subject: [PATCH 93/97] Prepare changelog for v3.66.0-rc.1 --- CHANGELOG.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dbf6ebc47d..0a73b946ba9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,52 @@ +Changes in [3.66.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.66.0-rc.1) (2023-02-07) +=============================================================================================================== + +## ✨ Features + * Indicate unread messages in tab title ([\#10096](https://github.com/matrix-org/matrix-react-sdk/pull/10096)). Contributed by @tnt7864. + * Open message in editing mode when keyboard up is pressed (RTE) ([\#10079](https://github.com/matrix-org/matrix-react-sdk/pull/10079)). Contributed by @florianduros. + * Hide superseded rooms from the room list using dynamic room predecessors ([\#10068](https://github.com/matrix-org/matrix-react-sdk/pull/10068)). Contributed by @andybalaam. + * Support MSC3946 in RoomListStore ([\#10054](https://github.com/matrix-org/matrix-react-sdk/pull/10054)). Fixes vector-im/element-web#24325. Contributed by @andybalaam. + * Auto focus security key field ([\#10048](https://github.com/matrix-org/matrix-react-sdk/pull/10048)). + * use Poll model with relations API in poll rendering ([\#9877](https://github.com/matrix-org/matrix-react-sdk/pull/9877)). Contributed by @kerryarchibald. + * Support MSC3946 in the RoomCreate tile ([\#10041](https://github.com/matrix-org/matrix-react-sdk/pull/10041)). Fixes vector-im/element-web#24323. Contributed by @andybalaam. + * Update labs flag description for RTE ([\#10058](https://github.com/matrix-org/matrix-react-sdk/pull/10058)). Contributed by @florianduros. + * Change ul list style to disc when editing message ([\#10043](https://github.com/matrix-org/matrix-react-sdk/pull/10043)). Contributed by @alunturner. + * Improved click detection within PiP windows ([\#10040](https://github.com/matrix-org/matrix-react-sdk/pull/10040)). Fixes vector-im/element-web#24371. + * Add RTE keyboard navigation in editing ([\#9980](https://github.com/matrix-org/matrix-react-sdk/pull/9980)). Fixes vector-im/element-web#23621. Contributed by @florianduros. + * Paragraph integration for rich text editor ([\#10008](https://github.com/matrix-org/matrix-react-sdk/pull/10008)). Contributed by @alunturner. + * Add indentation increasing/decreasing to RTE ([\#10034](https://github.com/matrix-org/matrix-react-sdk/pull/10034)). Contributed by @florianduros. + * Add ignore user confirmation dialog ([\#6116](https://github.com/matrix-org/matrix-react-sdk/pull/6116)). Fixes vector-im/element-web#14746. + * Use monospace font for room, message IDs in View Source modal ([\#9956](https://github.com/matrix-org/matrix-react-sdk/pull/9956)). Fixes vector-im/element-web#21937. Contributed by @paragpoddar. + * Implement MSC3946 for AdvancedRoomSettingsTab ([\#9995](https://github.com/matrix-org/matrix-react-sdk/pull/9995)). Fixes vector-im/element-web#24322. Contributed by @andybalaam. + * Implementation of MSC3824 to make the client OIDC-aware ([\#8681](https://github.com/matrix-org/matrix-react-sdk/pull/8681)). Contributed by @hughns. + * Improves a11y for avatar uploads ([\#9985](https://github.com/matrix-org/matrix-react-sdk/pull/9985)). Contributed by @GoodGuyMarco. + * Add support for [token authenticated registration](https ([\#7275](https://github.com/matrix-org/matrix-react-sdk/pull/7275)). Fixes vector-im/element-web#18931. Contributed by @govynnus. + +## 🐛 Bug Fixes + * Fix a crash when removing persistent widgets (updated) ([\#10099](https://github.com/matrix-org/matrix-react-sdk/pull/10099)). Fixes vector-im/element-web#24412. Contributed by @andybalaam. + * Fix wrongly grouping 3pid invites into a single repeated transition ([\#10087](https://github.com/matrix-org/matrix-react-sdk/pull/10087)). Fixes vector-im/element-web#24432. + * Fix scrollbar colliding with checkbox in add to space section ([\#10093](https://github.com/matrix-org/matrix-react-sdk/pull/10093)). Fixes vector-im/element-web#23189. Contributed by @Arnabdaz. + * Add a whitespace character after 'broadcast?' ([\#10097](https://github.com/matrix-org/matrix-react-sdk/pull/10097)). Contributed by @luixxiul. + * Seekbar in broadcast PiP view is now updated when switching between different broadcasts ([\#10072](https://github.com/matrix-org/matrix-react-sdk/pull/10072)). Fixes vector-im/element-web#24415. + * Add border to "reject" button on room preview card for clickable area indication. It fixes vector-im/element-web#22623 ([\#9205](https://github.com/matrix-org/matrix-react-sdk/pull/9205)). Contributed by @gefgu. + * Element-R: fix rageshages ([\#10081](https://github.com/matrix-org/matrix-react-sdk/pull/10081)). Fixes vector-im/element-web#24430. + * Fix markdown paragraph display in timeline ([\#10071](https://github.com/matrix-org/matrix-react-sdk/pull/10071)). Fixes vector-im/element-web#24419. Contributed by @alunturner. + * Prevent the remaining broadcast time from being exceeded ([\#10070](https://github.com/matrix-org/matrix-react-sdk/pull/10070)). + * Fix cursor position when new line is created by pressing enter (RTE) ([\#10064](https://github.com/matrix-org/matrix-react-sdk/pull/10064)). Contributed by @florianduros. + * Ensure room is actually in space hierarchy when resolving its latest version ([\#10010](https://github.com/matrix-org/matrix-react-sdk/pull/10010)). + * Fix new line for inline code ([\#10062](https://github.com/matrix-org/matrix-react-sdk/pull/10062)). Contributed by @florianduros. + * Replace list, code block and quote icons by new icons ([\#10035](https://github.com/matrix-org/matrix-react-sdk/pull/10035)). Contributed by @florianduros. + * fix regional emojis converted to flags ([\#9294](https://github.com/matrix-org/matrix-react-sdk/pull/9294)). Fixes vector-im/element-web#19000. Contributed by @grimhilt. + * resolved emoji description text overflowing issue ([\#10028](https://github.com/matrix-org/matrix-react-sdk/pull/10028)). Contributed by @fahadNoufal. + * Fix MessageEditHistoryDialog crashing on complex input ([\#10018](https://github.com/matrix-org/matrix-react-sdk/pull/10018)). Fixes vector-im/element-web#23665. Contributed by @clarkf. + * Unify unread notification state determination ([\#9941](https://github.com/matrix-org/matrix-react-sdk/pull/9941)). Contributed by @clarkf. + * Fix useUnreadNotifications exploding with falsey room, like in notif panel ([\#10030](https://github.com/matrix-org/matrix-react-sdk/pull/10030)). Fixes matrix-org/element-web-rageshakes#19334. + * Fix "[object Promise]" appearing in HTML exports ([\#9975](https://github.com/matrix-org/matrix-react-sdk/pull/9975)). Fixes vector-im/element-web#24272. Contributed by @clarkf. + * Should open new 1:1 chat room after leaving the old one ([\#9880](https://github.com/matrix-org/matrix-react-sdk/pull/9880)). Contributed by @ahmadkadri. + * changing the color of message time stamp ([\#10016](https://github.com/matrix-org/matrix-react-sdk/pull/10016)). Contributed by @nawarajshah. + * Fix link creation with backward selection ([\#9986](https://github.com/matrix-org/matrix-react-sdk/pull/9986)). Fixes vector-im/element-web#24315. Contributed by @florianduros. + * Misaligned reply preview in thread composer #23396 ([\#9977](https://github.com/matrix-org/matrix-react-sdk/pull/9977)). Fixes vector-im/element-web#23396. Contributed by @mustafa-kapadia1483. + Changes in [3.65.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.65.0) (2023-01-31) ===================================================================================================== From a7b828223855e40eac6bdf0427327ccf04726466 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 7 Feb 2023 12:11:59 +0000 Subject: [PATCH 94/97] v3.66.0-rc.1 --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 730c0c672a5..016a95c0369 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.65.0", + "version": "3.66.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", @@ -260,5 +260,6 @@ "outputDirectory": "coverage", "outputName": "jest-sonar-report.xml", "relativePaths": true - } + }, + "typings": "./lib/index.d.ts" } From 6fd5c1e655255f655f160ae21d2fcf6d6170f18c Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 14 Feb 2023 10:27:44 +0000 Subject: [PATCH 95/97] Upgrade matrix-js-sdk to 23.3.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 016a95c0369..277715d1f39 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "maplibre-gl": "^2.0.0", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "0.0.1", - "matrix-js-sdk": "23.3.0-rc.1", + "matrix-js-sdk": "23.3.0", "matrix-widget-api": "^1.1.1", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index 103921ffd0f..977b511da7e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6496,10 +6496,10 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -matrix-js-sdk@23.3.0-rc.1: - version "23.3.0-rc.1" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-23.3.0-rc.1.tgz#eb78f6b10864d5f6f8edb2738795e79e9aee9e02" - integrity sha512-EX7yW49Z8+u1+zb7CaDBx0i6eLBQuQZ0Owh51vOpf81G+j60sEPZ76uwsz2rLe3bD0BkCFjLPKN4Dm+RTKpUXQ== +matrix-js-sdk@23.3.0: + version "23.3.0" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-23.3.0.tgz#d0c4a6c022039df2b76d8a062d695d2991296ded" + integrity sha512-5wCYxbWeOBbfegqNJGCbzbZnjiUjZQ3N/d+/S2e+utjI7+m/2TH27seZRdrOzlIOf0DAqhTxdtGkmcmW/6/+LQ== dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-js" "^0.1.0-alpha.3" From 51a012bc0ec3b67644b7130fa30d0ab176c28e46 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 14 Feb 2023 10:31:21 +0000 Subject: [PATCH 96/97] Prepare changelog for v3.66.0 --- CHANGELOG.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a73b946ba9..bfd402f79ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ -Changes in [3.66.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.66.0-rc.1) (2023-02-07) -=============================================================================================================== +Changes in [3.66.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.66.0) (2023-02-14) +===================================================================================================== ## ✨ Features + * Add option to find own location in map views ([\#10083](https://github.com/matrix-org/matrix-react-sdk/pull/10083)). + * Render poll end events in timeline ([\#10027](https://github.com/matrix-org/matrix-react-sdk/pull/10027)). Contributed by @kerryarchibald. * Indicate unread messages in tab title ([\#10096](https://github.com/matrix-org/matrix-react-sdk/pull/10096)). Contributed by @tnt7864. * Open message in editing mode when keyboard up is pressed (RTE) ([\#10079](https://github.com/matrix-org/matrix-react-sdk/pull/10079)). Contributed by @florianduros. * Hide superseded rooms from the room list using dynamic room predecessors ([\#10068](https://github.com/matrix-org/matrix-react-sdk/pull/10068)). Contributed by @andybalaam. @@ -23,6 +25,14 @@ Changes in [3.66.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases * Add support for [token authenticated registration](https ([\#7275](https://github.com/matrix-org/matrix-react-sdk/pull/7275)). Fixes vector-im/element-web#18931. Contributed by @govynnus. ## 🐛 Bug Fixes + * Remove duplicate white space characters from translation keys ([\#10152](https://github.com/matrix-org/matrix-react-sdk/pull/10152)). Contributed by @luixxiul. + * Fix the caption of new sessions manager on Labs settings page for localization ([\#10143](https://github.com/matrix-org/matrix-react-sdk/pull/10143)). Contributed by @luixxiul. + * Prevent start another DM with a user if one already exists ([\#10127](https://github.com/matrix-org/matrix-react-sdk/pull/10127)). Fixes vector-im/element-web#23138. + * Remove white space characters before the horizontal ellipsis ([\#10130](https://github.com/matrix-org/matrix-react-sdk/pull/10130)). Contributed by @luixxiul. + * Fix Selectable Text on 'Delete All' and 'Retry All' Buttons ([\#10128](https://github.com/matrix-org/matrix-react-sdk/pull/10128)). Fixes vector-im/element-web#23232. Contributed by @akshattchhabra. + * Correctly Identify emoticons ([\#10108](https://github.com/matrix-org/matrix-react-sdk/pull/10108)). Fixes vector-im/element-web#19472. Contributed by @adarsh-sgh. + * Should open new 1:1 chat room after leaving the old one ([\#9880](https://github.com/matrix-org/matrix-react-sdk/pull/9880)). Contributed by @ahmadkadri. + * Remove a redundant white space ([\#10129](https://github.com/matrix-org/matrix-react-sdk/pull/10129)). Contributed by @luixxiul. * Fix a crash when removing persistent widgets (updated) ([\#10099](https://github.com/matrix-org/matrix-react-sdk/pull/10099)). Fixes vector-im/element-web#24412. Contributed by @andybalaam. * Fix wrongly grouping 3pid invites into a single repeated transition ([\#10087](https://github.com/matrix-org/matrix-react-sdk/pull/10087)). Fixes vector-im/element-web#24432. * Fix scrollbar colliding with checkbox in add to space section ([\#10093](https://github.com/matrix-org/matrix-react-sdk/pull/10093)). Fixes vector-im/element-web#23189. Contributed by @Arnabdaz. @@ -35,18 +45,21 @@ Changes in [3.66.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases * Fix cursor position when new line is created by pressing enter (RTE) ([\#10064](https://github.com/matrix-org/matrix-react-sdk/pull/10064)). Contributed by @florianduros. * Ensure room is actually in space hierarchy when resolving its latest version ([\#10010](https://github.com/matrix-org/matrix-react-sdk/pull/10010)). * Fix new line for inline code ([\#10062](https://github.com/matrix-org/matrix-react-sdk/pull/10062)). Contributed by @florianduros. + * Member avatars without canvas ([\#9990](https://github.com/matrix-org/matrix-react-sdk/pull/9990)). Contributed by @clarkf. + * Apply more general fix for base avatar regressions ([\#10045](https://github.com/matrix-org/matrix-react-sdk/pull/10045)). Fixes vector-im/element-web#24382 and vector-im/element-web#24370. * Replace list, code block and quote icons by new icons ([\#10035](https://github.com/matrix-org/matrix-react-sdk/pull/10035)). Contributed by @florianduros. * fix regional emojis converted to flags ([\#9294](https://github.com/matrix-org/matrix-react-sdk/pull/9294)). Fixes vector-im/element-web#19000. Contributed by @grimhilt. * resolved emoji description text overflowing issue ([\#10028](https://github.com/matrix-org/matrix-react-sdk/pull/10028)). Contributed by @fahadNoufal. * Fix MessageEditHistoryDialog crashing on complex input ([\#10018](https://github.com/matrix-org/matrix-react-sdk/pull/10018)). Fixes vector-im/element-web#23665. Contributed by @clarkf. * Unify unread notification state determination ([\#9941](https://github.com/matrix-org/matrix-react-sdk/pull/9941)). Contributed by @clarkf. + * Fix layout and visual regressions around default avatars ([\#10031](https://github.com/matrix-org/matrix-react-sdk/pull/10031)). Fixes vector-im/element-web#24375 and vector-im/element-web#24369. * Fix useUnreadNotifications exploding with falsey room, like in notif panel ([\#10030](https://github.com/matrix-org/matrix-react-sdk/pull/10030)). Fixes matrix-org/element-web-rageshakes#19334. * Fix "[object Promise]" appearing in HTML exports ([\#9975](https://github.com/matrix-org/matrix-react-sdk/pull/9975)). Fixes vector-im/element-web#24272. Contributed by @clarkf. - * Should open new 1:1 chat room after leaving the old one ([\#9880](https://github.com/matrix-org/matrix-react-sdk/pull/9880)). Contributed by @ahmadkadri. * changing the color of message time stamp ([\#10016](https://github.com/matrix-org/matrix-react-sdk/pull/10016)). Contributed by @nawarajshah. * Fix link creation with backward selection ([\#9986](https://github.com/matrix-org/matrix-react-sdk/pull/9986)). Fixes vector-im/element-web#24315. Contributed by @florianduros. * Misaligned reply preview in thread composer #23396 ([\#9977](https://github.com/matrix-org/matrix-react-sdk/pull/9977)). Fixes vector-im/element-web#23396. Contributed by @mustafa-kapadia1483. + Changes in [3.65.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.65.0) (2023-01-31) ===================================================================================================== From 45cce2421978731c218726cf0afee75d46097577 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 14 Feb 2023 10:31:23 +0000 Subject: [PATCH 97/97] v3.66.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 277715d1f39..60ca4fa4ec1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.66.0-rc.1", + "version": "3.66.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": {