From 5370f258705d123d4f787fdbd8076f9bc1596987 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 2 Dec 2025 13:47:14 +0000 Subject: [PATCH] Introduce i18nContext (#31347) * Introduce i18nContext * Adds a context that holds the module i1n API * Switches shared components to use that instead of importing it directly * Adds the context to MatrixChat and BaseDalog so it should be available most places in EW This is a relatively small PR but does change the way the shared components do i18n so just doing this one by itself (it stands by itself anyway). This will allow shared components to use i18n when used in modules. * Add the file * Fix import lint * Name the translate function _t Then it should continue to get picked up by the script This seems a bit flaky and ew but I'm not sure I want to get into changing this in this PR. * Put humanize back to calling something called _t too * Missed one * Add i18n context wrapper to stories * Unused import * Fix imports * wrap richitem * Wrap other richitem & richlist * One day I will get my head around this syntax * Fix import spacing * Add wrapper to test * unused import * Hack around dependency cycle * Make a moduleapi instance for tests * Add i18n wrapper to jest-matrix-react * Simple test for i18napi * Import type * Move i18n context wrapper to storybook template * Unused imports & fix pill story * Move i18n to its own provider * Add i18ncontext wrapper to jest tests * imports * Bump module api to 1.7.0 * tsdoc --- package.json | 2 +- .../shared-components/.storybook/preview.tsx | 11 +++- .../AudioPlayerView/AudioPlayerView.test.tsx | 6 +- .../audio/AudioPlayerView/AudioPlayerView.tsx | 4 +- .../audio/PlayPauseButton/PlayPauseButton.tsx | 4 +- .../src/audio/SeekBar/SeekBar.tsx | 4 +- packages/shared-components/src/index.ts | 2 + .../src/pill-input/Pill/Pill.tsx | 3 +- .../rich-list/RichItem/RichItem.stories.tsx | 2 +- .../src/rich-list/RichItem/RichItem.tsx | 6 +- .../src/test/utils/jest-matrix-react.tsx | 16 +++-- .../src/utils/I18nApi.test.ts | 22 +++++++ .../shared-components/src/utils}/I18nApi.ts | 11 +++- .../shared-components/src/utils/humanize.ts | 8 ++- .../src/utils/i18nContext.ts | 27 ++++++++ src/components/structures/MatrixChat.tsx | 9 ++- src/components/views/dialogs/BaseDialog.tsx | 65 ++++++++++--------- src/modules/Api.ts | 2 +- test/setup/setupLanguage.ts | 6 ++ test/test-utils/jest-matrix-react.tsx | 24 +++++-- yarn.lock | 13 ++-- 21 files changed, 184 insertions(+), 63 deletions(-) create mode 100644 packages/shared-components/src/utils/I18nApi.test.ts rename {src/modules => packages/shared-components/src/utils}/I18nApi.ts (82%) create mode 100644 packages/shared-components/src/utils/i18nContext.ts diff --git a/package.json b/package.json index 236f88bd2b..7322eaa027 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ }, "dependencies": { "@babel/runtime": "^7.12.5", - "@element-hq/element-web-module-api": "1.6.0", + "@element-hq/element-web-module-api": "1.7.0", "@element-hq/web-shared-components": "link:packages/shared-components", "@fontsource/fira-code": "^5", "@fontsource/inter": "^5", diff --git a/packages/shared-components/.storybook/preview.tsx b/packages/shared-components/.storybook/preview.tsx index 52e7fd8b6c..91a6df646e 100644 --- a/packages/shared-components/.storybook/preview.tsx +++ b/packages/shared-components/.storybook/preview.tsx @@ -6,6 +6,7 @@ import React, { useLayoutEffect } from "react"; import { setLanguage } from "../src/utils/i18n"; import { TooltipProvider } from "@vector-im/compound-web"; import { StoryContext } from "storybook/internal/csf"; +import { I18nApi, I18nContext } from "../src"; export const globalTypes = { theme: { @@ -70,9 +71,17 @@ const withTooltipProvider: Decorator = (Story) => { ); }; +const withI18nProvider: Decorator = (Story) => { + return ( + + + + ); +}; + const preview: Preview = { tags: ["autodocs"], - decorators: [withThemeProvider, withTooltipProvider], + decorators: [withThemeProvider, withTooltipProvider, withI18nProvider], parameters: { options: { storySort: { diff --git a/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.test.tsx b/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.test.tsx index 018b388f6b..55e159f110 100644 --- a/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.test.tsx +++ b/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.test.tsx @@ -14,6 +14,8 @@ import { fireEvent } from "@testing-library/dom"; import * as stories from "./AudioPlayerView.stories.tsx"; import { AudioPlayerView, type AudioPlayerViewActions, type AudioPlayerViewSnapshot } from "./AudioPlayerView"; import { MockViewModel } from "../../viewmodel/MockViewModel.ts"; +import { I18nContext } from "../../utils/i18nContext.ts"; +import { I18nApi } from "../../index.ts"; const { Default, NoMediaName, NoSize, HasError } = composeStories(stories); @@ -64,7 +66,9 @@ describe("AudioPlayerView", () => { error: false, }); - render(); + render(, { + wrapper: ({ children }) => {children}, + }); await user.click(screen.getByRole("button", { name: "Play" })); expect(togglePlay).toHaveBeenCalled(); diff --git a/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.tsx b/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.tsx index 29fb02ba34..d3c9fb87ee 100644 --- a/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.tsx +++ b/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.tsx @@ -14,7 +14,7 @@ import { Flex } from "../../utils/Flex"; import styles from "./AudioPlayerView.module.css"; import { PlayPauseButton } from "../PlayPauseButton"; import { type PlaybackState } from "../playback"; -import { _t } from "../../utils/i18n"; +import { useI18n } from "../../utils/i18nContext"; import { formatBytes } from "../../utils/FormattingUtils"; import { Clock } from "../Clock"; import { SeekBar } from "../SeekBar"; @@ -90,6 +90,8 @@ interface AudioPlayerViewProps { * ``` */ export function AudioPlayerView({ vm }: Readonly): JSX.Element { + const { translate: _t } = useI18n(); + const { playbackState, mediaName = _t("timeline|m.audio|unnamed_audio"), diff --git a/packages/shared-components/src/audio/PlayPauseButton/PlayPauseButton.tsx b/packages/shared-components/src/audio/PlayPauseButton/PlayPauseButton.tsx index 400357a3f5..cc2ab5bb65 100644 --- a/packages/shared-components/src/audio/PlayPauseButton/PlayPauseButton.tsx +++ b/packages/shared-components/src/audio/PlayPauseButton/PlayPauseButton.tsx @@ -11,7 +11,7 @@ import Play from "@vector-im/compound-design-tokens/assets/web/icons/play-solid" import Pause from "@vector-im/compound-design-tokens/assets/web/icons/pause-solid"; import styles from "./PlayPauseButton.module.css"; -import { _t } from "../../utils/i18n"; +import { useI18n } from "../../utils/i18nContext"; export interface PlayPauseButtonProps extends HTMLAttributes { /** @@ -46,6 +46,8 @@ export function PlayPauseButton({ togglePlay, ...rest }: Readonly): JSX.Element { + const { translate: _t } = useI18n(); + const label = playing ? _t("action|pause") : _t("action|play"); return ( diff --git a/packages/shared-components/src/audio/SeekBar/SeekBar.tsx b/packages/shared-components/src/audio/SeekBar/SeekBar.tsx index 3063e2442d..a30e527eee 100644 --- a/packages/shared-components/src/audio/SeekBar/SeekBar.tsx +++ b/packages/shared-components/src/audio/SeekBar/SeekBar.tsx @@ -10,7 +10,7 @@ import { throttle } from "lodash"; import classNames from "classnames"; import style from "./SeekBar.module.css"; -import { _t } from "../../utils/i18n"; +import { useI18n } from "../../utils/i18nContext"; export interface SeekBarProps extends React.InputHTMLAttributes { /** @@ -33,6 +33,8 @@ interface ISeekCSS extends CSSProperties { * ``` */ export function SeekBar({ value = 0, className, ...rest }: Readonly): JSX.Element { + const { translate: _t } = useI18n(); + const [newValue, setNewValue] = useState(value); // Throttle the value setting to avoid excessive re-renders const setThrottledValue = useMemo(() => throttle(setNewValue, 10), []); diff --git a/packages/shared-components/src/index.ts b/packages/shared-components/src/index.ts index 565f7013d3..8440a4fb0a 100644 --- a/packages/shared-components/src/index.ts +++ b/packages/shared-components/src/index.ts @@ -23,10 +23,12 @@ export * from "./utils/Flex"; // Utils export * from "./utils/i18n"; +export * from "./utils/i18nContext"; export * from "./utils/humanize"; export * from "./utils/DateUtils"; export * from "./utils/numbers"; export * from "./utils/FormattingUtils"; +export * from "./utils/I18nApi"; // MVVM export * from "./viewmodel"; diff --git a/packages/shared-components/src/pill-input/Pill/Pill.tsx b/packages/shared-components/src/pill-input/Pill/Pill.tsx index b2ac2e28b2..64fdac3d2d 100644 --- a/packages/shared-components/src/pill-input/Pill/Pill.tsx +++ b/packages/shared-components/src/pill-input/Pill/Pill.tsx @@ -12,7 +12,7 @@ import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close" import { Flex } from "../../utils/Flex"; import styles from "./Pill.module.css"; -import { _t } from "../../utils/i18n"; +import { useI18n } from "../../utils/i18nContext"; export interface PillProps extends Omit, "onClick"> { /** @@ -39,6 +39,7 @@ export interface PillProps extends Omit, "onClick */ export function Pill({ className, children, label, onClick, ...props }: PropsWithChildren): JSX.Element { const id = useId(); + const { translate: _t } = useI18n(); return ( { /** @@ -63,6 +63,8 @@ export const RichItem = memo(function RichItem({ selected, ...props }: RichItemProps): JSX.Element { + const i18n = useI18n(); + return (
  • {description} {timestamp && ( - {humanizeTime(timestamp)} + {i18n.humanizeTime(timestamp)} )}
  • diff --git a/packages/shared-components/src/test/utils/jest-matrix-react.tsx b/packages/shared-components/src/test/utils/jest-matrix-react.tsx index 30d673aa22..d610d87211 100644 --- a/packages/shared-components/src/test/utils/jest-matrix-react.tsx +++ b/packages/shared-components/src/test/utils/jest-matrix-react.tsx @@ -16,16 +16,24 @@ import React, { type ReactElement } from "react"; import { render, type RenderOptions } from "@testing-library/react"; import { TooltipProvider } from "@vector-im/compound-web"; +import { I18nApi, I18nContext } from "../.."; + const wrapWithTooltipProvider = (Wrapper: RenderOptions["wrapper"]) => { return ({ children }: { children: React.ReactNode }) => { if (Wrapper) { return ( - - {children} - + + + {children} + + ); } else { - return {children}; + return ( + + {children} + + ); } }; }; diff --git a/packages/shared-components/src/utils/I18nApi.test.ts b/packages/shared-components/src/utils/I18nApi.test.ts new file mode 100644 index 0000000000..2b3431f07c --- /dev/null +++ b/packages/shared-components/src/utils/I18nApi.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2025 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { type TranslationKey } from "../i18nKeys"; +import { I18nApi } from "./I18nApi"; + +describe("I18nApi", () => { + it("can register a translation and use it", () => { + const i18n = new I18nApi(); + i18n.register({ + "hello.world": { + en: "Hello, World!", + }, + }); + + expect(i18n.translate("hello.world" as TranslationKey)).toBe("Hello, World!"); + }); +}); diff --git a/src/modules/I18nApi.ts b/packages/shared-components/src/utils/I18nApi.ts similarity index 82% rename from src/modules/I18nApi.ts rename to packages/shared-components/src/utils/I18nApi.ts index 9a195a47ec..20d641f5ce 100644 --- a/src/modules/I18nApi.ts +++ b/packages/shared-components/src/utils/I18nApi.ts @@ -6,16 +6,17 @@ Please see LICENSE files in the repository root for full details. */ import { type I18nApi as II18nApi, type Variables, type Translations } from "@element-hq/element-web-module-api"; -import { registerTranslations } from "@element-hq/web-shared-components"; -import { _t, getCurrentLanguage, type TranslationKey } from "../languageHandler.tsx"; +import { humanizeTime } from "./humanize"; +import { _t, getLocale, registerTranslations } from "./i18n"; +import { type TranslationKey } from "../i18nKeys"; export class I18nApi implements II18nApi { /** * Read the current language of the user in IETF Language Tag format */ public get language(): string { - return getCurrentLanguage(); + return getLocale(); } /** @@ -44,4 +45,8 @@ export class I18nApi implements II18nApi { public translate(key: TranslationKey, variables?: Variables): string { return _t(key, variables); } + + public humanizeTime(timeMillis: number): string { + return humanizeTime(timeMillis, this); + } } diff --git a/packages/shared-components/src/utils/humanize.ts b/packages/shared-components/src/utils/humanize.ts index 61f7705ace..1e00e69a9d 100644 --- a/packages/shared-components/src/utils/humanize.ts +++ b/packages/shared-components/src/utils/humanize.ts @@ -6,7 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { _t } from "./i18n"; +import { type I18nApi } from "@element-hq/element-web-module-api"; + +import { _t as _tFromModule } from "./i18n"; // These are the constants we use for when to break the text const MILLISECONDS_RECENT = 15000; @@ -21,13 +23,15 @@ const HOURS_1_DAY = 26; * @param {number} timeMillis The time in millis to compare against. * @returns {string} The humanized time. */ -export function humanizeTime(timeMillis: number): string { +export function humanizeTime(timeMillis: number, i18nApi?: I18nApi): string { const now = Date.now(); let msAgo = now - timeMillis; const minutes = Math.abs(Math.ceil(msAgo / 60000)); const hours = Math.ceil(minutes / 60); const days = Math.ceil(hours / 24); + const _t = i18nApi?.translate ?? _tFromModule; + if (msAgo >= 0) { // Past if (msAgo <= MILLISECONDS_RECENT) return _t("time|few_seconds_ago"); diff --git a/packages/shared-components/src/utils/i18nContext.ts b/packages/shared-components/src/utils/i18nContext.ts new file mode 100644 index 0000000000..46c4185329 --- /dev/null +++ b/packages/shared-components/src/utils/i18nContext.ts @@ -0,0 +1,27 @@ +/* +Copyright 2025 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { createContext, useContext } from "react"; +import { type I18nApi } from "@element-hq/element-web-module-api"; + +export const I18nContext = createContext(null); +I18nContext.displayName = "I18nContext"; + +/** + * A hook to get the i18n API from the context. Will throw if no i18n context is found. + * @throws If no i18n context is found + * @returns The i18n API from the context + */ +export function useI18n(): I18nApi { + const i18n = useContext(I18nContext); + + if (!i18n) { + throw new Error("useI18n must be used within an I18nContext.Provider"); + } + + return i18n; +} diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 21706e9f34..3764051384 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -28,6 +28,7 @@ import { TooltipProvider } from "@vector-im/compound-web"; // what-input helps improve keyboard accessibility import "what-input"; import sanitizeHtml from "sanitize-html"; +import { I18nContext } from "@element-hq/web-shared-components"; import PosthogTrackers from "../../PosthogTrackers"; import { DecryptionFailureTracker } from "../../DecryptionFailureTracker"; @@ -2272,9 +2273,11 @@ export default class MatrixChat extends React.PureComponent { return ( - - {view} - + + + {view} + + ); } diff --git a/src/components/views/dialogs/BaseDialog.tsx b/src/components/views/dialogs/BaseDialog.tsx index a946a44e91..8eba4d3fc6 100644 --- a/src/components/views/dialogs/BaseDialog.tsx +++ b/src/components/views/dialogs/BaseDialog.tsx @@ -12,6 +12,7 @@ import React, { type JSX } from "react"; import FocusLock from "react-focus-lock"; import classNames from "classnames"; import { type MatrixClient } from "matrix-js-sdk/src/matrix"; +import { I18nContext } from "@element-hq/web-shared-components"; import AccessibleButton from "../elements/AccessibleButton"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; @@ -164,38 +165,42 @@ export default class BaseDialog extends React.Component { } return ( - - {this.props.screenName && } - - {this.props.top} -
    + + {this.props.screenName && } + - {!!(this.props.title || headerImage) && ( - - {headerImage} - {this.props.title} - - )} - {this.props.headerButton} -
    - {this.props.children} - {cancelButton} -
    -
    + {this.props.top} +
    + {!!(this.props.title || headerImage) && ( + + {headerImage} + {this.props.title} + + )} + {this.props.headerButton} +
    + {this.props.children} + {cancelButton} + + + ); } } diff --git a/src/modules/Api.ts b/src/modules/Api.ts index 2ff85c968f..057cbf71b0 100644 --- a/src/modules/Api.ts +++ b/src/modules/Api.ts @@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details. import { createRoot, type Root } from "react-dom/client"; import { type Api, type RuntimeModuleConstructor } from "@element-hq/element-web-module-api"; +import { I18nApi } from "@element-hq/web-shared-components"; import { ModuleRunner } from "./ModuleRunner.ts"; import AliasCustomisations from "../customisations/Alias.ts"; @@ -20,7 +21,6 @@ import UserIdentifierCustomisations from "../customisations/UserIdentifier.ts"; import { WidgetPermissionCustomisations } from "../customisations/WidgetPermissions.ts"; import { WidgetVariableCustomisations } from "../customisations/WidgetVariables.ts"; import { ConfigApi } from "./ConfigApi.ts"; -import { I18nApi } from "./I18nApi.ts"; import { CustomComponentsApi } from "./customComponentApi"; import { WatchableProfile } from "./Profile.ts"; import { NavigationApi } from "./Navigation.ts"; diff --git a/test/setup/setupLanguage.ts b/test/setup/setupLanguage.ts index 347f38edfc..124ff7cbbd 100644 --- a/test/setup/setupLanguage.ts +++ b/test/setup/setupLanguage.ts @@ -7,10 +7,12 @@ Please see LICENSE files in the repository root for full details. */ import fetchMock from "fetch-mock-jest"; +import { ModuleLoader } from "@element-hq/element-web-module-api"; import * as languageHandler from "../../src/languageHandler"; import en from "../../src/i18n/strings/en_EN.json"; import de from "../../src/i18n/strings/de_DE.json"; +import { ModuleApi } from "../../src/modules/Api"; const lv = { Save: "Saglabāt", @@ -43,3 +45,7 @@ setupLanguageMock(); languageHandler.setLanguage("en"); languageHandler.setMissingEntryGenerator((key) => key.split("|", 2)[1]); + +// Set up the mdule API (so the i18n API exists) +const moduleLoader = new ModuleLoader(ModuleApi.instance); +window.mxModuleLoader = moduleLoader; diff --git a/test/test-utils/jest-matrix-react.tsx b/test/test-utils/jest-matrix-react.tsx index ed5ff0075e..76150df478 100644 --- a/test/test-utils/jest-matrix-react.tsx +++ b/test/test-utils/jest-matrix-react.tsx @@ -10,17 +10,33 @@ import React, { type ReactElement } from "react"; // eslint-disable-next-line no-restricted-imports import { render, type RenderOptions } from "@testing-library/react"; import { TooltipProvider } from "@vector-im/compound-web"; +import { I18nContext } from "@element-hq/web-shared-components"; -const wrapWithTooltipProvider = (Wrapper: RenderOptions["wrapper"]) => { +/** + * Wraps the provided components in: + * * A TooltipProvider + * * An I18nContext.Provider + * + * ...plus any wrapper provided in the options. + * @param Wrapper Additional wrapper to include + * @returns The wrapped component + */ +const wrapWithStandardContexts = (Wrapper: RenderOptions["wrapper"]) => { return ({ children }: { children: React.ReactNode }) => { if (Wrapper) { return ( - {children} + + {children} + ); } else { - return {children}; + return ( + + {children} + + ); } }; }; @@ -28,7 +44,7 @@ const wrapWithTooltipProvider = (Wrapper: RenderOptions["wrapper"]) => { const customRender = (ui: ReactElement, options: RenderOptions = {}) => { return render(ui, { ...options, - wrapper: wrapWithTooltipProvider(options?.wrapper) as RenderOptions["wrapper"], + wrapper: wrapWithStandardContexts(options?.wrapper) as RenderOptions["wrapper"], }) as ReturnType; }; diff --git a/yarn.lock b/yarn.lock index 14ae639e6b..b70d671120 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1559,10 +1559,10 @@ resolved "https://registry.yarnpkg.com/@element-hq/element-call-embedded/-/element-call-embedded-0.16.1.tgz#28bdbde426051cc2a3228a36e7196e0a254569d3" integrity sha512-g3v/QFuNy8YVRGrKC5SxjIYvgBh6biOHgejhJT2Jk/yjOOUEuP0y2PBaADm+suPD9BB/Vk1jPxFk2uEIpEzhpA== -"@element-hq/element-web-module-api@1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@element-hq/element-web-module-api/-/element-web-module-api-1.6.0.tgz#bb109035cd0c82c094e2e83ba66b6c5b9788d58f" - integrity sha512-7xew6AVX4T3J37KyhdgHuiEYdDMMYJC0/aIQOmBvVylWQFnmMmbkmkuqOBqkumcx7q6LgkB0z3cSzdKAKHIw/g== +"@element-hq/element-web-module-api@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@element-hq/element-web-module-api/-/element-web-module-api-1.7.0.tgz#7657df25cc1e7075718af2c6ea8a4ebfaa9cfb2c" + integrity sha512-WhiJTmdETK8vvaYExqyhQ9rtLjxBv9PprWr6dCa1/1VRFSkfFZRlzy2P08nHX2YXpRMTpXb39SLeleR1dgLzow== "@element-hq/element-web-playwright-common@^2.0.0": version "2.0.0" @@ -4153,13 +4153,14 @@ "@vector-im/matrix-wysiwyg-wasm@link:../../../.cache/yarn/v6/npm-@vector-im-matrix-wysiwyg-2.40.0-53c9ca5ea907d91e4515da64f20a82e5586b882c-integrity/node_modules/bindings/wysiwyg-wasm": version "0.0.0" + uid "" "@vector-im/matrix-wysiwyg@2.40.0": version "2.40.0" resolved "https://registry.yarnpkg.com/@vector-im/matrix-wysiwyg/-/matrix-wysiwyg-2.40.0.tgz#53c9ca5ea907d91e4515da64f20a82e5586b882c" integrity sha512-8LRFLs5PEKYs4lOL7aJ4lL/hGCrvEvOYkCR3JggXYXDVMtX4LmfdlKYucSAe98pCmqAAbLRvlRcR1bTOYvM8ug== dependencies: - "@vector-im/matrix-wysiwyg-wasm" "link:../../../.cache/yarn/v6/npm-@vector-im-matrix-wysiwyg-2.40.0-53c9ca5ea907d91e4515da64f20a82e5586b882c-integrity/node_modules/bindings/wysiwyg-wasm" + "@vector-im/matrix-wysiwyg-wasm" "link:../../Library/Caches/Yarn/v6/npm-@vector-im-matrix-wysiwyg-2.40.0-53c9ca5ea907d91e4515da64f20a82e5586b882c-integrity/node_modules/bindings/wysiwyg-wasm" "@vitest/expect@3.2.4": version "3.2.4" @@ -9599,7 +9600,7 @@ matrix-events-sdk@0.0.1: jwt-decode "^4.0.0" loglevel "^1.9.2" matrix-events-sdk "0.0.1" - matrix-widget-api "^1.14.0" + matrix-widget-api "^1.10.0" oidc-client-ts "^3.0.1" p-retry "7" sdp-transform "^3.0.0"