From e662c1959b45c2fcc2c55e6fba71d5a87fdc6383 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 18 Mar 2025 14:23:24 +0000 Subject: [PATCH 01/78] Add ability to hide images after clicking "show image" (#29467) * start hide * Move useSettingsValueWithSetter to useSettings * Add new setting showMediaEventIds * Add a migration path * Add an action button to hide settings. * Tweaks to MImageBody to support new setting. * Fixup and add tests * add description for migration * docs fixes * add type * i18n * appese prettier * Add tests for HideActionButton * lint * lint * Use a hook for media visibility. * Drop setting hook usage. * Fixup MImageBody test * Fixup tests * Support functional components for message body rendering. * Add a comment * Move props into IProps --- playwright/e2e/timeline/timeline.spec.ts | 15 +++- .../views/messages/HideActionButton.tsx | 44 +++++++++++ src/components/views/messages/MFileBody.tsx | 13 ++-- src/components/views/messages/MImageBody.tsx | 62 +++++++++------ .../views/messages/MImageReplyBody.tsx | 12 ++- .../views/messages/MStickerBody.tsx | 19 +++-- .../views/messages/MessageActionBar.tsx | 2 + .../views/messages/MessageEvent.tsx | 10 +-- src/components/views/rooms/ReplyTile.tsx | 5 +- src/events/EventTileFactory.tsx | 5 +- src/hooks/useMediaVisible.ts | 35 +++++++++ src/i18n/strings/en_EN.json | 1 + src/settings/Settings.tsx | 6 ++ src/settings/SettingsStore.ts | 24 ++++++ .../views/messages/HideActionButton-test.tsx | 76 +++++++++++++++++++ .../views/messages/MImageBody-test.tsx | 26 ++++++- .../unit-tests/hooks/useMediaVisible-test.tsx | 71 +++++++++++++++++ 17 files changed, 376 insertions(+), 50 deletions(-) create mode 100644 src/components/views/messages/HideActionButton.tsx create mode 100644 src/hooks/useMediaVisible.ts create mode 100644 test/unit-tests/components/views/messages/HideActionButton-test.tsx create mode 100644 test/unit-tests/hooks/useMediaVisible-test.tsx diff --git a/playwright/e2e/timeline/timeline.spec.ts b/playwright/e2e/timeline/timeline.spec.ts index 7b13d1ccb1..1f353e3e84 100644 --- a/playwright/e2e/timeline/timeline.spec.ts +++ b/playwright/e2e/timeline/timeline.spec.ts @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024, 2025 New Vector Ltd. Copyright 2022, 2023 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -905,6 +905,19 @@ test.describe("Timeline", () => { mask: [page.locator(".mx_MessageTimestamp")], }); }); + + test("should be able to hide an image", { tag: "@screenshot" }, async ({ page, app, room, context }) => { + await app.viewRoomById(room.roomId); + await sendImage(app.client, room.roomId, NEW_AVATAR); + await app.timeline.scrollToBottom(); + const imgTile = page.locator(".mx_MImageBody").first(); + await expect(imgTile).toBeVisible(); + await imgTile.hover(); + await page.getByRole("button", { name: "Hide" }).click(); + + // Check that the image is now hidden. + await expect(page.getByRole("link", { name: "Show image" })).toBeVisible(); + }); }); test.describe("message sending", { tag: ["@no-firefox", "@no-webkit"] }, () => { diff --git a/src/components/views/messages/HideActionButton.tsx b/src/components/views/messages/HideActionButton.tsx new file mode 100644 index 0000000000..8d2baf0220 --- /dev/null +++ b/src/components/views/messages/HideActionButton.tsx @@ -0,0 +1,44 @@ +/* +Copyright 2024 New Vector Ltd. +Copyright 2021 The Matrix.org Foundation C.I.C. + +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 MatrixEvent } from "matrix-js-sdk/src/matrix"; +import React from "react"; +import { VisibilityOffIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; + +import { RovingAccessibleButton } from "../../../accessibility/RovingTabIndex"; +import { _t } from "../../../languageHandler"; +import { useMediaVisible } from "../../../hooks/useMediaVisible"; + +interface IProps { + /** + * Matrix event that this action applies to. + */ + mxEvent: MatrixEvent; +} + +/** + * Quick action button for marking a media event as hidden. + */ +export const HideActionButton: React.FC = ({ mxEvent }) => { + const [mediaIsVisible, setVisible] = useMediaVisible(mxEvent.getId()!); + + if (!mediaIsVisible) { + return; + } + + return ( + setVisible(false)} + placement="left" + > + + + ); +}; diff --git a/src/components/views/messages/MFileBody.tsx b/src/components/views/messages/MFileBody.tsx index 674e0f8583..3bb9fb1069 100644 --- a/src/components/views/messages/MFileBody.tsx +++ b/src/components/views/messages/MFileBody.tsx @@ -93,7 +93,7 @@ export function computedStyle(element: HTMLElement | null): string { interface IProps extends IBodyProps { /* whether or not to show the default placeholder for the file. Defaults to true. */ - showGenericPlaceholder: boolean; + showGenericPlaceholder?: boolean; } interface IState { @@ -105,11 +105,6 @@ export default class MFileBody extends React.Component { declare public context: React.ContextType; public state: IState = {}; - - public static defaultProps = { - showGenericPlaceholder: true, - }; - private iframe: React.RefObject = createRef(); private dummyLink: React.RefObject = createRef(); private userDidClick = false; @@ -191,15 +186,17 @@ export default class MFileBody extends React.Component { const contentUrl = this.getContentUrl(); const contentFileSize = this.content.info ? this.content.info.size : null; const fileType = this.content.info?.mimetype ?? "application/octet-stream"; + // defaultProps breaks types on IBodyProps, so instead define the default here. + const showGenericPlaceholder = this.props.showGenericPlaceholder ?? true; let showDownloadLink = - !this.props.showGenericPlaceholder || + !showGenericPlaceholder || (this.context.timelineRenderingType !== TimelineRenderingType.Room && this.context.timelineRenderingType !== TimelineRenderingType.Search && this.context.timelineRenderingType !== TimelineRenderingType.Pinned); let placeholder: React.ReactNode = null; - if (this.props.showGenericPlaceholder) { + if (showGenericPlaceholder) { placeholder = ( diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 59d8b988a8..3fa4f01bfd 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -33,6 +33,7 @@ import { presentableTextForFile } from "../../../utils/FileUtils"; import { createReconnectedListener } from "../../../utils/connection"; import MediaProcessingError from "./shared/MediaProcessingError"; import { DecryptError, DownloadError } from "../../../utils/DecryptFile"; +import { useMediaVisible } from "../../../hooks/useMediaVisible"; enum Placeholder { NoImage, @@ -52,11 +53,25 @@ interface IState { }; hover: boolean; focus: boolean; - showImage: boolean; placeholder: Placeholder; } -export default class MImageBody extends React.Component { +interface IProps extends IBodyProps { + /** + * Should the media be behind a preview. + */ + mediaVisible: boolean; + /** + * Set the visibility of the media event. + * @param visible Should the event be visible. + */ + setMediaVisible: (visible: boolean) => void; +} + +/** + * @private Only use for inheritance. Use the default export for presentation. + */ +export class MImageBodyInner extends React.Component { public static contextType = RoomContext; declare public context: React.ContextType; @@ -73,21 +88,14 @@ export default class MImageBody extends React.Component { imgLoaded: false, hover: false, focus: false, - showImage: SettingsStore.getValue("showImages"), placeholder: Placeholder.NoImage, }; - protected showImage(): void { - localStorage.setItem("mx_ShowImage_" + this.props.mxEvent.getId(), "true"); - this.setState({ showImage: true }); - this.downloadImage(); - } - protected onClick = (ev: React.MouseEvent): void => { if (ev.button === 0 && !ev.metaKey) { ev.preventDefault(); - if (!this.state.showImage) { - this.showImage(); + if (!this.props.mediaVisible) { + this.props.setMediaVisible?.(true); return; } @@ -125,7 +133,7 @@ export default class MImageBody extends React.Component { private get shouldAutoplay(): boolean { return !( !this.state.contentUrl || - !this.state.showImage || + !this.props.mediaVisible || !this.state.isAnimated || SettingsStore.getValue("autoplayGifs") ); @@ -346,14 +354,10 @@ export default class MImageBody extends React.Component { public componentDidMount(): void { this.unmounted = false; - const showImage = - this.state.showImage || localStorage.getItem("mx_ShowImage_" + this.props.mxEvent.getId()) === "true"; - - if (showImage) { + if (this.props.mediaVisible) { // noinspection JSIgnoredPromiseFromCall this.downloadImage(); - this.setState({ showImage: true }); - } // else don't download anything because we don't want to display anything. + } // Add a 150ms timer for blurhash to first appear. if (this.props.mxEvent.getContent().info?.[BLURHASH_FIELD]) { @@ -372,6 +376,13 @@ export default class MImageBody extends React.Component { }); } + public componentDidUpdate(prevProps: Readonly): void { + if (!prevProps.mediaVisible && this.props.mediaVisible) { + // noinspection JSIgnoredPromiseFromCall + this.downloadImage(); + } + } + public componentWillUnmount(): void { this.unmounted = true; MatrixClientPeg.get()?.off(ClientEvent.Sync, this.reconnectedListener); @@ -425,7 +436,7 @@ export default class MImageBody extends React.Component { // by the same width and height logic below. if (!this.state.loadedImageDimensions) { let imageElement: JSX.Element; - if (!this.state.showImage) { + if (!this.props.mediaVisible) { imageElement = ; } else { imageElement = ( @@ -495,7 +506,7 @@ export default class MImageBody extends React.Component { ); } - if (!this.state.showImage) { + if (!this.props.mediaVisible) { img = ; showPlaceholder = false; // because we're hiding the image, so don't show the placeholder. } @@ -506,7 +517,7 @@ export default class MImageBody extends React.Component { } let banner: ReactNode | undefined; - if (this.state.showImage && hoverOrFocus) { + if (this.props.mediaVisible && hoverOrFocus) { banner = this.getBanner(content); } @@ -585,7 +596,7 @@ export default class MImageBody extends React.Component { {children} ); - } else if (!this.state.showImage) { + } else if (!this.props.mediaVisible) { return (
{children} @@ -686,3 +697,10 @@ export class HiddenImagePlaceholder extends React.PureComponent = (props) => { + const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId()!); + return ; +}; + +export default MImageBody; diff --git a/src/components/views/messages/MImageReplyBody.tsx b/src/components/views/messages/MImageReplyBody.tsx index 32fd27c62a..5bbb00ef44 100644 --- a/src/components/views/messages/MImageReplyBody.tsx +++ b/src/components/views/messages/MImageReplyBody.tsx @@ -9,11 +9,13 @@ Please see LICENSE files in the repository root for full details. import React from "react"; import { type ImageContent } from "matrix-js-sdk/src/types"; -import MImageBody from "./MImageBody"; +import { MImageBodyInner } from "./MImageBody"; +import { type IBodyProps } from "./IBodyProps"; +import { useMediaVisible } from "../../../hooks/useMediaVisible"; const FORCED_IMAGE_HEIGHT = 44; -export default class MImageReplyBody extends MImageBody { +class MImageReplyBodyInner extends MImageBodyInner { public onClick = (ev: React.MouseEvent): void => { ev.preventDefault(); }; @@ -35,3 +37,9 @@ export default class MImageReplyBody extends MImageBody { return
{thumbnail}
; } } +const MImageReplyBody: React.FC = (props) => { + const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId()!); + return ; +}; + +export default MImageReplyBody; diff --git a/src/components/views/messages/MStickerBody.tsx b/src/components/views/messages/MStickerBody.tsx index ccb9bb437b..29082512d0 100644 --- a/src/components/views/messages/MStickerBody.tsx +++ b/src/components/views/messages/MStickerBody.tsx @@ -9,16 +9,18 @@ import React, { type ComponentProps, type ReactNode } from "react"; import { type Tooltip } from "@vector-im/compound-web"; import { type MediaEventContent } from "matrix-js-sdk/src/types"; -import MImageBody from "./MImageBody"; +import { MImageBodyInner } from "./MImageBody"; import { BLURHASH_FIELD } from "../../../utils/image-media"; import IconsShowStickersSvg from "../../../../res/img/icons-show-stickers.svg"; +import { type IBodyProps } from "./IBodyProps"; +import { useMediaVisible } from "../../../hooks/useMediaVisible"; -export default class MStickerBody extends MImageBody { +class MStickerBodyInner extends MImageBodyInner { // Mostly empty to prevent default behaviour of MImageBody protected onClick = (ev: React.MouseEvent): void => { ev.preventDefault(); - if (!this.state.showImage) { - this.showImage(); + if (!this.props.mediaVisible) { + this.props.setMediaVisible?.(true); } }; @@ -26,7 +28,7 @@ export default class MStickerBody extends MImageBody { // which is added by mx_MStickerBody_wrapper protected wrapImage(contentUrl: string, children: React.ReactNode): JSX.Element { let onClick: React.MouseEventHandler | undefined; - if (!this.state.showImage) { + if (!this.props.mediaVisible) { onClick = this.onClick; } return ( @@ -75,3 +77,10 @@ export default class MStickerBody extends MImageBody { return null; // we don't need a banner, we have a tooltip } } + +const MStickerBody: React.FC = (props) => { + const [mediaVisible, setVisible] = useMediaVisible(props.mxEvent.getId()!); + return ; +}; + +export default MStickerBody; diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index e6838c635f..0120947b31 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -61,6 +61,7 @@ import { type GetRelationsForEvent, type IEventTileType } from "../rooms/EventTi import { type ButtonEvent } from "../elements/AccessibleButton"; import PinningUtils from "../../../utils/PinningUtils"; import PosthogTrackers from "../../../PosthogTrackers.ts"; +import { HideActionButton } from "./HideActionButton.tsx"; interface IOptionsButtonProps { mxEvent: MatrixEvent; @@ -535,6 +536,7 @@ export default class MessageActionBar extends React.PureComponent this.props.getTile()?.getMediaHelper?.()} key="download" />, + , ); } } else if ( diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx index be63457902..ff97edd1db 100644 --- a/src/components/views/messages/MessageEvent.tsx +++ b/src/components/views/messages/MessageEvent.tsx @@ -45,8 +45,8 @@ import { type GetRelationsForEvent, type IEventTileOps } from "../rooms/EventTil // onMessageAllowed is handled internally interface IProps extends Omit { /* overrides for the msgtype-specific components, used by ReplyTile to override file rendering */ - overrideBodyTypes?: Record; - overrideEventTypes?: Record; + overrideBodyTypes?: Record>; + overrideEventTypes?: Record>; // helper function to access relations for this event getRelationsForEvent?: GetRelationsForEvent; @@ -58,7 +58,7 @@ export interface IOperableEventTile { getEventTileOps(): IEventTileOps | null; } -const baseBodyTypes = new Map([ +const baseBodyTypes = new Map>([ [MsgType.Text, TextualBody], [MsgType.Notice, TextualBody], [MsgType.Emote, TextualBody], @@ -80,7 +80,7 @@ const baseEvTypes = new Map>([ export default class MessageEvent extends React.Component implements IMediaBody, IOperableEventTile { private body: React.RefObject = createRef(); private mediaHelper?: MediaEventHelper; - private bodyTypes = new Map(baseBodyTypes.entries()); + private bodyTypes = new Map>(baseBodyTypes.entries()); private evTypes = new Map>(baseEvTypes.entries()); public static contextType = MatrixClientContext; @@ -115,7 +115,7 @@ export default class MessageEvent extends React.Component implements IMe } private updateComponentMaps(): void { - this.bodyTypes = new Map(baseBodyTypes.entries()); + this.bodyTypes = new Map>(baseBodyTypes.entries()); for (const [bodyType, bodyComponent] of Object.entries(this.props.overrideBodyTypes ?? {})) { this.bodyTypes.set(bodyType, bodyComponent); } diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index 8084b59ba3..0f2b580d7e 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -26,6 +26,7 @@ import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPaylo import { renderReplyTile } from "../../../events/EventTileFactory"; import { type GetRelationsForEvent } from "../rooms/EventTile"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { type IBodyProps } from "../messages/IBodyProps"; interface IProps { mxEvent: MatrixEvent; @@ -139,13 +140,13 @@ export default class ReplyTile extends React.PureComponent { ); } - const msgtypeOverrides: Record = { + const msgtypeOverrides: Record> = { [MsgType.Image]: MImageReplyBody, // Override audio and video body with file body. We also hide the download/decrypt button using CSS [MsgType.Audio]: isVoiceMessage(mxEvent) ? MVoiceMessageBody : MFileBody, [MsgType.Video]: MFileBody, }; - const evOverrides: Record = { + const evOverrides: Record> = { // Use MImageReplyBody so that the sticker isn't taking up a lot of space [EventType.Sticker]: MImageReplyBody, }; diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx index e0d2a91afb..633583485a 100644 --- a/src/events/EventTileFactory.tsx +++ b/src/events/EventTileFactory.tsx @@ -42,6 +42,7 @@ import HiddenBody from "../components/views/messages/HiddenBody"; import ViewSourceEvent from "../components/views/messages/ViewSourceEvent"; import { shouldDisplayAsBeaconTile } from "../utils/beacon/timeline"; import { ElementCall } from "../models/Call"; +import { type IBodyProps } from "../components/views/messages/IBodyProps"; // Subset of EventTile's IProps plus some mixins export interface EventTileTypeProps @@ -64,8 +65,8 @@ export interface EventTileTypeProps ref?: React.RefObject; // `any` because it's effectively impossible to convince TS of a reasonable type timestamp?: JSX.Element; maxImageHeight?: number; // pixels - overrideBodyTypes?: Record; - overrideEventTypes?: Record; + overrideBodyTypes?: Record>; + overrideEventTypes?: Record>; } type FactoryProps = Omit; diff --git a/src/hooks/useMediaVisible.ts b/src/hooks/useMediaVisible.ts new file mode 100644 index 0000000000..e244d500cf --- /dev/null +++ b/src/hooks/useMediaVisible.ts @@ -0,0 +1,35 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { useCallback } from "react"; + +import { SettingLevel } from "../settings/SettingLevel"; +import { useSettingValue } from "./useSettings"; +import SettingsStore from "../settings/SettingsStore"; + +/** + * Should the media event be visible in the client, or hidden. + * @param eventId The eventId of the media event. + * @returns A boolean describing the hidden status, and a function to set the visiblity. + */ +export function useMediaVisible(eventId: string): [boolean, (visible: boolean) => void] { + const defaultShowImages = useSettingValue("showImages", SettingLevel.DEVICE); + const eventVisibility = useSettingValue("showMediaEventIds", SettingLevel.DEVICE); + const setMediaVisible = useCallback( + (visible: boolean) => { + SettingsStore.setValue("showMediaEventIds", null, SettingLevel.DEVICE, { + ...eventVisibility, + [eventId]: visible, + }); + }, + [eventId, eventVisibility], + ); + + // Always prefer the explicit per-event user preference here. + const imgIsVisible = eventVisibility[eventId] ?? defaultShowImages; + return [imgIsVisible, setMediaVisible]; +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index aa71cac8a7..322fcf4c93 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -64,6 +64,7 @@ "go": "Go", "go_back": "Go back", "got_it": "Got it", + "hide": "Hide", "hide_advanced": "Hide advanced", "hold": "Hold", "ignore": "Ignore", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index d887c9bb49..8784e12b6c 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -273,6 +273,7 @@ export interface Settings { "language": IBaseSetting; "breadcrumb_rooms": IBaseSetting; "recent_emoji": IBaseSetting; + "showMediaEventIds": IBaseSetting<{ [eventId: string]: boolean }>; "SpotlightSearch.recentSearches": IBaseSetting; "SpotlightSearch.showNsfwPublicRooms": IBaseSetting; "room_directory_servers": IBaseSetting; @@ -970,6 +971,11 @@ export const SETTINGS: Settings = { supportedLevels: [SettingLevel.ACCOUNT], default: [], // list of room IDs, most recent first }, + "showMediaEventIds": { + // not really a setting + supportedLevels: [SettingLevel.DEVICE], + default: {}, // List of events => is visible + }, "SpotlightSearch.showNsfwPublicRooms": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td("settings|show_nsfw_content"), diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index 671a2ac676..7dac777399 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -697,6 +697,24 @@ export default class SettingsStore { client.on(ClientEvent.Sync, onSync); } + /** + * Migrate the setting for visible images to a setting. + */ + private static migrateShowImagesToSettings(): void { + const MIGRATION_DONE_FLAG = "mx_show_images_migration_done"; + if (localStorage.getItem(MIGRATION_DONE_FLAG)) return; + + logger.info("Performing one-time settings migration of shown images to settings store"); + const newValue = Object.fromEntries( + Object.keys(localStorage) + .filter((k) => k.startsWith("mx_ShowImage_")) + .map((k) => [k.slice("mx_ShowImage_".length), true]), + ); + this.setValue("showMediaEventIds", null, SettingLevel.DEVICE, newValue); + + localStorage.setItem(MIGRATION_DONE_FLAG, "true"); + } + /** * Runs or queues any setting migrations needed. */ @@ -708,6 +726,12 @@ export default class SettingsStore { // be disabled in E2EE rooms. SettingsStore.migrateURLPreviewsE2EE(isFreshLogin); + // This can be removed once enough users have run a version of Element with + // this migration. + // The consequences of missing the migration are that previously shown images + // will now be hidden again, so this fails safely. + SettingsStore.migrateShowImagesToSettings(); + // Dev notes: to add your migration, just add a new `migrateMyFeature` function, call it, and // add a comment to note when it can be removed. return; diff --git a/test/unit-tests/components/views/messages/HideActionButton-test.tsx b/test/unit-tests/components/views/messages/HideActionButton-test.tsx new file mode 100644 index 0000000000..57e92b02b8 --- /dev/null +++ b/test/unit-tests/components/views/messages/HideActionButton-test.tsx @@ -0,0 +1,76 @@ +/* +Copyright 2024,2025 New Vector Ltd. +Copyright 2024 The Matrix.org Foundation C.I.C. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import React from "react"; +import { fireEvent, render, screen } from "jest-matrix-react"; +import { MatrixEvent } from "matrix-js-sdk/src/matrix"; + +import { HideActionButton } from "../../../../../src/components/views/messages/HideActionButton"; +import SettingsStore from "../../../../../src/settings/SettingsStore"; +import { SettingLevel } from "../../../../../src/settings/SettingLevel"; +import type { Settings } from "../../../../../src/settings/Settings"; + +function mockSetting( + showImages: Settings["showImages"]["default"], + showMediaEventIds: Settings["showMediaEventIds"]["default"], +) { + jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName) => { + if (settingName === "showImages") { + return showImages; + } else if (settingName === "showMediaEventIds") { + return showMediaEventIds; + } + throw Error(`Unexpected setting ${settingName}`); + }); +} + +const event = new MatrixEvent({ + event_id: "$foo:bar", + room_id: "!room:id", + sender: "@user:id", + type: "m.room.message", + content: { + body: "test", + msgtype: "m.image", + url: "mxc://matrix.org/1234", + }, +}); + +describe("HideActionButton", () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + it("should show button when event is visible by showMediaEventIds setting", async () => { + mockSetting(false, { "$foo:bar": true }); + render(); + expect(screen.getByRole("button")).toBeVisible(); + }); + it("should show button when event is visible by showImages setting", async () => { + mockSetting(true, {}); + render(); + expect(screen.getByRole("button")).toBeVisible(); + }); + it("should hide button when event is hidden by showMediaEventIds setting", async () => { + jest.spyOn(SettingsStore, "getValue").mockReturnValue({ "$foo:bar": false }); + render(); + expect(screen.queryByRole("button")).toBeNull(); + }); + it("should hide button when event is hidden by showImages setting", async () => { + mockSetting(false, {}); + render(); + expect(screen.queryByRole("button")).toBeNull(); + }); + it("should store event as hidden when clicked", async () => { + const spy = jest.spyOn(SettingsStore, "setValue"); + render(); + fireEvent.click(screen.getByRole("button")); + expect(spy).toHaveBeenCalledWith("showMediaEventIds", null, SettingLevel.DEVICE, { "$foo:bar": false }); + // Button should be hidden after the setting is set. + expect(screen.queryByRole("button")).toBeNull(); + }); +}); diff --git a/test/unit-tests/components/views/messages/MImageBody-test.tsx b/test/unit-tests/components/views/messages/MImageBody-test.tsx index f9ba25d7a9..9d324f13e8 100644 --- a/test/unit-tests/components/views/messages/MImageBody-test.tsx +++ b/test/unit-tests/components/views/messages/MImageBody-test.tsx @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React from "react"; +import React, { act } from "react"; import { fireEvent, render, screen, waitForElementToBeRemoved } from "jest-matrix-react"; import { EventType, getHttpUriForMxc, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import fetchMock from "fetch-mock-jest"; @@ -27,6 +27,7 @@ import { } from "../../../../test-utils"; import { MediaEventHelper } from "../../../../../src/utils/MediaEventHelper"; import SettingsStore from "../../../../../src/settings/SettingsStore"; +import { SettingLevel } from "../../../../../src/settings/SettingLevel"; jest.mock("matrix-encrypt-attachment", () => ({ decryptAttachment: jest.fn(), @@ -57,6 +58,7 @@ describe("", () => { }, ); const encryptedMediaEvent = new MatrixEvent({ + event_id: "$foo:bar", room_id: "!room:server", sender: userId, type: EventType.RoomMessage, @@ -131,7 +133,26 @@ describe("", () => { describe("with image previews/thumbnails disabled", () => { beforeEach(() => { - jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); + act(() => { + SettingsStore.setValue("showImages", null, SettingLevel.DEVICE, false); + }); + }); + + afterEach(() => { + act(() => { + SettingsStore.setValue( + "showImages", + null, + SettingLevel.DEVICE, + SettingsStore.getDefaultValue("showImages"), + ); + SettingsStore.setValue( + "showMediaEventIds", + null, + SettingLevel.DEVICE, + SettingsStore.getDefaultValue("showMediaEventIds"), + ); + }); }); it("should not download image", async () => { @@ -163,7 +184,6 @@ describe("", () => { fireEvent.click(screen.getByRole("button")); - // image fetched after clicking show image expect(fetchMock).toHaveFetched(url); // spinner while downloading image diff --git a/test/unit-tests/hooks/useMediaVisible-test.tsx b/test/unit-tests/hooks/useMediaVisible-test.tsx new file mode 100644 index 0000000000..cafc32f1bf --- /dev/null +++ b/test/unit-tests/hooks/useMediaVisible-test.tsx @@ -0,0 +1,71 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { act, renderHook, waitFor } from "jest-matrix-react"; + +import { useMediaVisible } from "../../../src/hooks/useMediaVisible"; +import SettingsStore from "../../../src/settings/SettingsStore"; +import { SettingLevel } from "../../../src/settings/SettingLevel"; + +const EVENT_ID = "$fibble:example.org"; + +function render() { + return renderHook(() => useMediaVisible(EVENT_ID)); +} + +describe("useMediaVisible", () => { + afterEach(() => { + // Using act here as otherwise React warns about state updates not being wrapped. + act(() => { + SettingsStore.setValue( + "showMediaEventIds", + null, + SettingLevel.DEVICE, + SettingsStore.getDefaultValue("showMediaEventIds"), + ); + SettingsStore.setValue( + "showImages", + null, + SettingLevel.DEVICE, + SettingsStore.getDefaultValue("showImages"), + ); + }); + }); + + it("should display images by default", async () => { + const { result } = render(); + expect(result.current[0]).toEqual(true); + }); + + it("should hide images when the default is changed", async () => { + SettingsStore.setValue("showImages", null, SettingLevel.DEVICE, false); + const { result } = render(); + expect(result.current[0]).toEqual(false); + }); + + it("should hide images after function is called", async () => { + const { result } = render(); + expect(result.current[0]).toEqual(true); + act(() => { + result.current[1](false); + }); + await waitFor(() => { + expect(result.current[0]).toEqual(false); + }); + }); + it("should show images after function is called", async () => { + SettingsStore.setValue("showImages", null, SettingLevel.DEVICE, false); + const { result } = render(); + expect(result.current[0]).toEqual(false); + act(() => { + result.current[1](true); + }); + await waitFor(() => { + expect(result.current[0]).toEqual(true); + }); + }); +}); From cdd2622151f5452079d132c68b902569cb2833dd Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 18 Mar 2025 14:35:39 +0000 Subject: [PATCH 02/78] Doc improvements from #29138 (#29503) * Rename props & fix typo * Docs * Better docs * Add comment * Fix typo * Paragraphs in tsdoc * Add comment * Hopefully clearer comment * Really fix typo Co-authored-by: Will Hunt * Stray word Co-authored-by: Andy Balaam * Hopefully clearer comment * Typo * Formatting & clarity Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --------- Co-authored-by: Will Hunt Co-authored-by: Andy Balaam Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- playwright/e2e/crypto/toasts.spec.ts | 4 +++- playwright/e2e/crypto/utils.ts | 3 +++ src/components/views/dialogs/UserSettingsDialog.tsx | 7 ++++++- .../views/settings/encryption/ResetIdentityPanel.tsx | 2 ++ .../settings/tabs/user/EncryptionUserSettingsTab.tsx | 8 ++++---- src/toasts/SetupEncryptionToast.ts | 1 + 6 files changed, 19 insertions(+), 6 deletions(-) diff --git a/playwright/e2e/crypto/toasts.spec.ts b/playwright/e2e/crypto/toasts.spec.ts index 905a8fb1ed..5b80f822a0 100644 --- a/playwright/e2e/crypto/toasts.spec.ts +++ b/playwright/e2e/crypto/toasts.spec.ts @@ -29,7 +29,9 @@ test.describe("Key storage out of sync toast", () => { }); test("should prompt for recovery key if 'enter recovery key' pressed", { tag: "@screenshot" }, async ({ page }) => { - // Need to wait for 2 to appear since playwright only evaluates 'first()' initially, so the waiting won't work + // We need to wait for there to be two toasts as the wait below won't work in isolation: + // playwright only evaluates the 'first()' call initially, not subsequent times it checks, so + // it would always be checking the same toast, even if another one is now the first. await expect(page.getByRole("alert")).toHaveCount(2); await expect(page.getByRole("alert").first()).toMatchScreenshot("key-storage-out-of-sync-toast.png"); diff --git a/playwright/e2e/crypto/utils.ts b/playwright/e2e/crypto/utils.ts index a317a2a215..0030dc02d0 100644 --- a/playwright/e2e/crypto/utils.ts +++ b/playwright/e2e/crypto/utils.ts @@ -221,6 +221,9 @@ export async function logIntoElement(page: Page, credentials: Credentials, secur await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key" }).click(); const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "use your Recovery Key" }); + // If the user has set a recovery *passphrase*, they'll be prompted for that first and have to click + // through to enter the recovery key which is what we have here. If they haven't, they'll be prompted + // for a recovery key straight away. We click the button if it's there so this works in both cases. if (await useSecurityKey.isVisible()) { await useSecurityKey.click(); } diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx index 6a94ae8842..4eb0a66f1a 100644 --- a/src/components/views/dialogs/UserSettingsDialog.tsx +++ b/src/components/views/dialogs/UserSettingsDialog.tsx @@ -50,6 +50,11 @@ import { EncryptionUserSettingsTab } from "../settings/tabs/user/EncryptionUserS interface IProps { initialTabId?: UserTab; showMsc4108QrCode?: boolean; + /** + * If `true`, the flow for a user to reset their encryption will be shown. In this case, `initialTabId` must be `UserTab.Encryption`. + * + * If false or undefined, show the tab as normal. + */ showResetIdentity?: boolean; sdkContext: SdkContextClass; onFinished(): void; @@ -92,7 +97,7 @@ function titleForTabID(tabId: UserTab): React.ReactNode { export default function UserSettingsDialog(props: IProps): JSX.Element { const voipEnabled = useSettingValue(UIFeature.Voip); const mjolnirEnabled = useSettingValue("feature_mjolnir"); - // store these props in state as changing tabs back and forth should clear it + // store these props in state as changing tabs back and forth should clear them const [showMsc4108QrCode, setShowMsc4108QrCode] = useState(props.showMsc4108QrCode); const [showResetIdentity, setShowResetIdentity] = useState(props.showResetIdentity); diff --git a/src/components/views/settings/encryption/ResetIdentityPanel.tsx b/src/components/views/settings/encryption/ResetIdentityPanel.tsx index 6c25985a22..9507e52872 100644 --- a/src/components/views/settings/encryption/ResetIdentityPanel.tsx +++ b/src/components/views/settings/encryption/ResetIdentityPanel.tsx @@ -31,8 +31,10 @@ interface ResetIdentityPanelProps { /** * The variant of the panel to show. We show more warnings in the 'compromised' variant (no use in showing a user this * warning if they have to reset because they no longer have their key) + * * "compromised" is shown when the user chooses 'reset' explicitly in settings, usually because they believe their * identity has been compromised. + * * "forgot" is shown when the user has just forgotten their passphrase. */ variant: "compromised" | "forgot"; diff --git a/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx b/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx index 3af555ed49..987b1d95b6 100644 --- a/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx @@ -37,7 +37,7 @@ import { DeleteKeyStoragePanel } from "../../encryption/DeleteKeyStoragePanel"; * This happens when the user has a recovery key and the user clicks on "Change recovery key" button of the RecoveryPanel. * - "set_recovery_key": The panel to show when the user is setting up their recovery key. * This happens when the user doesn't have a key a recovery key and the user clicks on "Set up recovery key" button of the RecoveryPanel. - * - "reset_identity_compromised": The panel to show when the user is resetting their identity, in te case where their key is compromised. + * - "reset_identity_compromised": The panel to show when the user is resetting their identity, in the case where their key is compromised. * - "reset_identity_forgot": The panel to show when the user is resetting their identity, in the case where they forgot their recovery key. * - "secrets_not_cached": The secrets are not cached locally. This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets. * If the "set_up_encryption" and "secrets_not_cached" conditions are both filled, "set_up_encryption" prevails. @@ -55,9 +55,9 @@ export type State = | "secrets_not_cached" | "key_storage_delete"; -interface EncryptionUserSettingsTabProps { +interface Props { /** - * If the tab should start in a state other than the deasult + * If the tab should start in a state other than the default */ initialState?: State; } @@ -65,7 +65,7 @@ interface EncryptionUserSettingsTabProps { /** * The encryption settings tab. */ -export function EncryptionUserSettingsTab({ initialState = "loading" }: EncryptionUserSettingsTabProps): JSX.Element { +export function EncryptionUserSettingsTab({ initialState = "loading" }: Props): JSX.Element { const [state, setState] = useState(initialState); const checkEncryptionState = useCheckEncryptionState(state, setState); diff --git a/src/toasts/SetupEncryptionToast.ts b/src/toasts/SetupEncryptionToast.ts index 8a743dfe8b..1613c5b9c9 100644 --- a/src/toasts/SetupEncryptionToast.ts +++ b/src/toasts/SetupEncryptionToast.ts @@ -161,6 +161,7 @@ export const showToast = (kind: Kind): void => { const onSecondaryClick = (): void => { if (kind === Kind.KEY_STORAGE_OUT_OF_SYNC) { + // Open the user settings dialog to the encryption tab and start the flow to reset encryption const payload: OpenToTabPayload = { action: Action.ViewUserSettings, initialTabId: UserTab.Encryption, From 56d115c2ff02842ea93b99f51bb5c6057656b01d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 14:38:09 +0000 Subject: [PATCH 03/78] Update dependency testcontainers to v10.21.0 (#29520) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 7843a43c27..28ffb3d2d8 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "oidc-client-ts": "3.1.0", "jwt-decode": "4.0.0", "caniuse-lite": "1.0.30001701", - "testcontainers": "10.20.0", + "testcontainers": "10.21.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0", "wrap-ansi": "npm:wrap-ansi@^7.0.0" }, diff --git a/yarn.lock b/yarn.lock index 600521f2b9..2f3f84e092 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3614,7 +3614,6 @@ "@vector-im/matrix-wysiwyg-wasm@link:../../Library/Caches/Yarn/v6/npm-@vector-im-matrix-wysiwyg-2.38.2-3fa19a2a17fd12d955ef1e14fd63aecbcf3b95e8-integrity/node_modules/bindings/wysiwyg-wasm": version "0.0.0" - uid "" "@vector-im/matrix-wysiwyg@2.38.2": version "2.38.2" @@ -12001,10 +12000,10 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" -testcontainers@10.20.0, testcontainers@^10.18.0, testcontainers@^10.19.0, testcontainers@^10.20.0: - version "10.20.0" - resolved "https://registry.yarnpkg.com/testcontainers/-/testcontainers-10.20.0.tgz#45c524ae4be9b1ffe2fb42b701f6c0a04ee2d90a" - integrity sha512-pOPm/OUIT41aMijAZ9RsYg5xOq9ciy93+pCf2D9qDI0oV8rwk91XpPoUlizll4qwxmmHsLmfZFHJTpeB+BIfmw== +testcontainers@10.21.0, testcontainers@^10.18.0, testcontainers@^10.19.0, testcontainers@^10.20.0: + version "10.21.0" + resolved "https://registry.yarnpkg.com/testcontainers/-/testcontainers-10.21.0.tgz#ee1973d9f783984853965da22035b58627253b81" + integrity sha512-avzqNfP0HyHaOPTztHbGnHycZWzWaeUwSPnJGCYin2LB+n/tlGjAzExVrkIaCSBwIjFkMlufEYuKWzGc8mupDQ== dependencies: "@balena/dockerignore" "^1.0.2" "@types/dockerode" "^3.3.29" From 0bef21267990d4237f5b72801de97c83f77e6fc3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 14:56:34 +0000 Subject: [PATCH 04/78] Update docker/login-action digest to 74a5d14 (#29521) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/docker.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 35630e4c2e..1cf24facde 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -37,14 +37,14 @@ jobs: install: true - name: Login to Docker Hub - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 if: github.event_name != 'pull_request' with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 if: github.event_name != 'pull_request' with: registry: ghcr.io From 7b809171fcfa6ddbdabac8943237a2615525a572 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:01:19 +0000 Subject: [PATCH 05/78] Update guibranco/github-status-action-v2 digest to fe98467 (#29522) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 710e996b39..e94c35299a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -104,7 +104,7 @@ jobs: - name: Skip SonarCloud in merge queue if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true' - uses: guibranco/github-status-action-v2@5ef6e175c333bc629f3718b083c8a2ff6e0bbfbc + uses: guibranco/github-status-action-v2@fe98467f9071758c7fc214af9dbac7f301bd23d4 with: authToken: ${{ secrets.GITHUB_TOKEN }} state: success From c6b1a92f2ebc98db30389238720296580975db67 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:01:24 +0000 Subject: [PATCH 06/78] Update babel monorepo to v7.26.10 (#29523) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 126 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 73 insertions(+), 53 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2f3f84e092..5a4be83d3e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -57,20 +57,20 @@ integrity sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ== "@babel/core@^7.0.0", "@babel/core@^7.11.6", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.18.5", "@babel/core@^7.21.3", "@babel/core@^7.23.9", "@babel/core@^7.24.4": - version "7.26.9" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.9.tgz#71838542a4b1e49dfed353d7acbc6eb89f4a76f2" - integrity sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw== + version "7.26.10" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.10.tgz#5c876f83c8c4dcb233ee4b670c0606f2ac3000f9" + integrity sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ== dependencies: "@ampproject/remapping" "^2.2.0" "@babel/code-frame" "^7.26.2" - "@babel/generator" "^7.26.9" + "@babel/generator" "^7.26.10" "@babel/helper-compilation-targets" "^7.26.5" "@babel/helper-module-transforms" "^7.26.0" - "@babel/helpers" "^7.26.9" - "@babel/parser" "^7.26.9" + "@babel/helpers" "^7.26.10" + "@babel/parser" "^7.26.10" "@babel/template" "^7.26.9" - "@babel/traverse" "^7.26.9" - "@babel/types" "^7.26.9" + "@babel/traverse" "^7.26.10" + "@babel/types" "^7.26.10" convert-source-map "^2.0.0" debug "^4.1.0" gensync "^1.0.0-beta.2" @@ -78,18 +78,18 @@ semver "^6.3.1" "@babel/eslint-parser@^7.12.10": - version "7.26.8" - resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.26.8.tgz#55c4f4aae4970ae127f7a12369182ed6250e6f09" - integrity sha512-3tBctaHRW6xSub26z7n8uyOTwwUsCdvIug/oxBH9n6yCO5hMj2vwDJAo7RbBMKrM7P+W2j61zLKviJQFGOYKMg== + version "7.26.10" + resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.26.10.tgz#4423cb3f84c26978439feabfe23c5aa929400737" + integrity sha512-QsfQZr4AiLpKqn7fz+j7SN+f43z2DZCgGyYbNJ2vJOqKfG4E6MZer1+jqGZqKJaxq/gdO2DC/nUu45+pOL5p2Q== dependencies: "@nicolo-ribaudo/eslint-scope-5-internals" "5.1.1-v1" eslint-visitor-keys "^2.1.0" semver "^6.3.1" "@babel/eslint-plugin@^7.12.10": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/eslint-plugin/-/eslint-plugin-7.25.9.tgz#a5b6cc46085e0a7d45c5dae36055ce30c5125dab" - integrity sha512-MWg1lz+JiP9l1fXkE0qCUVo+1XwgNRPs6GTc88hmw6qN3AdgmfTSkyHt0e1xOTsKdXW5xlh2Lsk3wrFZbW5rzQ== + version "7.26.10" + resolved "https://registry.yarnpkg.com/@babel/eslint-plugin/-/eslint-plugin-7.26.10.tgz#63c70823e8f4875dab8b976aa03d7246ca23d531" + integrity sha512-vOFlmZX0tzi58ggN8w2LGNOY2VOoqgYBtj2FRy+3BjFSvNvU+RnapXywHACzLqzbDi9yvS0CZEBokJ26ioUVLA== dependencies: eslint-rule-composer "^0.3.0" @@ -103,13 +103,13 @@ "@jridgewell/trace-mapping" "^0.3.25" jsesc "^3.0.2" -"@babel/generator@^7.26.9": - version "7.26.9" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.9.tgz#75a9482ad3d0cc7188a537aa4910bc59db67cbca" - integrity sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg== +"@babel/generator@^7.26.10", "@babel/generator@^7.26.9": + version "7.26.10" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.10.tgz#a60d9de49caca16744e6340c3658dfef6138c3f7" + integrity sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang== dependencies: - "@babel/parser" "^7.26.9" - "@babel/types" "^7.26.9" + "@babel/parser" "^7.26.10" + "@babel/types" "^7.26.10" "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.25" jsesc "^3.0.2" @@ -154,7 +154,7 @@ regexpu-core "^6.2.0" semver "^6.3.1" -"@babel/helper-define-polyfill-provider@^0.6.2", "@babel/helper-define-polyfill-provider@^0.6.3": +"@babel/helper-define-polyfill-provider@^0.6.3": version "0.6.3" resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz#f4f2792fae2ef382074bc2d713522cf24e6ddb21" integrity sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg== @@ -265,13 +265,13 @@ "@babel/traverse" "^7.25.9" "@babel/types" "^7.25.9" -"@babel/helpers@^7.26.9": - version "7.26.9" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.9.tgz#28f3fb45252fc88ef2dc547c8a911c255fc9fef6" - integrity sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA== +"@babel/helpers@^7.26.10": + version "7.26.10" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.10.tgz#6baea3cd62ec2d0c1068778d63cb1314f6637384" + integrity sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g== dependencies: "@babel/template" "^7.26.9" - "@babel/types" "^7.26.9" + "@babel/types" "^7.26.10" "@babel/highlight@^7.25.7": version "7.25.9" @@ -297,12 +297,12 @@ dependencies: "@babel/types" "^7.26.3" -"@babel/parser@^7.26.9": - version "7.26.9" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.9.tgz#d9e78bee6dc80f9efd8f2349dcfbbcdace280fd5" - integrity sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A== +"@babel/parser@^7.26.10", "@babel/parser@^7.26.9": + version "7.26.10" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.10.tgz#e9bdb82f14b97df6569b0b038edd436839c57749" + integrity sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA== dependencies: - "@babel/types" "^7.26.9" + "@babel/types" "^7.26.10" "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.9": version "7.25.9" @@ -903,14 +903,14 @@ "@babel/helper-plugin-utils" "^7.25.9" "@babel/plugin-transform-runtime@^7.12.10": - version "7.26.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.26.9.tgz#ea8be19ef134668e98f7b54daf7c4f853859dc44" - integrity sha512-Jf+8y9wXQbbxvVYTM8gO5oEF2POdNji0NMltEkG7FtmzD9PVz7/lxpqSdTvwsjTMU5HIHuDVNf2SOxLkWi+wPQ== + version "7.26.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.26.10.tgz#6b4504233de8238e7d666c15cde681dc62adff87" + integrity sha512-NWaL2qG6HRpONTnj4JvDU6th4jYeZOJgu3QhmFTCihib0ermtOJqktA5BduGm3suhhVe9EMP9c9+mfJ/I9slqw== dependencies: "@babel/helper-module-imports" "^7.25.9" "@babel/helper-plugin-utils" "^7.26.5" babel-plugin-polyfill-corejs2 "^0.4.10" - babel-plugin-polyfill-corejs3 "^0.10.6" + babel-plugin-polyfill-corejs3 "^0.11.0" babel-plugin-polyfill-regenerator "^0.6.1" semver "^6.3.1" @@ -1137,7 +1137,20 @@ debug "^4.3.1" globals "^11.1.0" -"@babel/traverse@^7.25.9", "@babel/traverse@^7.26.5", "@babel/traverse@^7.26.8", "@babel/traverse@^7.26.9": +"@babel/traverse@^7.25.9", "@babel/traverse@^7.26.10": + version "7.26.10" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.10.tgz#43cca33d76005dbaa93024fae536cc1946a4c380" + integrity sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A== + dependencies: + "@babel/code-frame" "^7.26.2" + "@babel/generator" "^7.26.10" + "@babel/parser" "^7.26.10" + "@babel/template" "^7.26.9" + "@babel/types" "^7.26.10" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/traverse@^7.26.5", "@babel/traverse@^7.26.8", "@babel/traverse@^7.26.9": version "7.26.9" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.9.tgz#4398f2394ba66d05d988b2ad13c219a2c857461a" integrity sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg== @@ -1167,10 +1180,10 @@ "@babel/helper-string-parser" "^7.25.9" "@babel/helper-validator-identifier" "^7.25.9" -"@babel/types@^7.25.9", "@babel/types@^7.26.9", "@babel/types@^7.4.4": - version "7.26.9" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.9.tgz#08b43dec79ee8e682c2ac631c010bdcac54a21ce" - integrity sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw== +"@babel/types@^7.25.9", "@babel/types@^7.26.10", "@babel/types@^7.26.9": + version "7.26.10" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.10.tgz#396382f6335bd4feb65741eacfc808218f859259" + integrity sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ== dependencies: "@babel/helper-string-parser" "^7.25.9" "@babel/helper-validator-identifier" "^7.25.9" @@ -1183,6 +1196,14 @@ "@babel/helper-string-parser" "^7.25.9" "@babel/helper-validator-identifier" "^7.25.9" +"@babel/types@^7.4.4": + version "7.26.9" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.9.tgz#08b43dec79ee8e682c2ac631c010bdcac54a21ce" + integrity sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@balena/dockerignore@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@balena/dockerignore/-/dockerignore-1.0.2.tgz#9ffe4726915251e8eb69f44ef3547e0da2c03e0d" @@ -4294,14 +4315,6 @@ babel-plugin-polyfill-corejs2@^0.4.10: "@babel/helper-define-polyfill-provider" "^0.6.3" semver "^6.3.1" -babel-plugin-polyfill-corejs3@^0.10.6: - version "0.10.6" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz#2deda57caef50f59c525aeb4964d3b2f867710c7" - integrity sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.6.2" - core-js-compat "^3.38.0" - babel-plugin-polyfill-corejs3@^0.11.0: version "0.11.1" resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz#4e4e182f1bb37c7ba62e2af81d8dd09df31344f6" @@ -4512,7 +4525,7 @@ braces@^3.0.3, braces@~3.0.2: dependencies: fill-range "^7.1.1" -browserslist@^4.0.0, browserslist@^4.23.1, browserslist@^4.23.2, browserslist@^4.23.3, browserslist@^4.24.0, browserslist@^4.24.3: +browserslist@^4.0.0, browserslist@^4.23.1, browserslist@^4.23.2, browserslist@^4.23.3, browserslist@^4.24.0, browserslist@^4.24.3, browserslist@^4.24.4: version "4.24.4" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.4.tgz#c6b2865a3f08bcb860a0e827389003b9fe686e4b" integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A== @@ -5011,13 +5024,20 @@ copy-webpack-plugin@^13.0.0: serialize-javascript "^6.0.2" tinyglobby "^0.2.12" -core-js-compat@^3.38.0, core-js-compat@^3.38.1, core-js-compat@^3.40.0: +core-js-compat@^3.38.1: version "3.40.0" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.40.0.tgz#7485912a5a4a4315c2fdb2cbdc623e6881c88b38" integrity sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ== dependencies: browserslist "^4.24.3" +core-js-compat@^3.40.0: + version "3.41.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.41.0.tgz#4cdfce95f39a8f27759b667cf693d96e5dda3d17" + integrity sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A== + dependencies: + browserslist "^4.24.4" + core-js@^3.0.0, core-js@^3.38.1: version "3.40.0" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.40.0.tgz#2773f6b06877d8eda102fc42f828176437062476" @@ -5761,9 +5781,9 @@ ejs@^3.1.8: jake "^10.8.5" electron-to-chromium@^1.5.73: - version "1.5.112" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.112.tgz#8d3d95d4d5653836327890282c8eda5c6f26626d" - integrity sha512-oen93kVyqSb3l+ziUgzIOlWt/oOuy4zRmpwestMn4rhFWAoFJeFuCVte9F2fASjeZZo7l/Cif9TiyrdW4CwEMA== + version "1.5.120" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.120.tgz#ccfdd28e9795fb8c2221cefa2c9a071501c86247" + integrity sha512-oTUp3gfX1gZI+xfD2djr2rzQdHCwHzPQrrK0CD7WpTdF0nPdQ/INcRVjWgLdCT4a9W3jFObR9DAfsuyFQnI8CQ== emittery@^0.13.1: version "0.13.1" From 7e3866dd9a04e45a47afaead24b3468333c71611 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:01:50 +0000 Subject: [PATCH 07/78] Update dependency @types/node to v18.19.80 (#29524) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5a4be83d3e..7343e6d2ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3207,14 +3207,7 @@ dependencies: undici-types "~6.20.0" -"@types/node@18": - version "18.19.79" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.79.tgz#82fde7ac17809f4738a494b22273f0f7e6754f6e" - integrity sha512-90K8Oayimbctc5zTPHPfZloc/lGVs7f3phUAAMcTgEPtg8kKquGZDERC8K4vkBYkQQh48msiYUslYtxTWvqcAg== - dependencies: - undici-types "~5.26.4" - -"@types/node@^18.11.18": +"@types/node@18", "@types/node@^18.11.18": version "18.19.80" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.80.tgz#6d6008e8920dddcd23f9dd33da24684ef57d487c" integrity sha512-kEWeMwMeIvxYkeg1gTc01awpwLbfMRZXdIhwRcakd/KlK53jmRC26LqcbIt7fnAQTu5GzlnWmzA3H6+l1u6xxQ== From 98c691670e5cd3281ba0b7d1f2e101668f36212d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:02:08 +0000 Subject: [PATCH 08/78] Update dependency @vector-im/compound-design-tokens to v4.0.1 (#29525) 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 7343e6d2ed..8f9182e44e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3607,9 +3607,9 @@ integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== "@vector-im/compound-design-tokens@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-4.0.0.tgz#4922404f355f09fb753e6a91cc7efe4e661c9ff1" - integrity sha512-hFfLSKrGc58rPRp9JH1mkgw3moFEgpL8RQzyDESHErq7P1lUmlIuwKFTVfK5SbdFM5GvHp7nQaFpVmxUQ3Xp+w== + version "4.0.1" + resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-4.0.1.tgz#5c4ea7ad664d8e6206dc42e41649c80ef060a760" + integrity sha512-V4AsK1FVFxZ6DmmCoeAi8FyvE7ODMlXPWjqRGotcnVaoGNrDQrVz2ZGV85DCz5ISxB3iynYASe6OXsDVXT1zFA== "@vector-im/compound-web@^7.7.2": version "7.7.2" From c85e6d196d780b749c178d7d0450d78de2654d30 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:02:36 +0000 Subject: [PATCH 09/78] Update dependency @playwright/test to v1.51.0 (#29528) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 23 +---------------------- 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 28ffb3d2d8..a62686b89d 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js" }, "resolutions": { - "@playwright/test": "1.50.1", + "@playwright/test": "1.51.0", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "oidc-client-ts": "3.1.0", diff --git a/yarn.lock b/yarn.lock index 8f9182e44e..ed842f9734 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2201,14 +2201,7 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@playwright/test@1.50.1": - version "1.50.1" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.50.1.tgz#027d00ca77ec79688764eb765cfe9a688807bf0b" - integrity sha512-Jii3aBg+CEDpgnuDxEp/h7BimHcUTDlpEtce89xEumlJ5ef2hqepZ+PWp1DDpYC/VO9fmWVI1IlEaoI5fK9FXQ== - dependencies: - playwright "1.50.1" - -"@playwright/test@^1.50.1": +"@playwright/test@1.51.0", "@playwright/test@^1.50.1": version "1.51.0" resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.51.0.tgz#8d5c8400b465a0bfdbcf993e390ceecb903ea6d2" integrity sha512-dJ0dMbZeHhI+wb77+ljx/FeC8VBP6j/rj9OAojO08JI80wTZy6vRk9KvHKiDCUh4iMpEiseMgqRBIeW+eKX6RA== @@ -9607,25 +9600,11 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -playwright-core@1.50.1: - version "1.50.1" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.50.1.tgz#6a0484f1f1c939168f40f0ab3828c4a1592c4504" - integrity sha512-ra9fsNWayuYumt+NiM069M6OkcRb1FZSK8bgi66AtpFoWkg2+y0bJSNmkFrWhMbEBbVKC/EruAHH3g0zmtwGmQ== - playwright-core@1.51.0, playwright-core@^1.51.0: version "1.51.0" resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.51.0.tgz#bb23ea6bb6298242d088ae5e966ffcf8dc9827e8" integrity sha512-x47yPE3Zwhlil7wlNU/iktF7t2r/URR3VLbH6EknJd/04Qc/PSJ0EY3CMXipmglLG+zyRxW6HNo2EGbKLHPWMg== -playwright@1.50.1: - version "1.50.1" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.50.1.tgz#2f93216511d65404f676395bfb97b41aa052b180" - integrity sha512-G8rwsOQJ63XG6BbKj2w5rHeavFjy5zynBA9zsJMMtBoe/Uf757oG12NXz6e6OirF7RCrTVAKFXbLmn1RbL7Qaw== - dependencies: - playwright-core "1.50.1" - optionalDependencies: - fsevents "2.3.2" - playwright@1.51.0: version "1.51.0" resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.51.0.tgz#9ba154497ba62bc6dc199c58ee19295eb35a4707" From 6ed811d4c994b75c424b878766202d15b9410eac Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:02:39 +0000 Subject: [PATCH 10/78] Update typescript-eslint monorepo to v8.26.1 (#29527) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 102 +++++++++++++++++++++++++++--------------------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/yarn.lock b/yarn.lock index ed842f9734..d28c129469 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1593,9 +1593,9 @@ eslint-visitor-keys "^3.3.0" "@eslint-community/eslint-utils@^4.4.0": - version "4.4.1" - resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz#d1145bf2c20132d6400495d6df4bf59362fd9d56" - integrity sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA== + version "4.5.1" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz#b0fc7e06d0c94f801537fd4237edc2706d3b8e4c" + integrity sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w== dependencies: eslint-visitor-keys "^3.4.3" @@ -3424,29 +3424,29 @@ "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^8.19.0": - version "8.26.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.0.tgz#7e880faf91f89471c30c141951e15f0eb3a0599e" - integrity sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q== + version "8.26.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.1.tgz#3e48eb847924161843b092c87a9b65176b53782f" + integrity sha512-2X3mwqsj9Bd3Ciz508ZUtoQQYpOhU/kWoUqIf49H8Z0+Vbh6UF/y0OEYp0Q0axOGzaBGs7QxRwq0knSQ8khQNA== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.26.0" - "@typescript-eslint/type-utils" "8.26.0" - "@typescript-eslint/utils" "8.26.0" - "@typescript-eslint/visitor-keys" "8.26.0" + "@typescript-eslint/scope-manager" "8.26.1" + "@typescript-eslint/type-utils" "8.26.1" + "@typescript-eslint/utils" "8.26.1" + "@typescript-eslint/visitor-keys" "8.26.1" graphemer "^1.4.0" ignore "^5.3.1" natural-compare "^1.4.0" ts-api-utils "^2.0.1" "@typescript-eslint/parser@^8.19.0": - version "8.26.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.26.0.tgz#9b4d2198e89f64fb81e83167eedd89a827d843a9" - integrity sha512-mNtXP9LTVBy14ZF3o7JG69gRPBK/2QWtQd0j0oH26HcY/foyJJau6pNUez7QrM5UHnSvwlQcJXKsk0I99B9pOA== + version "8.26.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.26.1.tgz#0e2f915a497519fc43f52cf2ecbfa607ff56f72e" + integrity sha512-w6HZUV4NWxqd8BdeFf81t07d7/YV9s7TCWrQQbG5uhuvGUAW+fq1usZ1Hmz9UPNLniFnD8GLSsDpjP0hm1S4lQ== dependencies: - "@typescript-eslint/scope-manager" "8.26.0" - "@typescript-eslint/types" "8.26.0" - "@typescript-eslint/typescript-estree" "8.26.0" - "@typescript-eslint/visitor-keys" "8.26.0" + "@typescript-eslint/scope-manager" "8.26.1" + "@typescript-eslint/types" "8.26.1" + "@typescript-eslint/typescript-estree" "8.26.1" + "@typescript-eslint/visitor-keys" "8.26.1" debug "^4.3.4" "@typescript-eslint/scope-manager@8.23.0": @@ -3465,21 +3465,21 @@ "@typescript-eslint/types" "8.24.1" "@typescript-eslint/visitor-keys" "8.24.1" -"@typescript-eslint/scope-manager@8.26.0": - version "8.26.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.26.0.tgz#b06623fad54a3a77fadab5f652ef75ed3780b545" - integrity sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA== +"@typescript-eslint/scope-manager@8.26.1": + version "8.26.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.26.1.tgz#5e6ad0ac258ccf79462e91c3f43a3f1f7f31a6cc" + integrity sha512-6EIvbE5cNER8sqBu6V7+KeMZIC1664d2Yjt+B9EWUXrsyWpxx4lEZrmvxgSKRC6gX+efDL/UY9OpPZ267io3mg== dependencies: - "@typescript-eslint/types" "8.26.0" - "@typescript-eslint/visitor-keys" "8.26.0" + "@typescript-eslint/types" "8.26.1" + "@typescript-eslint/visitor-keys" "8.26.1" -"@typescript-eslint/type-utils@8.26.0": - version "8.26.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.26.0.tgz#9ee8cc98184b5f66326578de9c097edc89da6f68" - integrity sha512-ruk0RNChLKz3zKGn2LwXuVoeBcUMh+jaqzN461uMMdxy5H9epZqIBtYj7UiPXRuOpaALXGbmRuZQhmwHhaS04Q== +"@typescript-eslint/type-utils@8.26.1": + version "8.26.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.26.1.tgz#462f0bae09de72ac6e8e1af2ebe588c23224d7f8" + integrity sha512-Kcj/TagJLwoY/5w9JGEFV0dclQdyqw9+VMndxOJKtoFSjfZhLXhYjzsQEeyza03rwHx2vFEGvrJWJBXKleRvZg== dependencies: - "@typescript-eslint/typescript-estree" "8.26.0" - "@typescript-eslint/utils" "8.26.0" + "@typescript-eslint/typescript-estree" "8.26.1" + "@typescript-eslint/utils" "8.26.1" debug "^4.3.4" ts-api-utils "^2.0.1" @@ -3493,10 +3493,10 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.24.1.tgz#8777a024f3afc4ace5e48f9a804309c6dd38f95a" integrity sha512-9kqJ+2DkUXiuhoiYIUvIYjGcwle8pcPpdlfkemGvTObzgmYfJ5d0Qm6jwb4NBXP9W1I5tss0VIAnWFumz3mC5A== -"@typescript-eslint/types@8.26.0": - version "8.26.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.26.0.tgz#c4e93a8faf3a38a8d8adb007dc7834f1c89ee7bf" - integrity sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA== +"@typescript-eslint/types@8.26.1": + version "8.26.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.26.1.tgz#d5978721670cff263348d5062773389231a64132" + integrity sha512-n4THUQW27VmQMx+3P+B0Yptl7ydfceUj4ON/AQILAASwgYdZ/2dhfymRMh5egRUrvK5lSmaOm77Ry+lmXPOgBQ== "@typescript-eslint/typescript-estree@8.23.0": version "8.23.0" @@ -3526,13 +3526,13 @@ semver "^7.6.0" ts-api-utils "^2.0.1" -"@typescript-eslint/typescript-estree@8.26.0": - version "8.26.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.0.tgz#128972172005a7376e34ed2ecba4e29363b0cad1" - integrity sha512-tiJ1Hvy/V/oMVRTbEOIeemA2XoylimlDQ03CgPPNaHYZbpsc78Hmngnt+WXZfJX1pjQ711V7g0H7cSJThGYfPQ== +"@typescript-eslint/typescript-estree@8.26.1": + version "8.26.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.1.tgz#eb0e4ce31753683d83be53441a409fd5f0b34afd" + integrity sha512-yUwPpUHDgdrv1QJ7YQal3cMVBGWfnuCdKbXw1yyjArax3353rEJP1ZA+4F8nOlQ3RfS2hUN/wze3nlY+ZOhvoA== dependencies: - "@typescript-eslint/types" "8.26.0" - "@typescript-eslint/visitor-keys" "8.26.0" + "@typescript-eslint/types" "8.26.1" + "@typescript-eslint/visitor-keys" "8.26.1" debug "^4.3.4" fast-glob "^3.3.2" is-glob "^4.0.3" @@ -3540,15 +3540,15 @@ semver "^7.6.0" ts-api-utils "^2.0.1" -"@typescript-eslint/utils@8.26.0": - version "8.26.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.26.0.tgz#845d20ed8378a5594e6445f54e53b972aee7b3e6" - integrity sha512-2L2tU3FVwhvU14LndnQCA2frYC8JnPDVKyQtWFPf8IYFMt/ykEN1bPolNhNbCVgOmdzTlWdusCTKA/9nKrf8Ig== +"@typescript-eslint/utils@8.26.1": + version "8.26.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.26.1.tgz#54cc58469955f25577f659753b71a0e117a0539f" + integrity sha512-V4Urxa/XtSUroUrnI7q6yUTD3hDtfJ2jzVfeT3VK0ciizfK2q/zGC0iDh1lFMUZR8cImRrep6/q0xd/1ZGPQpg== dependencies: "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.26.0" - "@typescript-eslint/types" "8.26.0" - "@typescript-eslint/typescript-estree" "8.26.0" + "@typescript-eslint/scope-manager" "8.26.1" + "@typescript-eslint/types" "8.26.1" + "@typescript-eslint/typescript-estree" "8.26.1" "@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0": version "8.23.0" @@ -3586,12 +3586,12 @@ "@typescript-eslint/types" "8.24.1" eslint-visitor-keys "^4.2.0" -"@typescript-eslint/visitor-keys@8.26.0": - version "8.26.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.0.tgz#a4876216756c69130ea958df3b77222c2ad95290" - integrity sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg== +"@typescript-eslint/visitor-keys@8.26.1": + version "8.26.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.1.tgz#c5267fcc82795cf10280363023837deacad2647c" + integrity sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg== dependencies: - "@typescript-eslint/types" "8.26.0" + "@typescript-eslint/types" "8.26.1" eslint-visitor-keys "^4.2.0" "@ungap/structured-clone@^1.2.0": From 2a8b26d90ab04c3332f44676d286e59cb2a3b06f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:03:01 +0000 Subject: [PATCH 11/78] Update dependency @sentry/browser to v9.5.0 (#29529) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 68 +++++++++++++++++++++++++++---------------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/yarn.lock b/yarn.lock index d28c129469..17d89bc76c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2504,35 +2504,35 @@ resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== -"@sentry-internal/browser-utils@9.3.0": - version "9.3.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-9.3.0.tgz#345fa61da239186fa0ad6c7194d4a1f7df7646c7" - integrity sha512-G3z4HCUyb5nJe03EPUhWjnaHqMDt4mOTFJDNha3DGoB51lMYojpQI1Qo1u6bY4qkWVSO1c+HqOU0RVsXoAchtQ== +"@sentry-internal/browser-utils@9.6.0": + version "9.6.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-9.6.0.tgz#a3c0cc17f3562e40bca0085df5c4374f91aea44d" + integrity sha512-zk64wOc3jIHDeZ26sksRg4TfdpuAG6VP0/VZyXKX0f3QVi/jzb1VoVGfP7/BdtLo2Be1+60f7gGPyt/wvcJkqg== dependencies: - "@sentry/core" "9.3.0" + "@sentry/core" "9.6.0" -"@sentry-internal/feedback@9.3.0": - version "9.3.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-9.3.0.tgz#d60bcedd01907478b286512ce54f59682b2910e0" - integrity sha512-LQmIbQaATlN5QEwCD2Xt+7VKfwfR5W3dbn0jdF1x4hQFE/srdnOj60xMz/mj3tP5BxV552xJniGsyZ8lXHDb2A== +"@sentry-internal/feedback@9.6.0": + version "9.6.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-9.6.0.tgz#29ee78a66ff13513c84b0060a05fd8a5aa8ba361" + integrity sha512-SWGJC2eRl28KU7IHLDhHj1Kv0S9a6m1F+YYuve3ypIoYNkIyLJjy8EV6KM/Byepm+QhVTlFLh2bOQl8LUMjsNw== dependencies: - "@sentry/core" "9.3.0" + "@sentry/core" "9.6.0" -"@sentry-internal/replay-canvas@9.3.0": - version "9.3.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-9.3.0.tgz#b9432ab65152304b5f86f99efdeffcd92091f733" - integrity sha512-MhDMJeRGa55a0D541+OzTFMWwbabthhDGbAL90/NpappfyeBbAiktmCNl0BFTZuRbCGrC2m1LLCqHegCVKW4fQ== +"@sentry-internal/replay-canvas@9.6.0": + version "9.6.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-9.6.0.tgz#7ecadcfab98a1d9a7734e3edf3e0c37e1e3c1417" + integrity sha512-xNZPQvgNJ4CJSn+3tGaYao57yRqDLh79r2cf29P69jLSmKN5TT04thdMr/urCb9Ba9rcmG3uIAcJv8Y3SNm8gA== dependencies: - "@sentry-internal/replay" "9.3.0" - "@sentry/core" "9.3.0" + "@sentry-internal/replay" "9.6.0" + "@sentry/core" "9.6.0" -"@sentry-internal/replay@9.3.0": - version "9.3.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-9.3.0.tgz#c846cf77cd8deefda84316f1a938bbeae5861564" - integrity sha512-ZkH+Gahn89JygpuiFn26ZgAqJXHtnr+HjfQ2ONOFoWQHNH6X5wk75UTma55aYk1d8VcBPFoU6WjFhZoQ55SV1g== +"@sentry-internal/replay@9.6.0": + version "9.6.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-9.6.0.tgz#eb8f75a66bbea708c7a668e452c64808f8254ed7" + integrity sha512-+F9xcc4XIMzLgItis3kScbUZgOPrtMIwRL61gzSoYFyYAAp+in3Kq9vJ1eY+LcQKKnJ0DgzUFXU8YVs3yRTWxg== dependencies: - "@sentry-internal/browser-utils" "9.3.0" - "@sentry/core" "9.3.0" + "@sentry-internal/browser-utils" "9.6.0" + "@sentry/core" "9.6.0" "@sentry/babel-plugin-component-annotate@3.2.1": version "3.2.1" @@ -2540,15 +2540,15 @@ integrity sha512-tUp2e+CERpRFzTftjPxt7lg4BF0R3K+wGfeJyIqrc0tbJ2y6duT8OD0ArWoOi1g8xQ73NDn1/mEeS8pC+sbjTQ== "@sentry/browser@^9.0.0": - version "9.3.0" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-9.3.0.tgz#270e93c0f1bc10abb6651255bcaa30ba21c4928a" - integrity sha512-yPwWWQo/hpN63p0NGmk/Dd1Fx5CQRWNMfuV7dtfPBtg3vRjDecA9OLyK29AqK5h3Fl8FuJOyOqB87CvtXUqh5g== + version "9.6.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-9.6.0.tgz#220235296ffde3edbb3fffe39d8f9ca4413763f8" + integrity sha512-8tOmdbaEUVcousqnqhQ0bx+IG8kXVYV1dXgSvFQUaCsm0TEWMosoN25cSridImTN9B0M6YBY0ONx1cdKdRmw/w== dependencies: - "@sentry-internal/browser-utils" "9.3.0" - "@sentry-internal/feedback" "9.3.0" - "@sentry-internal/replay" "9.3.0" - "@sentry-internal/replay-canvas" "9.3.0" - "@sentry/core" "9.3.0" + "@sentry-internal/browser-utils" "9.6.0" + "@sentry-internal/feedback" "9.6.0" + "@sentry-internal/replay" "9.6.0" + "@sentry-internal/replay-canvas" "9.6.0" + "@sentry/core" "9.6.0" "@sentry/bundler-plugin-core@3.2.1": version "3.2.1" @@ -2618,10 +2618,10 @@ "@sentry/cli-win32-i686" "2.42.2" "@sentry/cli-win32-x64" "2.42.2" -"@sentry/core@9.3.0": - version "9.3.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-9.3.0.tgz#d5a1ab7deecd95f86316e658129d3874557cbfd9" - integrity sha512-SxQ4z7wTkfguvYb2ctNEMU9kVAbhl9ymfjhLnrvtygTwL5soLqAKdco/lX/4P9K9Osgb2Dl6urQWRl+AhzKVbQ== +"@sentry/core@9.6.0": + version "9.6.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-9.6.0.tgz#15e1a43d4e6371fac66c2d83233aeb09c4cea035" + integrity sha512-t51h6HKlPYW3TfeM09mZ6uDd95A7lgYpD5lUV54ilBA3TefS+M9I32MKwAW7yHzzWs0WQxOdm56eoDBOmRDpHQ== "@sentry/webpack-plugin@^3.0.0": version "3.2.1" From f3654e45d6647234736e03161089dfd76058f710 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:04:34 +0000 Subject: [PATCH 12/78] Update dependency stylelint to v16.16.0 (#29530) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/yarn.lock b/yarn.lock index 17d89bc76c..686accf0a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6545,7 +6545,7 @@ fflate@^0.4.8: resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae" integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA== -file-entry-cache@^10.0.6: +file-entry-cache@^10.0.7: version "10.0.7" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-10.0.7.tgz#e0ac34d4b8c44bea8a0a27ceb4dae982f2d32749" integrity sha512-txsf5fu3anp2ff3+gOJJzRImtrtm/oa9tYLN0iTuINZ++EyVR/nRrg2fKYwvG/pXDofcrvvb0scEbX3NyW/COw== @@ -7087,9 +7087,9 @@ hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: react-is "^16.7.0" hookified@^1.7.1: - version "1.7.1" - resolved "https://registry.yarnpkg.com/hookified/-/hookified-1.7.1.tgz#b08228173e06e9e8767bae1dffb216b8c6171b41" - integrity sha512-OXcdHsXeOiD7OJ5zvWj8Oy/6RCdLwntAX+wUrfemNcMGn6sux4xbEHi2QXwqePYhjQ/yvxxq2MvCRirdlHscBw== + version "1.8.1" + resolved "https://registry.yarnpkg.com/hookified/-/hookified-1.8.1.tgz#74a8c97d36e5f8004d230ee2156a607cc84c358c" + integrity sha512-GrO2l93P8xCWBSTBX9l2BxI78VU/MAAYag+pG8curS3aBGy0++ZlxrQ7PdUOUVMbn5BwkGb6+eRrnf43ipnFEA== hosted-git-info@^2.1.4: version "2.8.9" @@ -8416,9 +8416,9 @@ keyv@^4.5.3: json-buffer "3.0.1" keyv@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-5.3.1.tgz#d3acebeecedafd4bf2b929e8866bcd79db071f1e" - integrity sha512-13hQT2q2VIwOoaJdJa7nY3J8UVbYtMTJFHnwm9LI+SaQRfUiM6Em9KZeOVTCKbMnGcRIL3NSUFpAdjZCq24nLQ== + version "5.3.2" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-5.3.2.tgz#32edd461b51d44d42926eb72946236d79c71ae78" + integrity sha512-Lji2XRxqqa5Wg+CHLVfFKBImfJZ4pCSccu9eVWK6w4c2SDFLd8JAn1zqTuSFnsxb7ope6rMsnIHfp+eBbRBRZQ== dependencies: "@keyv/serialize" "^1.0.3" @@ -9104,11 +9104,16 @@ nan@^2.19.0, nan@^2.20.0: resolved "https://registry.yarnpkg.com/nan/-/nan-2.22.2.tgz#6b504fd029fb8f38c0990e52ad5c26772fdacfbb" integrity sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ== -nanoid@^3.3.7, nanoid@^3.3.8: +nanoid@^3.3.7: version "3.3.8" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== +nanoid@^3.3.8: + version "3.3.10" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.10.tgz#7bc882237698ef787d5cbba109e3b0168ba6e7b1" + integrity sha512-vSJJTG+t/dIKAUhUDw/dLdZ9s//5OxcHqLaDWWrW4Cdq7o6tdLIczUkMXt2MBNmk6sJRZBZRXVixs7URY1CmIg== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -11751,9 +11756,9 @@ stylelint-value-no-unknown-custom-properties@^6.0.1: resolve "^1.22.8" stylelint@^16.13.0: - version "16.15.0" - resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-16.15.0.tgz#a561cd50ad468cc4898f5c57cc1f223287fdea59" - integrity sha512-OK6Rs7EPdcdmjqiDycadZY4fw3f5/TC1X6/tGjnF3OosbwCeNs7nG+79MCAtjEg7ckwqTJTsku08e0Rmaz5nUw== + version "16.16.0" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-16.16.0.tgz#ebecf27becd277aaf752e75de324023a1e13e486" + integrity sha512-40X5UOb/0CEFnZVEHyN260HlSSUxPES+arrUphOumGWgXERHfwCD0kNBVILgQSij8iliYVwlc0V7M5bcLP9vPg== dependencies: "@csstools/css-parser-algorithms" "^3.0.4" "@csstools/css-tokenizer" "^3.0.3" @@ -11768,7 +11773,7 @@ stylelint@^16.13.0: debug "^4.3.7" fast-glob "^3.3.3" fastest-levenshtein "^1.0.16" - file-entry-cache "^10.0.6" + file-entry-cache "^10.0.7" global-modules "^2.0.0" globby "^11.1.0" globjoin "^0.1.4" From e4f9c650ee1019b33f0f78017fc22325426c302b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 16:12:40 +0000 Subject: [PATCH 13/78] Update react monorepo (#28905) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 45 +++++++++++++++++++-------------------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/yarn.lock b/yarn.lock index 686accf0a8..390c172af9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -132,7 +132,7 @@ lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-create-class-features-plugin@^7.25.9": +"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.25.9": version "7.26.9" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.26.9.tgz#d6f83e3039547fbb39967e78043cd3c8b7820c71" integrity sha512-ubbUqCofvxPRurw5L8WTsCLSkQiVpov4Qx0WMA+jUN+nXBK8ADPlJO1grkFw5CWKC5+sZSOfuGMdX1aI1iT9Sg== @@ -290,14 +290,7 @@ dependencies: "@babel/types" "^7.25.8" -"@babel/parser@^7.24.4": - version "7.26.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.3.tgz#8c51c5db6ddf08134af1ddbacf16aaab48bac234" - integrity sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA== - dependencies: - "@babel/types" "^7.26.3" - -"@babel/parser@^7.26.10", "@babel/parser@^7.26.9": +"@babel/parser@^7.24.4", "@babel/parser@^7.26.10", "@babel/parser@^7.26.9": version "7.26.10" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.10.tgz#e9bdb82f14b97df6569b0b038edd436839c57749" integrity sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA== @@ -359,6 +352,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.25.9" +"@babel/plugin-proposal-private-methods@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz#5209de7d213457548a98436fa2882f52f4be6bea" + integrity sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": version "7.21.0-placeholder-for-preset-env.2" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" @@ -1137,7 +1138,7 @@ debug "^4.3.1" globals "^11.1.0" -"@babel/traverse@^7.25.9", "@babel/traverse@^7.26.10": +"@babel/traverse@^7.25.9", "@babel/traverse@^7.26.10", "@babel/traverse@^7.26.5", "@babel/traverse@^7.26.9": version "7.26.10" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.10.tgz#43cca33d76005dbaa93024fae536cc1946a4c380" integrity sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A== @@ -1150,7 +1151,7 @@ debug "^4.3.1" globals "^11.1.0" -"@babel/traverse@^7.26.5", "@babel/traverse@^7.26.8", "@babel/traverse@^7.26.9": +"@babel/traverse@^7.26.8": version "7.26.9" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.9.tgz#4398f2394ba66d05d988b2ad13c219a2c857461a" integrity sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg== @@ -1188,14 +1189,6 @@ "@babel/helper-string-parser" "^7.25.9" "@babel/helper-validator-identifier" "^7.25.9" -"@babel/types@^7.26.3": - version "7.26.7" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.7.tgz#5e2b89c0768e874d4d061961f3a5a153d71dc17a" - integrity sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg== - dependencies: - "@babel/helper-string-parser" "^7.25.9" - "@babel/helper-validator-identifier" "^7.25.9" - "@babel/types@^7.4.4": version "7.26.9" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.9.tgz#08b43dec79ee8e682c2ac631c010bdcac54a21ce" @@ -6126,21 +6119,21 @@ eslint-plugin-matrix-org@^2.0.2: integrity sha512-YjVQ0qunzVV34tpUchLWhOrOalGfRLm0tclS4dPYnXS8Ui+p12o/YtRHt+26Mg5tJ0QH76HsGC0LJKLVLNoqfg== eslint-plugin-react-compiler@^19.0.0-beta-df7b47d-20241124: - version "19.0.0-beta-df7b47d-20241124" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-compiler/-/eslint-plugin-react-compiler-19.0.0-beta-df7b47d-20241124.tgz#468751d3a8a6781189405ee56b39b80545306df8" - integrity sha512-82PfnllC8jP/68KdLAbpWuYTcfmtGLzkqy2IW85WopKMTr+4rdQpp+lfliQ/QE79wWrv/dRoADrk3Pdhq25nTw== + version "19.0.0-beta-e552027-20250112" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-compiler/-/eslint-plugin-react-compiler-19.0.0-beta-e552027-20250112.tgz#f4ad9cebe47615ebf6097a8084a30d761ee164f4" + integrity sha512-VjkIXHouCYyJHgk5HmZ1LH+fAK5CX+ULRX9iNYtwYJ+ljbivFhIT+JJyxNT/USQpCeS2Dt5ahjFeeMv0RRwTww== dependencies: "@babel/core" "^7.24.4" "@babel/parser" "^7.24.4" - "@babel/plugin-transform-private-methods" "^7.25.9" + "@babel/plugin-proposal-private-methods" "^7.18.6" hermes-parser "^0.25.1" zod "^3.22.4" zod-validation-error "^3.0.3" eslint-plugin-react-hooks@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0.tgz#72e2eefbac4b694f5324154619fee44f5f60f101" - integrity sha512-hIOwI+5hYGpJEc4uPRmz2ulCjAGD/N13Lukkh8cLV0i2IRk/bdZDYjgLVHj+U9Z704kLIdIO6iueGvxNur0sgw== + version "5.2.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz#1be0080901e6ac31ce7971beed3d3ec0a423d9e3" + integrity sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg== eslint-plugin-react@^7.28.0: version "7.37.4" From 4fa540962a503d4c6682975bd9443f77dedaf52e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 16:16:41 +0000 Subject: [PATCH 14/78] Update robinraju/release-downloader digest to daf26c5 (#29532) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/actions/download-verify-element-tarball/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/download-verify-element-tarball/action.yml b/.github/actions/download-verify-element-tarball/action.yml index 75d325c543..a64bc3241b 100644 --- a/.github/actions/download-verify-element-tarball/action.yml +++ b/.github/actions/download-verify-element-tarball/action.yml @@ -11,7 +11,7 @@ runs: using: composite steps: - name: Download release tarball - uses: robinraju/release-downloader@a96f54c1b5f5e09e47d9504526e96febd949d4c2 # v1 + uses: robinraju/release-downloader@daf26c55d821e836577a15f77d86ddc078948b05 # v1 with: tag: ${{ inputs.tag }} fileName: element-*.tar.gz* From f59af3786e9de82dc94585d55648e8f4050c0e27 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 18 Mar 2025 17:54:32 +0000 Subject: [PATCH 15/78] Simplified Sliding Sync (#28515) * Experimental SSS Working branch to get SSS functional on element-web. Requires https://github.com/matrix-org/matrix-js-sdk/pull/4400 * Adjust tests to use new behaviour * Remove well-known proxy URL lookup; always use native This is actually required for SSS because otherwise it would use the proxy over native support. * Linting * Debug logging * Control the race condition when swapping between rooms * Dont' filter by space as synapse doesn't support it * Remove SS code related to registering lists and managing ranges - Update the spidering code to spider all the relevant lists. - Add canonical alias to the required_state to allow room name calcs to work. Room sort order is busted because we don't yet look at `bump_stamp`. * User bumpStamp if it is present * Drop initial room load from 20 per list to 10 * Half the batch size to trickle more quickly * Prettier * prettier on tests too * Remove proxy URL & unused import * Hopefully fix tests to assert what the behaviour is supposed to be * Move the singleton to the manager tyo fix import loop * Very well, code, I will remove you Why were you there in the first place? * Strip out more unused stuff * Fix playwright test Seems like this lack of order updating unless a room is selected was just always a bug with both regular and non-sliding sync. I have no idea how the test passed on develop because it won't run. * Fix test to do maybe what it was supposed to do... possibly? * Remove test for old pre-simplified sliding sync behaviour * Unused import * Remove sliding sync proxy & test I was wrong about what this test was asserting, it was suposed to assert that notification dots aren't shown (because SS didn't support them somehow I guess) but they are fine in SSS so the test is just no longer relevant. * Remove now pointless credentials * Remove subscription removal as SSS doesn't do that * Update tests * add test * Switch to new labs flag & break if old labs flag is enabled * Remove unused import & fix test * Fix other test * Remove name & description from old labs flag as they're not displayed anywhere so not useful * Remove old sliding sync option by making it not a feature * Add back unread nindicator test but inverted and minus the bit about disabling notification which surely would have defeated the original point anyway? * Reinstate test for room_subscriptions ...and also make tests actually use sliding sync * Use UserFriendlyError * Remove empty constructor * Remove unrelated changes * Unused import * Fix import * Avoid moving import --------- Co-authored-by: Kegan Dougal <7190048+kegsay@users.noreply.github.com> --- .../e2e/sliding-sync/sliding-sync.spec.ts | 133 ++---- src/MatrixClientPeg.ts | 6 + src/SlidingSyncManager.ts | 370 +++++++++-------- src/Unread.ts | 7 - src/components/views/rooms/RoomSublist.tsx | 33 +- src/hooks/useSlidingSyncRoomSearch.ts | 82 ---- src/i18n/strings/en_EN.json | 1 + src/settings/Settings.tsx | 16 +- .../controllers/SlidingSyncController.ts | 7 +- src/stores/MemberListStore.ts | 4 +- src/stores/RoomViewStore.tsx | 15 +- src/stores/room-list/RoomListStore.ts | 17 +- src/stores/room-list/SlidingRoomListStore.ts | 391 ------------------ .../algorithms/tag-sorting/RecentAlgorithm.ts | 6 + test/test-utils/test-utils.ts | 1 + test/unit-tests/SlidingSyncManager-test.ts | 241 ++++------- .../roomlist/RoomListViewModel-test.tsx | 2 +- .../hooks/useSlidingSyncRoomSearch-test.tsx | 84 ---- .../unit-tests/stores/MemberListStore-test.ts | 2 +- test/unit-tests/stores/RoomViewStore-test.ts | 26 +- .../room-list/SlidingRoomListStore-test.ts | 341 --------------- 21 files changed, 367 insertions(+), 1418 deletions(-) delete mode 100644 src/hooks/useSlidingSyncRoomSearch.ts delete mode 100644 src/stores/room-list/SlidingRoomListStore.ts delete mode 100644 test/unit-tests/hooks/useSlidingSyncRoomSearch-test.tsx delete mode 100644 test/unit-tests/stores/room-list/SlidingRoomListStore-test.ts diff --git a/playwright/e2e/sliding-sync/sliding-sync.spec.ts b/playwright/e2e/sliding-sync/sliding-sync.spec.ts index bf992a0edd..118bd4585e 100644 --- a/playwright/e2e/sliding-sync/sliding-sync.spec.ts +++ b/playwright/e2e/sliding-sync/sliding-sync.spec.ts @@ -7,47 +7,15 @@ Please see LICENSE files in the repository root for full details. */ import { type Page, type Request } from "@playwright/test"; -import { GenericContainer, type StartedTestContainer, Wait } from "testcontainers"; import { test as base, expect } from "../../element-web-test"; import type { ElementAppPage } from "../../pages/ElementAppPage"; import type { Bot } from "../../pages/bot"; const test = base.extend<{ - slidingSyncProxy: StartedTestContainer; testRoom: { roomId: string; name: string }; joinedBot: Bot; }>({ - slidingSyncProxy: async ({ logger, network, postgres, page, homeserver }, use, testInfo) => { - const container = await new GenericContainer("ghcr.io/matrix-org/sliding-sync:v0.99.3") - .withNetwork(network) - .withExposedPorts(8008) - .withLogConsumer(logger.getConsumer("sliding-sync-proxy")) - .withWaitStrategy(Wait.forHttp("/client/server.json", 8008)) - .withEnvironment({ - SYNCV3_SECRET: "bwahahaha", - SYNCV3_DB: `user=${postgres.getUsername()} dbname=postgres password=${postgres.getPassword()} host=postgres sslmode=disable`, - SYNCV3_SERVER: `http://homeserver:8008`, - }) - .start(); - - const proxyAddress = `http://${container.getHost()}:${container.getMappedPort(8008)}`; - await page.addInitScript((proxyAddress) => { - window.localStorage.setItem( - "mx_local_settings", - JSON.stringify({ - feature_sliding_sync_proxy_url: proxyAddress, - }), - ); - window.localStorage.setItem("mx_labs_feature_feature_sliding_sync", "true"); - }, proxyAddress); - await use(container); - await container.stop(); - }, - // Ensure slidingSyncProxy is set up before the user fixture as it relies on an init script - credentials: async ({ slidingSyncProxy, credentials }, use) => { - await use(credentials); - }, testRoom: async ({ user, app }, use) => { const name = "Test Room"; const roomId = await app.client.createRoom({ name }); @@ -82,6 +50,14 @@ test.describe("Sliding Sync", () => { }); }; + test.use({ + config: { + features: { + feature_simplified_sliding_sync: true, + }, + }, + }); + // Load the user fixture for all tests test.beforeEach(({ user }) => {}); @@ -188,15 +164,7 @@ test.describe("Sliding Sync", () => { ).not.toBeAttached(); }); - test("should not show unread indicators", async ({ page, app, joinedBot: bot, testRoom }) => { - // TODO: for now. Later we should. - - // disable notifs in this room (TODO: CS API call?) - const locator = page.getByRole("treeitem", { name: "Test Room" }); - await locator.hover(); - await locator.getByRole("button", { name: "Notification options" }).click(); - await page.getByRole("menuitemradio", { name: "Mute room" }).click(); - + test("should show unread indicators", async ({ page, app, joinedBot: bot, testRoom }) => { // create a new room so we know when the message has been received as it'll re-shuffle the room list await app.client.createRoom({ name: "Dummy" }); @@ -207,9 +175,7 @@ test.describe("Sliding Sync", () => { // wait for this message to arrive, tell by the room list resorting await checkOrder(["Test Room", "Dummy"], page); - await expect( - page.getByRole("treeitem", { name: "Test Room" }).locator(".mx_NotificationBadge"), - ).not.toBeAttached(); + await expect(page.getByRole("treeitem", { name: "Test Room" }).locator(".mx_NotificationBadge")).toBeAttached(); }); test("should update user settings promptly", async ({ page, app }) => { @@ -221,6 +187,37 @@ test.describe("Sliding Sync", () => { await expect(locator.locator(".mx_ToggleSwitch_on")).toBeAttached(); }); + test("should send subscribe_rooms on room switch if room not already subscribed", async ({ page, app }) => { + // create rooms and check room names are correct + const roomIds: string[] = []; + for (const fruit of ["Apple", "Pineapple", "Orange"]) { + const id = await app.client.createRoom({ name: fruit }); + roomIds.push(id); + await expect(page.getByRole("treeitem", { name: fruit })).toBeVisible(); + } + const [roomAId, roomPId] = roomIds; + + const matchRoomSubRequest = (subRoomId: string) => (request: Request) => { + if (!request.url().includes("/sync")) return false; + const body = request.postDataJSON(); + return body.room_subscriptions?.[subRoomId]; + }; + + // Select the Test Room and wait for playwright to get the request + const [request] = await Promise.all([ + page.waitForRequest(matchRoomSubRequest(roomAId)), + page.getByRole("treeitem", { name: "Apple", exact: true }).click(), + ]); + const roomSubscriptions = request.postDataJSON().room_subscriptions; + expect(roomSubscriptions, "room_subscriptions is object").toBeDefined(); + + // Switch to another room and wait for playwright to get the request + await Promise.all([ + page.waitForRequest(matchRoomSubRequest(roomPId)), + page.getByRole("treeitem", { name: "Pineapple", exact: true }).click(), + ]); + }); + test("should show and be able to accept/reject/rescind invites", async ({ page, app, @@ -361,52 +358,4 @@ test.describe("Sliding Sync", () => { // ensure the reply-to does not disappear await expect(page.locator(".mx_ReplyPreview")).toBeVisible(); }); - - test("should send unsubscribe_rooms for every room switch", async ({ page, app }) => { - // create rooms and check room names are correct - const roomIds: string[] = []; - for (const fruit of ["Apple", "Pineapple", "Orange"]) { - const id = await app.client.createRoom({ name: fruit }); - roomIds.push(id); - await expect(page.getByRole("treeitem", { name: fruit })).toBeVisible(); - } - const [roomAId, roomPId, roomOId] = roomIds; - - const matchRoomSubRequest = (subRoomId: string) => (request: Request) => { - if (!request.url().includes("/sync")) return false; - const body = request.postDataJSON(); - return body.txn_id && body.room_subscriptions?.[subRoomId]; - }; - const matchRoomUnsubRequest = (unsubRoomId: string) => (request: Request) => { - if (!request.url().includes("/sync")) return false; - const body = request.postDataJSON(); - return ( - body.txn_id && body.unsubscribe_rooms?.includes(unsubRoomId) && !body.room_subscriptions?.[unsubRoomId] - ); - }; - - // Select the Test Room and wait for playwright to get the request - const [request] = await Promise.all([ - page.waitForRequest(matchRoomSubRequest(roomAId)), - page.getByRole("treeitem", { name: "Apple", exact: true }).click(), - ]); - const roomSubscriptions = request.postDataJSON().room_subscriptions; - expect(roomSubscriptions, "room_subscriptions is object").toBeDefined(); - - // Switch to another room and wait for playwright to get the request - await Promise.all([ - page.waitForRequest(matchRoomSubRequest(roomPId)), - page.waitForRequest(matchRoomUnsubRequest(roomAId)), - page.getByRole("treeitem", { name: "Pineapple", exact: true }).click(), - ]); - - // And switch to even another room and wait for playwright to get the request - await Promise.all([ - page.waitForRequest(matchRoomSubRequest(roomOId)), - page.waitForRequest(matchRoomUnsubRequest(roomPId)), - page.getByRole("treeitem", { name: "Orange", exact: true }).click(), - ]); - - // TODO: Add tests for encrypted rooms - }); }); diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 9682b41800..288878cddc 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -299,6 +299,12 @@ class MatrixClientPegClass implements IMatrixClientPeg { opts.threadSupport = true; if (SettingsStore.getValue("feature_sliding_sync")) { + throw new UserFriendlyError("sliding_sync_legacy_no_longer_supported"); + } + + // If the user has enabled the labs feature for sliding sync, set it up + // otherwise check if the feature is supported + if (SettingsStore.getValue("feature_simplified_sliding_sync")) { opts.slidingSync = await SlidingSyncManager.instance.setup(this.matrixClient); } else { SlidingSyncManager.instance.checkSupport(this.matrixClient); diff --git a/src/SlidingSyncManager.ts b/src/SlidingSyncManager.ts index adfca7c3c3..88b839312d 100644 --- a/src/SlidingSyncManager.ts +++ b/src/SlidingSyncManager.ts @@ -36,45 +36,51 @@ Please see LICENSE files in the repository root for full details. * list ops) */ -import { type MatrixClient, EventType, AutoDiscovery, Method, timeoutSignal } from "matrix-js-sdk/src/matrix"; +import { type MatrixClient, ClientEvent, EventType, type Room } from "matrix-js-sdk/src/matrix"; import { type MSC3575Filter, type MSC3575List, + type MSC3575SlidingSyncResponse, MSC3575_STATE_KEY_LAZY, MSC3575_STATE_KEY_ME, MSC3575_WILDCARD, SlidingSync, + SlidingSyncEvent, + SlidingSyncState, } from "matrix-js-sdk/src/sliding-sync"; import { logger } from "matrix-js-sdk/src/logger"; import { defer, sleep } from "matrix-js-sdk/src/utils"; -import SettingsStore from "./settings/SettingsStore"; -import SlidingSyncController from "./settings/controllers/SlidingSyncController"; - // how long to long poll for const SLIDING_SYNC_TIMEOUT_MS = 20 * 1000; +// The state events we will get for every single room/space/old room/etc +// This list is only augmented when a direct room subscription is made. (e.g you view a room) +const REQUIRED_STATE_LIST = [ + [EventType.RoomJoinRules, ""], // the public icon on the room list + [EventType.RoomAvatar, ""], // any room avatar + [EventType.RoomCanonicalAlias, ""], // for room name calculations + [EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead + [EventType.RoomEncryption, ""], // lets rooms be configured for E2EE correctly + [EventType.RoomCreate, ""], // for isSpaceRoom checks + [EventType.SpaceChild, MSC3575_WILDCARD], // all space children + [EventType.SpaceParent, MSC3575_WILDCARD], // all space parents + [EventType.RoomMember, MSC3575_STATE_KEY_ME], // lets the client calculate that we are in fact in the room +]; + // the things to fetch when a user clicks on a room const DEFAULT_ROOM_SUBSCRIPTION_INFO = { timeline_limit: 50, // missing required_state which will change depending on the kind of room include_old_rooms: { timeline_limit: 0, - required_state: [ - // state needed to handle space navigation and tombstone chains - [EventType.RoomCreate, ""], - [EventType.RoomTombstone, ""], - [EventType.SpaceChild, MSC3575_WILDCARD], - [EventType.SpaceParent, MSC3575_WILDCARD], - [EventType.RoomMember, MSC3575_STATE_KEY_ME], - ], + required_state: REQUIRED_STATE_LIST, }, }; // lazy load room members so rooms like Matrix HQ don't take forever to load const UNENCRYPTED_SUBSCRIPTION_NAME = "unencrypted"; const UNENCRYPTED_SUBSCRIPTION = { required_state: [ - [MSC3575_WILDCARD, MSC3575_WILDCARD], // all events [EventType.RoomMember, MSC3575_STATE_KEY_ME], // except for m.room.members, get our own membership [EventType.RoomMember, MSC3575_STATE_KEY_LAZY], // ...and lazy load the rest. ], @@ -90,6 +96,72 @@ const ENCRYPTED_SUBSCRIPTION = { ...DEFAULT_ROOM_SUBSCRIPTION_INFO, }; +// the complete set of lists made in SSS. The manager will spider all of these lists depending +// on the count for each one. +const sssLists: Record = { + spaces: { + ranges: [[0, 10]], + timeline_limit: 0, // we don't care about the most recent message for spaces + required_state: REQUIRED_STATE_LIST, + include_old_rooms: { + timeline_limit: 0, + required_state: REQUIRED_STATE_LIST, + }, + filters: { + room_types: ["m.space"], + }, + }, + invites: { + ranges: [[0, 10]], + timeline_limit: 1, // most recent message display + required_state: REQUIRED_STATE_LIST, + include_old_rooms: { + timeline_limit: 0, + required_state: REQUIRED_STATE_LIST, + }, + filters: { + is_invite: true, + }, + }, + favourites: { + ranges: [[0, 10]], + timeline_limit: 1, // most recent message display + required_state: REQUIRED_STATE_LIST, + include_old_rooms: { + timeline_limit: 0, + required_state: REQUIRED_STATE_LIST, + }, + filters: { + tags: ["m.favourite"], + }, + }, + dms: { + ranges: [[0, 10]], + timeline_limit: 1, // most recent message display + required_state: REQUIRED_STATE_LIST, + include_old_rooms: { + timeline_limit: 0, + required_state: REQUIRED_STATE_LIST, + }, + filters: { + is_dm: true, + is_invite: false, + // If a DM has a Favourite & Low Prio tag then it'll be shown in those lists instead + not_tags: ["m.favourite", "m.lowpriority"], + }, + }, + untagged: { + // SSS will dupe suppress invites/dms from here, so we don't need "not dms, not invites" + ranges: [[0, 10]], + timeline_limit: 1, // most recent message display + required_state: REQUIRED_STATE_LIST, + include_old_rooms: { + timeline_limit: 0, + required_state: REQUIRED_STATE_LIST, + }, + }, +}; + export type PartialSlidingSyncRequest = { filters?: MSC3575Filter; sort?: string[]; @@ -103,6 +175,8 @@ export type PartialSlidingSyncRequest = { * sync options and code. */ export class SlidingSyncManager { + public static serverSupportsSlidingSync: boolean; + public static readonly ListSpaces = "space_list"; public static readonly ListSearch = "search_list"; private static readonly internalInstance = new SlidingSyncManager(); @@ -116,48 +190,17 @@ export class SlidingSyncManager { return SlidingSyncManager.internalInstance; } - public configure(client: MatrixClient, proxyUrl: string): SlidingSync { + private configure(client: MatrixClient, proxyUrl: string): SlidingSync { this.client = client; + // create the set of lists we will use. + const lists = new Map(); + for (const listName in sssLists) { + lists.set(listName, sssLists[listName]); + } // by default use the encrypted subscription as that gets everything, which is a safer // default than potentially missing member events. - this.slidingSync = new SlidingSync( - proxyUrl, - new Map(), - ENCRYPTED_SUBSCRIPTION, - client, - SLIDING_SYNC_TIMEOUT_MS, - ); + this.slidingSync = new SlidingSync(proxyUrl, lists, ENCRYPTED_SUBSCRIPTION, client, SLIDING_SYNC_TIMEOUT_MS); this.slidingSync.addCustomSubscription(UNENCRYPTED_SUBSCRIPTION_NAME, UNENCRYPTED_SUBSCRIPTION); - // set the space list - this.slidingSync.setList(SlidingSyncManager.ListSpaces, { - ranges: [[0, 20]], - sort: ["by_name"], - slow_get_all_rooms: true, - timeline_limit: 0, - required_state: [ - [EventType.RoomJoinRules, ""], // the public icon on the room list - [EventType.RoomAvatar, ""], // any room avatar - [EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead - [EventType.RoomEncryption, ""], // lets rooms be configured for E2EE correctly - [EventType.RoomCreate, ""], // for isSpaceRoom checks - [EventType.SpaceChild, MSC3575_WILDCARD], // all space children - [EventType.SpaceParent, MSC3575_WILDCARD], // all space parents - [EventType.RoomMember, MSC3575_STATE_KEY_ME], // lets the client calculate that we are in fact in the room - ], - include_old_rooms: { - timeline_limit: 0, - required_state: [ - [EventType.RoomCreate, ""], - [EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead - [EventType.SpaceChild, MSC3575_WILDCARD], // all space children - [EventType.SpaceParent, MSC3575_WILDCARD], // all space parents - [EventType.RoomMember, MSC3575_STATE_KEY_ME], // lets the client calculate that we are in fact in the room - ], - }, - filters: { - room_types: ["m.space"], - }, - }); this.configureDefer.resolve(); return this.slidingSync; } @@ -220,99 +263,113 @@ export class SlidingSyncManager { return this.slidingSync!.getListParams(listKey)!; } - public async setRoomVisible(roomId: string, visible: boolean): Promise { + /** + * Announces that the user has chosen to view the given room and that room will now + * be displayed, so it should have more state loaded. + * @param roomId The room to set visible + */ + public async setRoomVisible(roomId: string): Promise { await this.configureDefer.promise; const subscriptions = this.slidingSync!.getRoomSubscriptions(); - if (visible) { - subscriptions.add(roomId); - } else { - subscriptions.delete(roomId); - } + if (subscriptions.has(roomId)) return; + + subscriptions.add(roomId); + const room = this.client?.getRoom(roomId); - let shouldLazyLoad = !(await this.client?.getCrypto()?.isEncryptionEnabledInRoom(roomId)); - if (!room) { - // default to safety: request all state if we can't work it out. This can happen if you - // refresh the app whilst viewing a room: we call setRoomVisible before we know anything - // about the room. - shouldLazyLoad = false; + // default to safety: request all state if we can't work it out. This can happen if you + // refresh the app whilst viewing a room: we call setRoomVisible before we know anything + // about the room. + let shouldLazyLoad = false; + if (room) { + // do not lazy load encrypted rooms as we need the entire member list. + shouldLazyLoad = !(await this.client?.getCrypto()?.isEncryptionEnabledInRoom(roomId)); } - logger.log("SlidingSync setRoomVisible:", roomId, visible, "shouldLazyLoad:", shouldLazyLoad); + logger.log("SlidingSync setRoomVisible:", roomId, "shouldLazyLoad:", shouldLazyLoad); if (shouldLazyLoad) { // lazy load this room this.slidingSync!.useCustomSubscription(roomId, UNENCRYPTED_SUBSCRIPTION_NAME); } - const p = this.slidingSync!.modifyRoomSubscriptions(subscriptions); + this.slidingSync!.modifyRoomSubscriptions(subscriptions); if (room) { - return roomId; // we have data already for this room, show immediately e.g it's in a list + return; // we have data already for this room, show immediately e.g it's in a list } - try { - // wait until the next sync before returning as RoomView may need to know the current state - await p; - } catch { - logger.warn("SlidingSync setRoomVisible:", roomId, visible, "failed to confirm transaction"); - } - return roomId; + // wait until we know about this room. This may take a little while. + return new Promise((resolve) => { + logger.log(`SlidingSync setRoomVisible room ${roomId} not found, waiting for ClientEvent.Room`); + const waitForRoom = (r: Room): void => { + if (r.roomId === roomId) { + this.client?.off(ClientEvent.Room, waitForRoom); + logger.log(`SlidingSync room ${roomId} found, resolving setRoomVisible`); + resolve(); + } + }; + this.client?.on(ClientEvent.Room, waitForRoom); + }); } /** - * Retrieve all rooms on the user's account. Used for pre-populating the local search cache. - * Retrieval is gradual over time. + * Retrieve all rooms on the user's account. Retrieval is gradual over time. + * This function MUST be called BEFORE the first sync request goes out. * @param batchSize The number of rooms to return in each request. * @param gapBetweenRequestsMs The number of milliseconds to wait between requests. */ - public async startSpidering(batchSize: number, gapBetweenRequestsMs: number): Promise { - await sleep(gapBetweenRequestsMs); // wait a bit as this is called on first render so let's let things load - let startIndex = batchSize; - let hasMore = true; - let firstTime = true; - while (hasMore) { - const endIndex = startIndex + batchSize - 1; - try { - const ranges = [ - [0, batchSize - 1], - [startIndex, endIndex], - ]; - if (firstTime) { - await this.slidingSync!.setList(SlidingSyncManager.ListSearch, { - // e.g [0,19] [20,39] then [0,19] [40,59]. We keep [0,20] constantly to ensure - // any changes to the list whilst spidering are caught. - ranges: ranges, - sort: [ - "by_recency", // this list isn't shown on the UI so just sorting by timestamp is enough - ], - timeline_limit: 0, // we only care about the room details, not messages in the room - required_state: [ - [EventType.RoomJoinRules, ""], // the public icon on the room list - [EventType.RoomAvatar, ""], // any room avatar - [EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead - [EventType.RoomEncryption, ""], // lets rooms be configured for E2EE correctly - [EventType.RoomCreate, ""], // for isSpaceRoom checks - [EventType.RoomMember, MSC3575_STATE_KEY_ME], // lets the client calculate that we are in fact in the room - ], - // we don't include_old_rooms here in an effort to reduce the impact of spidering all rooms - // on the user's account. This means some data in the search dialog results may be inaccurate - // e.g membership of space, but this will be corrected when the user clicks on the room - // as the direct room subscription does include old room iterations. - filters: { - // we get spaces via a different list, so filter them out - not_room_types: ["m.space"], - }, - }); - } else { - await this.slidingSync!.setListRanges(SlidingSyncManager.ListSearch, ranges); - } - } catch { - // do nothing, as we reject only when we get interrupted but that's fine as the next - // request will include our data - } finally { - // gradually request more over time, even on errors. - await sleep(gapBetweenRequestsMs); + private async startSpidering( + slidingSync: SlidingSync, + batchSize: number, + gapBetweenRequestsMs: number, + ): Promise { + // The manager has created several lists (see `sssLists` in this file), all of which will be spidered simultaneously. + // There are multiple lists to ensure that we can populate invites/favourites/DMs sections immediately, rather than + // potentially waiting minutes if they are all very old rooms (and hence are returned last by the server). In this + // way, the lists are effectively priority requests. We don't actually care which room goes into which list at this + // point, as the RoomListStore will calculate this based on the returned data. + + // copy the initial set of list names and ranges, we'll keep this map updated. + const listToUpperBound = new Map( + Object.keys(sssLists).map((listName) => { + return [listName, sssLists[listName].ranges[0][1]]; + }), + ); + console.log("startSpidering:", listToUpperBound); + + // listen for a response from the server. ANY 200 OK will do here, as we assume that it is ACKing + // the request change we have sent out. TODO: this may not be true if you concurrently subscribe to a room :/ + // but in that case, for spidering at least, it isn't the end of the world as request N+1 includes all indexes + // from request N. + const lifecycle = async ( + state: SlidingSyncState, + _: MSC3575SlidingSyncResponse | null, + err?: Error, + ): Promise => { + if (state !== SlidingSyncState.Complete) { + return; } - const listData = this.slidingSync!.getListData(SlidingSyncManager.ListSearch)!; - hasMore = endIndex + 1 < listData.joinedCount; - startIndex += batchSize; - firstTime = false; - } + await sleep(gapBetweenRequestsMs); // don't tightloop; even on errors + if (err) { + return; + } + + // for all lists with total counts > range => increase the range + let hasSetRanges = false; + listToUpperBound.forEach((currentUpperBound, listName) => { + const totalCount = slidingSync.getListData(listName)?.joinedCount || 0; + if (currentUpperBound < totalCount) { + // increment the upper bound + const newUpperBound = currentUpperBound + batchSize; + console.log(`startSpidering: ${listName} ${currentUpperBound} => ${newUpperBound}`); + listToUpperBound.set(listName, newUpperBound); + // make the next request. This will only send the request when this callback has finished, so if + // we set all the list ranges at once we will only send 1 new request. + slidingSync.setListRanges(listName, [[0, newUpperBound]]); + hasSetRanges = true; + } + }); + if (!hasSetRanges) { + // finish spidering + slidingSync.off(SlidingSyncEvent.Lifecycle, lifecycle); + } + }; + slidingSync.on(SlidingSyncEvent.Lifecycle, lifecycle); } /** @@ -325,42 +382,10 @@ export class SlidingSyncManager { * @returns A working Sliding Sync or undefined */ public async setup(client: MatrixClient): Promise { - const baseUrl = client.baseUrl; - const proxyUrl = SettingsStore.getValue("feature_sliding_sync_proxy_url"); - const wellKnownProxyUrl = await this.getProxyFromWellKnown(client); - - const slidingSyncEndpoint = proxyUrl || wellKnownProxyUrl || baseUrl; - - this.configure(client, slidingSyncEndpoint); - logger.info("Sliding sync activated at", slidingSyncEndpoint); - this.startSpidering(100, 50); // 100 rooms at a time, 50ms apart - - return this.slidingSync; - } - - /** - * Get the sliding sync proxy URL from the client well known - * @param client The MatrixClient to use - * @return The proxy url - */ - public async getProxyFromWellKnown(client: MatrixClient): Promise { - let proxyUrl: string | undefined; - - try { - const clientDomain = await client.getDomain(); - if (clientDomain === null) { - throw new RangeError("Homeserver domain is null"); - } - const clientWellKnown = await AutoDiscovery.findClientConfig(clientDomain); - proxyUrl = clientWellKnown?.["org.matrix.msc3575.proxy"]?.url; - } catch { - // Either client.getDomain() is null so we've shorted out, or is invalid so `AutoDiscovery.findClientConfig` has thrown - } - - if (proxyUrl != undefined) { - logger.log("getProxyFromWellKnown: client well-known declares sliding sync proxy at", proxyUrl); - } - return proxyUrl; + const slidingSync = this.configure(client, client.baseUrl); + logger.info("Simplified Sliding Sync activated at", client.baseUrl); + this.startSpidering(slidingSync, 50, 50); // 50 rooms at a time, 50ms apart + return slidingSync; } /** @@ -371,9 +396,9 @@ export class SlidingSyncManager { public async nativeSlidingSyncSupport(client: MatrixClient): Promise { // Per https://github.com/matrix-org/matrix-spec-proposals/pull/3575/files#r1589542561 // `client` can be undefined/null in tests for some reason. - const support = await client?.doesServerSupportUnstableFeature("org.matrix.msc3575"); + const support = await client?.doesServerSupportUnstableFeature("org.matrix.simplified_msc3575"); if (support) { - logger.log("nativeSlidingSyncSupport: sliding sync advertised as unstable"); + logger.log("nativeSlidingSyncSupport: org.matrix.simplified_msc3575 sliding sync advertised as unstable"); } return support; } @@ -387,20 +412,9 @@ export class SlidingSyncManager { */ public async checkSupport(client: MatrixClient): Promise { if (await this.nativeSlidingSyncSupport(client)) { - SlidingSyncController.serverSupportsSlidingSync = true; + SlidingSyncManager.serverSupportsSlidingSync = true; return; } - - const proxyUrl = await this.getProxyFromWellKnown(client); - if (proxyUrl != undefined) { - const response = await fetch(new URL("/client/server.json", proxyUrl), { - method: Method.Get, - signal: timeoutSignal(10 * 1000), // 10s - }); - if (response.status === 200) { - logger.log("checkSupport: well-known sliding sync proxy is up at", proxyUrl); - SlidingSyncController.serverSupportsSlidingSync = true; - } - } + SlidingSyncManager.serverSupportsSlidingSync = false; } } diff --git a/src/Unread.ts b/src/Unread.ts index 2c8fa0cff3..e8f4769e25 100644 --- a/src/Unread.ts +++ b/src/Unread.ts @@ -11,7 +11,6 @@ import { logger } from "matrix-js-sdk/src/logger"; import shouldHideEvent from "./shouldHideEvent"; import { haveRendererForEvent } from "./events/EventTileFactory"; -import SettingsStore from "./settings/SettingsStore"; import { RoomNotifState, getRoomNotifsState } from "./RoomNotifs"; /** @@ -44,12 +43,6 @@ export function eventTriggersUnreadCount(client: MatrixClient, ev: MatrixEvent): } export function doesRoomHaveUnreadMessages(room: Room, includeThreads: boolean): boolean { - if (SettingsStore.getValue("feature_sliding_sync")) { - // TODO: https://github.com/vector-im/element-web/issues/23207 - // Sliding Sync doesn't support unread indicator dots (yet...) - return false; - } - const toCheck: Array = [room]; if (includeThreads) { toCheck.push(...room.getThreads()); diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index de313246b1..3c54b98b0e 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -42,8 +42,6 @@ import ContextMenu, { } from "../../structures/ContextMenu"; import AccessibleButton, { type ButtonEvent } from "../../views/elements/AccessibleButton"; import type ExtraTile from "./ExtraTile"; -import SettingsStore from "../../../settings/SettingsStore"; -import { SlidingSyncManager } from "../../../SlidingSyncManager"; import NotificationBadge from "./NotificationBadge"; import RoomTile from "./RoomTile"; @@ -106,12 +104,8 @@ export default class RoomSublist extends React.Component { private heightAtStart: number; private notificationState: ListNotificationState; - private slidingSyncMode: boolean; - public constructor(props: IProps) { super(props); - // when this setting is toggled it restarts the app so it's safe to not watch this. - this.slidingSyncMode = SettingsStore.getValue("feature_sliding_sync"); this.layout = RoomListLayoutStore.instance.getLayoutFor(this.props.tagId); this.heightAtStart = 0; @@ -165,9 +159,6 @@ export default class RoomSublist extends React.Component { } private get numVisibleTiles(): number { - if (this.slidingSyncMode) { - return this.state.rooms.length; - } const nVisible = Math.ceil(this.layout.visibleTiles); return Math.min(nVisible, this.numTiles); } @@ -329,12 +320,6 @@ export default class RoomSublist extends React.Component { }; private onShowAllClick = async (): Promise => { - if (this.slidingSyncMode) { - const count = RoomListStore.instance.getCount(this.props.tagId); - await SlidingSyncManager.instance.ensureListRegistered(this.props.tagId, { - ranges: [[0, count]], - }); - } // read number of visible tiles before we mutate it const numVisibleTiles = this.numVisibleTiles; const newHeight = this.layout.tilesToPixelsWithPadding(this.numTiles, this.padding); @@ -554,13 +539,8 @@ export default class RoomSublist extends React.Component { let contextMenu: JSX.Element | undefined; if (this.state.contextMenuPosition) { - let isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic; - let isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance; - if (this.slidingSyncMode) { - const slidingList = SlidingSyncManager.instance.slidingSync?.getListParams(this.props.tagId); - isAlphabetical = (slidingList?.sort || [])[0] === "by_name"; - isUnreadFirst = (slidingList?.sort || [])[0] === "by_notification_level"; - } + const isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic; + const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance; // Invites don't get some nonsense options, so only add them if we have to. let otherSections: JSX.Element | undefined; @@ -763,17 +743,12 @@ export default class RoomSublist extends React.Component { // floats above the resize handle, if we have one present. If the user has all // tiles visible, it becomes 'show less'. let showNButton: JSX.Element | undefined; - const hasMoreSlidingSync = - this.slidingSyncMode && RoomListStore.instance.getCount(this.props.tagId) > this.state.rooms.length; - if (maxTilesPx > this.state.height || hasMoreSlidingSync) { + if (maxTilesPx > this.state.height) { // the height of all the tiles is greater than the section height: we need a 'show more' button const nonPaddedHeight = this.state.height - RESIZE_HANDLE_HEIGHT - SHOW_N_BUTTON_HEIGHT; const amountFullyShown = Math.floor(nonPaddedHeight / this.layout.tileHeight); - let numMissing = this.numTiles - amountFullyShown; - if (this.slidingSyncMode) { - numMissing = RoomListStore.instance.getCount(this.props.tagId) - amountFullyShown; - } + const numMissing = this.numTiles - amountFullyShown; const label = _t("room_list|show_n_more", { count: numMissing }); let showMoreText: ReactNode = {label}; if (this.props.isMinimized) showMoreText = null; diff --git a/src/hooks/useSlidingSyncRoomSearch.ts b/src/hooks/useSlidingSyncRoomSearch.ts deleted file mode 100644 index 17eca74474..0000000000 --- a/src/hooks/useSlidingSyncRoomSearch.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -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 { useCallback, useState } from "react"; -import { type Room } from "matrix-js-sdk/src/matrix"; - -import { MatrixClientPeg } from "../MatrixClientPeg"; -import { useLatestResult } from "./useLatestResult"; -import { SlidingSyncManager } from "../SlidingSyncManager"; - -export interface SlidingSyncRoomSearchOpts { - limit: number; - query: string; -} - -export const useSlidingSyncRoomSearch = (): { - loading: boolean; - rooms: Room[]; - search(opts: SlidingSyncRoomSearchOpts): Promise; -} => { - const [rooms, setRooms] = useState([]); - - const [loading, setLoading] = useState(false); - - const [updateQuery, updateResult] = useLatestResult<{ term: string; limit?: number }, Room[]>(setRooms); - - const search = useCallback( - async ({ limit = 100, query: term }: SlidingSyncRoomSearchOpts): Promise => { - const opts = { limit, term }; - updateQuery(opts); - - if (!term?.length) { - setRooms([]); - return true; - } - - try { - setLoading(true); - await SlidingSyncManager.instance.ensureListRegistered(SlidingSyncManager.ListSearch, { - ranges: [[0, limit]], - filters: { - room_name_like: term, - }, - }); - const rooms: Room[] = []; - const { roomIndexToRoomId } = SlidingSyncManager.instance.slidingSync!.getListData( - SlidingSyncManager.ListSearch, - )!; - let i = 0; - while (roomIndexToRoomId[i]) { - const roomId = roomIndexToRoomId[i]; - const room = MatrixClientPeg.safeGet().getRoom(roomId); - if (room) { - rooms.push(room); - } - i++; - } - updateResult(opts, rooms); - return true; - } catch (e) { - console.error("Could not fetch sliding sync rooms for params", { limit, term }, e); - updateResult(opts, []); - return false; - } finally { - setLoading(false); - // TODO: delete the list? - } - }, - [updateQuery, updateResult], - ); - - return { - loading, - rooms, - search, - } as const; -}; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 322fcf4c93..5607a613eb 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3148,6 +3148,7 @@ "view": "Views room with given address", "whois": "Displays information about a user" }, + "sliding_sync_legacy_no_longer_supported": "Legacy sliding sync is no longer supported: please log out and back in to enable the new sliding sync flag", "space": { "add_existing_room_space": { "create": "Want to add a new room instead?", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 8784e12b6c..6984932fd9 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -198,7 +198,8 @@ export interface Settings { "feature_html_topic": IFeature; "feature_bridge_state": IFeature; "feature_jump_to_date": IFeature; - "feature_sliding_sync": IFeature; + "feature_sliding_sync": IBaseSetting; + "feature_simplified_sliding_sync": IFeature; "feature_element_call_video_rooms": IFeature; "feature_group_calls": IFeature; "feature_disable_call_per_sender_encryption": IFeature; @@ -210,7 +211,6 @@ export interface Settings { "feature_ask_to_join": IFeature; "feature_notifications": IFeature; // These are in the feature namespace but aren't actually features - "feature_sliding_sync_proxy_url": IBaseSetting; "feature_hidebold": IBaseSetting; "useOnlyCurrentProfiles": IBaseSetting; @@ -539,7 +539,14 @@ export const SETTINGS: Settings = { true, ), }, + // legacy sliding sync flag: no longer works, will error for anyone who's still using it "feature_sliding_sync": { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG_PRIORITISED, + supportedLevelsAreOrdered: true, + shouldWarn: true, + default: false, + }, + "feature_simplified_sliding_sync": { isFeature: true, labsGroup: LabGroup.Developer, supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG_PRIORITISED, @@ -550,11 +557,6 @@ export const SETTINGS: Settings = { default: false, controller: new SlidingSyncController(), }, - "feature_sliding_sync_proxy_url": { - // This is not a distinct feature, it is a legacy setting for feature_sliding_sync above - supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, - default: "", - }, "feature_element_call_video_rooms": { isFeature: true, labsGroup: LabGroup.VoiceAndVideo, diff --git a/src/settings/controllers/SlidingSyncController.ts b/src/settings/controllers/SlidingSyncController.ts index 11ae1a2ba0..44de3ba8ee 100644 --- a/src/settings/controllers/SlidingSyncController.ts +++ b/src/settings/controllers/SlidingSyncController.ts @@ -11,20 +11,19 @@ import SettingController from "./SettingController"; import PlatformPeg from "../../PlatformPeg"; import SettingsStore from "../SettingsStore"; import { _t } from "../../languageHandler"; +import { SlidingSyncManager } from "../../SlidingSyncManager"; export default class SlidingSyncController extends SettingController { - public static serverSupportsSlidingSync: boolean; - public async onChange(): Promise { PlatformPeg.get()?.reload(); } public get settingDisabled(): boolean | string { // Cannot be disabled once enabled, user has been warned and must log out and back in. - if (SettingsStore.getValue("feature_sliding_sync")) { + if (SettingsStore.getValue("feature_simplified_sliding_sync")) { return _t("labs|sliding_sync_disabled_notice"); } - if (!SlidingSyncController.serverSupportsSlidingSync) { + if (!SlidingSyncManager.serverSupportsSlidingSync) { return _t("labs|sliding_sync_server_no_support"); } diff --git a/src/stores/MemberListStore.ts b/src/stores/MemberListStore.ts index 04c5824d99..29269bac94 100644 --- a/src/stores/MemberListStore.ts +++ b/src/stores/MemberListStore.ts @@ -122,7 +122,7 @@ export class MemberListStore { * @returns True if enabled */ private async isLazyLoadingEnabled(roomId: string): Promise { - if (SettingsStore.getValue("feature_sliding_sync")) { + if (SettingsStore.getValue("feature_simplified_sliding_sync")) { // only unencrypted rooms use lazy loading return !(await this.stores.client?.getCrypto()?.isEncryptionEnabledInRoom(roomId)); } @@ -134,7 +134,7 @@ export class MemberListStore { * @returns True if there is storage for lazy loading members */ private isLazyMemberStorageEnabled(): boolean { - if (SettingsStore.getValue("feature_sliding_sync")) { + if (SettingsStore.getValue("feature_simplified_sliding_sync")) { return false; } return this.stores.client!.hasLazyLoadMembersEnabled(); diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index 91edcbb412..633d55f3f6 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -372,11 +372,7 @@ export class RoomViewStore extends EventEmitter { if (prevRoomCall !== null && (!payload.view_call || payload.room_id !== this.state.roomId)) prevRoomCall.presented = false; - if (SettingsStore.getValue("feature_sliding_sync") && this.state.roomId !== payload.room_id) { - if (this.state.subscribingRoomId && this.state.subscribingRoomId !== payload.room_id) { - // unsubscribe from this room, but don't await it as we don't care when this gets done. - this.stores.slidingSyncManager.setRoomVisible(this.state.subscribingRoomId, false); - } + if (SettingsStore.getValue("feature_simplified_sliding_sync") && this.state.roomId !== payload.room_id) { this.setState({ subscribingRoomId: payload.room_id, roomId: payload.room_id, @@ -392,13 +388,8 @@ export class RoomViewStore extends EventEmitter { }); // set this room as the room subscription. We need to await for it as this will fetch // all room state for this room, which is required before we get the state below. - await this.stores.slidingSyncManager.setRoomVisible(payload.room_id, true); - // Whilst we were subscribing another room was viewed, so stop what we're doing and - // unsubscribe - if (this.state.subscribingRoomId !== payload.room_id) { - this.stores.slidingSyncManager.setRoomVisible(payload.room_id, false); - return; - } + await this.stores.slidingSyncManager.setRoomVisible(payload.room_id); + // Re-fire the payload: we won't re-process it because the prev room ID == payload room ID now this.dis?.dispatch({ ...payload, diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index e1d2f7b7e8..6985e007bd 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -33,7 +33,6 @@ import { VisibilityProvider } from "./filters/VisibilityProvider"; import { SpaceWatcher } from "./SpaceWatcher"; import { type IRoomTimelineActionPayload } from "../../actions/MatrixActionCreators"; import { type RoomListStore as Interface, RoomListStoreEvent } from "./Interface"; -import { SlidingRoomListStoreClass } from "./SlidingRoomListStore"; import { UPDATE_EVENT } from "../AsyncStore"; import { SdkContextClass } from "../../contexts/SDKContext"; import { getChangedOverrideRoomMutePushRules } from "./utils/roomMute"; @@ -406,6 +405,9 @@ export class RoomListStoreClass extends AsyncStoreWithClient implem public setTagSorting(tagId: TagID, sort: SortAlgorithm): void { this.setAndPersistTagSorting(tagId, sort); + // We'll always need an update after changing the sort order, so mark for update and trigger + // immediately. + this.updateFn.mark(); this.updateFn.trigger(); } @@ -642,16 +644,9 @@ export default class RoomListStore { public static get instance(): Interface { if (!RoomListStore.internalInstance) { - if (SettingsStore.getValue("feature_sliding_sync")) { - logger.info("using SlidingRoomListStoreClass"); - const instance = new SlidingRoomListStoreClass(defaultDispatcher, SdkContextClass.instance); - instance.start(); - RoomListStore.internalInstance = instance; - } else { - const instance = new RoomListStoreClass(defaultDispatcher); - instance.start(); - RoomListStore.internalInstance = instance; - } + const instance = new RoomListStoreClass(defaultDispatcher); + instance.start(); + RoomListStore.internalInstance = instance; } return this.internalInstance; diff --git a/src/stores/room-list/SlidingRoomListStore.ts b/src/stores/room-list/SlidingRoomListStore.ts deleted file mode 100644 index b7ff70c671..0000000000 --- a/src/stores/room-list/SlidingRoomListStore.ts +++ /dev/null @@ -1,391 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -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 EmptyObject, type Room } from "matrix-js-sdk/src/matrix"; -import { logger } from "matrix-js-sdk/src/logger"; -import { type MSC3575Filter, SlidingSyncEvent } from "matrix-js-sdk/src/sliding-sync"; -import { type Optional } from "matrix-events-sdk"; - -import { type RoomUpdateCause, type TagID, OrderedDefaultTagIDs, DefaultTagID } from "./models"; -import { type ITagMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models"; -import { type ActionPayload } from "../../dispatcher/payloads"; -import { type MatrixDispatcher } from "../../dispatcher/dispatcher"; -import { type IFilterCondition } from "./filters/IFilterCondition"; -import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; -import { type RoomListStore as Interface, RoomListStoreEvent } from "./Interface"; -import { MetaSpace, type SpaceKey, UPDATE_SELECTED_SPACE } from "../spaces"; -import { LISTS_LOADING_EVENT } from "./RoomListStore"; -import { UPDATE_EVENT } from "../AsyncStore"; -import { type SdkContextClass } from "../../contexts/SDKContext"; - -export const SlidingSyncSortToFilter: Record = { - [SortAlgorithm.Alphabetic]: ["by_name", "by_recency"], - [SortAlgorithm.Recent]: ["by_notification_level", "by_recency"], - [SortAlgorithm.Manual]: ["by_recency"], -}; - -const filterConditions: Record = { - [DefaultTagID.Invite]: { - is_invite: true, - }, - [DefaultTagID.Favourite]: { - tags: ["m.favourite"], - }, - [DefaultTagID.DM]: { - is_dm: true, - is_invite: false, - // If a DM has a Favourite & Low Prio tag then it'll be shown in those lists instead - not_tags: ["m.favourite", "m.lowpriority"], - }, - [DefaultTagID.Untagged]: { - is_dm: false, - is_invite: false, - not_room_types: ["m.space"], - not_tags: ["m.favourite", "m.lowpriority"], - // spaces filter added dynamically - }, - [DefaultTagID.LowPriority]: { - tags: ["m.lowpriority"], - // If a room has both Favourite & Low Prio tags then it'll be shown under Favourites - not_tags: ["m.favourite"], - }, - // TODO https://github.com/vector-im/element-web/issues/23207 - // DefaultTagID.ServerNotice, - // DefaultTagID.Suggested, - // DefaultTagID.Archived, -}; - -export const LISTS_UPDATE_EVENT = RoomListStoreEvent.ListsUpdate; - -export class SlidingRoomListStoreClass extends AsyncStoreWithClient implements Interface { - private tagIdToSortAlgo: Record = {}; - private tagMap: ITagMap = {}; - private counts: Record = {}; - private stickyRoomId: Optional; - - public constructor( - dis: MatrixDispatcher, - private readonly context: SdkContextClass, - ) { - super(dis); - this.setMaxListeners(20); // RoomList + LeftPanel + 8xRoomSubList + spares - } - - public async setTagSorting(tagId: TagID, sort: SortAlgorithm): Promise { - logger.info("SlidingRoomListStore.setTagSorting ", tagId, sort); - this.tagIdToSortAlgo[tagId] = sort; - switch (sort) { - case SortAlgorithm.Alphabetic: - await this.context.slidingSyncManager.ensureListRegistered(tagId, { - sort: SlidingSyncSortToFilter[SortAlgorithm.Alphabetic], - }); - break; - case SortAlgorithm.Recent: - await this.context.slidingSyncManager.ensureListRegistered(tagId, { - sort: SlidingSyncSortToFilter[SortAlgorithm.Recent], - }); - break; - case SortAlgorithm.Manual: - logger.error("cannot enable manual sort in sliding sync mode"); - break; - default: - logger.error("unknown sort mode: ", sort); - } - } - - public getTagSorting(tagId: TagID): SortAlgorithm { - let algo = this.tagIdToSortAlgo[tagId]; - if (!algo) { - logger.warn("SlidingRoomListStore.getTagSorting: no sort algorithm for tag ", tagId); - algo = SortAlgorithm.Recent; // why not, we have to do something.. - } - return algo; - } - - public getCount(tagId: TagID): number { - return this.counts[tagId] || 0; - } - - public setListOrder(tagId: TagID, order: ListAlgorithm): void { - // TODO: https://github.com/vector-im/element-web/issues/23207 - } - - public getListOrder(tagId: TagID): ListAlgorithm { - // TODO: handle unread msgs first? https://github.com/vector-im/element-web/issues/23207 - return ListAlgorithm.Natural; - } - - /** - * Adds a filter condition to the room list store. Filters may be applied async, - * and thus might not cause an update to the store immediately. - * @param {IFilterCondition} filter The filter condition to add. - */ - public async addFilter(filter: IFilterCondition): Promise { - // Do nothing, the filters are only used by SpaceWatcher to see if a room should appear - // in the room list. We do not support arbitrary code for filters in sliding sync. - } - - /** - * Removes a filter condition from the room list store. If the filter was - * not previously added to the room list store, this will no-op. The effects - * of removing a filter may be applied async and therefore might not cause - * an update right away. - * @param {IFilterCondition} filter The filter condition to remove. - */ - public removeFilter(filter: IFilterCondition): void { - // Do nothing, the filters are only used by SpaceWatcher to see if a room should appear - // in the room list. We do not support arbitrary code for filters in sliding sync. - } - - /** - * Gets the tags for a room identified by the store. The returned set - * should never be empty, and will contain DefaultTagID.Untagged if - * the store is not aware of any tags. - * @param room The room to get the tags for. - * @returns The tags for the room. - */ - public getTagsForRoom(room: Room): TagID[] { - // check all lists for each tag we know about and see if the room is there - const tags: TagID[] = []; - for (const tagId in this.tagIdToSortAlgo) { - const listData = this.context.slidingSyncManager.slidingSync?.getListData(tagId); - if (!listData) { - continue; - } - for (const roomIndex in listData.roomIndexToRoomId) { - const roomId = listData.roomIndexToRoomId[roomIndex]; - if (roomId === room.roomId) { - tags.push(tagId); - break; - } - } - } - return tags; - } - - /** - * Manually update a room with a given cause. This should only be used if the - * room list store would otherwise be incapable of doing the update itself. Note - * that this may race with the room list's regular operation. - * @param {Room} room The room to update. - * @param {RoomUpdateCause} cause The cause to update for. - */ - public async manualRoomUpdate(room: Room, cause: RoomUpdateCause): Promise { - // TODO: this is only used when you forget a room, not that important for now. - } - - public get orderedLists(): ITagMap { - return this.tagMap; - } - - private refreshOrderedLists(tagId: string, roomIndexToRoomId: Record): void { - const tagMap = this.tagMap; - - // this room will not move due to it being viewed: it is sticky. This can be null to indicate - // no sticky room if you aren't viewing a room. - this.stickyRoomId = this.context.roomViewStore.getRoomId(); - let stickyRoomNewIndex = -1; - const stickyRoomOldIndex = (tagMap[tagId] || []).findIndex((room): boolean => { - return room.roomId === this.stickyRoomId; - }); - - // order from low to high - const orderedRoomIndexes = Object.keys(roomIndexToRoomId) - .map((numStr) => { - return Number(numStr); - }) - .sort((a, b) => { - return a - b; - }); - const seenRoomIds = new Set(); - const orderedRoomIds = orderedRoomIndexes.map((i) => { - const rid = roomIndexToRoomId[i]; - if (seenRoomIds.has(rid)) { - logger.error("room " + rid + " already has an index position: duplicate room!"); - } - seenRoomIds.add(rid); - if (!rid) { - throw new Error("index " + i + " has no room ID: Map => " + JSON.stringify(roomIndexToRoomId)); - } - if (rid === this.stickyRoomId) { - stickyRoomNewIndex = i; - } - return rid; - }); - logger.debug( - `SlidingRoomListStore.refreshOrderedLists ${tagId} sticky: ${this.stickyRoomId}`, - `${stickyRoomOldIndex} -> ${stickyRoomNewIndex}`, - "rooms:", - orderedRoomIds.length < 30 ? orderedRoomIds : orderedRoomIds.length, - ); - - if (this.stickyRoomId && stickyRoomOldIndex >= 0 && stickyRoomNewIndex >= 0) { - // this update will move this sticky room from old to new, which we do not want. - // Instead, keep the sticky room ID index position as it is, swap it with - // whatever was in its place. - // Some scenarios with sticky room S and bump room B (other letters unimportant): - // A, S, C, B S, A, B - // B, A, S, C <---- without sticky rooms ---> B, S, A - // B, S, A, C <- with sticky rooms applied -> S, B, A - // In other words, we need to swap positions to keep it locked in place. - const inWayRoomId = orderedRoomIds[stickyRoomOldIndex]; - orderedRoomIds[stickyRoomOldIndex] = this.stickyRoomId; - orderedRoomIds[stickyRoomNewIndex] = inWayRoomId; - } - - // now set the rooms - const rooms: Room[] = []; - orderedRoomIds.forEach((roomId) => { - const room = this.matrixClient?.getRoom(roomId); - if (!room) { - return; - } - rooms.push(room); - }); - tagMap[tagId] = rooms; - this.tagMap = tagMap; - } - - private onSlidingSyncListUpdate(tagId: string, joinCount: number, roomIndexToRoomId: Record): void { - this.counts[tagId] = joinCount; - this.refreshOrderedLists(tagId, roomIndexToRoomId); - // let the UI update - this.emit(LISTS_UPDATE_EVENT); - } - - private onRoomViewStoreUpdated(): void { - // we only care about this to know when the user has clicked on a room to set the stickiness value - if (this.context.roomViewStore.getRoomId() === this.stickyRoomId) { - return; - } - - let hasUpdatedAnyList = false; - - // every list with the OLD sticky room ID needs to be resorted because it now needs to take - // its proper place as it is no longer sticky. The newly sticky room can remain the same though, - // as we only actually care about its sticky status when we get list updates. - const oldStickyRoom = this.stickyRoomId; - // it's not safe to check the data in slidingSync as it is tracking the server's view of the - // room list. There's an edge case whereby the sticky room has gone outside the window and so - // would not be present in the roomIndexToRoomId map anymore, and hence clicking away from it - // will make it disappear eventually. We need to check orderedLists as that is the actual - // sorted renderable list of rooms which sticky rooms apply to. - for (const tagId in this.orderedLists) { - const list = this.orderedLists[tagId]; - const room = list.find((room) => { - return room.roomId === oldStickyRoom; - }); - if (room) { - // resort it based on the slidingSync view of the list. This may cause this old sticky - // room to cease to exist. - const listData = this.context.slidingSyncManager.slidingSync?.getListData(tagId); - if (!listData) { - continue; - } - this.refreshOrderedLists(tagId, listData.roomIndexToRoomId); - hasUpdatedAnyList = true; - } - } - // in the event we didn't call refreshOrderedLists, it helps to still remember the sticky room ID. - this.stickyRoomId = this.context.roomViewStore.getRoomId(); - - if (hasUpdatedAnyList) { - this.emit(LISTS_UPDATE_EVENT); - } - } - - protected async onReady(): Promise { - logger.info("SlidingRoomListStore.onReady"); - // permanent listeners: never get destroyed. Could be an issue if we want to test this in isolation. - this.context.slidingSyncManager.slidingSync!.on(SlidingSyncEvent.List, this.onSlidingSyncListUpdate.bind(this)); - this.context.roomViewStore.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdated.bind(this)); - this.context.spaceStore.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated.bind(this)); - if (this.context.spaceStore.activeSpace) { - this.onSelectedSpaceUpdated(this.context.spaceStore.activeSpace, false); - } - - // sliding sync has an initial response for spaces. Now request all the lists. - // We do the spaces list _first_ to avoid potential flickering on DefaultTagID.Untagged list - // which would be caused by initially having no `spaces` filter set, and then suddenly setting one. - OrderedDefaultTagIDs.forEach((tagId) => { - const filter = filterConditions[tagId]; - if (!filter) { - logger.info("SlidingRoomListStore.onReady unsupported list ", tagId); - return; // we do not support this list yet. - } - const sort = SortAlgorithm.Recent; // default to recency sort, TODO: read from config - this.tagIdToSortAlgo[tagId] = sort; - this.emit(LISTS_LOADING_EVENT, tagId, true); - this.context.slidingSyncManager - .ensureListRegistered(tagId, { - filters: filter, - sort: SlidingSyncSortToFilter[sort], - }) - .then(() => { - this.emit(LISTS_LOADING_EVENT, tagId, false); - }); - }); - } - - private onSelectedSpaceUpdated = (activeSpace: SpaceKey, allRoomsInHome: boolean): void => { - logger.info("SlidingRoomListStore.onSelectedSpaceUpdated", activeSpace); - // update the untagged filter - const tagId = DefaultTagID.Untagged; - const filters = filterConditions[tagId]; - const oldSpace = filters.spaces?.[0]; - filters.spaces = activeSpace && activeSpace != MetaSpace.Home ? [activeSpace] : undefined; - if (oldSpace !== activeSpace) { - // include subspaces in this list - this.context.spaceStore.traverseSpace( - activeSpace, - (roomId: string) => { - if (roomId === activeSpace) { - return; - } - if (!filters.spaces) { - filters.spaces = []; - } - filters.spaces.push(roomId); // add subspace - }, - false, - ); - - this.emit(LISTS_LOADING_EVENT, tagId, true); - this.context.slidingSyncManager - .ensureListRegistered(tagId, { - filters: filters, - }) - .then(() => { - this.emit(LISTS_LOADING_EVENT, tagId, false); - }); - } - }; - - // Intended for test usage - public async resetStore(): Promise { - // Test function - } - - /** - * Regenerates the room whole room list, discarding any previous results. - * - * Note: This is only exposed externally for the tests. Do not call this from within - * the app. - * @param trigger Set to false to prevent a list update from being sent. Should only - * be used if the calling code will manually trigger the update. - */ - public regenerateAllLists({ trigger = true }): void { - // Test function - } - - protected async onNotReady(): Promise { - await this.resetStore(); - } - - protected async onAction(payload: ActionPayload): Promise {} -} diff --git a/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts index e84f538993..e41eeee816 100644 --- a/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts +++ b/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts @@ -69,6 +69,12 @@ export const getLastTs = (r: Room, userId: string): number => { if (!r?.timeline) { return Number.MAX_SAFE_INTEGER; } + // MSC4186: Simplified Sliding Sync sets this. + // If it's present, sort by it. + const bumpStamp = r.getBumpStamp(); + if (bumpStamp) { + return bumpStamp; + } // If the room hasn't been joined yet, it probably won't have a timeline to // parse. We'll still fall back to the timeline if this fails, but chances diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 1f6351b7b9..75a42c86bc 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -661,6 +661,7 @@ export function mkStubRoom( getUnreadNotificationCount: jest.fn(() => 0), getRoomUnreadNotificationCount: jest.fn().mockReturnValue(0), getVersion: jest.fn().mockReturnValue("1"), + getBumpStamp: jest.fn().mockReturnValue(0), hasMembershipState: () => false, isElementVideoRoom: jest.fn().mockReturnValue(false), isSpaceRoom: jest.fn().mockReturnValue(false), diff --git a/test/unit-tests/SlidingSyncManager-test.ts b/test/unit-tests/SlidingSyncManager-test.ts index ddc5911262..20d9110bcc 100644 --- a/test/unit-tests/SlidingSyncManager-test.ts +++ b/test/unit-tests/SlidingSyncManager-test.ts @@ -6,18 +6,31 @@ 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 { SlidingSync } from "matrix-js-sdk/src/sliding-sync"; +import { type SlidingSync, SlidingSyncEvent, SlidingSyncState } from "matrix-js-sdk/src/sliding-sync"; import { mocked } from "jest-mock"; -import { type MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { ClientEvent, type MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import fetchMockJest from "fetch-mock-jest"; +import EventEmitter from "events"; +import { waitFor } from "jest-matrix-react"; import { SlidingSyncManager } from "../../src/SlidingSyncManager"; -import { stubClient } from "../test-utils"; -import SlidingSyncController from "../../src/settings/controllers/SlidingSyncController"; -import SettingsStore from "../../src/settings/SettingsStore"; +import { mkStubRoom, stubClient } from "../test-utils"; -jest.mock("matrix-js-sdk/src/sliding-sync"); -const MockSlidingSync = >(SlidingSync); +class MockSlidingSync extends EventEmitter { + lists = {}; + listModifiedCount = 0; + terminated = false; + needsResend = false; + modifyRoomSubscriptions = jest.fn(); + getRoomSubscriptions = jest.fn(); + useCustomSubscription = jest.fn(); + getListParams = jest.fn(); + setList = jest.fn(); + setListRanges = jest.fn(); + getListData = jest.fn(); + extensions = jest.fn(); + desiredRoomSubscriptions = jest.fn(); +} describe("SlidingSyncManager", () => { let manager: SlidingSyncManager; @@ -25,12 +38,12 @@ describe("SlidingSyncManager", () => { let client: MatrixClient; beforeEach(() => { - slidingSync = new MockSlidingSync(); + slidingSync = new MockSlidingSync() as unknown as SlidingSync; manager = new SlidingSyncManager(); client = stubClient(); // by default the client has no rooms: stubClient magically makes rooms annoyingly. mocked(client.getRoom).mockReturnValue(null); - manager.configure(client, "invalid"); + (manager as any).configure(client, "invalid"); manager.slidingSync = slidingSync; fetchMockJest.reset(); fetchMockJest.get("https://proxy/client/server.json", {}); @@ -39,12 +52,13 @@ describe("SlidingSyncManager", () => { describe("setRoomVisible", () => { it("adds a subscription for the room", async () => { const roomId = "!room:id"; + mocked(client.getRoom).mockReturnValue(mkStubRoom(roomId, "foo", client)); const subs = new Set(); mocked(slidingSync.getRoomSubscriptions).mockReturnValue(subs); - mocked(slidingSync.modifyRoomSubscriptions).mockResolvedValue("yep"); - await manager.setRoomVisible(roomId, true); + await manager.setRoomVisible(roomId); expect(slidingSync.modifyRoomSubscriptions).toHaveBeenCalledWith(new Set([roomId])); }); + it("adds a custom subscription for a lazy-loadable room", async () => { const roomId = "!lazy:id"; const room = new Room(roomId, client, client.getUserId()!); @@ -67,19 +81,37 @@ describe("SlidingSyncManager", () => { }); const subs = new Set(); mocked(slidingSync.getRoomSubscriptions).mockReturnValue(subs); - mocked(slidingSync.modifyRoomSubscriptions).mockResolvedValue("yep"); - await manager.setRoomVisible(roomId, true); + await manager.setRoomVisible(roomId); expect(slidingSync.modifyRoomSubscriptions).toHaveBeenCalledWith(new Set([roomId])); // we aren't prescriptive about what the sub name is. expect(slidingSync.useCustomSubscription).toHaveBeenCalledWith(roomId, expect.anything()); }); + + it("waits if the room is not yet known", async () => { + const roomId = "!room:id"; + mocked(client.getRoom).mockReturnValue(null); + const subs = new Set(); + mocked(slidingSync.getRoomSubscriptions).mockReturnValue(subs); + + const setVisibleDone = jest.fn(); + manager.setRoomVisible(roomId).then(setVisibleDone); + + await waitFor(() => expect(client.getRoom).toHaveBeenCalledWith(roomId)); + + expect(setVisibleDone).not.toHaveBeenCalled(); + + const stubRoom = mkStubRoom(roomId, "foo", client); + mocked(client.getRoom).mockReturnValue(stubRoom); + client.emit(ClientEvent.Room, stubRoom); + + await waitFor(() => expect(setVisibleDone).toHaveBeenCalled()); + }); }); describe("ensureListRegistered", () => { it("creates a new list based on the key", async () => { const listKey = "key"; mocked(slidingSync.getListParams).mockReturnValue(null); - mocked(slidingSync.setList).mockResolvedValue("yep"); await manager.ensureListRegistered(listKey, { sort: ["by_recency"], }); @@ -96,7 +128,6 @@ describe("SlidingSyncManager", () => { mocked(slidingSync.getListParams).mockReturnValue({ ranges: [[0, 42]], }); - mocked(slidingSync.setList).mockResolvedValue("yep"); await manager.ensureListRegistered(listKey, { sort: ["by_recency"], }); @@ -114,7 +145,6 @@ describe("SlidingSyncManager", () => { mocked(slidingSync.getListParams).mockReturnValue({ ranges: [[0, 42]], }); - mocked(slidingSync.setList).mockResolvedValue("yep"); await manager.ensureListRegistered(listKey, { ranges: [[0, 52]], }); @@ -128,7 +158,6 @@ describe("SlidingSyncManager", () => { ranges: [[0, 42]], sort: ["by_recency"], }); - mocked(slidingSync.setList).mockResolvedValue("yep"); await manager.ensureListRegistered(listKey, { ranges: [[0, 42]], sort: ["by_recency"], @@ -139,183 +168,77 @@ describe("SlidingSyncManager", () => { }); describe("startSpidering", () => { - it("requests in batchSizes", async () => { + it("requests in expanding batchSizes", async () => { const gapMs = 1; const batchSize = 10; - mocked(slidingSync.setList).mockResolvedValue("yep"); - mocked(slidingSync.setListRanges).mockResolvedValue("yep"); mocked(slidingSync.getListData).mockImplementation((key) => { return { joinedCount: 64, roomIndexToRoomId: {}, }; }); - await manager.startSpidering(batchSize, gapMs); + await (manager as any).startSpidering(slidingSync, batchSize, gapMs); + // we expect calls for 10,19 -> 20,29 -> 30,39 -> 40,49 -> 50,59 -> 60,69 const wantWindows = [ - [10, 19], - [20, 29], - [30, 39], - [40, 49], - [50, 59], - [60, 69], + [0, 10], + [0, 20], + [0, 30], + [0, 40], + [0, 50], + [0, 60], + [0, 70], ]; - expect(slidingSync.getListData).toHaveBeenCalledTimes(wantWindows.length); - expect(slidingSync.setList).toHaveBeenCalledTimes(1); - expect(slidingSync.setListRanges).toHaveBeenCalledTimes(wantWindows.length - 1); - wantWindows.forEach((range, i) => { - if (i === 0) { - // eslint-disable-next-line jest/no-conditional-expect - expect(slidingSync.setList).toHaveBeenCalledWith( - SlidingSyncManager.ListSearch, - // eslint-disable-next-line jest/no-conditional-expect - expect.objectContaining({ - ranges: [[0, batchSize - 1], range], - }), - ); - return; - } - expect(slidingSync.setListRanges).toHaveBeenCalledWith(SlidingSyncManager.ListSearch, [ - [0, batchSize - 1], - range, - ]); - }); + + for (let i = 1; i < wantWindows.length; ++i) { + // each time we emit, it should expand the range of all 5 lists by 10 until + // they all include all the rooms (64), which is 6 emits. + slidingSync.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, null, undefined); + await waitFor(() => expect(slidingSync.getListData).toHaveBeenCalledTimes(i * 5)); + expect(slidingSync.setListRanges).toHaveBeenCalledTimes(i * 5); + expect(slidingSync.setListRanges).toHaveBeenCalledWith("spaces", [wantWindows[i]]); + } }); it("handles accounts with zero rooms", async () => { const gapMs = 1; const batchSize = 10; - mocked(slidingSync.setList).mockResolvedValue("yep"); mocked(slidingSync.getListData).mockImplementation((key) => { return { joinedCount: 0, roomIndexToRoomId: {}, }; }); - await manager.startSpidering(batchSize, gapMs); - expect(slidingSync.getListData).toHaveBeenCalledTimes(1); - expect(slidingSync.setList).toHaveBeenCalledTimes(1); - expect(slidingSync.setList).toHaveBeenCalledWith( - SlidingSyncManager.ListSearch, - expect.objectContaining({ - ranges: [ - [0, batchSize - 1], - [batchSize, batchSize + batchSize - 1], - ], - }), - ); - }); - it("continues even when setList rejects", async () => { - const gapMs = 1; - const batchSize = 10; - mocked(slidingSync.setList).mockRejectedValue("narp"); - mocked(slidingSync.getListData).mockImplementation((key) => { - return { - joinedCount: 0, - roomIndexToRoomId: {}, - }; - }); - await manager.startSpidering(batchSize, gapMs); - expect(slidingSync.getListData).toHaveBeenCalledTimes(1); - expect(slidingSync.setList).toHaveBeenCalledTimes(1); - expect(slidingSync.setList).toHaveBeenCalledWith( - SlidingSyncManager.ListSearch, - expect.objectContaining({ - ranges: [ - [0, batchSize - 1], - [batchSize, batchSize + batchSize - 1], - ], - }), - ); + await (manager as any).startSpidering(slidingSync, batchSize, gapMs); + slidingSync.emit(SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, null, undefined); + await waitFor(() => expect(slidingSync.getListData).toHaveBeenCalledTimes(5)); + // should not have needed to expand the range + expect(slidingSync.setListRanges).not.toHaveBeenCalled(); }); }); describe("checkSupport", () => { beforeEach(() => { - SlidingSyncController.serverSupportsSlidingSync = false; - jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("https://proxy/"); + SlidingSyncManager.serverSupportsSlidingSync = false; }); it("shorts out if the server has 'native' sliding sync support", async () => { jest.spyOn(manager, "nativeSlidingSyncSupport").mockResolvedValue(true); - expect(SlidingSyncController.serverSupportsSlidingSync).toBeFalsy(); + expect(SlidingSyncManager.serverSupportsSlidingSync).toBeFalsy(); await manager.checkSupport(client); - expect(manager.getProxyFromWellKnown).not.toHaveBeenCalled(); // We return earlier - expect(SlidingSyncController.serverSupportsSlidingSync).toBeTruthy(); - }); - it("tries to find a sliding sync proxy url from the client well-known if there's no 'native' support", async () => { - jest.spyOn(manager, "nativeSlidingSyncSupport").mockResolvedValue(false); - expect(SlidingSyncController.serverSupportsSlidingSync).toBeFalsy(); - await manager.checkSupport(client); - expect(manager.getProxyFromWellKnown).toHaveBeenCalled(); - expect(SlidingSyncController.serverSupportsSlidingSync).toBeTruthy(); - }); - it("should query well-known on server_name not baseUrl", async () => { - fetchMockJest.get("https://matrix.org/.well-known/matrix/client", { - "m.homeserver": { - base_url: "https://matrix-client.matrix.org", - server: "matrix.org", - }, - "org.matrix.msc3575.proxy": { - url: "https://proxy/", - }, - }); - fetchMockJest.get("https://matrix-client.matrix.org/_matrix/client/versions", { versions: ["v1.4"] }); - - mocked(manager.getProxyFromWellKnown).mockRestore(); - jest.spyOn(manager, "nativeSlidingSyncSupport").mockResolvedValue(false); - expect(SlidingSyncController.serverSupportsSlidingSync).toBeFalsy(); - await manager.checkSupport(client); - expect(SlidingSyncController.serverSupportsSlidingSync).toBeTruthy(); - expect(fetchMockJest).not.toHaveFetched("https://matrix-client.matrix.org/.well-known/matrix/client"); - }); - }); - describe("nativeSlidingSyncSupport", () => { - beforeEach(() => { - SlidingSyncController.serverSupportsSlidingSync = false; - }); - it("should make an OPTIONS request to avoid unintended side effects", async () => { - // See https://github.com/element-hq/element-web/issues/27426 - - const unstableSpy = jest - .spyOn(client, "doesServerSupportUnstableFeature") - .mockImplementation(async (feature: string) => { - expect(feature).toBe("org.matrix.msc3575"); - return true; - }); - const proxySpy = jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("https://proxy/"); - - expect(SlidingSyncController.serverSupportsSlidingSync).toBeFalsy(); - await manager.checkSupport(client); // first thing it does is call nativeSlidingSyncSupport - expect(proxySpy).not.toHaveBeenCalled(); - expect(unstableSpy).toHaveBeenCalled(); - expect(SlidingSyncController.serverSupportsSlidingSync).toBeTruthy(); + expect(SlidingSyncManager.serverSupportsSlidingSync).toBeTruthy(); }); }); describe("setup", () => { + let untypedManager: any; + beforeEach(() => { - jest.spyOn(manager, "configure"); - jest.spyOn(manager, "startSpidering"); + untypedManager = manager; + jest.spyOn(untypedManager, "configure"); + jest.spyOn(untypedManager, "startSpidering"); }); - it("uses the baseUrl as a proxy if no proxy is set in the client well-known and the server has no native support", async () => { + it("uses the baseUrl", async () => { await manager.setup(client); - expect(manager.configure).toHaveBeenCalled(); - expect(manager.configure).toHaveBeenCalledWith(client, client.baseUrl); - expect(manager.startSpidering).toHaveBeenCalled(); - }); - it("uses the proxy declared in the client well-known", async () => { - jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("https://proxy/"); - await manager.setup(client); - expect(manager.configure).toHaveBeenCalled(); - expect(manager.configure).toHaveBeenCalledWith(client, "https://proxy/"); - expect(manager.startSpidering).toHaveBeenCalled(); - }); - it("uses the legacy `feature_sliding_sync_proxy_url` if it was set", async () => { - jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("https://proxy/"); - jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string): any => { - if (name === "feature_sliding_sync_proxy_url") return "legacy-proxy"; - }); - await manager.setup(client); - expect(manager.configure).toHaveBeenCalled(); - expect(manager.configure).toHaveBeenCalledWith(client, "legacy-proxy"); - expect(manager.startSpidering).toHaveBeenCalled(); + expect(untypedManager.configure).toHaveBeenCalled(); + expect(untypedManager.configure).toHaveBeenCalledWith(client, client.baseUrl); + expect(untypedManager.startSpidering).toHaveBeenCalled(); }); }); }); diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx index 4b986f8d2d..f14a7e0acd 100644 --- a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx +++ b/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx @@ -11,7 +11,7 @@ import { mocked } from "jest-mock"; import RoomListStoreV3 from "../../../../../src/stores/room-list-v3/RoomListStoreV3"; import { mkStubRoom } from "../../../../test-utils"; -import { LISTS_UPDATE_EVENT } from "../../../../../src/stores/room-list/SlidingRoomListStore"; +import { LISTS_UPDATE_EVENT } from "../../../../../src/stores/room-list/RoomListStore"; import { useRoomListViewModel } from "../../../../../src/components/viewmodels/roomlist/RoomListViewModel"; import { FilterKey } from "../../../../../src/stores/room-list-v3/skip-list/filters"; import { SecondaryFilters } from "../../../../../src/components/viewmodels/roomlist/useFilteredRooms"; diff --git a/test/unit-tests/hooks/useSlidingSyncRoomSearch-test.tsx b/test/unit-tests/hooks/useSlidingSyncRoomSearch-test.tsx deleted file mode 100644 index d2ee9ad3bb..0000000000 --- a/test/unit-tests/hooks/useSlidingSyncRoomSearch-test.tsx +++ /dev/null @@ -1,84 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -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 { waitFor, renderHook, act } from "jest-matrix-react"; -import { mocked } from "jest-mock"; -import { type SlidingSync } from "matrix-js-sdk/src/sliding-sync"; -import { Room } from "matrix-js-sdk/src/matrix"; - -import { useSlidingSyncRoomSearch } from "../../../src/hooks/useSlidingSyncRoomSearch"; -import { MockEventEmitter, stubClient } from "../../test-utils"; -import { SlidingSyncManager } from "../../../src/SlidingSyncManager"; - -describe("useSlidingSyncRoomSearch", () => { - afterAll(() => { - jest.restoreAllMocks(); - }); - - it("should display rooms when searching", async () => { - const client = stubClient(); - const roomA = new Room("!a:localhost", client, client.getUserId()!); - const roomB = new Room("!b:localhost", client, client.getUserId()!); - const slidingSync = mocked( - new MockEventEmitter({ - getListData: jest.fn(), - }) as unknown as SlidingSync, - ); - jest.spyOn(SlidingSyncManager.instance, "ensureListRegistered").mockResolvedValue({ - ranges: [[0, 9]], - }); - SlidingSyncManager.instance.slidingSync = slidingSync; - mocked(slidingSync.getListData).mockReturnValue({ - joinedCount: 2, - roomIndexToRoomId: { - 0: roomA.roomId, - 1: roomB.roomId, - }, - }); - mocked(client.getRoom).mockImplementation((roomId) => { - switch (roomId) { - case roomA.roomId: - return roomA; - case roomB.roomId: - return roomB; - default: - return null; - } - }); - - // first check that everything is empty - const { result } = renderHook(() => useSlidingSyncRoomSearch()); - const query = { - limit: 10, - query: "foo", - }; - expect(result.current.loading).toBe(false); - expect(result.current.rooms).toEqual([]); - - // run the query - act(() => { - result.current.search(query); - }); - - // wait for loading to finish - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - - // now we expect there to be rooms - expect(result.current.rooms).toEqual([roomA, roomB]); - - // run the query again - act(() => { - result.current.search(query); - }); - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - }); -}); diff --git a/test/unit-tests/stores/MemberListStore-test.ts b/test/unit-tests/stores/MemberListStore-test.ts index 883bb13f11..9139dde85d 100644 --- a/test/unit-tests/stores/MemberListStore-test.ts +++ b/test/unit-tests/stores/MemberListStore-test.ts @@ -161,7 +161,7 @@ describe("MemberListStore", () => { describe("sliding sync", () => { beforeEach(() => { jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName, roomId, value) => { - return settingName === "feature_sliding_sync"; // this is enabled, everything else is disabled. + return settingName === "feature_simplified_sliding_sync"; // this is enabled, everything else is disabled. }); client.members = jest.fn(); }); diff --git a/test/unit-tests/stores/RoomViewStore-test.ts b/test/unit-tests/stores/RoomViewStore-test.ts index be08015fc1..b730853a89 100644 --- a/test/unit-tests/stores/RoomViewStore-test.ts +++ b/test/unit-tests/stores/RoomViewStore-test.ts @@ -383,43 +383,35 @@ describe("RoomViewStore", function () { describe("Sliding Sync", function () { beforeEach(() => { jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName, roomId, value) => { - return settingName === "feature_sliding_sync"; // this is enabled, everything else is disabled. + return settingName === "feature_simplified_sliding_sync"; // this is enabled, everything else is disabled. }); }); it("subscribes to the room", async () => { - const setRoomVisible = jest - .spyOn(slidingSyncManager, "setRoomVisible") - .mockReturnValue(Promise.resolve("")); + const setRoomVisible = jest.spyOn(slidingSyncManager, "setRoomVisible").mockReturnValue(Promise.resolve()); const subscribedRoomId = "!sub1:localhost"; dis.dispatch({ action: Action.ViewRoom, room_id: subscribedRoomId }); await untilDispatch(Action.ActiveRoomChanged, dis); expect(roomViewStore.getRoomId()).toBe(subscribedRoomId); - expect(setRoomVisible).toHaveBeenCalledWith(subscribedRoomId, true); + expect(setRoomVisible).toHaveBeenCalledWith(subscribedRoomId); }); - // Regression test for an in-the-wild bug where rooms would rapidly switch forever in sliding sync mode + // Previously a regression test for an in-the-wild bug where rooms would rapidly switch forever in sliding sync mode + // although that was before the complexity was removed with similified mode. I've removed the complexity but kept the + // test anyway. it("doesn't get stuck in a loop if you view rooms quickly", async () => { - const setRoomVisible = jest - .spyOn(slidingSyncManager, "setRoomVisible") - .mockReturnValue(Promise.resolve("")); + const setRoomVisible = jest.spyOn(slidingSyncManager, "setRoomVisible").mockReturnValue(Promise.resolve()); const subscribedRoomId = "!sub1:localhost"; const subscribedRoomId2 = "!sub2:localhost"; dis.dispatch({ action: Action.ViewRoom, room_id: subscribedRoomId }, true); dis.dispatch({ action: Action.ViewRoom, room_id: subscribedRoomId2 }, true); await untilDispatch(Action.ActiveRoomChanged, dis); - // sub(1) then unsub(1) sub(2), unsub(1) - const wantCalls = [ - [subscribedRoomId, true], - [subscribedRoomId, false], - [subscribedRoomId2, true], - [subscribedRoomId, false], - ]; + // should view 1, then 2 + const wantCalls = [[subscribedRoomId], [subscribedRoomId2]]; expect(setRoomVisible).toHaveBeenCalledTimes(wantCalls.length); wantCalls.forEach((v, i) => { try { expect(setRoomVisible.mock.calls[i][0]).toEqual(v[0]); - expect(setRoomVisible.mock.calls[i][1]).toEqual(v[1]); } catch { throw new Error(`i=${i} got ${setRoomVisible.mock.calls[i]} want ${v}`); } diff --git a/test/unit-tests/stores/room-list/SlidingRoomListStore-test.ts b/test/unit-tests/stores/room-list/SlidingRoomListStore-test.ts deleted file mode 100644 index dbe06ab326..0000000000 --- a/test/unit-tests/stores/room-list/SlidingRoomListStore-test.ts +++ /dev/null @@ -1,341 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -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 { mocked } from "jest-mock"; -import { type SlidingSync, SlidingSyncEvent } from "matrix-js-sdk/src/sliding-sync"; -import { Room } from "matrix-js-sdk/src/matrix"; - -import { - LISTS_UPDATE_EVENT, - SlidingRoomListStoreClass, - SlidingSyncSortToFilter, -} from "../../../../src/stores/room-list/SlidingRoomListStore"; -import { type SpaceStoreClass } from "../../../../src/stores/spaces/SpaceStore"; -import { MockEventEmitter, stubClient, untilEmission } from "../../../test-utils"; -import { TestSdkContext } from "../../TestSdkContext"; -import { SlidingSyncManager } from "../../../../src/SlidingSyncManager"; -import { type RoomViewStore } from "../../../../src/stores/RoomViewStore"; -import { MatrixDispatcher } from "../../../../src/dispatcher/dispatcher"; -import { SortAlgorithm } from "../../../../src/stores/room-list/algorithms/models"; -import { DefaultTagID, type TagID } from "../../../../src/stores/room-list/models"; -import { MetaSpace, UPDATE_SELECTED_SPACE } from "../../../../src/stores/spaces"; -import { LISTS_LOADING_EVENT } from "../../../../src/stores/room-list/RoomListStore"; -import { UPDATE_EVENT } from "../../../../src/stores/AsyncStore"; - -jest.mock("../../../../src/SlidingSyncManager"); -const MockSlidingSyncManager = >(SlidingSyncManager); - -describe("SlidingRoomListStore", () => { - let store: SlidingRoomListStoreClass; - let context: TestSdkContext; - let dis: MatrixDispatcher; - let activeSpace: string; - - beforeEach(async () => { - context = new TestSdkContext(); - context.client = stubClient(); - context._SpaceStore = new MockEventEmitter({ - traverseSpace: jest.fn(), - get activeSpace() { - return activeSpace; - }, - }) as SpaceStoreClass; - context._SlidingSyncManager = new MockSlidingSyncManager(); - context._SlidingSyncManager.slidingSync = mocked( - new MockEventEmitter({ - getListData: jest.fn(), - }) as unknown as SlidingSync, - ); - context._RoomViewStore = mocked( - new MockEventEmitter({ - getRoomId: jest.fn(), - }) as unknown as RoomViewStore, - ); - mocked(context._SlidingSyncManager.ensureListRegistered).mockResolvedValue({ - ranges: [[0, 10]], - }); - - dis = new MatrixDispatcher(); - store = new SlidingRoomListStoreClass(dis, context); - }); - - describe("spaces", () => { - it("alters 'filters.spaces' on the DefaultTagID.Untagged list when the selected space changes", async () => { - await store.start(); // call onReady - const spaceRoomId = "!foo:bar"; - - const p = untilEmission(store, LISTS_LOADING_EVENT, (listName, isLoading) => { - return listName === DefaultTagID.Untagged && !isLoading; - }); - - // change the active space - activeSpace = spaceRoomId; - context._SpaceStore!.emit(UPDATE_SELECTED_SPACE, spaceRoomId, false); - await p; - - expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(DefaultTagID.Untagged, { - filters: expect.objectContaining({ - spaces: [spaceRoomId], - }), - }); - }); - - it("gracefully handles subspaces in the home metaspace", async () => { - const subspace = "!sub:space"; - mocked(context._SpaceStore!.traverseSpace).mockImplementation( - (spaceId: string, fn: (roomId: string) => void) => { - fn(subspace); - }, - ); - activeSpace = MetaSpace.Home; - await store.start(); // call onReady - - expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(DefaultTagID.Untagged, { - filters: expect.objectContaining({ - spaces: [subspace], - }), - }); - }); - - it("alters 'filters.spaces' on the DefaultTagID.Untagged list if it loads with an active space", async () => { - // change the active space before we are ready - const spaceRoomId = "!foo2:bar"; - activeSpace = spaceRoomId; - const p = untilEmission(store, LISTS_LOADING_EVENT, (listName, isLoading) => { - return listName === DefaultTagID.Untagged && !isLoading; - }); - await store.start(); // call onReady - await p; - expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith( - DefaultTagID.Untagged, - expect.objectContaining({ - filters: expect.objectContaining({ - spaces: [spaceRoomId], - }), - }), - ); - }); - - it("includes subspaces in 'filters.spaces' when the selected space has subspaces", async () => { - await store.start(); // call onReady - const spaceRoomId = "!foo:bar"; - const subSpace1 = "!ss1:bar"; - const subSpace2 = "!ss2:bar"; - - const p = untilEmission(store, LISTS_LOADING_EVENT, (listName, isLoading) => { - return listName === DefaultTagID.Untagged && !isLoading; - }); - - mocked(context._SpaceStore!.traverseSpace).mockImplementation( - (spaceId: string, fn: (roomId: string) => void) => { - if (spaceId === spaceRoomId) { - fn(subSpace1); - fn(subSpace2); - } - }, - ); - - // change the active space - activeSpace = spaceRoomId; - context._SpaceStore!.emit(UPDATE_SELECTED_SPACE, spaceRoomId, false); - await p; - - expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(DefaultTagID.Untagged, { - filters: expect.objectContaining({ - spaces: [spaceRoomId, subSpace1, subSpace2], - }), - }); - }); - }); - - it("setTagSorting alters the 'sort' option in the list", async () => { - const tagId: TagID = "foo"; - await store.setTagSorting(tagId, SortAlgorithm.Alphabetic); - expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(tagId, { - sort: SlidingSyncSortToFilter[SortAlgorithm.Alphabetic], - }); - expect(store.getTagSorting(tagId)).toEqual(SortAlgorithm.Alphabetic); - - await store.setTagSorting(tagId, SortAlgorithm.Recent); - expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(tagId, { - sort: SlidingSyncSortToFilter[SortAlgorithm.Recent], - }); - expect(store.getTagSorting(tagId)).toEqual(SortAlgorithm.Recent); - }); - - it("getTagsForRoom gets the tags for the room", async () => { - await store.start(); - const roomA = "!a:localhost"; - const roomB = "!b:localhost"; - const keyToListData: Record }> = { - [DefaultTagID.Untagged]: { - joinedCount: 10, - roomIndexToRoomId: { - 0: roomA, - 1: roomB, - }, - }, - [DefaultTagID.Favourite]: { - joinedCount: 2, - roomIndexToRoomId: { - 0: roomB, - }, - }, - }; - mocked(context._SlidingSyncManager!.slidingSync!.getListData).mockImplementation((key: string) => { - return keyToListData[key] || null; - }); - - expect(store.getTagsForRoom(new Room(roomA, context.client!, context.client!.getUserId()!))).toEqual([ - DefaultTagID.Untagged, - ]); - expect(store.getTagsForRoom(new Room(roomB, context.client!, context.client!.getUserId()!))).toEqual([ - DefaultTagID.Favourite, - DefaultTagID.Untagged, - ]); - }); - - it("emits LISTS_UPDATE_EVENT when slidingSync lists update", async () => { - await store.start(); - const roomA = "!a:localhost"; - const roomB = "!b:localhost"; - const roomC = "!c:localhost"; - const tagId = DefaultTagID.Favourite; - const joinCount = 10; - const roomIndexToRoomId = { - // mixed to ensure we sort - 1: roomB, - 2: roomC, - 0: roomA, - }; - const rooms = [ - new Room(roomA, context.client!, context.client!.getUserId()!), - new Room(roomB, context.client!, context.client!.getUserId()!), - new Room(roomC, context.client!, context.client!.getUserId()!), - ]; - mocked(context.client!.getRoom).mockImplementation((roomId: string) => { - switch (roomId) { - case roomA: - return rooms[0]; - case roomB: - return rooms[1]; - case roomC: - return rooms[2]; - } - return null; - }); - const p = untilEmission(store, LISTS_UPDATE_EVENT); - context.slidingSyncManager.slidingSync!.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId); - await p; - expect(store.getCount(tagId)).toEqual(joinCount); - expect(store.orderedLists[tagId]).toEqual(rooms); - }); - - it("sets the sticky room on the basis of the viewed room in RoomViewStore", async () => { - await store.start(); - // seed the store with 3 rooms - const roomIdA = "!a:localhost"; - const roomIdB = "!b:localhost"; - const roomIdC = "!c:localhost"; - const tagId = DefaultTagID.Favourite; - const joinCount = 10; - const roomIndexToRoomId = { - // mixed to ensure we sort - 1: roomIdB, - 2: roomIdC, - 0: roomIdA, - }; - const roomA = new Room(roomIdA, context.client!, context.client!.getUserId()!); - const roomB = new Room(roomIdB, context.client!, context.client!.getUserId()!); - const roomC = new Room(roomIdC, context.client!, context.client!.getUserId()!); - mocked(context.client!.getRoom).mockImplementation((roomId: string) => { - switch (roomId) { - case roomIdA: - return roomA; - case roomIdB: - return roomB; - case roomIdC: - return roomC; - } - return null; - }); - mocked(context._SlidingSyncManager!.slidingSync!.getListData).mockImplementation((key: string) => { - if (key !== tagId) { - return null; - } - return { - roomIndexToRoomId: roomIndexToRoomId, - joinedCount: joinCount, - }; - }); - let p = untilEmission(store, LISTS_UPDATE_EVENT); - context.slidingSyncManager.slidingSync!.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId); - await p; - expect(store.orderedLists[tagId]).toEqual([roomA, roomB, roomC]); - - // make roomB sticky and inform the store - mocked(context.roomViewStore.getRoomId).mockReturnValue(roomIdB); - context.roomViewStore.emit(UPDATE_EVENT); - - // bump room C to the top, room B should not move from i=1 despite the list update saying to - roomIndexToRoomId[0] = roomIdC; - roomIndexToRoomId[1] = roomIdA; - roomIndexToRoomId[2] = roomIdB; - p = untilEmission(store, LISTS_UPDATE_EVENT); - context.slidingSyncManager.slidingSync!.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId); - await p; - - // check that B didn't move and that A was put below B - expect(store.orderedLists[tagId]).toEqual([roomC, roomB, roomA]); - - // make room C sticky: rooms should move as a result, without needing an additional list update - mocked(context.roomViewStore.getRoomId).mockReturnValue(roomIdC); - p = untilEmission(store, LISTS_UPDATE_EVENT); - context.roomViewStore.emit(UPDATE_EVENT); - await p; - expect(store.orderedLists[tagId].map((r) => r.roomId)).toEqual([roomC, roomA, roomB].map((r) => r.roomId)); - }); - - it("gracefully handles unknown room IDs", async () => { - await store.start(); - const roomIdA = "!a:localhost"; - const roomIdB = "!b:localhost"; // does not exist - const roomIdC = "!c:localhost"; - const roomIndexToRoomId = { - 0: roomIdA, - 1: roomIdB, // does not exist - 2: roomIdC, - }; - const tagId = DefaultTagID.Favourite; - const joinCount = 10; - // seed the store with 2 rooms - const roomA = new Room(roomIdA, context.client!, context.client!.getUserId()!); - const roomC = new Room(roomIdC, context.client!, context.client!.getUserId()!); - mocked(context.client!.getRoom).mockImplementation((roomId: string) => { - switch (roomId) { - case roomIdA: - return roomA; - case roomIdC: - return roomC; - } - return null; - }); - mocked(context._SlidingSyncManager!.slidingSync!.getListData).mockImplementation((key: string) => { - if (key !== tagId) { - return null; - } - return { - roomIndexToRoomId: roomIndexToRoomId, - joinedCount: joinCount, - }; - }); - const p = untilEmission(store, LISTS_UPDATE_EVENT); - context.slidingSyncManager.slidingSync!.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId); - await p; - expect(store.orderedLists[tagId]).toEqual([roomA, roomC]); - }); -}); From 4d97af0baf1a4fb2b89a8689b54f569b883b0148 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 18:07:41 +0000 Subject: [PATCH 16/78] Update dependency caniuse-lite to v1.0.30001704 (#29526) * Update dependency caniuse-lite to v1.0.30001704 * Update tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- package.json | 2 +- test/unit-tests/SupportedBrowser-test.ts | 4 ++-- yarn.lock | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index a62686b89d..65adce904d 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "@types/react-dom": "18.3.5", "oidc-client-ts": "3.1.0", "jwt-decode": "4.0.0", - "caniuse-lite": "1.0.30001701", + "caniuse-lite": "1.0.30001704", "testcontainers": "10.21.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0", "wrap-ansi": "npm:wrap-ansi@^7.0.0" diff --git a/test/unit-tests/SupportedBrowser-test.ts b/test/unit-tests/SupportedBrowser-test.ts index 2da2016392..3769798d46 100644 --- a/test/unit-tests/SupportedBrowser-test.ts +++ b/test/unit-tests/SupportedBrowser-test.ts @@ -68,9 +68,9 @@ describe("SupportedBrowser", () => { // Latest Firefox on macOS Sonoma "Mozilla/5.0 (Macintosh; Intel Mac OS X 14.7; rv:135.0) Gecko/20100101 Firefox/135.0", // Latest Edge on Windows - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/131.0.2903.86", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/134.0.3124.72", // Latest Edge on macOS - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/131.0.2903.86", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/134.0.3124.72", // Latest Firefox on Windows "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0", // Latest Firefox on Linux diff --git a/yarn.lock b/yarn.lock index 390c172af9..172b151347 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4653,10 +4653,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@1.0.30001701, caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001688: - version "1.0.30001701" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001701.tgz#ad9c90301f7153cf6b3314d16cc30757285bf9e7" - integrity sha512-faRs/AW3jA9nTwmJBSO1PQ6L/EOgsB5HMQQq4iCu5zhPgVVgO/pZRHlmatwijZKetFw8/Pr4q6dEN8sJuq8qTw== +caniuse-lite@1.0.30001704, caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001688: + version "1.0.30001704" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001704.tgz#6644fe909d924ac3a7125e8a0ab6af95b1f32990" + integrity sha512-+L2IgBbV6gXB4ETf0keSvLr7JUrRVbIaB/lrQ1+z8mRcQiisG5k+lG6O4n6Y5q6f5EuNfaYXKgymucphlEXQew== chalk@5.2.0: version "5.2.0" From 98470b804580c4e4bffc452efa2e4222feed83f3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 18:08:06 +0000 Subject: [PATCH 17/78] Update all non-major dependencies (#29533) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 4 +- yarn.lock | 113 ++++++++++++++++++++++++--------------------------- 2 files changed, 56 insertions(+), 61 deletions(-) diff --git a/package.json b/package.json index 65adce904d..cc4e06fdd1 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "@playwright/test": "1.51.0", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", - "oidc-client-ts": "3.1.0", + "oidc-client-ts": "3.2.0", "jwt-decode": "4.0.0", "caniuse-lite": "1.0.30001704", "testcontainers": "10.21.0", @@ -274,7 +274,7 @@ "postcss-preset-env": "^10.0.0", "postcss-scss": "^4.0.4", "postcss-simple-vars": "^7.0.1", - "prettier": "3.5.2", + "prettier": "3.5.3", "process": "^0.11.10", "raw-loader": "^4.0.2", "rimraf": "^6.0.0", diff --git a/yarn.lock b/yarn.lock index 172b151347..23e8cd8ce1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2527,10 +2527,10 @@ "@sentry-internal/browser-utils" "9.6.0" "@sentry/core" "9.6.0" -"@sentry/babel-plugin-component-annotate@3.2.1": - version "3.2.1" - resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.2.1.tgz#90e40750a63ef5ce3d67498ed0cfccc587ab4cd8" - integrity sha512-tUp2e+CERpRFzTftjPxt7lg4BF0R3K+wGfeJyIqrc0tbJ2y6duT8OD0ArWoOi1g8xQ73NDn1/mEeS8pC+sbjTQ== +"@sentry/babel-plugin-component-annotate@3.2.2": + version "3.2.2" + resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.2.2.tgz#0c5f26e417b8f524924fa4531b82ad5603216e90" + integrity sha512-D+SKQ266ra/wo87s9+UI/rKQi3qhGPCR8eSCDe0VJudhjHsqyNU+JJ5lnIGCgmZaWFTXgdBP/gdr1Iz1zqGs4Q== "@sentry/browser@^9.0.0": version "9.6.0" @@ -2543,13 +2543,13 @@ "@sentry-internal/replay-canvas" "9.6.0" "@sentry/core" "9.6.0" -"@sentry/bundler-plugin-core@3.2.1": - version "3.2.1" - resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.2.1.tgz#9c1c8b9acaca3ae5eeae2a04529f70212dd2937e" - integrity sha512-1wId05LXf6LyTeNwqyhSDSWYbYtFT/NQRqq3sW7hcL4nZuAgzT82PSvxeeCgR/D2qXOj7RCYXXZtyWzzo3wtXA== +"@sentry/bundler-plugin-core@3.2.2": + version "3.2.2" + resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.2.2.tgz#c9193b0c97acf0097fdb820d86eaaad9c9b6b2c4" + integrity sha512-YGrtmqQ2jMixccX2slVG/Lw7pCGJL3DGB3clmY9mO8QBEBIN3/gEANiHJVWwRidpUOS/0b7yVVGAdwZ87oPwTg== dependencies: "@babel/core" "^7.18.5" - "@sentry/babel-plugin-component-annotate" "3.2.1" + "@sentry/babel-plugin-component-annotate" "3.2.2" "@sentry/cli" "2.42.2" dotenv "^16.3.1" find-up "^5.0.0" @@ -2617,11 +2617,11 @@ integrity sha512-t51h6HKlPYW3TfeM09mZ6uDd95A7lgYpD5lUV54ilBA3TefS+M9I32MKwAW7yHzzWs0WQxOdm56eoDBOmRDpHQ== "@sentry/webpack-plugin@^3.0.0": - version "3.2.1" - resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-3.2.1.tgz#83756c64c59a2be39e05beda30b08753f7b80780" - integrity sha512-wP/JDljhB9pCFc62rSwWbIglF2Os8FLV68pQuyJnmImM9cjGjlK6UO+qKa2pOLYsmAcnn+t3Bhu77bbzPIStCg== + version "3.2.2" + resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-3.2.2.tgz#716ab462279c25cea17490d02cb1d22b00f3f661" + integrity sha512-6OkVKNOjKk8P9j7oh6svZ+kEP1i9YIHBC2aGWL2XsgeZTIrMBxJAXtOf+qSrfMAxEtibSroGVOMQc/y3WJTQtg== dependencies: - "@sentry/bundler-plugin-core" "3.2.1" + "@sentry/bundler-plugin-core" "3.2.2" unplugin "1.0.1" uuid "^9.0.0" @@ -3830,12 +3830,12 @@ acorn@^8.0.4, acorn@^8.1.0, acorn@^8.11.0, acorn@^8.9.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.13.0.tgz#2a30d670818ad16ddd6a35d3842dacec9e5d7ca3" integrity sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w== -acorn@^8.14.0, acorn@^8.8.1, acorn@^8.8.2: +acorn@^8.14.0: version "8.14.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== -acorn@^8.4.1: +acorn@^8.4.1, acorn@^8.8.1, acorn@^8.8.2: version "8.14.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb" integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== @@ -5018,9 +5018,9 @@ core-js-compat@^3.40.0: browserslist "^4.24.4" core-js@^3.0.0, core-js@^3.38.1: - version "3.40.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.40.0.tgz#2773f6b06877d8eda102fc42f828176437062476" - integrity sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ== + version "3.41.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.41.0.tgz#57714dafb8c751a6095d028a7428f1fb5834a776" + integrity sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA== core-util-is@~1.0.0: version "1.0.3" @@ -5173,14 +5173,14 @@ css-loader@^7.0.0: semver "^7.5.4" css-minimizer-webpack-plugin@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-7.0.0.tgz#b77a3d2f7c0fd02d3ac250dcc2f79065363f3cd3" - integrity sha512-niy66jxsQHqO+EYbhPuIhqRQ1mNcNVUHrMnkzzir9kFOERJUaQDDRhh7dKDz33kBpkWMF9M8Vx0QlDbc5AHOsw== + version "7.0.2" + resolved "https://registry.yarnpkg.com/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-7.0.2.tgz#aa1b01c6033f5b2f86ddb60c1f5bddd012b50cac" + integrity sha512-nBRWZtI77PBZQgcXMNqiIXVshiQOVLGSf2qX/WZfG8IQfMbeHUMXaBWQmiiSTmPJUflQxHjZjzAmuyO7tpL2Jg== dependencies: "@jridgewell/trace-mapping" "^0.3.25" - cssnano "^7.0.1" + cssnano "^7.0.4" jest-worker "^29.7.0" - postcss "^8.4.38" + postcss "^8.4.40" schema-utils "^4.2.0" serialize-javascript "^6.0.2" @@ -5301,7 +5301,7 @@ cssnano-utils@^5.0.0: resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-5.0.0.tgz#b53a0343dd5d21012911882db6ae7d2eae0e3687" integrity sha512-Uij0Xdxc24L6SirFr25MlwC2rCFX6scyUmuKpzI+JQ7cyqDEwD42fJ0xfB3yLfOnRDU5LKGgjQ9FA6LYh76GWQ== -cssnano@^7.0.1: +cssnano@^7.0.4: version "7.0.6" resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-7.0.6.tgz#63d54fd42bc017f6aaed69e47d9aaef85b7850ec" integrity sha512-54woqx8SCbp8HwvNZYn68ZFAepuouZW4lTwiMVnBErM3VkO7/Sd4oTOt3Zz3bPx3kxQ36aISppyXj2Md4lg8bw== @@ -5693,9 +5693,9 @@ domutils@^2.5.2, domutils@^2.8.0: domhandler "^4.2.0" domutils@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" - integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== + version "3.2.2" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78" + integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw== dependencies: dom-serializer "^2.0.0" domelementtype "^2.3.0" @@ -6035,9 +6035,9 @@ eslint-config-google@^0.14.0: integrity sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw== eslint-config-prettier@^10.0.0: - version "10.0.2" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.0.2.tgz#47444de8aa104ce82c2f91ad2a5e96b62c01e20d" - integrity sha512-1105/17ZIMjmCOJOPNfVdbXafLCLj3hPmkmB7dLgt7XsQ/zkxSuDerE/xgO3RxoHysR1N1whmquY0lSn2O0VLg== + version "10.1.1" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.1.tgz#cf0ff6e5c4e7e15f129f1f1ce2a5ecba92dec132" + integrity sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw== eslint-import-resolver-node@^0.3.9: version "0.3.9" @@ -8426,9 +8426,9 @@ kleur@^3.0.3: integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== knip@^5.36.2: - version "5.45.0" - resolved "https://registry.yarnpkg.com/knip/-/knip-5.45.0.tgz#18eda62ffca7ec9cbee7732470b8415ae29e2d8f" - integrity sha512-OUyO9FUEVCM6/j0gl+PP/LDeJEs4hIdE8n4vK4xrtjN1g3Qu4Ws1oexbWTCJ+8xt8Tgse4Yvhx96OqF/UVl3Ug== + version "5.46.0" + resolved "https://registry.yarnpkg.com/knip/-/knip-5.46.0.tgz#8444881f9382901b4022775ba99e2df4de1df991" + integrity sha512-WedHSK5xNBWYgm64Rt5B9b0CVXL2kRBcyCeet3NHgdv9en3QE4AWSDPEiX48NoPUBW3h//9S0VwLF5MG/MPi3g== dependencies: "@nodelib/fs.walk" "3.0.1" "@snyk/github-codeowners" "1.1.0" @@ -8499,12 +8499,7 @@ lie@~3.3.0: dependencies: immediate "~3.0.5" -lilconfig@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.2.tgz#e4a7c3cb549e3a606c8dcc32e5ae1005e62c05cb" - integrity sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow== - -lilconfig@^3.1.3: +lilconfig@^3.1.2, lilconfig@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.3.tgz#a1bcfd6257f9585bf5ae14ceeebb7b559025e4c4" integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw== @@ -8542,9 +8537,9 @@ linkifyjs@4.2.0: integrity sha512-pCj3PrQyATaoTYKHrgWRF3SJwsm61udVh+vuls/Rl6SptiDhgE7ziUIudAedRY9QEfynmM7/RmLEfPUyw1HPCw== lint-staged@^15.0.2: - version "15.4.3" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.4.3.tgz#e73587cc857f580c99f907abefe9ac8d8d5e74c1" - integrity sha512-FoH1vOeouNh1pw+90S+cnuoFwRfUD9ijY2GKy5h7HS3OR7JVir2N2xrsa0+Twc1B7cW72L+88geG5cW4wIhn7g== + version "15.5.0" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.5.0.tgz#fa6464cfb06e0faf5bb167f83186e952ff6e569e" + integrity sha512-WyCzSbfYGhK7cU+UuDDkzUiytbfbi0ZdPy2orwtM75P3WTtQBzmG40cCxIa8Ii2+XjfxzLH6Be46tUfWS85Xfg== dependencies: chalk "^5.4.1" commander "^13.1.0" @@ -9282,10 +9277,10 @@ obuf@^1.0.0, obuf@^1.1.2: resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== -oidc-client-ts@3.1.0, oidc-client-ts@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/oidc-client-ts/-/oidc-client-ts-3.1.0.tgz#28d3254951a1c64cc9780042c61492a71b2240dd" - integrity sha512-IDopEXjiwjkmJLYZo6BTlvwOtnlSniWZkKZoXforC/oLZHC9wkIxd25Kwtmo5yKFMMVcsp3JY6bhcNJqdYk8+g== +oidc-client-ts@3.2.0, oidc-client-ts@^3.0.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/oidc-client-ts/-/oidc-client-ts-3.2.0.tgz#37e6e563bfeabc4aa1497a9a963f8e3d73168316" + integrity sha512-wUvVcG3SXzZDKHxi/VGQGaTUk9qguMKfYh26Y1zOVrQsu1zp85JWx/SjZzKSXK5j3NA1RcasgMoaHe6gt1WNtw== dependencies: jwt-decode "^4.0.0" @@ -9647,11 +9642,11 @@ postcss-attribute-case-insensitive@^7.0.1: postcss-selector-parser "^7.0.0" postcss-calc@^10.0.2: - version "10.0.2" - resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-10.0.2.tgz#15f01635a27b9d38913a98c4ef2877f5b715b439" - integrity sha512-DT/Wwm6fCKgpYVI7ZEWuPJ4az8hiEHtCUeYjZXqU7Ou4QqYh1Df2yCQ7Ca6N7xqKPFkxN3fhf+u9KSoOCJNAjg== + version "10.1.1" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-10.1.1.tgz#52b385f2e628239686eb6e3a16207a43f36064ca" + integrity sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw== dependencies: - postcss-selector-parser "^6.1.2" + postcss-selector-parser "^7.0.0" postcss-value-parser "^4.2.0" postcss-clamp@^4.1.0: @@ -10244,7 +10239,7 @@ postcss@8.4.46: picocolors "^1.1.0" source-map-js "^1.2.1" -postcss@^8.3.11, postcss@^8.4.33, postcss@^8.4.38: +postcss@^8.3.11, postcss@^8.4.33: version "8.4.47" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365" integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ== @@ -10253,7 +10248,7 @@ postcss@^8.3.11, postcss@^8.4.33, postcss@^8.4.38: picocolors "^1.1.0" source-map-js "^1.2.1" -postcss@^8.5.3: +postcss@^8.4.40, postcss@^8.5.3: version "8.5.3" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.3.tgz#1463b6f1c7fb16fe258736cba29a2de35237eafb" integrity sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A== @@ -10286,10 +10281,10 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier@3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.5.2.tgz#d066c6053200da0234bf8fa1ef45168abed8b914" - integrity sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg== +prettier@3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.5.3.tgz#4fc2ce0d657e7a02e602549f053b239cb7dfe1b5" + integrity sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw== prettier@^2.6.2: version "2.8.8" @@ -11951,9 +11946,9 @@ temporal-spec@^0.2.4: integrity sha512-lDMFv4nKQrSjlkHKAlHVqKrBG4DyFfa9F74cmBZ3Iy3ed8yvWnlWSIdi4IKfSqwmazAohBNwiN64qGx4y5Q3IQ== terser-webpack-plugin@^5.3.11, terser-webpack-plugin@^5.3.9: - version "5.3.12" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.12.tgz#d9518c80493081bace668aa8613b22e4a838810c" - integrity sha512-jDLYqo7oF8tJIttjXO6jBY5Hk8p3A8W4ttih7cCEq64fQFWmgJ4VqAQjKr7WwIDlmXKEc6QeoRb5ecjZ+2afcg== + version "5.3.14" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz#9031d48e57ab27567f02ace85c7d690db66c3e06" + integrity sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw== dependencies: "@jridgewell/trace-mapping" "^0.3.25" jest-worker "^27.4.5" From d2c334dd250755c5475534121ba1546289add425 Mon Sep 17 00:00:00 2001 From: ElementRobot Date: Wed, 19 Mar 2025 06:16:03 +0000 Subject: [PATCH 18/78] [create-pull-request] automated change (#29540) Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com> --- playwright/testcontainers/synapse.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index f9ea2cd1c7..c7c9a96d23 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers"; -const TAG = "develop@sha256:65a8084668f7b468310cf4e1f29ac6987439ab3f4cb5608d6e02ef0acb819b67"; +const TAG = "develop@sha256:ab201cdfc4d8ddae71d7157132aa1da6e479b9247aafeaab310c9a8af1de6a44"; /** * SynapseContainer which freezes the docker digest to stabilise tests, From 35aed69604aa3df44ee77418b4a97fa2553f4ca5 Mon Sep 17 00:00:00 2001 From: ElementRobot Date: Wed, 19 Mar 2025 06:20:49 +0000 Subject: [PATCH 19/78] [create-pull-request] automated change (#29541) Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com> --- src/i18n/strings/cy.json | 24 +++++++++++++ src/i18n/strings/fr.json | 23 +++++++++++++ src/i18n/strings/nb_NO.json | 21 +++++++++++ src/i18n/strings/pl.json | 69 ++++++++++++++++++++++++++++++++++++- src/i18n/strings/uk.json | 27 ++++++++++++++- 5 files changed, 162 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/cy.json b/src/i18n/strings/cy.json index a5614f8cd5..4cfd2a8921 100644 --- a/src/i18n/strings/cy.json +++ b/src/i18n/strings/cy.json @@ -447,6 +447,7 @@ "access_token": "Tocyn Mynediad", "accessibility": "Hygyrchedd", "advanced": "Uwch", + "all_chats": "Pob Sgwrs", "analytics": "Dadansoddi Gwe", "appearance": "Gwedd", "application": "Rhaglen", @@ -1983,6 +1984,19 @@ "add_space_label": "Ychwanegu gofod", "breadcrumbs_empty": "Dim ystafelloedd yr ymwelwyd â nhw yn ddiweddar", "breadcrumbs_label": "Ymwelwyd ag ystafelloedd yn ddiweddar", + "empty": { + "no_chats": "Dim sgyrsiau eto", + "no_chats_description": "Dechreuwch drwy anfon neges at rywun neu drwy greu ystafell", + "no_chats_description_no_room_rights": "Dechreuwch trwy anfon neges at rywun", + "no_favourites": "Nid oes gennych hoff sgwrs eto", + "no_favourites_description": "Gallwch ychwanegu sgwrs at eich ffefrynnau yn y gosodiadau sgwrsio", + "no_people": "Nid oes gennych chi sgyrsiau uniongyrchol gydag unrhyw un eto", + "no_people_description": "Gallwch ddad-ddewis hidlwyr er mwyn gweld eich sgyrsiau eraill", + "no_rooms": "Nid ydych mewn unrhyw ystafell eto", + "no_rooms_description": "Gallwch ddad-ddewis hidlwyr er mwyn gweld eich sgyrsiau eraill", + "no_unread": "Llongyfarchiadau! Nid oes gennych unrhyw negeseuon heb eu darllen", + "show_chats": "Dangos pob sgwrs" + }, "failed_add_tag": "Wedi methu ag ychwanegu tag %(tagName)s i'r ystafell", "failed_remove_tag": "Wedi methu â thynnu'r tag %(tagName)s o'r ystafell", "failed_set_dm_tag": "Wedi methu gosod tag neges uniongyrchol", @@ -1995,10 +2009,19 @@ "home_menu_label": "Dewisiadau cartref", "join_public_room_label": "Ymuno â'r ystafell gyhoeddus", "list_title": "Rhestr ystafelloedd", + "more_options": { + "copy_link": "Copïo dolen ystafell", + "favourited": "Ffafrio", + "leave_room": "Gadael yr ystafell", + "low_priority": "Blaenoriaeth isel", + "mark_read": "Marcio fel wedi'i ddarllen", + "mark_unread": "Marcio fel heb ei ddarllen" + }, "notification_options": "Dewisiadau hysbysu", "open_space_menu": "Agor dewislen gofod", "primary_filters": "Hidlau rhestr ystafelloedd", "room": { + "more_options": "Rhagor o Ddewisiadau", "open_room": "Agor ystafell %(roomName)s" }, "show_less": "Dangos llai", @@ -2502,6 +2525,7 @@ "inline_url_previews_room": "Galluogi rhagolygon URL fel rhagosodiad ar gyfer cyfranogwyr yn yr ystafell hon", "inline_url_previews_room_account": "Galluogi rhagolygon URL ar gyfer yr ystafell hon (yn effeithio arnoch chi yn unig)", "insert_trailing_colon_mentions": "Mewnosod colon sy'n llusgo ar ôl i'r defnyddiwr sôn amdano ar ddechrau neges", + "invite_avatars": "Dangos afatarau ystafelloedd y cawsoch eich gwahodd iddynn nhw", "jump_to_bottom_on_send": "Neidio i waelod y llinell amser pan fyddwch chi'n anfon neges", "key_backup": { "backup_in_progress": "Mae copi wrth gefn o'ch allweddi (gallai'r copi wrth gefn cyntaf gymryd ychydig funudau).", diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index f4edcccf82..bfd2e2608e 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -64,6 +64,7 @@ "go": "C’est parti", "go_back": "Revenir en arrière", "got_it": "Compris", + "hide": "Masquer", "hide_advanced": "Masquer les paramètres avancés", "hold": "Mettre en pause", "ignore": "Ignorer", @@ -2095,6 +2096,19 @@ "add_space_label": "Ajouter un espace", "breadcrumbs_empty": "Aucun salon visité récemment", "breadcrumbs_label": "Salons visités récemment", + "empty": { + "no_chats": "Pas encore de discussions", + "no_chats_description": "Commencez par envoyer un message à quelqu'un ou en créant un salon", + "no_chats_description_no_room_rights": "Commencez par envoyer un message à quelqu'un", + "no_favourites": "Vous n'avez pas encore de discussion favorite", + "no_favourites_description": "Vous pouvez ajouter une discussion à vos favoris dans les paramètres de discussion", + "no_people": "Vous n'avez encore de discussions", + "no_people_description": "Veuillez désélectionner des filtres pour voir vos discussions", + "no_rooms": "Vous n’êtes membre d’aucun salon", + "no_rooms_description": "Veuillez désélectionner des filtres pour voir vos discussions", + "no_unread": "Félicitations ! Vous n'avez aucun message non lu", + "show_chats": "Afficher toutes les discussions" + }, "failed_add_tag": "Échec de l’ajout de l’étiquette %(tagName)s au salon", "failed_remove_tag": "Échec de la suppression de l’étiquette %(tagName)s du salon", "failed_set_dm_tag": "Échec de l’ajout de l’étiquette de conversation privée", @@ -2111,6 +2125,14 @@ "other": "Vous êtes en train de rejoindre %(count)s salons" }, "list_title": "Liste de salons", + "more_options": { + "copy_link": "Copier le lien du salon", + "favourited": "Favorisé", + "leave_room": "Quitter le salon", + "low_priority": "Priorité basse", + "mark_read": "Marquer comme lu", + "mark_unread": "Marquer comme non lu" + }, "notification_options": "Paramètres de notifications", "open_space_menu": "Ouvrir le menu de l’espace", "primary_filters": "Filtre de la liste des salons", @@ -2119,6 +2141,7 @@ "other": "Actuellement en train de supprimer les messages dans %(count)s salons" }, "room": { + "more_options": "Plus d’options", "open_room": "Ouvrir salon %(roomName)s" }, "show_less": "En voir moins", diff --git a/src/i18n/strings/nb_NO.json b/src/i18n/strings/nb_NO.json index d609580baa..cdf5f0a620 100644 --- a/src/i18n/strings/nb_NO.json +++ b/src/i18n/strings/nb_NO.json @@ -64,6 +64,7 @@ "go": "Gå", "go_back": "Gå tilbake", "got_it": "Jeg forstår", + "hide": "Skjul", "hide_advanced": "Skjul avansert", "hold": "Hold", "ignore": "Ignorer", @@ -455,6 +456,7 @@ "access_token": "Tilgangstoken", "accessibility": "Tilgjengelighet", "advanced": "Avansert", + "all_chats": "Alle chatter", "analytics": "Statistikk", "and_n_others": { "og %(count)s andre …": "other", @@ -2095,6 +2097,16 @@ "add_space_label": "Legg til område", "breadcrumbs_empty": "Ingen nylig besøkte rom", "breadcrumbs_label": "Nylig besøkte rom", + "empty": { + "no_chats": "Ingen chatter ennå", + "no_chats_description": "Kom i gang ved å sende meldinger til noen eller ved å opprette et rom", + "no_chats_description_no_room_rights": "Kom i gang med å sende meldinger til noen", + "no_favourites_description": "Du kan legge til en chat til dine favoritter i chat-innstillingene", + "no_people": "Du har ikke direkte chatter med noen ennå", + "no_rooms": "Du er ikke med i noen rom ennå", + "no_unread": "Gratulerer! Du har ingen uleste meldinger", + "show_chats": "Vis alle chatter" + }, "failed_add_tag": "Kunne ikke legge til tagg %(tagName)s til rom", "failed_remove_tag": "Kunne ikke fjerne tagg %(tagName)s fra rommet", "failed_set_dm_tag": "Kan ikke sette kode på direktemeldingen", @@ -2111,6 +2123,14 @@ "other": "Blir for øyeblikket med i %(count)s rom" }, "list_title": "Romliste", + "more_options": { + "copy_link": "Kopier romlenke", + "favourited": "Favorittmerket", + "leave_room": "Forlat rommet", + "low_priority": "Lav prioritet", + "mark_read": "Marker som lest", + "mark_unread": "Marker som ulest" + }, "notification_options": "Varselsinnstillinger", "open_space_menu": "Åpne Område-meny", "primary_filters": "Filtre for romliste", @@ -2119,6 +2139,7 @@ "other": "Fjerner for øyeblikket meldinger i %(count)s rom" }, "room": { + "more_options": "Flere alternativer", "open_room": "Åpne rom %(roomName)s" }, "show_less": "Vis mindre", diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index 2f96ced9e5..249902cc70 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -64,6 +64,7 @@ "go": "Przejdź", "go_back": "Wróć", "got_it": "Rozumiem", + "hide": "Ukryj", "hide_advanced": "Ukryj zaawansowane", "hold": "Wstrzymaj", "ignore": "Ignoruj", @@ -407,6 +408,15 @@ "download_logs": "Pobierz dzienniki", "downloading_logs": "Pobieranie logów", "error_empty": "Powiedz nam, co poszło nie tak, lub nawet lepiej - utwórz zgłoszenie na platformie GitHub, które opisuje problem.", + "failed_download_logs": "Nie udało się pobrać dzienników debugowania: ", + "failed_send_logs_causes": { + "disallowed_app": "Twój raport o błędzie został odrzucony. Serwer gniewnego potrząśnięcia nie obsługuje tej aplikacji.", + "rejected_generic": "Twój raport o błędzie został odrzucony. Serwer gniewnego potrząśnięcia odrzucił zawartość zgłoszenia ze względu na jej politykę.", + "rejected_recovery_key": "Twój raport o błędzie został odrzucony ze względów bezpieczeństwa, ponieważ zawierał on klucz przywracania.", + "rejected_version": "Twój raport o błędzie został odrzucony, ponieważ wersja aplikacji jest zbyt stara.", + "server_unknown_error": "Serwer gniewnego potrząśnięcia napotkał nieznany błąd i nie mógł obsłużyć raportu.", + "unknown_error": "Nie udało się wysłać logów." + }, "github_issue": "Zgłoszenie GitHub", "introduction": "Jeśli zgłosiłeś błąd za pomocą serwisu GitHub, dzienniki debugowania mogą pomóc nam w namierzeniu problemu. ", "log_request": "Aby uniknąć tego problemu w przyszłości, wyślij nam dzienniki.", @@ -446,6 +456,7 @@ "access_token": "Token dostępu", "accessibility": "Ułatwienia dostępu", "advanced": "Zaawansowane", + "all_chats": "Wszystkie czaty", "analytics": "Analityka", "and_n_others": { "one": "i jeden inny...", @@ -1305,6 +1316,7 @@ "error_connecting_heading": "Nie udało się połączyć z menedżerem integracji", "explainer": "Zarządcy integracji otrzymują dane konfiguracji, mogą modyfikować widżety, wysyłać zaproszenia do pokojów i ustawiać poziom uprawnień w Twoim imieniu.", "manage_title": "Zarządzaj integracjami", + "toggle_label": "Włącz menedżer integracji", "use_im": "Użyj zarządcy integracji aby zarządzać botami, widżetami i pakietami naklejek.", "use_im_default": "Użyj zarządcy Integracji %(serverName)s aby zarządzać botami, widżetami i pakietami naklejek." }, @@ -2098,20 +2110,54 @@ "add_space_label": "Dodaj przestrzeń", "breadcrumbs_empty": "Brak ostatnio odwiedzonych pokojów", "breadcrumbs_label": "Ostatnio odwiedzane pokoje", + "empty": { + "no_chats": "Nie ma jeszcze czatów", + "no_chats_description": "Zacznij od wysłania wiadomości lub utworzenia pokoju", + "no_chats_description_no_room_rights": "Wyślij komuś wiadomość, aby rozpocząć.", + "no_favourites": "Nie masz jeszcze ulubionego czatu", + "no_favourites_description": "Dodaj czat do ulubionych w ustawieniach czatu", + "no_people": "Nie prowadzisz jeszcze z nikim czatów prywatnych", + "no_people_description": "Wyczyść filtry, aby zobaczyć pozostałe czaty", + "no_rooms": "Nie jesteś jeszcze w żadnym pokoju", + "no_rooms_description": "Wyczyść filtry, aby zobaczyć pozostałe czaty", + "no_unread": "Brawo! Nie masz żadnych nieprzeczytanych wiadomości", + "show_chats": "Pokaż wszystkie czaty" + }, "failed_add_tag": "Nie można dodać tagu %(tagName)s do pokoju", "failed_remove_tag": "Nie udało się usunąć tagu %(tagName)s z pokoju", "failed_set_dm_tag": "Nie udało się ustawić tagu wiadomości prywatnych", + "filters": { + "favourite": "Ulubione", + "people": "Osoby", + "rooms": "Pokoje", + "unread": "Nieprzeczytane" + }, "home_menu_label": "Opcje głównej", "join_public_room_label": "Dołącz do publicznego pokoju", "joining_rooms_status": { "one": "Aktualnie dołączanie do %(count)s pokoju", "other": "Aktualnie dołączanie do %(count)s pokoi" }, + "list_title": "Lista pokoi", + "more_options": { + "copy_link": "Kopiuj link pokoju", + "favourited": "Ulubiony", + "leave_room": "Opuść pokój", + "low_priority": "Niski priorytet", + "mark_read": "Oznacz jako przeczytane", + "mark_unread": "Oznacz jako nieprzeczytane" + }, "notification_options": "Opcje powiadomień", + "open_space_menu": "Otwórz menu przestrzeni", + "primary_filters": "Filtry listy pokoi", "redacting_messages_status": { "one": "Aktualnie usuwanie wiadomości z %(count)s pokoju", "other": "Aktualnie usuwanie wiadomości z %(count)s pokoi" }, + "room": { + "more_options": "Więcej opcji", + "open_room": "Pokój otwarty %(roomName)s" + }, "show_less": "Pokaż mniej", "show_n_more": { "one": "Pokaż %(count)s więcej", @@ -2123,6 +2169,10 @@ "sort_by_activity": "Aktywności", "sort_by_alphabet": "A-Z", "sort_unread_first": "Pokazuj najpierw pokoje z nieprzeczytanymi wiadomościami", + "space_menu": { + "home": "Główna przestrzeni", + "space_settings": "Ustawienia przestrzeni" + }, "space_menu_label": "menu %(spaceName)s", "sublist_options": "Ustawienia listy", "suggested_rooms_heading": "Sugerowane pokoje" @@ -2485,20 +2535,35 @@ "breadcrumb_title_forgot": "Nie pamiętasz klucza przywracania? Musisz zresetować swoją tożsamość.", "breadcrumb_warning": "Zrób to tylko wtedy, gdy uważasz, że Twoje konto zostało naruszone.", "details_title": "Szczegóły szyfrowania", + "do_not_close_warning": "Nie zamykaj tego okna, dopóki reset nie zostanie zakończony", "export_keys": "Eksportuj klucze", "import_keys": "Importuj klucze", "other_people_device_description": "Domyślnie w pokojach szyfrowanych nie będziesz mógł wysyłać wiadomości, jeśli nie zweryfikujesz członków w pokoju", "other_people_device_label": "Nigdy nie wysyłaj wiadomości szyfrowanych do niezweryfikowanych urządzeń", "other_people_device_title": "Urządzenia innych osób", "reset_identity": "Zresetuj tożsamość kryptograficzną", + "reset_in_progress": "Resetowanie w toku...", "session_id": "ID sesji:", "session_key": "Klucz sesji:", "title": "Zaawansowane" }, + "delete_key_storage": { + "breadcrumb_page": "Usuń magazyn kluczy", + "confirm": "Usuń magazyn kluczy", + "description": "Usunięcie magazynu kluczy usunie Twoją tożsamość kryptograficzną, klucze wiadomości z serwera i wyłączy następujące funkcje bezpieczeństwa:", + "list_first": "Nowe urządzenia nie będą posiadały Twojej historii wiadomości szyfrowanych", + "list_second": "Utracisz dostęp do swoich wiadomości szyfrowanych, jeśli wylogujesz się wszędzie z %(brand)s", + "title": "Czy na pewno chcesz wyłączyć magazyn kluczy i go wyłączyć?" + }, "device_not_verified_button": "Zweryfikuj to urządzenie", "device_not_verified_description": "Aby wyświetlić ustawienia szyfrowania, musisz zweryfikować to urządzenie.", "device_not_verified_title": "Urządzenie niezweryfikowane", "dialog_title": "Ustawienia: Szyfrowanie", + "key_storage": { + "allow_key_storage": "Zezwól na magazynowanie kluczy", + "description": "Przechowuj swoją tożsamość kryptograficzną i klucze wiadomości bezpiecznie na serwerze. Umożliwi Ci to przeglądanie historii wiadomości na wszystkich nowych urządzeniach.Dowiedz się więcej", + "title": "Magazyn kluczy" + }, "recovery": { "change_recovery_confirm_button": "Potwierdź nowy klucz przywracania", "change_recovery_confirm_description": "Wprowadź poniżej nowy klucz przywracania, aby zakończyć. Stary klucz przestanie działać.", @@ -2615,6 +2680,7 @@ "inline_url_previews_room": "Włącz domyślny podgląd URL dla uczestników w tym pokoju", "inline_url_previews_room_account": "Włącz podgląd URL dla tego pokoju (dotyczy tylko Ciebie)", "insert_trailing_colon_mentions": "Wstawiaj dwukropek po wzmiance użytkownika na początku wiadomości", + "invite_avatars": "Pokaż awatary pokoi, do których zostałeś zaproszony", "jump_to_bottom_on_send": "Przejdź na dół osi czasu po wysłaniu wiadomości", "key_backup": { "backup_in_progress": "Tworzy się kopia zapasowa Twoich kluczy (pierwsza kopia może potrwać kilka minut).", @@ -3096,6 +3162,7 @@ "view": "Przegląda pokój z podanym adresem", "whois": "Pokazuje informacje na temat użytkownika" }, + "sliding_sync_legacy_no_longer_supported": "Starsza metoda synchronizacji sliding sync nie jest już obsługiwana: zaloguj się ponownie, aby włączyć nową flagę synchronizacji sliding sync", "space": { "add_existing_room_space": { "create": "Chcesz zamiast tego dodać nowy pokój?", @@ -4069,7 +4136,7 @@ "error_need_to_be_logged_in": "Musisz być zalogowany.", "error_unable_start_audio_stream_description": "Nie można rozpocząć przesyłania strumienia audio.", "error_unable_start_audio_stream_title": "Nie udało się rozpocząć transmisji na żywo", - "modal_data_warning": "Dane na tym ekranie są współdzielone z %(widgetDomain)s", + "modal_data_warning": "Poniższe dane są współdzielone z %(widgetDomain)s", "modal_title_default": "Widżet modalny", "no_name": "Nieznana aplikacja", "open_id_permissions_dialog": { diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json index 3081cdd219..d882295447 100644 --- a/src/i18n/strings/uk.json +++ b/src/i18n/strings/uk.json @@ -455,6 +455,7 @@ "access_token": "Токен доступу", "accessibility": "Доступність", "advanced": "Подробиці", + "all_chats": "Усі бесіди", "analytics": "Аналітика", "and_n_others": { "one": "і інше...", @@ -2115,13 +2116,23 @@ "other": "Приєднання до %(count)s кімнат" }, "list_title": "Список кімнат", + "more_options": { + "copy_link": "Копіювати посилання на кімнату", + "favourited": "Обране", + "leave_room": "Вийти з кімнати", + "low_priority": "Неважливі", + "mark_read": "Позначити прочитаним", + "mark_unread": "Позначити непрочитаним" + }, "notification_options": "Параметри сповіщень", "open_space_menu": "Відкрити меню простору", + "primary_filters": "Фільтри списку кімнат", "redacting_messages_status": { "one": "Триває видалення повідомлень в %(count)s кімнаті", "other": "Триває видалення повідомлень у %(count)s кімнатах" }, "room": { + "more_options": "Інші опції", "open_room": "Відкрити кімнату %(roomName)s" }, "show_less": "Згорнути", @@ -2512,10 +2523,23 @@ "session_key": "Ключ сеансу:", "title": "Додатково" }, + "delete_key_storage": { + "breadcrumb_page": "Видалити сховище ключів", + "confirm": "Видалити сховище ключів", + "description": "Видалення сховища ключів вилучить вашу криптографічну ідентичність і ключі повідомлень з сервера, а також вимкне такі функції безпеки:", + "list_first": "Ви не матимете історії зашифрованих повідомлень на нових пристроях", + "list_second": "Ви втратите доступ до своїх зашифрованих повідомлень, якщо ви вийдете з %(brand)s на всіх пристроях", + "title": "Ви впевнені, що хочете вимкнути зберігання ключів і видалити їх?" + }, "device_not_verified_button": "Верифікувати цей пристрій", "device_not_verified_description": "Верифікуйте цей пристрій, щоб переглянути налаштування шифрування.", "device_not_verified_title": "Пристрій не верифіковано", "dialog_title": "Налаштування: Шифрування", + "key_storage": { + "allow_key_storage": "Дозволити зберігання ключів", + "description": "Надійно зберігайте свою криптографічну ідентичність і ключі повідомлень на сервері. Це дозволить вам переглядати історію повідомлень на будь-яких нових пристроях. Докладніше", + "title": "Сховище ключів" + }, "recovery": { "change_recovery_confirm_button": "Підтвердити новий ключ відновлення", "change_recovery_confirm_description": "Введіть новий ключ відновлення нижче, щоб завершити. Ваш старий ключ більше не працюватиме.", @@ -2632,6 +2656,7 @@ "inline_url_previews_room": "Увімкнути попередній перегляд гіперпосилань за умовчанням для учасників цієї кімнати", "inline_url_previews_room_account": "Увімкнути попередній перегляд гіперпосилань в цій кімнаті (стосується тільки вас)", "insert_trailing_colon_mentions": "Додавати двокрапку після згадки користувача на початку повідомлення", + "invite_avatars": "Показувати аватари кімнат, до яких вас запросили", "jump_to_bottom_on_send": "Переходити вниз стрічки під час надсилання повідомлення", "key_backup": { "backup_in_progress": "Створюється резервна копія ваших ключів (перше копіювання може тривати кілька хвилин).", @@ -4084,7 +4109,7 @@ "error_need_to_be_logged_in": "Вам потрібно увійти.", "error_unable_start_audio_stream_description": "Не вдалося почати аудіотрансляцію.", "error_unable_start_audio_stream_title": "Не вдалося почати живу трансляцію", - "modal_data_warning": "Дані на цьому екрані надсилаються до %(widgetDomain)s", + "modal_data_warning": "Наведені далі дані надсилаються на %(widgetDomain)s", "modal_title_default": "Модальний віджет", "no_name": "Невідомий додаток", "open_id_permissions_dialog": { From 3587161a2ce0a26febe919552dc09db6d2563608 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 19 Mar 2025 09:39:12 +0100 Subject: [PATCH 20/78] New room list: add selection decoration (#29531) * fix(room list): remove 1px extra padding * feat(room list): add selection decoration to room list item and scroll list to this element * test(room list item): add is selected test * test(room list): update snapshot * test(e2e): add test to keep the room list item visible * test(e2e): update snapshots --- .../room-list-panel/room-list.spec.ts | 16 ++++++ .../room-list-item-hover-linux.png | Bin 2293 -> 2293 bytes ...room-list-item-open-more-options-linux.png | Bin 81877 -> 81869 bytes .../views/rooms/RoomListPanel/_RoomList.pcss | 5 -- .../RoomListPanel/_RoomListItemView.pcss | 4 ++ .../views/rooms/RoomListPanel/RoomList.tsx | 9 ++-- .../rooms/RoomListPanel/RoomListItemView.tsx | 8 ++- .../RoomListPanel/RoomListItemView-test.tsx | 15 ++++-- .../__snapshots__/RoomList-test.tsx.snap | 10 ++++ .../RoomListItemView-test.tsx.snap | 51 ++++++++++++++++++ 10 files changed, 106 insertions(+), 12 deletions(-) diff --git a/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts index 493ed0d1ab..ed402bc625 100644 --- a/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts +++ b/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts @@ -77,4 +77,20 @@ test.describe("Room list", () => { await page.getByRole("menuitem", { name: "leave room" }).click(); await expect(roomItem).not.toBeVisible(); }); + + test("should scroll to the current room", async ({ page, app, user }) => { + const roomListView = getRoomList(page); + await roomListView.hover(); + // Scroll to the end of the room list + await page.mouse.wheel(0, 1000); + + await roomListView.getByRole("gridcell", { name: "Open room room0" }).click(); + + const filters = page.getByRole("listbox", { name: "Room list filters" }); + await filters.getByRole("option", { name: "People" }).click(); + await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).not.toBeVisible(); + + await filters.getByRole("option", { name: "People" }).click(); + await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible(); + }); }); diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-hover-linux.png index 45d5588733d88ba0b0061e92c2e7c436ab95195a..0188cb76b2fd4a68dba4a09f7122ac3bdaf818fe 100644 GIT binary patch literal 2293 zcmZ{mdpOe#8^?bmtfYq$<&i^^h*BmwMlnZ247d9Wwi(*NUD|)^u*E8kT}@PoW(%_Cb=c3g#HxwWg;SwSIoNqnaCJwLlR$^v z4|6&AF*)zZ6;su($BrXBJj8P+IQ?78IG!SBIy-wyQ!0rK1^`^lfUia~_YwdIN^WM$ z0Kn}pF>U}b^6>xRjuxT~RlV)U+rtYzIkD>qFm)Kjn%_J7;)2&d5sEQ^f_eSpNT;TAbL2#`$FdP7b=NW z2&Ibo$WhY($-GR$2SvrdHx392Lk1wg_gZ;^aHLLq%<|74UHN%_7vRFbCPzOKBW~GBmhKPYS&4RR4%f31885^r0d)5I;&yE zypnN zCIv*p;DOhM8CZ02&L~A~A$49Syrl9V5Fj(KlGV3Za(R^;8lP%nG?-41^vl}V2AMIR zAU3f(rQ7%3QJ1%qB3WJU5V-6*2xG~!DTjfos@z!5E-bRPev*<>^VGm9TU#Zj(@Q>X zHGJnK)c3k`o}Gqr!jZL`)wjn7?^?1trmpCF14>3n%Bx|iDj9!_F>m;jcz7lCczUj9 zjmxK=>(kp|>|c}Zfta_kv9Wc2J!<`D9*;LRJ_FhXU8)&S zckNwdML|y-t~&r~4Zh%Bc1~%8di$dK)ms@C0|luPnEj&N!Zq-rXq|+Hrl%)QB!~{` z4d_MuR#H`nqp=xvg3peKK8EuaS@SnSC@4awSHYdk?Cg(DaWS4&iiA!d24x7PN%N}B z>Yd${8M+sY!%_cuB73#RMfct}vpCWGenjmYq1!=`WP^a<;i$2!{G1#O&Cm+712jnV z{h4Xhz5CcNOlE4+ndDGmZExchF#!WnWG3(Pnrq)$~qUT3&pHUO4Q|G5ev(&)Nzu_MAmcj_2PslS;l=Kg}2lt5F`@Yv1K#rnT$M_XGB`{nc0#^OBTYD@I_ zH7swjysE6PuP@Dl&0Y1CO|vf@{eTX1b&Zei&oFCikoJ495MrdKzrC|#P8J3p_4M_< z{>lx~(bv7tzx1Ec#(D5rHZb*`?0;;6tiRe@SgG!}(UAdIlqpb9sSZXehj~niuSzAu1 zs1T8nSyUJo06Kc}2s&7svcmngKgCZ@j_+;kE~y|)3JVGf^-tRy;lg%f%N%#U<|cuB zF1NQgpE~h#Z45l^)zbL7rJH+XC|H0)d zpVxP~8UC6651YM`(IR2;Om?r>U>8()$29px-ZrgUdB6d4#XnWiZ0oEzK;{Rtq9$h> znuMHgkl%LzoNH{ey821@coE3v!RCkG!v)YTEiDufEiAT~F8oYphL}zAv7sRN!8}qR*E*OwJ*ia zPd4K<`hY6O5(Fr5M(jeaASOR~`L^^≥z&P@TmFuTz} zXpy9qJtU70EC2{XQ~L|rD?^P+r}B00Jwk{muTB^FSmnR<)_Z;=LxKNdiS`}_cCsCoE6Bcq}_ z-?e!F*;whL0MM!{i0c9Y>vEF&_T+z|gnXTBdGauSuFn2%{1OFPS8raBYOP1?`xwC7 L*alt&yB+&4i)(X^ delta 2291 zcmaKuYdq5r8^(W5OLG2en8TQwvmx46P9c#j%3+0}v>ak&OssyC^KwchlROS3#7YNe zBbviP5gA2>p#zCDr#znL{qwxJKKGmZbKUpn`{KHO%lwv6Pj$u}lMsW60RSN3U~hX0 z00g{tW+PF-9aRiWa?Dm&201Fbe&#oQ*HsgJtfi&qXeJos2HjtLsd@@?!7GL}tXei0 zW7~P;Ooc|VeSszM-SF_1D5f~h?U^b%#ZXe8er}94~X?g!! z2wTdu3avxZU5tvVjEyykv9+O6s(nTK4|g31BH-;e18(k~7iw*ce}qwPxg&yDmBFhIxBnvoL?5tEy3PrxzdW~|__|>nEW0X9nM}o|2A?~Z zd(!u*p{%(eFfMLf^selHGTtS8actMV^_+bW{V{B=hJVh8Jn$2OQ5LJXTfN0*yH9}BS&tK=EQqdDj1 zoEai}c>3P2J~-)J8?%AL!4imKF;$aamyjN0uD^FFBUj{etN+&`Dz+Brsiz7gKtmT{9aLq6o~78!ZT>yjO- zOfX(;|Ngx8$u56WZEfw(pFhv```ot#2}B|!v-3dS3G1RSAIb+E98zT+_+zRVKu%3g zthT~4wMo1)yW-`y#^;t8GJeuMQa9V#xx!TuR8&e@Td#0cadL9Pa?V8X`dX;hluuj3187YYChQ0~koFCLg<^&Fu?)EZ}SMTsQ)+hlH zJ!~C%zNea8YrjwtQ|ia|sfJMD4w{k#tI~s^k2hr2{JKrHqelLo>CLZ=j+N@|-J&jW zUyRb}O-&)*-rkrD54UbYL$|xFL2iH6ahjv#20xlXXkP8PqdQhh8mpybb_eJfSQHO^ z#p}7_@sA59L}BSk(m~*6T}z7}c;6NQrE!!E3UD4ecc3&8^@J(@)GEBu#QRXea3ov? zAjuZxU9T?5?_$p>gb;|EO>g;sp-zoa{iGg6 zn7`w%*EDr-K_;+g_zx51$A^|?auYEbsIw~;`Mt1tG{V@p=JDfwt(I!YgJ+SIoaPM% zEDG0YNTC=BwM$#mHR;Jx@WjM~ zpV4Mx!*TBg5ZQC!cynyQ*FKMs8oVA^JR;&BPMV?N&2Imi5xp@FdM^+E#Qizv(?rSH zs=j^{pRlxbQ8D;fIxJP1yS!HEe%1I&&-BoI@V;9qA)d=QIXNsy=HApVb3$Q7_wPG} zgh=g>y<9sjLsCz^zM;Lc(QU1fp<$EhGF1HNDGlvVIdm2LJyad+!I%kMqYiYWZbby*r5XE$=9D+a~F_KHvA*8rQ%+M2wr?gZIGCxzou6D>JjwM2tem5wkOjE1 z!FkRZrIgw%n}A1ZE3yHj-Y>I|E3#P}N3CdZGS>6gD=2g!jYe~$RM66nBM_{U_YD2w zoYpPOv$=L9&+6(*>-b%t>LU^mel}Zf3DU_RTT)UI+L(&dos0-DC!HRp7Z1GmU;nRe~l8h+o4cUQg%v)}M}LF0JU#G|y%hcNSWvS&SUG z92)SLEN+&j8b@&u5h(%yJ~kd6_Wx>t4ryuO6r|m1_SnuZzu*trnt$kE(8W7F25>lv Kv#qxFz5Z`*Q%C0j diff --git a/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png b/playwright/snapshots/left-panel/room-list-panel/room-list.spec.ts/room-list-item-open-more-options-linux.png index 0501bf1e4c848e3338b9b4f15e22ee9d21b2d20a..083cab778e3fabba0cf7b1ca2d2b88e97f3e95c3 100644 GIT binary patch delta 76178 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

L4q&o8M2}xP7CO3C$e9eQK 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 D@}f#Y diff --git a/res/css/views/rooms/RoomListPanel/_RoomList.pcss b/res/css/views/rooms/RoomListPanel/_RoomList.pcss index 2563c1b675..54798f1ea9 100644 --- a/res/css/views/rooms/RoomListPanel/_RoomList.pcss +++ b/res/css/views/rooms/RoomListPanel/_RoomList.pcss @@ -7,9 +7,4 @@ .mx_RoomList { height: 100%; - - .mx_RoomList_List { - /* Avoid when on hover, the background color to be on top of the right border */ - padding-right: 1px; - } } diff --git a/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss b/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss index e53ba3dc79..2e1d42d62e 100644 --- a/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss +++ b/res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss @@ -47,3 +47,7 @@ .mx_RoomListItemView_menu_open { background-color: var(--cpd-color-bg-action-secondary-hovered); } + +.mx_RoomListItemView_selected { + background-color: var(--cpd-color-bg-action-secondary-pressed); +} diff --git a/src/components/views/rooms/RoomListPanel/RoomList.tsx b/src/components/views/rooms/RoomListPanel/RoomList.tsx index 006d1b9732..67f36b2a19 100644 --- a/src/components/views/rooms/RoomListPanel/RoomList.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomList.tsx @@ -22,10 +22,12 @@ interface RoomListProps { /** * A virtualized list of rooms. */ -export function RoomList({ vm: { rooms } }: RoomListProps): JSX.Element { +export function RoomList({ vm: { rooms, activeIndex } }: RoomListProps): JSX.Element { const roomRendererMemoized = useCallback( - ({ key, index, style }: ListRowProps) => , - [rooms], + ({ key, index, style }: ListRowProps) => ( + + ), + [rooms, activeIndex], ); // The first div is needed to make the virtualized list take all the remaining space and scroll correctly @@ -41,6 +43,7 @@ export function RoomList({ vm: { rooms } }: RoomListProps): JSX.Element { rowHeight={48} height={height} width={width} + scrollToIndex={activeIndex} /> )} diff --git a/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx b/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx index 20173e324e..37ad4ec848 100644 --- a/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomListItemView.tsx @@ -20,12 +20,16 @@ interface RoomListItemViewPropsProps extends React.HTMLAttributes vm.openRoom()} onMouseOver={() => setIsHover(true)} diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx index 3023f9a9a7..217015451c 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomListPanel/RoomListItemView-test.tsx @@ -42,13 +42,13 @@ describe("", () => { test("should render a room item", () => { const onClick = jest.fn(); - const { asFragment } = render(); + const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); test("should call openRoom when clicked", async () => { const user = userEvent.setup(); - render(); + render(); await user.click(screen.getByRole("button", { name: `Open room ${room.name}` })); expect(defaultValue.openRoom).toHaveBeenCalled(); @@ -58,11 +58,20 @@ describe("", () => { mocked(useRoomListItemViewModel).mockReturnValue({ ...defaultValue, showHoverMenu: true }); const user = userEvent.setup(); - render(, withClientContextRenderOptions(matrixClient)); + render(, withClientContextRenderOptions(matrixClient)); const listItem = screen.getByRole("button", { name: `Open room ${room.name}` }); expect(screen.queryByRole("button", { name: "More Options" })).toBeNull(); await user.hover(listItem); await waitFor(() => expect(screen.getByRole("button", { name: "More Options" })).toBeInTheDocument()); }); + + test("should be selected if isSelected=true", async () => { + const { asFragment } = render(); + expect(screen.queryByRole("button", { name: `Open room ${room.name}` })).toHaveAttribute( + "aria-selected", + "true", + ); + expect(asFragment()).toMatchSnapshot(); + }); }); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomList-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomList-test.tsx.snap index 37f8a0364a..00c0793080 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomList-test.tsx.snap +++ b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomList-test.tsx.snap @@ -24,6 +24,7 @@ exports[` should render a room list 1`] = ` > + +`; + exports[` should render a room item 1`] = `

  • @@ -297,7 +382,11 @@ exports[`SpaceHierarchy renders 1`] = `
    - Nested space + + Nested space +
    renders 1`] = ` > Join
    - - - - +
    +
    +
    renders 1`] = ` />
  • @@ -374,7 +487,11 @@ exports[`SpaceHierarchy renders 1`] = `
    - Nested room + + Nested room +
    renders 1`] = ` Join
    - - - - +
    +
    +
    diff --git a/test/unit-tests/components/views/dialogs/__snapshots__/ExportDialog-test.tsx.snap b/test/unit-tests/components/views/dialogs/__snapshots__/ExportDialog-test.tsx.snap index 4d2a2d54a4..bb11ed96b2 100644 --- a/test/unit-tests/components/views/dialogs/__snapshots__/ExportDialog-test.tsx.snap +++ b/test/unit-tests/components/views/dialogs/__snapshots__/ExportDialog-test.tsx.snap @@ -150,28 +150,53 @@ exports[` renders export dialog 1`] = ` - - -
    Lær mer ", + "title": "Nøkkellager" }, "recovery": { "change_recovery_confirm_button": "Bekreft ny gjenopprettingsnøkkel", @@ -3636,12 +3643,12 @@ "send_state_sent": "Meldingen ble sendt", "summary": { "banned": { - "ble bannlyst": "one", - "ble bannlyst %(count)s ganger": "other" + "one": "ble utestengt", + "other": "ble utestengt %(count)s ganger" }, "banned_multiple": { - "ble bannlyst": "one", - "ble bannlyst %(count)s ganger": "other" + "one": "ble utestengt", + "other": "ble utestengt %(count)s ganger" }, "changed_avatar": { "one": "%(oneUser)sendret profilbildet deres", @@ -3652,8 +3659,8 @@ "other": "%(severalUsers)sendret profilbildet deres %(count)s ganger" }, "changed_name": { - "%(oneUser)s endret navnet sitt": "one", - "%(oneUser)sendret navnet sitt %(count)s ganger": "other" + "one": "%(oneUser)s endret navn", + "other": "%(oneUser)s endret navn %(count)s ganger" }, "changed_name_multiple": { "%(severalUsers)s endret navnene sine": "one" @@ -3706,12 +3713,12 @@ "other": "ble fjernet %(count)s ganger" }, "left": { - "%(oneUser)s forlot rommet %(count)s ganger": "other", - "%(oneUser)s forlot rommet": "one" + "one": "%(oneUser)s forlot", + "other": "%(oneUser)s forlot %(count)s ganger" }, "left_multiple": { - "%(severalUsers)s forlot rommet %(count)s ganger": "other", - "%(severalUsers)s forlot rommet": "one" + "one": "%(severalUsers)s forlot", + "other": "%(severalUsers)s forlot %(count)s ganger" }, "no_change": { "one": "%(oneUser)sgjorde ingen endringer", @@ -3762,12 +3769,12 @@ "other": "%(severalUsers)sendret serverens ACLer %(count)s ganger" }, "unbanned": { - "fikk bannlysningen sin opphevet %(count)s ganger": "other", - "fikk bannlysningen sin opphevet": "one" + "one": "fikk opphevet forbudet", + "other": "fikk opphevet forbudet %(count)s ganger" }, "unbanned_multiple": { - "fikk bannlysningene sine opphevet %(count)s ganger": "other", - "fikk bannlysningene sine opphevet": "one" + "one": "fikk opphevet forbudet", + "other": "fikk opphevet forbudet %(count)s ganger" } }, "thread_info_basic": "Fra en tråd", diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json index d882295447..6a5156a14a 100644 --- a/src/i18n/strings/uk.json +++ b/src/i18n/strings/uk.json @@ -64,6 +64,7 @@ "go": "Уперед", "go_back": "Назад", "got_it": "Зрозуміло", + "hide": "Сховати", "hide_advanced": "Сховати розширені", "hold": "Утримувати", "ignore": "Ігнорувати", @@ -2100,6 +2101,19 @@ "add_space_label": "Додати простір", "breadcrumbs_empty": "Немає недавно відвіданих кімнат", "breadcrumbs_label": "Недавно відвідані кімнати", + "empty": { + "no_chats": "Ще немає бесід", + "no_chats_description": "Почніть користування, надіславши комусь повідомлення або створивши кімнату", + "no_chats_description_no_room_rights": "Розпочніть користування, написавши комусь повідомлення", + "no_favourites": "У вас ще немає обраних бесід", + "no_favourites_description": "Ви можете додати бесіду до обраних у її налаштуваннях", + "no_people": "У вас ще немає особистих бесід", + "no_people_description": "Ви можете очистити фільтри, щоб побачити інші ваші бесіди", + "no_rooms": "Ви ще не входили до кімнат", + "no_rooms_description": "Ви можете очистити фільтри, щоб побачити інші ваші бесіди", + "no_unread": "Вітаємо! У вас немає непрочитаних повідомлень", + "show_chats": "Показати всі бесіди" + }, "failed_add_tag": "Не вдалось додати до кімнати мітку %(tagName)s", "failed_remove_tag": "Не вдалося прибрати з кімнати мітку %(tagName)s", "failed_set_dm_tag": "Не вдалося встановити мітку особистого повідомлення", @@ -3138,6 +3152,7 @@ "view": "Перегляд кімнати з вказаною адресою", "whois": "Показує відомості про користувача" }, + "sliding_sync_legacy_no_longer_supported": "Застаріла ковзна синхронізація більше не підтримується: вийдіть і ввійдіть знову, щоб увімкнути новий прапорець ковзної синхронізації", "space": { "add_existing_room_space": { "create": "Хочете додати нову кімнату натомість?", From 0dc295e3b8c706cd2af036a6aa13d9e6bbec9c8f Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Fri, 21 Mar 2025 13:58:00 +0530 Subject: [PATCH 30/78] RoomListStore: Unread filter should only filter rooms having unread counts (#29555) * Use `hasUnreadCount` instead of `isUnread` * Fix broken test * Write test --- .../room-list-panel/room-list.spec.ts | 37 +++++++++++++++++++ .../skip-list/filters/UnreadFilter.ts | 2 +- .../room-list-v3/RoomListStoreV3-test.ts | 4 +- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts index ed402bc625..6f19bf60d1 100644 --- a/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts +++ b/playwright/e2e/left-panel/room-list-panel/room-list.spec.ts @@ -93,4 +93,41 @@ test.describe("Room list", () => { await filters.getByRole("option", { name: "People" }).click(); await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible(); }); + + test("unread filter should only match unread rooms that have a count", async ({ page, app, bot }) => { + const roomListView = getRoomList(page); + // Let's create a new room and invite the bot + const room1Id = await app.client.createRoom({ + name: "Unread Room 1", + invite: [bot.credentials?.userId], + }); + await bot.awaitRoomMembership(room1Id); + + // Let's create another room as well + const room2Id = await app.client.createRoom({ + name: "Unread Room 2", + invite: [bot.credentials?.userId], + }); + await bot.awaitRoomMembership(room2Id); + + // Let's configure unread room 1 so that we only get notification for mentions and keywords + await app.viewRoomById(room1Id); + await app.settings.openRoomSettings("Notifications"); + await page.getByText("@mentions & keywords").click(); + await app.settings.closeDialog(); + + // Let's open a room other than room 1 or room 2 + await roomListView.getByRole("gridcell", { name: "Open room room29" }).click(); + + // Let's make the bot send a new message in both room 1 and room 2 + await bot.sendMessage(room1Id, "Hello!"); + await bot.sendMessage(room2Id, "Hello!"); + + // Let's activate the unread filter now + await page.getByRole("option", { name: "Unread" }).click(); + + // Unread filter should only show room 2!! + await expect(roomListView.getByRole("gridcell", { name: "Open room Unread Room 2" })).toBeVisible(); + await expect(roomListView.getByRole("gridcell", { name: "Open room Unread Room 1" })).not.toBeVisible(); + }); }); diff --git a/src/stores/room-list-v3/skip-list/filters/UnreadFilter.ts b/src/stores/room-list-v3/skip-list/filters/UnreadFilter.ts index c830f1d55b..e2ffc8f0f4 100644 --- a/src/stores/room-list-v3/skip-list/filters/UnreadFilter.ts +++ b/src/stores/room-list-v3/skip-list/filters/UnreadFilter.ts @@ -11,7 +11,7 @@ import { RoomNotificationStateStore } from "../../../notifications/RoomNotificat export class UnreadFilter implements Filter { public matches(room: Room): boolean { - return RoomNotificationStateStore.instance.getRoomState(room).isUnread; + return RoomNotificationStateStore.instance.getRoomState(room).hasUnreadCount; } public get key(): FilterKey.UnreadFilter { diff --git a/test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts b/test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts index 0fdef53729..573e7a38b4 100644 --- a/test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts +++ b/test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts @@ -457,7 +457,7 @@ describe("RoomListStoreV3", () => { // Let's say 8, 27 are unread jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockImplementation((room) => { const state = { - isUnread: [rooms[8], rooms[27]].includes(room), + hasUnreadCount: [rooms[8], rooms[27]].includes(room), } as unknown as RoomNotificationState; return state; }); @@ -588,7 +588,7 @@ describe("RoomListStoreV3", () => { // Let's say 8, 27 are unread jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockImplementation((room) => { const state = { - isUnread: [rooms[8], rooms[27]].includes(room), + hasUnreadCount: [rooms[8], rooms[27]].includes(room), } as unknown as RoomNotificationState; return state; }); From 3a39486468acd4ec40dc3c2e99751914bd31ad0e Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Fri, 21 Mar 2025 16:16:20 +0530 Subject: [PATCH 31/78] RoomListViewModel: Reset any primary filter on secondary filter change (#29562) * Reset primary filter when secondary filter is applied * Add test --- .../viewmodels/roomlist/useFilteredRooms.tsx | 20 +++++----------- .../roomlist/RoomListViewModel-test.tsx | 23 +++++++++++++++++++ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/components/viewmodels/roomlist/useFilteredRooms.tsx b/src/components/viewmodels/roomlist/useFilteredRooms.tsx index 68f8e3e380..5e1554fcdc 100644 --- a/src/components/viewmodels/roomlist/useFilteredRooms.tsx +++ b/src/components/viewmodels/roomlist/useFilteredRooms.tsx @@ -145,22 +145,14 @@ export function useFilteredRooms(): FilteredRooms { // SecondaryFilter is an enum for the UI, let's convert it to something // that the store will understand. const secondary = secondaryFiltersToFilterKeyMap.get(filter); - - // Active primary filter may need to be toggled off when applying this secondary filer. - let primary = primaryFilter; - if ( - primaryFilter !== undefined && - secondary !== undefined && - !isPrimaryFilterCompatible(primaryFilter, secondary) - ) { - primary = undefined; - } - setActiveSecondaryFilter(filter); - setPrimaryFilter(primary); - updateRoomsFromStore(filterUndefined([primary, secondary])); + + // Reset any active primary filters. + setPrimaryFilter(undefined); + + updateRoomsFromStore(filterUndefined([secondary])); }, - [activeSecondaryFilter, primaryFilter, updateRoomsFromStore], + [activeSecondaryFilter, updateRoomsFromStore], ); /** diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx index f14a7e0acd..07421fc0b3 100644 --- a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx +++ b/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx @@ -162,6 +162,29 @@ describe("RoomListViewModel", () => { expect(vm.current.activePrimaryFilter).toEqual(vm.current.primaryFilters[i]); }); + it("should remove any active primary filters when secondary filter is changed", async () => { + const { fn } = mockAndCreateRooms(); + const { result: vm } = renderHook(() => useRoomListViewModel()); + + // Let's first toggle the People filter + const i = vm.current.primaryFilters.findIndex((f) => f.name === "People"); + act(() => { + vm.current.primaryFilters[i].toggle(); + }); + expect(vm.current.primaryFilters[i].active).toEqual(true); + + // Let's say we toggle the mentions secondary filter + act(() => { + vm.current.activateSecondaryFilter(SecondaryFilters.MentionsOnly); + }); + + // Primary filer should have been unapplied + expect(vm.current.primaryFilters[i].active).toEqual(false); + + // RLS call must include only the secondary filter + expect(fn).toHaveBeenLastCalledWith(expect.arrayContaining([FilterKey.MentionsFilter])); + }); + const testcases: Array<[string, { secondary: SecondaryFilters; filterKey: FilterKey }, string]> = [ [ "Mentions only", From 0d28df0f67022db00c65a2af52fde45f69d81c77 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 Mar 2025 11:34:06 +0000 Subject: [PATCH 32/78] Reuse PushProcessor from MatrixClient (#29561) * Reuse PushProcessor from MatrixClient Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Reuse PushProcessor getPushRuleGlobRegex Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update regex handling Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/LegacyCallHandler.tsx | 3 +-- src/components/views/messages/TextualBody.tsx | 19 ++++++++----------- .../controllers/NotificationControllers.ts | 5 +---- src/utils/pillify.tsx | 6 ++---- src/utils/pushRules/monitorSyncedPushRules.ts | 14 ++++++-------- src/utils/pushRules/updatePushRuleActions.ts | 5 +---- test/unit-tests/LegacyCallHandler-test.ts | 4 ++++ .../structures/LoggedInView-test.tsx | 3 +++ .../views/messages/TextualBody-test.tsx | 3 +++ .../views/settings/Notifications-test.tsx | 3 +++ test/unit-tests/utils/pillify-test.tsx | 3 +++ 11 files changed, 35 insertions(+), 33 deletions(-) diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx index 4361d3d027..608d396dda 100644 --- a/src/LegacyCallHandler.tsx +++ b/src/LegacyCallHandler.tsx @@ -22,7 +22,6 @@ import { type MatrixCall, } from "matrix-js-sdk/src/webrtc/call"; import { logger } from "matrix-js-sdk/src/logger"; -import { PushProcessor } from "matrix-js-sdk/src/pushprocessor"; import { CallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/callEventHandler"; import { MatrixClientPeg } from "./MatrixClientPeg"; @@ -596,7 +595,7 @@ export default class LegacyCallHandler extends TypedEventEmitter { pushDetails.rule.kind === PushRuleKind.ContentSpecific && pushDetails.rule.pattern ) { - this.pillifyNotificationKeywords([content], this.regExpForKeywordPattern(pushDetails.rule.pattern)); + this.pillifyNotificationKeywords( + [content], + PushProcessor.getPushRuleGlobRegex(pushDetails.rule.pattern, true), + ); } } @@ -235,12 +238,12 @@ export default class TextualBody extends React.Component { continue; } const match = text.match(exp); - if (!match || match.length < 3) { + if (!match || match.length < 2) { node = node.nextSibling; continue; } - const keywordText = match[2]; - const idx = match.index! + match[1].length; + const keywordText = match[1]; + const idx = match.index!; const before = text.substring(0, idx); const after = text.substring(idx + keywordText.length); @@ -265,12 +268,6 @@ export default class TextualBody extends React.Component { } } - private regExpForKeywordPattern(pattern: string): RegExp { - // Reflects the push notification pattern-matching implementation at - // https://github.com/matrix-org/matrix-js-sdk/blob/dbd7d26968b94700827bac525c39afff2c198e61/src/pushprocessor.ts#L570 - return new RegExp("(^|\\W)(" + globToRegexp(pattern) + ")(\\W|$)", "i"); - } - private findLinks(nodes: ArrayLike): string[] { let links: string[] = []; diff --git a/src/settings/controllers/NotificationControllers.ts b/src/settings/controllers/NotificationControllers.ts index 20b0d7cf7c..8ec17977b5 100644 --- a/src/settings/controllers/NotificationControllers.ts +++ b/src/settings/controllers/NotificationControllers.ts @@ -8,8 +8,6 @@ Please see LICENSE files in the repository root for full details. */ import { logger } from "matrix-js-sdk/src/logger"; -// XXX: This feels wrong. -import { PushProcessor } from "matrix-js-sdk/src/pushprocessor"; import { PushRuleActionName } from "matrix-js-sdk/src/matrix"; import SettingController from "./SettingController"; @@ -20,8 +18,7 @@ import { type SettingLevel } from "../SettingLevel"; // default action on this rule is dont_notify, but it could be something else export function isPushNotifyDisabled(): boolean { // Return the value of the master push rule as a default - const processor = new PushProcessor(MatrixClientPeg.safeGet()); - const masterRule = processor.getPushRuleById(".m.rule.master"); + const masterRule = MatrixClientPeg.safeGet().pushProcessor.getPushRuleById(".m.rule.master"); if (!masterRule) { logger.warn("No master push rule! Notifications are disabled for this user."); diff --git a/src/utils/pillify.tsx b/src/utils/pillify.tsx index e39fd813bb..6c83ad6553 100644 --- a/src/utils/pillify.tsx +++ b/src/utils/pillify.tsx @@ -7,7 +7,6 @@ Please see LICENSE files in the repository root for full details. */ import React, { StrictMode } from "react"; -import { PushProcessor } from "matrix-js-sdk/src/pushprocessor"; import { type MatrixClient, type MatrixEvent, RuleId } from "matrix-js-sdk/src/matrix"; import { TooltipProvider } from "@vector-im/compound-web"; @@ -119,11 +118,10 @@ export function pillifyLinks( } if (roomNotifTextNodes.length > 0) { - const pushProcessor = new PushProcessor(matrixClient); - const atRoomRule = pushProcessor.getPushRuleById( + const atRoomRule = matrixClient.pushProcessor.getPushRuleById( mxEvent.getContent()["m.mentions"] !== undefined ? RuleId.IsRoomMention : RuleId.AtRoomNotification, ); - if (atRoomRule && pushProcessor.ruleMatchesEvent(atRoomRule, mxEvent)) { + if (atRoomRule && matrixClient.pushProcessor.ruleMatchesEvent(atRoomRule, mxEvent)) { // Now replace all those nodes with Pills for (const roomNotifTextNode of roomNotifTextNodes) { // Set the next node to be processed to the one after the node diff --git a/src/utils/pushRules/monitorSyncedPushRules.ts b/src/utils/pushRules/monitorSyncedPushRules.ts index 2c9684722c..0e9620fe13 100644 --- a/src/utils/pushRules/monitorSyncedPushRules.ts +++ b/src/utils/pushRules/monitorSyncedPushRules.ts @@ -12,15 +12,16 @@ import { EventType, type RuleId, type IAnnotatedPushRule, + type IPushRule, + type PushRuleKind, } from "matrix-js-sdk/src/matrix"; -import { PushProcessor } from "matrix-js-sdk/src/pushprocessor"; import { logger } from "matrix-js-sdk/src/logger"; import { VectorPushRulesDefinitions, type VectorPushRuleDefinition } from "../../notifications"; import { updateExistingPushRulesWithActions } from "./updatePushRuleActions"; const pushRuleAndKindToAnnotated = ( - ruleAndKind: ReturnType, + ruleAndKind: { rule: IPushRule; kind: PushRuleKind } | null, ): IAnnotatedPushRule | undefined => ruleAndKind ? { @@ -34,23 +35,21 @@ const pushRuleAndKindToAnnotated = ( * And updates any that are out of sync * Ignores ruleIds that do not exist for the user * @param matrixClient - cli - * @param pushProcessor - processor used to retrieve current state of rules * @param ruleId - primary rule * @param definition - VectorPushRuleDefinition of the primary rule */ const monitorSyncedRule = async ( matrixClient: MatrixClient, - pushProcessor: PushProcessor, ruleId: RuleId | string, definition: VectorPushRuleDefinition, ): Promise => { - const primaryRule = pushRuleAndKindToAnnotated(pushProcessor.getPushRuleAndKindById(ruleId)); + const primaryRule = pushRuleAndKindToAnnotated(matrixClient.pushProcessor.getPushRuleAndKindById(ruleId)); if (!primaryRule) { return; } const syncedRules: IAnnotatedPushRule[] | undefined = definition.syncedRuleIds - ?.map((ruleId) => pushRuleAndKindToAnnotated(pushProcessor.getPushRuleAndKindById(ruleId))) + ?.map((ruleId) => pushRuleAndKindToAnnotated(matrixClient.pushProcessor.getPushRuleAndKindById(ruleId))) .filter((n?: IAnnotatedPushRule): n is IAnnotatedPushRule => Boolean(n)); // no synced rules to manage @@ -94,11 +93,10 @@ export const monitorSyncedPushRules = async ( if (accountDataEvent?.getType() !== EventType.PushRules) { return; } - const pushProcessor = new PushProcessor(matrixClient); Object.entries(VectorPushRulesDefinitions).forEach(async ([ruleId, definition]) => { try { - await monitorSyncedRule(matrixClient, pushProcessor, ruleId, definition); + await monitorSyncedRule(matrixClient, ruleId, definition); } catch (error) { logger.error(`Failed to fully synchronise push rules for ${ruleId}`, error); } diff --git a/src/utils/pushRules/updatePushRuleActions.ts b/src/utils/pushRules/updatePushRuleActions.ts index 1d872e5207..8a383cace6 100644 --- a/src/utils/pushRules/updatePushRuleActions.ts +++ b/src/utils/pushRules/updatePushRuleActions.ts @@ -7,7 +7,6 @@ Please see LICENSE files in the repository root for full details. */ import { type MatrixClient, type IPushRule, type PushRuleAction, type PushRuleKind } from "matrix-js-sdk/src/matrix"; -import { PushProcessor } from "matrix-js-sdk/src/pushprocessor"; /** * Sets the actions for a given push rule id and kind @@ -51,10 +50,8 @@ export const updateExistingPushRulesWithActions = async ( ruleIds?: IPushRule["rule_id"][], actions?: PushRuleAction[], ): Promise => { - const pushProcessor = new PushProcessor(matrixClient); - const rules: PushRuleAndKind[] | undefined = ruleIds - ?.map((ruleId) => pushProcessor.getPushRuleAndKindById(ruleId)) + ?.map((ruleId) => matrixClient.pushProcessor.getPushRuleAndKindById(ruleId)) .filter((n: PushRuleAndKind | null): n is PushRuleAndKind => Boolean(n)); if (!rules?.length) { diff --git a/test/unit-tests/LegacyCallHandler-test.ts b/test/unit-tests/LegacyCallHandler-test.ts index d10c232b65..664e091393 100644 --- a/test/unit-tests/LegacyCallHandler-test.ts +++ b/test/unit-tests/LegacyCallHandler-test.ts @@ -22,6 +22,7 @@ import { mocked } from "jest-mock"; import { CallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/callEventHandler"; import fetchMock from "fetch-mock-jest"; import { waitFor } from "jest-matrix-react"; +import { PushProcessor } from "matrix-js-sdk/src/pushprocessor"; import LegacyCallHandler, { AudioID, @@ -473,6 +474,9 @@ describe("LegacyCallHandler without third party protocols", () => { throw new Error("Endpoint unsupported."); }; + // @ts-expect-error + MatrixClientPeg.safeGet().pushProcessor = new PushProcessor(MatrixClientPeg.safeGet()); + audioElement = document.createElement("audio"); audioElement.id = "remoteAudio"; document.body.appendChild(audioElement); diff --git a/test/unit-tests/components/structures/LoggedInView-test.tsx b/test/unit-tests/components/structures/LoggedInView-test.tsx index beca86ddcf..6d24cbe416 100644 --- a/test/unit-tests/components/structures/LoggedInView-test.tsx +++ b/test/unit-tests/components/structures/LoggedInView-test.tsx @@ -19,6 +19,7 @@ import { import { MediaHandler } from "matrix-js-sdk/src/webrtc/mediaHandler"; import { logger } from "matrix-js-sdk/src/logger"; import userEvent from "@testing-library/user-event"; +import { PushProcessor } from "matrix-js-sdk/src/pushprocessor"; import LoggedInView from "../../../../src/components/structures/LoggedInView"; import { SDKContext } from "../../../../src/contexts/SDKContext"; @@ -75,6 +76,8 @@ describe("", () => { jest.clearAllMocks(); mockClient.getMediaHandler.mockReturnValue(mediaHandler); mockClient.setPushRuleActions.mockReset().mockResolvedValue({}); + // @ts-expect-error + mockClient.pushProcessor = new PushProcessor(mockClient); }); describe("synced push rules", () => { diff --git a/test/unit-tests/components/views/messages/TextualBody-test.tsx b/test/unit-tests/components/views/messages/TextualBody-test.tsx index 753534a93f..db4790976a 100644 --- a/test/unit-tests/components/views/messages/TextualBody-test.tsx +++ b/test/unit-tests/components/views/messages/TextualBody-test.tsx @@ -10,6 +10,7 @@ import React from "react"; import { type MatrixClient, type MatrixEvent, PushRuleKind } from "matrix-js-sdk/src/matrix"; import { mocked, type MockedObject } from "jest-mock"; import { render, waitFor } from "jest-matrix-react"; +import { PushProcessor } from "matrix-js-sdk/src/pushprocessor"; import { getMockClientWithEventEmitter, mkEvent, mkMessage, mkStubRoom } from "../../../../test-utils"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; @@ -85,6 +86,8 @@ describe("", () => { throw new Error("MockClient event not found"); }, }); + // @ts-expect-error + defaultMatrixClient.pushProcessor = new PushProcessor(defaultMatrixClient); mocked(defaultRoom).findEventById.mockImplementation((eventId: string) => { if (eventId === defaultEvent.getId()) return defaultEvent; diff --git a/test/unit-tests/components/views/settings/Notifications-test.tsx b/test/unit-tests/components/views/settings/Notifications-test.tsx index 00653e1a3b..1566013aba 100644 --- a/test/unit-tests/components/views/settings/Notifications-test.tsx +++ b/test/unit-tests/components/views/settings/Notifications-test.tsx @@ -36,6 +36,7 @@ import { } from "jest-matrix-react"; import { mocked } from "jest-mock"; import userEvent from "@testing-library/user-event"; +import { PushProcessor } from "matrix-js-sdk/src/pushprocessor"; import Notifications from "../../../../../src/components/views/settings/Notifications"; import SettingsStore from "../../../../../src/settings/SettingsStore"; @@ -301,6 +302,8 @@ describe("", () => { mockClient.getPushRules.mockClear().mockResolvedValue(pushRules); mockClient.addPushRule.mockClear(); mockClient.deletePushRule.mockClear(); + // @ts-expect-error + mockClient.pushProcessor = new PushProcessor(mockClient); userEvent.setup(); diff --git a/test/unit-tests/utils/pillify-test.tsx b/test/unit-tests/utils/pillify-test.tsx index 4cca311c57..2b5a0859bb 100644 --- a/test/unit-tests/utils/pillify-test.tsx +++ b/test/unit-tests/utils/pillify-test.tsx @@ -10,6 +10,7 @@ import React from "react"; import { act, render } from "jest-matrix-react"; import { MatrixEvent, ConditionKind, EventType, PushRuleActionName, Room, TweakName } from "matrix-js-sdk/src/matrix"; import { mocked } from "jest-mock"; +import { PushProcessor } from "matrix-js-sdk/src/pushprocessor"; import { pillifyLinks } from "../../../src/utils/pillify"; import { stubClient } from "../../test-utils"; @@ -78,6 +79,8 @@ describe("pillify", () => { }, ], }; + // @ts-expect-error + cli.pushProcessor = new PushProcessor(cli); DMRoomMap.makeShared(cli); }); From b54122884c5a28fd72003e8d77581eea7cb69146 Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Fri, 21 Mar 2025 17:41:59 +0530 Subject: [PATCH 33/78] RoomListViewModel: Make the active room sticky in the list (#29551) * Add new hook for sticky room This hook takes the filtered, sorted rooms and returns a new list of rooms such that the active room is kept in the same index even when the list has changes. * Use new hook in view model * Add tests * Use single * in comments --- .../viewmodels/roomlist/RoomListViewModel.tsx | 13 +- .../roomlist/useIndexForActiveRoom.tsx | 44 ------- .../viewmodels/roomlist/useStickyRoomList.tsx | 117 +++++++++++++++++ .../roomlist/RoomListViewModel-test.tsx | 123 +++++++++++++++--- 4 files changed, 230 insertions(+), 67 deletions(-) delete mode 100644 src/components/viewmodels/roomlist/useIndexForActiveRoom.tsx create mode 100644 src/components/viewmodels/roomlist/useStickyRoomList.tsx diff --git a/src/components/viewmodels/roomlist/RoomListViewModel.tsx b/src/components/viewmodels/roomlist/RoomListViewModel.tsx index 584e436c57..217eaefbd9 100644 --- a/src/components/viewmodels/roomlist/RoomListViewModel.tsx +++ b/src/components/viewmodels/roomlist/RoomListViewModel.tsx @@ -18,7 +18,7 @@ import SpaceStore from "../../../stores/spaces/SpaceStore"; import dispatcher from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; -import { useIndexForActiveRoom } from "./useIndexForActiveRoom"; +import { useStickyRoomList } from "./useStickyRoomList"; export interface RoomListViewState { /** @@ -97,8 +97,14 @@ export interface RoomListViewState { */ export function useRoomListViewModel(): RoomListViewState { const matrixClient = useMatrixClientContext(); - const { primaryFilters, activePrimaryFilter, rooms, activateSecondaryFilter, activeSecondaryFilter } = - useFilteredRooms(); + const { + primaryFilters, + activePrimaryFilter, + rooms: filteredRooms, + activateSecondaryFilter, + activeSecondaryFilter, + } = useFilteredRooms(); + const { activeIndex, rooms } = useStickyRoomList(filteredRooms); const currentSpace = useEventEmitterState( SpaceStore.instance, @@ -107,7 +113,6 @@ export function useRoomListViewModel(): RoomListViewState { ); const canCreateRoom = hasCreateRoomRights(matrixClient, currentSpace); - const activeIndex = useIndexForActiveRoom(rooms); const { activeSortOption, sort } = useSorter(); const { shouldShowMessagePreview, toggleMessagePreview } = useMessagePreviewToggle(); diff --git a/src/components/viewmodels/roomlist/useIndexForActiveRoom.tsx b/src/components/viewmodels/roomlist/useIndexForActiveRoom.tsx deleted file mode 100644 index 210e0efd0f..0000000000 --- a/src/components/viewmodels/roomlist/useIndexForActiveRoom.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -import { useCallback, useEffect, useState } from "react"; - -import { SdkContextClass } from "../../../contexts/SDKContext"; -import { useDispatcher } from "../../../hooks/useDispatcher"; -import dispatcher from "../../../dispatcher/dispatcher"; -import { Action } from "../../../dispatcher/actions"; -import type { Room } from "matrix-js-sdk/src/matrix"; - -/** - * Tracks the index of the active room in the given array of rooms. - * @param rooms list of rooms - * @returns index of the active room or undefined otherwise. - */ -export function useIndexForActiveRoom(rooms: Room[]): number | undefined { - const [index, setIndex] = useState(undefined); - - const calculateIndex = useCallback( - (newRoomId?: string) => { - const activeRoomId = newRoomId ?? SdkContextClass.instance.roomViewStore.getRoomId(); - const index = rooms.findIndex((room) => room.roomId === activeRoomId); - setIndex(index === -1 ? undefined : index); - }, - [rooms], - ); - - // Re-calculate the index when the active room has changed. - useDispatcher(dispatcher, (payload) => { - if (payload.action === Action.ActiveRoomChanged) calculateIndex(payload.newRoomId); - }); - - // Re-calculate the index when the list of rooms has changed. - useEffect(() => { - calculateIndex(); - }, [calculateIndex, rooms]); - - return index; -} diff --git a/src/components/viewmodels/roomlist/useStickyRoomList.tsx b/src/components/viewmodels/roomlist/useStickyRoomList.tsx new file mode 100644 index 0000000000..e8234d14ae --- /dev/null +++ b/src/components/viewmodels/roomlist/useStickyRoomList.tsx @@ -0,0 +1,117 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { useCallback, useEffect, useState } from "react"; + +import { SdkContextClass } from "../../../contexts/SDKContext"; +import { useDispatcher } from "../../../hooks/useDispatcher"; +import dispatcher from "../../../dispatcher/dispatcher"; +import { Action } from "../../../dispatcher/actions"; +import type { Room } from "matrix-js-sdk/src/matrix"; +import type { Optional } from "matrix-events-sdk"; + +function getIndexByRoomId(rooms: Room[], roomId: Optional): number | undefined { + const index = rooms.findIndex((room) => room.roomId === roomId); + return index === -1 ? undefined : index; +} + +function getRoomsWithStickyRoom( + rooms: Room[], + oldIndex: number | undefined, + newIndex: number | undefined, + isRoomChange: boolean, +): { newRooms: Room[]; newIndex: number | undefined } { + const updated = { newIndex, newRooms: rooms }; + if (isRoomChange) { + /* + * When opening another room, the index should obviously change. + */ + return updated; + } + if (newIndex === undefined || oldIndex === undefined) { + /* + * If oldIndex is undefined, then there was no active room before. + * So nothing to do in regards to sticky room. + * Similarly, if newIndex is undefined, there's no active room anymore. + */ + return updated; + } + if (newIndex === oldIndex) { + /* + * If the index hasn't changed, we have nothing to do. + */ + return updated; + } + if (oldIndex > rooms.length - 1) { + /* + * If the old index falls out of the bounds of the rooms array + * (usually because rooms were removed), we can no longer place + * the active room in the same old index. + */ + return updated; + } + + /* + * Making the active room sticky is as simple as removing it from + * its new index and placing it in the old index. + */ + const newRooms = [...rooms]; + const [newRoom] = newRooms.splice(newIndex, 1); + newRooms.splice(oldIndex, 0, newRoom); + + return { newIndex: oldIndex, newRooms }; +} + +interface StickyRoomListResult { + /** + * List of rooms with sticky active room. + */ + rooms: Room[]; + /** + * Index of the active room in the room list. + */ + activeIndex: number | undefined; +} + +/** + * - Provides a list of rooms such that the active room is sticky i.e the active room is kept + * in the same index even when the order of rooms in the list changes. + * - Provides the index of the active room. + * @param rooms list of rooms + * @see {@link StickyRoomListResult} details what this hook returns.. + */ +export function useStickyRoomList(rooms: Room[]): StickyRoomListResult { + const [listState, setListState] = useState<{ index: number | undefined; roomsWithStickyRoom: Room[] }>({ + index: undefined, + roomsWithStickyRoom: rooms, + }); + + const updateRoomsAndIndex = useCallback( + (newRoomId?: string, isRoomChange: boolean = false) => { + setListState((current) => { + const activeRoomId = newRoomId ?? SdkContextClass.instance.roomViewStore.getRoomId(); + const newActiveIndex = getIndexByRoomId(rooms, activeRoomId); + const oldIndex = current.index; + const { newIndex, newRooms } = getRoomsWithStickyRoom(rooms, oldIndex, newActiveIndex, isRoomChange); + return { index: newIndex, roomsWithStickyRoom: newRooms }; + }); + }, + [rooms], + ); + + // Re-calculate the index when the active room has changed. + useDispatcher(dispatcher, (payload) => { + if (payload.action === Action.ActiveRoomChanged) updateRoomsAndIndex(payload.newRoomId, true); + }); + + // Re-calculate the index when the list of rooms has changed. + useEffect(() => { + updateRoomsAndIndex(); + }, [rooms, updateRoomsAndIndex]); + + return { activeIndex: listState.index, rooms: listState.roomsWithStickyRoom }; +} diff --git a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx b/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx index 07421fc0b3..238de4b270 100644 --- a/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx +++ b/test/unit-tests/components/viewmodels/roomlist/RoomListViewModel-test.tsx @@ -314,34 +314,51 @@ describe("RoomListViewModel", () => { }); }); - describe("active index", () => { - it("should recalculate active index when list of rooms change", () => { + describe("Sticky room and active index", () => { + function expectActiveRoom(vm: ReturnType, i: number, roomId: string) { + expect(vm.activeIndex).toEqual(i); + expect(vm.rooms[i].roomId).toEqual(roomId); + } + + it("active room and active index are retained on order change", () => { const { rooms } = mockAndCreateRooms(); - // Let's say that the first room is the active room initially - jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => rooms[0].roomId); + + // Let's say that the room at index 5 is active + const roomId = rooms[5].roomId; + jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId); const { result: vm } = renderHook(() => useRoomListViewModel()); - expect(vm.current.activeIndex).toEqual(0); + expect(vm.current.activeIndex).toEqual(5); - // Let's say that a new room is added and that becomes active - const newRoom = mkStubRoom("bar:matrix.org", "Bar", undefined); - jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => newRoom.roomId); - rooms.push(newRoom); + // Let's say that room at index 9 moves to index 5 + const room9 = rooms[9]; + rooms.splice(9, 1); + rooms.splice(5, 0, room9); act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT)); - // Now the active room should be the last room which we just added - expect(vm.current.activeIndex).toEqual(rooms.length - 1); + // Active room index should still be 5 + expectActiveRoom(vm.current, 5, roomId); + + // Let's add 2 new rooms from index 0 + const newRoom1 = mkStubRoom("bar1:matrix.org", "Bar 1", undefined); + const newRoom2 = mkStubRoom("bar2:matrix.org", "Bar 2", undefined); + rooms.unshift(newRoom1, newRoom2); + act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT)); + + // Active room index should still be 5 + expectActiveRoom(vm.current, 5, roomId); }); - it("should recalculate active index when active room changes", () => { + it("active room and active index are updated when another room is opened", () => { const { rooms } = mockAndCreateRooms(); + const roomId = rooms[5].roomId; + jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId); + const { result: vm } = renderHook(() => useRoomListViewModel()); + expectActiveRoom(vm.current, 5, roomId); - // No active room yet - expect(vm.current.activeIndex).toBeUndefined(); - - // Let's say that room at index 5 becomes active - const room = rooms[5]; + // Let's say that room at index 9 becomes active + const room = rooms[9]; act(() => { dispatcher.dispatch( { @@ -353,8 +370,76 @@ describe("RoomListViewModel", () => { ); }); - // We expect index 5 to be active now - expect(vm.current.activeIndex).toEqual(5); + // Active room index should change to reflect new room + expectActiveRoom(vm.current, 9, room.roomId); + }); + + it("active room and active index are updated when active index spills out of rooms array bounds", () => { + const { rooms } = mockAndCreateRooms(); + // Let's say that the room at index 5 is active + const roomId = rooms[5].roomId; + jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId); + + const { result: vm } = renderHook(() => useRoomListViewModel()); + expectActiveRoom(vm.current, 5, roomId); + + // Let's say that we remove rooms from the start of the array + for (let i = 0; i < 4; ++i) { + // We should be able to do 4 deletions before we run out of rooms + rooms.splice(0, 1); + act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT)); + expectActiveRoom(vm.current, 5, roomId); + } + + // If we remove one more room from the start, there's not going to be enough rooms + // to maintain the active index. + rooms.splice(0, 1); + act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT)); + expectActiveRoom(vm.current, 0, roomId); + }); + + it("active room and active index are retained when rooms that appear after the active room are deleted", () => { + const { rooms } = mockAndCreateRooms(); + // Let's say that the room at index 5 is active + const roomId = rooms[5].roomId; + jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId); + + const { result: vm } = renderHook(() => useRoomListViewModel()); + expectActiveRoom(vm.current, 5, roomId); + + // Let's say that we remove rooms from the start of the array + for (let i = 0; i < 4; ++i) { + // Deleting rooms after index 5 (active) should not update the active index + rooms.splice(6, 1); + act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT)); + expectActiveRoom(vm.current, 5, roomId); + } + }); + + it("active room index becomes undefined when active room is deleted", () => { + const { rooms } = mockAndCreateRooms(); + // Let's say that the room at index 5 is active + let roomId: string | undefined = rooms[5].roomId; + jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => roomId); + + const { result: vm } = renderHook(() => useRoomListViewModel()); + expectActiveRoom(vm.current, 5, roomId); + + // Let's remove the active room (i.e room at index 5) + rooms.splice(5, 1); + roomId = undefined; + act(() => RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT)); + expect(vm.current.activeIndex).toBeUndefined(); + }); + + it("active room index is initially undefined", () => { + mockAndCreateRooms(); + + // Let's say that there's no active room currently + jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockImplementation(() => undefined); + + const { result: vm } = renderHook(() => useRoomListViewModel()); + expect(vm.current.activeIndex).toEqual(undefined); }); }); }); From e1970df70458419ce0649fa8f30b74a44ded34e8 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Fri, 21 Mar 2025 17:08:37 +0000 Subject: [PATCH 34/78] Add report room dialog button/dialog. (#29513) * Add report room dialog button/dialog. * Update copy * fixup tests / lint * Fix title in test. * update snapshot * Add unit tests for dialog * lint --- .../e2e/right-panel/right-panel.spec.ts | 14 +- .../with-leave-room-linux.png | Bin 27602 -> 28527 bytes res/css/_components.pcss | 1 + res/css/views/dialogs/_ReportRoomDialog.pcss | 16 + .../views/right_panel/_RoomSummaryCard.pcss | 4 +- .../views/dialogs/ReportRoomDialog.tsx | 95 +++++ .../views/right_panel/RoomSummaryCard.tsx | 31 +- src/i18n/strings/en_EN.json | 7 + .../views/dialogs/ReportRoomDialog-test.tsx | 66 ++++ .../ReportRoomDialog-test.tsx.snap | 100 +++++ .../RoomSummaryCard-test.tsx.snap | 345 ++++++++++++------ 11 files changed, 556 insertions(+), 123 deletions(-) create mode 100644 res/css/views/dialogs/_ReportRoomDialog.pcss create mode 100644 src/components/views/dialogs/ReportRoomDialog.tsx create mode 100644 test/unit-tests/components/views/dialogs/ReportRoomDialog-test.tsx create mode 100644 test/unit-tests/components/views/dialogs/__snapshots__/ReportRoomDialog-test.tsx.snap diff --git a/playwright/e2e/right-panel/right-panel.spec.ts b/playwright/e2e/right-panel/right-panel.spec.ts index cc03963801..44b051c987 100644 --- a/playwright/e2e/right-panel/right-panel.spec.ts +++ b/playwright/e2e/right-panel/right-panel.spec.ts @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024, 2025 New Vector Ltd. Copyright 2022 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -10,6 +10,7 @@ import { type Locator, type Page } from "@playwright/test"; import { test, expect } from "../../element-web-test"; import { checkRoomSummaryCard, viewRoomSummaryByName } from "./utils"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; const ROOM_NAME = "Test room"; const ROOM_NAME_LONG = @@ -133,6 +134,17 @@ test.describe("RightPanel", () => { await page.getByLabel("Room info").nth(1).click(); await checkRoomSummaryCard(page, ROOM_NAME); }); + test.describe("room reporting", () => { + test.skip(isDendrite, "Dendrite does not implement room reporting"); + test("should handle reporting a room", async ({ page, app }) => { + await viewRoomSummaryByName(page, app, ROOM_NAME); + await page.getByRole("menuitem", { name: "Report room" }).click(); + const dialog = await page.getByRole("dialog", { name: "Report Room" }); + await dialog.getByLabel("reason").fill("This room should be reported"); + await dialog.getByRole("button", { name: "Send report" }).click(); + await expect(page.getByText("Your report was sent.")).toBeVisible(); + }); + }); }); test.describe("in spaces", () => { diff --git a/playwright/snapshots/right-panel/right-panel.spec.ts/with-leave-room-linux.png b/playwright/snapshots/right-panel/right-panel.spec.ts/with-leave-room-linux.png index a2bc63aaf4194ebb9da29ccfa94e32cf33de9623..63a49dc751266cb362eaea707378a70d114f5d52 100644 GIT binary patch literal 28527 zcmc$`1ymegwEM45T-9VHftm@5T>zi~z6}bdWm@JKfLP=OF52cKez2pnM4!ZT{m9A$~n>u9mx=~g-0q!*3c=RcLV_bx3n-xMOed?(eR+KGhz^3pU2 zzu*lvrzB4}g3Ome&R+Cs5>%3xWZwb0cV;9eq1M<)U-4r8=SFD|Jn+)W%B)|bbijSh z!&P={>;!y0%E@_Yi~+vrm5M*|iyU+4qxaWs4+9Ul+Li605;XHZCSVKJ*sp&&gXCZzyN*k=BNP13i zZpQvXPTj!`J3k-nr#?6Jit-jQgu)EgbYCEPh@0anbBGfg@{P;mrD;b8pH|A^9)YCl zO!A!!FyOWSMz6fOu(2|GPviBAxYvb+g=TU)GPw%1tVUp8xkZJgKW2OFg8D31MNB78@nu)hc90l#p8zewjn?!{N zPPXBS=Tcct&d|Ho+8nBZ6}}3^M(j%J)wv~4#}U2LHsVL#-;F~n_VLY-_R7JA(Tj@t zqijBf#KXEf1R_I_rG-t&)gN>byFtA)uo0+cPfbKb#I{s>_Y-;Xm4%8el?*hY`Rd}L z%x6#z;?pVPChfBx&J^-y+M~ZLo}+ADP*SQ7-hJ8wF>Gf`9Go)P0p&s&#eY9;!L=+G zg964rxh&bjSRZ`WEexJMg;q(4X=m>XO+#XvS7YXfboI;U(H4@U%my~C?Di~tlu|;X zCzsO`W%etzlQECxDCHJiO-CC@0yFa+u|;gEoXyBHGSH}3V<*^?V|~$PW(#+A4%EyH z^s)_ij*h5yWAy1rLqKyCg@xx(`ObhEK`Z509PE?(p(Vvs6twO?qU#^WH*s72&z5en zDm?QW+i8W?)*rA;&i5XqU&3nU90<$hNsIrefGZ_;aOspuV`j)hCe4Mw)5mUWVHt_g z)NA1;v*p=Y*R%UGtW;YxV{l=4Bg&`lg^zp+VIXLTF(sj~<4M{8mceS^lb{QP=SXQj zwIwj&?iO;uMn2D_r19G(ctT$++2FvFwhA!0m`x5RtbRHcB)-KLwFk?`5cFtV)aU=#+9xkT;I=m0; z^r=*IMBWwl^v)YgL+j>C@?G?BWoXt+%1Qi;GUN!O&{E$GUI(W-dc3J(xI@O=`l{_w zUfo?#l(%7U3D(bMP`YR0e%w0K!3Kgl|C}MADNGu5g6AyWE_&=eh(m3N?tp9@LfJ;l z3OoAN&aW~mPENRb!;O5XK`INyQ4i~4IiOd`o(SLPcsZx6G6p~l6BLsix0|hYt(W7B z9lmw8SOJ_l)c8jq3w-ti{fR6KH(HOe8F~#@kYa_~$J0D2i#HWkn(3fKlfL8X+S;w1 zo%j2W(Y1z-O)rOj<_zq6mcvKi`%_0@R+n)>bpmkBb~o)O^H?rko>!>qHwKrtQ5DA9 zpd(&LxSv-Im}G37UTj!j-`F`bUlKQ*^y{l4vD^pP@ASn^IvKrKGMi%BYV-r#`WF-{MiRYP)WPsvz~{?NN&%;P8XRy&(Vsp z(SYjX`apl@GLw|TfiDy1#*R>J>4ou=-1`O={c9K9(?~LPMyr`3SuI14lDj=Nx_ez$ z5eD5$o~+7>4yCSPv&@HG%Sshpcijb!`3`##sNdMV^m+%i>v^EC>cWJ6gGu+fwY;W0z{ijUiim%oydVKlAIn_rQgbw`gJYcs~5cb%S9r5;>xWZi9L z!HR6wl_Mlvr6;bwLOTwZAtxDpHW!CcGr8{?s1!>{X9u5Gw0ydwSpG0Ky#Rg%5*Yk* zYNb;ai%{D(Gd*43YQf7VzxM+tM)~fU{9L!&XPS^jO18N&*?#DZktiu@uDqqR@l$a;Ac#JlM|fz`qUsIc z24Q}z@0jg#RL+Y|7X^MV#kC5s@OAV|$Rs|duviH+${Z^Q7|ha~lh|FY=Du@m3hA6o zsviVgw>lo&y8CC$h*EhFoK7=cYdTgxnvpwV%T1*GB2&eUMQ`9o0n+xY!O17ks77UW zGZKdeJ`u%j-6INDe@f>%oIab4Q!1k1RGuDAP}UItRF~)hkBqThb=7M**x#kh&9wW-d-%_DuVeO_+9x^Y147qrV?%D{JRubNLp zixFs&=U6`IWWOs}XVfDzMB8K~gx@Fk!xjtNrxTFNTPzbCwo;I$?U>U&Vs%aCYL%+r8>alq?PvMNigI z(nI{M7)fF>NqIc~P?v%)E@NchT;?B2LUNK4^i$MJ1Q%4?T|AucPAk9jgKg9+19?Tp zb{*N>U}o5=#?UXO7RbOH)!3hGdg2M~ELi@Z$|~+#`lI7VGxo9HeuFc-D*SsDERz&Z z^*sB5#f+AywHR)puEq{f?P-C4io_7!z_S;y<{AH?WAIJcQ%prm%{cGV*#7SBZOe|i ztW_z3Dg5ibpy2SnAkx9v`^9@x`x>Abimk72xvSY2y0gQ{X=GLg831*Ip z-ED{Cp6%Va6*M+hV*%pOshLsaMUxA!dwGg~Ru8&Mp-U2Mx;B<-(Rq^t<}W&X{GM~a zQttAoWpy4#OL99cDG4s&<9%!ToDg%^UI*wq3cP;)Ei9x{Qt8L-pk zyFO<~cP?M!`qXXty`_$6P?>mAK`_6u`c|eAW}Ru1^Es{UH!rs=UoJ5p#Gz8mvwq>D z<&A(2z+MAZ{mc4Mfb)h@iIOr~_UGa5uE3eYY4V*N z-$vRLO)ywzY)pGVwwo>BW_iZHqa&`O?nzq1P#g>(^){@AeZL5Ic)B^SFs?XO?ef2$ zsJZys*n_D|%An35RQ3zOA4`^cJ3eOU+Am4>Eb*k@k#IAj@rt_p*SLP7(I5zxYeM2b38vxLt zh(i77tJ;^$2LQKWNYuy6x(1~a13xC8R}?0a7z9zrw;mlG!PD@3BzaPy7V6!dWA~8_ z?_7h*^2rx$u{-#)w0D5bB}3xr@vQD}Z&F7wI$!|SH zGM|(5jRAw>qo3*=G?nec<_9x|#H}ZhW$UNz^&zz#=&fE?WCPPK&OccXDC5JP!zkoShq1UtXHV(wtaMtc5U%A$|2o+vRrsxbD2! z6Aw$%jfQXw<^*=F7Am)P$|zr???1aG+-rW8-MXp#+<6w=n-LH%HCXgI6qJIX0NL5C zjy$|Pv9`tcmK}wSiKbkBob(a9dmgw>4CNa`-0pwEPx-X2Cy{W;`3_>{h0kNN%Lt4) z9!~=sjgPjKW;nr)7uwt5u#&CJnXMFgjtahtX)xIE&g`}p+!M`ie7LjY=S;igP*OSj zVkqesM?f`fWx-|!n#w+Rw1&`8dVXZc+3i_Tt!{8O&1l8?bftnWn;q@=bQU1O(ZES< zA?m}q9o%`meE1}3y>`qyPex|!o_pFGxCROIjf@UMl|$6bdtuZn^+dVu$`i-G&MX|EE-Tt1Mz&W9{i3xJ5P(snOg~LE$FHsTiTE0cgCE& z)8r*fe(EIHV&5i zdERjv8k!YKtPpeJ!OwWOrhkcK&$aBefyIy^HinyT)pyLREgQ!}nLlUt=HfCON1Olq zcd+0ENVRa5Z-c}Y7|?VsWJspD_+?n*I0XjRH<(u#hU7r%@W10|b|fiiYY&;X<>#R8 zg8N1obzQfYqBX@IeIx&ieLt%#RF<-H9sC*@oV`F3E^Mgm6=*Btx zruxGO)~ko!Yiy%TYmUQF7REhj%U0HE`>mSYBp?6OyxCVyvQPGMBs)0dCB9F74K=Y7 zb*3Gm!7y|)sX=tQ>aG-tOb&hmemTdMVK`5lkUbjtsdbGZQWYSsp4k5DD$D189V7p(-ep*g^E*#8b^^Dj<7FOpkhCHM zF^Hp)+j2Z3e;@DJipS*`8uA9FbNk!B#pCpWC>uX1ZQpUPhF) zF5=cbbp?%9Gy=V@*#WPudWh^~tji3$nMF@DCE%?HqjIRn0zX+N@bJh1CO=8e>r;rh z4GeYG9W*N5yA{KFwNavpyF?+2wyy=NHi2Ar7GsjOA3uH|1A6-UJ)(Xm#i<5774?Q` z6g$Mmm)nuptsBQz6WsGvwYFEXeoo_myyDdj_FBB>e$4J5kTIv$MonrwJYU_;zIJe) z_AG^uZ6p*oH@7Zh_sr{#`8w0@8WrAwNBj>qD(enSxu$H?NKe~hxj^qs4+?xpB&d4? zCSByh??cmjj=nSFKQfTXXo2}t8;hZDbsigU5r;A)fG=^Ow>1?miYzz3x6A!9X%0Ue z32@-w1e_c{@yKV~vlYI79ZSjasO_+A?V%^6N6*dkNlbQeIw2c?^_o- zsK)t(ZR`jxbUbh|TU@Vn*%em)=RkZHr0NG+D~)PoMj-(Zp=rLW`=4oob1hf=zTK6^ z!^?FL>%n1a$#`A!x|a`#h^*z$x@rP~$w-29p==x%oX5&-CKT5<@r7)RB4zLdAwz~*Co+e%fIaRIbsTbYld z_F<#VSPbAt-pHj9?4C`$`QUwEf2}E9P4mcbK+nffqJcVPWSoOf@Q43@3hPq!N3nipQ!IkVMI571{9^Ghmw1G zPWrLd?W@>W*{TL(4cpI-MtEZ-?+dUqA)Dw%2Yz?E8=B9rCp8EZ|0wnA{Y98aO zqG?r)zUY>++x^;BD79=td9k_vJ-T;!4kAZomsERL!9Ytaa*#nka~wajCDb6!YIF z!QNB7xG7PnCLtx`v-KcQXY41OhTv~bNj zyQcHLeez4`n3WuH_OUHi@hZh@gmkWm3Ahmm9_E+P(Bv>a zI5-}eSoAE?AlVz{O~QyvntQ(7y6sGoPyr&yw0s4$oevq@AbHVxJeX3MW zqyY1eSwRly08KZ4yy@Y=#_%g~`+=>qtZ@mogzT5Ex>^}OoiQTx4rNP&>R&N#Yk=lI z+DRqPlZeR2Bv{U;mIORWrDI0($ZI%Qd<|PjA2rrA+?(bRM1{1yLmwim5m1jkmOSb%>MHD~H)uJrj-2Whm`gQNz!;l|3MM=n-xW#X+x9vC=@K=e6{JC|Cp;>B1?3OT&d!HiNhJBESg+~r@P5FDk8dF8Lgbz*Co`p+7@&pdRtNap(-Tm# zh~ReNgrO`5ebi!gv$_Q7+sKBy z&%@;v#%Gh~zzkVE1pq7-H-9ATel3&~!$rA({Y_ozS?=WexwjuXhD%hR`Nx1<|CB9D zcyA`7rf5=zX?>Li4*a;P^?T_KP&ZEH6H?y!FW3mlez6~8seH(k*mL^VhkY`*$c4s`hVFxns zjAIc=Twh;*CN?h7!*6+R4i&h%x&oX||43K!T%bv{(0D`#v8fn0D%A5_MeqvP}#ZX=$-|j;OI6dPQy5~Z=wA!r93`Uvk zBtoC^x}8WK^4wtub^`m_;YV-jE!u|s;t7w3%9M$~Rd8np~MSXDr6zsGmN0jxxeIv|PUom-8)UfJDUS zLg)#dTEyqqUf??)@9~AitMrt+uivgLsD7@iZX0#Ix?_I&ZHC2>bu2VwmZETJTmiaM z->i)$zOkJe3b3{!zJrHX-~`e^vMEhAx}O^eYp3P=bp8dO4p{vY)_0UnbD@FUE#SFS z>RU+H59ji=Il5tA66UnbjGWbP`8wg{#gDE^uqh32XdiqG0STQBK-4=q7MEn7&OrjM zMhHQF{t#e9QX95Sb)FmOyFz-!R}`7>$$a26RW=N&8{Fd=AmO|g33IeO@fjF@;#Jk; zu$d5bb7PqF&f1F@k1GxXpxypUBMa^uSR7r~MnxuleugVzv6a3)J}%EFVLOGCXLEBE zQ7&d?;G@cSI~bj6o@_$b)~n>M?ry6W=~4SJ%GZvJ6lSKPiPs4gtpr9DAfMIB&#U53 z`EtkKmT|x0Rl9q5P~LO|3HT({$FGrKaVuh#knvye^T?BDW*oOPL)*MrPR83SAOUZ^ z;3C|eGpwL2Umh--E&qO{xgIZQ#+sHwWNF4WZro&sqf3nd*XTfN>P-Kyl)QgY75>k1 z-#?9J^@8Z1+sN%e8I_&Wc?Rh1Z%4`bKUr5=hE9n$5tx)WEo}4lz;8a=Kg$qiwBOd9 z9pm@I{ta(?m#c8DbiHWBr~>~iFVh8HH(zIN$&mbPV8~&SHeL?YwUs6OLFdeHI!mCI zhllSI{pj zSj6Cx9uR~`<=38B@~SwV`}cAk2yJ#A(^4qjL*=M1Q*}A+qg{o^J*bIQ9s6u1<{?rg-}@u*O=D}Y2Gc3`2LogKU6{jL9VW0# z1U}ivMT`?{QEGtC0ajP}cd?sAqekqlN(UXkzdX-XU96MOgrq`P0=0dO_4GP~3KN*V z7!a>ZD=1z|(^FLQdlGu-e$Z?WqurFzV*fc-T~O79(sf@e#i)aTQEn%aChkN;PyL&G zeZszWBv!ARU3C;Qb|syH+=OK!#)pNr5S4%Si%L@?7A0|KepQz+$Fa2$O~F9(bE%81%=n5v<3(9o(ex!Yh0|v1S4Jm-*TMm zqbW##JVqiXn!$#P@q9|GU-r(|7;>;O!>aXk^nGuyp&W4S_xF;jc|C(ty70)-lIz#v z&Bp`+#>E&CQG;`$`+u^b5MRp1hQe6wo0a)fhe%mMe)BqcRP;^c%u?0`_Ht(Y&vYXs z8AmU9Uh!Z$X)r=GhS!UK{!cH!Wh+|A(}U~4q?k5^u$T4s!MCO5>O>r{o`Gn@ z`y)kM6e)fF;Lec>socn3CA@vx6;*uH5CoSG&Zgzb+!5OpFZ$UQiFz!N2#@4P?XX-u zwl&^J^$bIPSUodj#GhfmMCy7P3winqBL=<|fgkv{$FH2MGPvPSD?(m<{fC)XBD!h` zzBj`Q#uM3(n_0o#u+J#x&5P#|@qU5)Tue5NB#u7XgvKD-d)6QgsJ z-a@c+QScbNOQvPw{QYSy_tTgD3tGMDb|27!@K|cCUyIOfb30^|pa)m| z^$Vzuvc%@yNd1X@4B(e&R4(kzlKV(=xDdl4f#AbewmOhSs4|>Jh3N;{O|7hJQZ- z{;ymS{~Spj9+cd1_`~xU(M5=x?wPsV5x;UsN$rpZcTWAA?u!5Kt|(2=(Q=1a+TBmz zsj8?*UTOE!kdA(}@f>hmBs&xzLHo%OJqX*{i zvV{Vc+d^;H8ZF1C>Ig%pkecvT59NhCAGXZ^Cyy;mL`YL}V{@)G8bHi$)Ztdnz{uFT zcfbMEiCP-ojJ7tQWz`CzOPuztbYjP{z6ANlOdbx{o{PTdUe**hbf*{&2&ZXWRyt6#JM+< z6RPkS-XrMew3jsUnDUo~B<%jU^^+8Kj>zYKvX(TGSgIN*znOxb^7NcP0cgMRv$$Qm z*(|LP8&^JM5C_yAF@}Ho0t%)OP{o6|H)coquqIo{wQ5Wy(-mi zEMGjH$f&9ZvMqLr(pWq=&oFjAjZFDxF$8yP@E?h5 zbp>r*+pGO;9b-F2fk$ptKGml(P`5B0!w<)BB%AHt%+`-~Iqys4d+^B+_t0)`PLBba z4adHY4p8!s|B-^31p*4%dS}nU;e&(Y`hR@&XTbjZ)il7(QRnlR4kh_w7vOUrVEwOC zOgQ4VRQgb4M?r({Ctsrsw^gIgpKGByFefZmO6hfQ>sk|1bh0tc5Jt72a6p}u&bGD8 zHFqneJ}NM6PqEo7@!xTW{oOqKL3T;;vRd62Z|s&nCz-dhpx_Mlf^mCSYp!Z*3xZi| z*lB4o?&2L8*dwlcOUh!GQhL45%u`75d-)*J`BT+Ju-VdjgHOuVsZZ& zyL(+u$AO7IRHCXR@wk0a#n7XuyOo^H;@G#2;&a1Zxa9@*%gZvcFW{LOM0ib4K%E8w zgR38;Wo6NT7kO=BV=3dPK)uf8VcK8N41+*1xM2+rr(7(=jT17lMU394Ptl7 z!Np?+E1{|0%^|A3OQ z4VI#*$N~c^oI~8=-eD%HviaYMsljBMwlfP$qyg7F~ofA*HVdFQBe`Fwzp4GD1sOHr=_{fSApK* z`S1Peb{|W#j6z|0JfM6rcnF0L=A}drm{&(tHE1Qv#&X`_`g6IBr>Ccs)H<9iprU&kVf@g7+EG|H(?7Hue19*jsQ-N-rH)=_}Q;WzU@w1#d7(3TcRoj&6;zhYiUADQ5>hb}y~#ojP*FncO*&M1?r*dcJ$1gL zi(6M-=$KZ>0R$Dv>FKdD_-ji?{Dv$&jBxhu+?Hg2ur}`DmnXlX3xK5cyl26=EJfOz zv>XieYOfoSulbx-$a@HggNxAW4u$E|`Oq!jeq(2el>-)~8E5BFZ`&FielNrMVFiQ0yyM?HrU+PK-G1 z&bcNVrCQDX?)Y+FawzKHr#OW>#mOls8*Hj1(N|%wXfPvv(_{!|hbAyGJem=#f6q3T z&Xs0^>geDC8ty*trZW8LT~w?O22IZVT>4G%!3Y2_)31zwdaJgCl9?!`jJ-a5J}3Js zSw6y+5}z!{!y~^(C_stjvMj=&?j_U27)5XHP|}2ND}-I;@V5z?h(%Oc=E%e|JtU|y?m{Jq#kBn zEi}_AP~f`vN2-y_?cIq)^{-lb)MX3(g05=VTcmfKzwHaUogAjlzSE+PEls*oL7Cdl z*3(4%C-m`=r&TpWt4%y#cD}8xYiiiDSLx^9#ppySUl8gVVQgeTXTn)~bAm^8T@gzo zF!wHk;%C;auxRekwrg4v1&LC4epAOu(wE(IgZEifJ?uYot+1~7Rw;Vm z>4|a(L(WLj5;Rh^)l?+jEU7&s(hg`1RyAN@d7+TA#aysGZM;U}#fdFHNz=1Zc~uon zJBM)-+O%PSy&l&-2yj8Y7ghxbn*#@5F>j5kPpgtiPtiDz9r>vE>LTW{c z#T7RN+G#7TmE2{i+cEs|^;NdFec8xZb&1lAAK&_J%_|?+)Dwxb2geD%Z}FOz z4$d1F!)QV^@&bY)aLDIYRzS{DKbHb=XKhpQBtwb=g2WcmkpUR2TkjB8QVs=PhSP=S z2>O*#{_h&m|1GGLMr2fd2G9GBKaFpGf4udMXjf6^b$kX7m_#kT9H5Qo9rnA0W1+Qv z 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%0Db+thfJT=gOie)akEK^2 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 F{{xXDTPpwn literal 27602 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�AYY-{zt)V=Uus-dxyxSNpfs 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 diff --git a/res/css/_components.pcss b/res/css/_components.pcss index bd927c9843..9df9dcb8cb 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -152,6 +152,7 @@ @import "./views/dialogs/_ModalWidgetDialog.pcss"; @import "./views/dialogs/_PollCreateDialog.pcss"; @import "./views/dialogs/_RegistrationEmailPromptDialog.pcss"; +@import "./views/dialogs/_ReportRoomDialog.pcss"; @import "./views/dialogs/_RoomSettingsDialog.pcss"; @import "./views/dialogs/_RoomSettingsDialogBridges.pcss"; @import "./views/dialogs/_RoomUpgradeDialog.pcss"; diff --git a/res/css/views/dialogs/_ReportRoomDialog.pcss b/res/css/views/dialogs/_ReportRoomDialog.pcss new file mode 100644 index 0000000000..7323ac65c9 --- /dev/null +++ b/res/css/views/dialogs/_ReportRoomDialog.pcss @@ -0,0 +1,16 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +.mx_ReportRoomDialog { + textarea { + font: var(--cpd-font-body-md-regular); + border: 1px solid var(--cpd-color-border-interactive-primary); + background: var(--cpd-color-bg-canvas-default); + border-radius: 0.5rem; + padding: var(--cpd-space-3x) var(--cpd-space-4x); + } +} diff --git a/res/css/views/right_panel/_RoomSummaryCard.pcss b/res/css/views/right_panel/_RoomSummaryCard.pcss index eb3b14579f..3c1c07677c 100644 --- a/res/css/views/right_panel/_RoomSummaryCard.pcss +++ b/res/css/views/right_panel/_RoomSummaryCard.pcss @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024, 2025 New Vector Ltd. Copyright 2020 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial @@ -101,6 +101,6 @@ Please see LICENSE files in the repository root for full details. margin: $spacing-12 0 $spacing-4; } -.mx_RoomSummaryCard_leave { +.mx_RoomSummaryCard_bottomOptions { margin: 0 0 var(--cpd-space-8x); } diff --git a/src/components/views/dialogs/ReportRoomDialog.tsx b/src/components/views/dialogs/ReportRoomDialog.tsx new file mode 100644 index 0000000000..ff4e96ea97 --- /dev/null +++ b/src/components/views/dialogs/ReportRoomDialog.tsx @@ -0,0 +1,95 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import React, { type ChangeEventHandler, useCallback, useState } from "react"; +import { Root, Field, Label, InlineSpinner, ErrorMessage } from "@vector-im/compound-web"; + +import { _t } from "../../../languageHandler"; +import SdkConfig from "../../../SdkConfig"; +import Markdown from "../../../Markdown"; +import BaseDialog from "./BaseDialog"; +import DialogButtons from "../elements/DialogButtons"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; + +interface IProps { + roomId: string; + onFinished(complete: boolean): void; +} + +/* + * A dialog for reporting a room. + */ + +export const ReportRoomDialog: React.FC = function ({ roomId, onFinished }) { + const [error, setErr] = useState(); + const [busy, setBusy] = useState(false); + const [sent, setSent] = useState(false); + const [reason, setReason] = useState(""); + const client = MatrixClientPeg.safeGet(); + + const onReasonChange = useCallback>((e) => setReason(e.target.value), []); + const onCancel = useCallback(() => onFinished(sent), [sent, onFinished]); + const onSubmit = useCallback(async () => { + setBusy(true); + try { + await client.reportRoom(roomId, reason); + setSent(true); + } catch (ex) { + if (ex instanceof Error) { + setErr(ex.message); + } else { + setErr("Unknown error"); + } + } finally { + setBusy(false); + } + }, [roomId, reason, client]); + + const adminMessageMD = SdkConfig.getObject("report_event")?.get("admin_message_md", "adminMessageMD"); + let adminMessage: JSX.Element | undefined; + if (adminMessageMD) { + const html = new Markdown(adminMessageMD).toHTML({ externalLinks: true }); + adminMessage =

    ; + } + + return ( + onFinished(sent)} + title={_t("report_room|title")} + contentId="mx_ReportEventDialog" + > + {sent &&

    {_t("report_room|sent")}

    } + {!sent && ( + +

    {_t("report_room|description")}

    + {adminMessage} + + +