diff --git a/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-custom-sections.spec.ts b/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-custom-sections.spec.ts index 29f95276fb..90eeccbb56 100644 --- a/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-custom-sections.spec.ts +++ b/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-custom-sections.spec.ts @@ -6,7 +6,7 @@ */ import { type Page } from "@playwright/test"; -import { rejectToast } from "@element-hq/element-web-playwright-common"; +import { closeReleaseAnnouncement, rejectToast } from "@element-hq/element-web-playwright-common"; import { expect, test } from "../../../element-web-test"; import { assertRoomInSection, dragRoomToSection, getRoomList, getRoomListHeader, getSectionHeader } from "./utils"; @@ -46,6 +46,9 @@ test.describe("Room list custom sections", () => { await rejectToast(page, "Verify this device"); await rejectToast(page, "Notifications"); + // Close the release announcement about the new room list sections + await closeReleaseAnnouncement(page, "Introducing Sections"); + // Focus the user menu to avoid hover decoration await page.getByRole("button", { name: "User menu" }).focus(); }); diff --git a/apps/web/playwright/e2e/release-announcement/releaseAnnouncement.spec.ts b/apps/web/playwright/e2e/release-announcement/releaseAnnouncement.spec.ts index 8f0a73ebff..e73c5fa7cd 100644 --- a/apps/web/playwright/e2e/release-announcement/releaseAnnouncement.spec.ts +++ b/apps/web/playwright/e2e/release-announcement/releaseAnnouncement.spec.ts @@ -6,15 +6,12 @@ * Please see LICENSE files in the repository root for full details. */ -import { test, expect } from "./"; +import { rejectToast } from "@element-hq/element-web-playwright-common"; + +import { test } from "./"; test.describe("Release announcement", () => { test.use({ - config: { - features: { - feature_release_announcement: true, - }, - }, room: async ({ app, user }, use) => { const roomId = await app.client.createRoom({ name: "Test room", @@ -22,37 +19,31 @@ test.describe("Release announcement", () => { await app.viewRoomById(roomId); await use({ roomId }); }, - labsFlags: ["feature_new_room_list"], + labsFlags: ["feature_room_list_sections"], + }); + + test.beforeEach(async ({ page, app, user }) => { + // The toasts are displayed above the search section + await rejectToast(page, "Verify this device"); + await rejectToast(page, "Notifications"); }); // There is no release announcement currently live - test.skip( - "should display the new room list release announcement", + test( + "should display the room list section release announcement", { tag: "@screenshot" }, async ({ page, app, room, util }) => { - // dismiss the toast so the announcement appears - await page.getByRole("button", { name: "Dismiss" }).click(); + const sectionName = "Introducing Sections"; + // The section release announcement should be displayed + await util.assertReleaseAnnouncementIsVisible(sectionName); + // Hide the section release announcement + const dialog = util.getReleaseAnnouncement(sectionName); + await dialog.getByRole("button", { name: "Ok" }).click(); - const newSoundsName = "We’ve refreshed your sounds"; - // The new sounds release announcement should be displayed - await util.assertReleaseAnnouncementIsVisible(newSoundsName); - // Hide the new sounds release announcement - const newSoundsDialog = util.getReleaseAnnouncement(newSoundsName); - await newSoundsDialog.getByRole("button", { name: "OK" }).click(); - - const newRoomListName = "Chats has a new look!"; - // The new room list release announcement should be displayed - await util.assertReleaseAnnouncementIsVisible(newRoomListName); - // Hide the new room list release announcement - const dialog = util.getReleaseAnnouncement(newRoomListName); - await dialog.getByRole("button", { name: "Next" }).click(); - - await util.assertReleaseAnnouncementIsNotVisible(newRoomListName); + await util.assertReleaseAnnouncementIsNotVisible(sectionName); await page.reload(); - await expect(page.getByRole("button", { name: "Room options" })).toBeVisible(); - // Check that once the release announcements has been marked as viewed, it does not appear again - await util.assertReleaseAnnouncementIsNotVisible(newRoomListName); + await util.assertReleaseAnnouncementIsNotVisible(sectionName); }, ); }); diff --git a/apps/web/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-Introducing-Sections-linux.png b/apps/web/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-Introducing-Sections-linux.png new file mode 100644 index 0000000000..059f80cdfb Binary files /dev/null and b/apps/web/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-Introducing-Sections-linux.png differ diff --git a/apps/web/src/stores/ReleaseAnnouncementStore.ts b/apps/web/src/stores/ReleaseAnnouncementStore.ts index 7d37cf2853..4bb77174b0 100644 --- a/apps/web/src/stores/ReleaseAnnouncementStore.ts +++ b/apps/web/src/stores/ReleaseAnnouncementStore.ts @@ -19,7 +19,7 @@ import ToastStore from "./ToastStore"; * We include a `_test_dummy` value to enable tests to function even where there are no running release announcements. * This value must be at the end of the list. */ -const FEATURES = ["_test_dummy1", "_test_dummy2"] as const; +const FEATURES = ["room_list_section", "_test_dummy1", "_test_dummy2"] as const; /** * All the features that can be shown in the release announcements. */ diff --git a/apps/web/src/viewmodels/room-list/RoomListHeaderViewModel.ts b/apps/web/src/viewmodels/room-list/RoomListHeaderViewModel.ts index 1464b6c41a..c61467ee0b 100644 --- a/apps/web/src/viewmodels/room-list/RoomListHeaderViewModel.ts +++ b/apps/web/src/viewmodels/room-list/RoomListHeaderViewModel.ts @@ -32,6 +32,7 @@ import RoomListStoreV3 from "../../stores/room-list-v3/RoomListStoreV3"; import { SortingAlgorithm } from "../../stores/room-list-v3/skip-list/sorters"; import { SettingLevel } from "../../settings/SettingLevel"; import { createRoom, hasCreateRoomRights } from "./utils"; +import { ReleaseAnnouncementStore } from "../../stores/ReleaseAnnouncementStore"; export interface Props { /** @@ -82,6 +83,12 @@ export class RoomListHeaderViewModel // Listen for section collapse state changes from RoomListViewModel const dispatcherRef = defaultDispatcher.register(this.onDispatch); this.disposables.track(() => defaultDispatcher.unregister(dispatcherRef)); + + this.disposables.trackListener( + ReleaseAnnouncementStore.instance, + "releaseAnnouncementChanged", + this.onReleaseAnnouncementChanged, + ); } /** @@ -229,7 +236,21 @@ export class RoomListHeaderViewModel }); } }; + + public closeSectionReleaseAnnouncement = (): void => { + ReleaseAnnouncementStore.instance.nextReleaseAnnouncement(); + this.snapshot.merge({ displaySectionReleaseAnnouncement: false }); + }; + + public onReleaseAnnouncementChanged = (): void => { + const isSectionFeatureEnabled = SettingsStore.getValue("feature_room_list_sections"); + const displaySectionReleaseAnnouncement = + isSectionFeatureEnabled && + ReleaseAnnouncementStore.instance.getReleaseAnnouncement() === "room_list_section"; + this.snapshot.merge({ displaySectionReleaseAnnouncement }); + }; } + /** * Get the initial snapshot for the RoomListHeaderViewModel. * @param spaceStore - The space store instance. @@ -290,6 +311,8 @@ function computeHeaderSpaceState( matrixClient: MatrixClient, ): Omit { const isSectionFeatureEnabled = SettingsStore.getValue("feature_room_list_sections"); + const displaySectionReleaseAnnouncement = + isSectionFeatureEnabled && ReleaseAnnouncementStore.instance.getReleaseAnnouncement() === "room_list_section"; const activeSpace = spaceStore.activeSpaceRoom; const title = getHeaderTitle(spaceStore); @@ -316,5 +339,6 @@ function computeHeaderSpaceState( canAccessSpaceSettings, canCreateSection, useComposeIcon, + displaySectionReleaseAnnouncement, }; } diff --git a/apps/web/test/unit-tests/components/structures/ReleaseAnnouncement-test.tsx b/apps/web/test/unit-tests/components/structures/ReleaseAnnouncement-test.tsx index bb0274e9af..75a1474ac9 100644 --- a/apps/web/test/unit-tests/components/structures/ReleaseAnnouncement-test.tsx +++ b/apps/web/test/unit-tests/components/structures/ReleaseAnnouncement-test.tsx @@ -22,7 +22,12 @@ describe("ReleaseAnnouncement", () => { function renderReleaseAnnouncement() { return render( - +
content
, ); diff --git a/apps/web/test/unit-tests/stores/ReleaseAnnouncementStore-test.tsx b/apps/web/test/unit-tests/stores/ReleaseAnnouncementStore-test.tsx index dca3429f1f..8a17c1913c 100644 --- a/apps/web/test/unit-tests/stores/ReleaseAnnouncementStore-test.tsx +++ b/apps/web/test/unit-tests/stores/ReleaseAnnouncementStore-test.tsx @@ -108,13 +108,13 @@ describe("ReleaseAnnouncementStore", () => { it("should listen to release announcement data changes in the store", async () => { const secondStore = new ReleaseAnnouncementStore(); - expect(secondStore.getReleaseAnnouncement()).toBe("_test_dummy1"); + expect(secondStore.getReleaseAnnouncement()).toBe("room_list_section"); const promise = listenReleaseAnnouncementChanged(); await secondStore.nextReleaseAnnouncement(); - expect(await promise).toBe("_test_dummy2"); - expect(releaseAnnouncementStore.getReleaseAnnouncement()).toBe("_test_dummy2"); + expect(await promise).toBe("_test_dummy1"); + expect(releaseAnnouncementStore.getReleaseAnnouncement()).toBe("_test_dummy1"); }); it("should return null when there are toasts on screen", async () => { diff --git a/apps/web/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts b/apps/web/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts index 9ff26c2236..006c4778d2 100644 --- a/apps/web/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts +++ b/apps/web/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts @@ -26,6 +26,7 @@ import { import { createTestClient, mkSpace } from "../../test-utils"; import { createRoom, hasCreateRoomRights } from "../../../src/viewmodels/room-list/utils"; import PosthogTrackers from "../../../src/PosthogTrackers"; +import { ReleaseAnnouncementStore } from "../../../src/stores/ReleaseAnnouncementStore"; jest.mock("../../../src/PosthogTrackers", () => ({ trackInteraction: jest.fn(), @@ -59,6 +60,9 @@ describe("RoomListHeaderViewModel", () => { mocked(hasCreateRoomRights).mockReturnValue(true); mocked(shouldShowSpaceSettings).mockReturnValue(true); + jest.spyOn(ReleaseAnnouncementStore.instance, "getReleaseAnnouncement").mockReturnValue(null); + jest.spyOn(ReleaseAnnouncementStore.instance, "nextReleaseAnnouncement").mockResolvedValue(undefined); + jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => { if (settingName === "RoomList.preferredSorting") return SortingAlgorithm.Recency; if (settingName === "feature_video_rooms") return true; @@ -191,6 +195,28 @@ describe("RoomListHeaderViewModel", () => { expect(vm.getSnapshot().useComposeIcon).toBe(expectedUseComposeIcon); }, ); + + it("should set displaySectionReleaseAnnouncement to true when sections feature is enabled and announcement is active", () => { + jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => { + if (settingName === "feature_room_list_sections") return true; + return false; + }); + jest.spyOn(ReleaseAnnouncementStore.instance, "getReleaseAnnouncement").mockReturnValue( + "room_list_section", + ); + + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + expect(vm.getSnapshot().displaySectionReleaseAnnouncement).toBe(true); + }); + + it("should set displaySectionReleaseAnnouncement to false when sections feature is disabled", () => { + jest.spyOn(ReleaseAnnouncementStore.instance, "getReleaseAnnouncement").mockReturnValue( + "room_list_section", + ); + + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + expect(vm.getSnapshot().displaySectionReleaseAnnouncement).toBe(false); + }); }); describe("event listeners", () => { @@ -435,5 +461,14 @@ describe("RoomListHeaderViewModel", () => { expect(setValueSpy).toHaveBeenCalledWith("RoomList.showMessagePreview", null, expect.anything(), false); expect(vm.getSnapshot().isMessagePreviewEnabled).toBe(false); }); + + it("should call nextReleaseAnnouncement and set displaySectionReleaseAnnouncement to false when closeSectionReleaseAnnouncement is called", () => { + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + + vm.closeSectionReleaseAnnouncement(); + + expect(ReleaseAnnouncementStore.instance.nextReleaseAnnouncement).toHaveBeenCalled(); + expect(vm.getSnapshot().displaySectionReleaseAnnouncement).toBe(false); + }); }); }); diff --git a/packages/playwright-common/src/index.ts b/packages/playwright-common/src/index.ts index 9644a15982..be96d53f0b 100644 --- a/packages/playwright-common/src/index.ts +++ b/packages/playwright-common/src/index.ts @@ -13,6 +13,7 @@ import { routeConfigJson } from "./utils/config_json.js"; export * from "./utils/config_json.js"; export * from "./utils/context.js"; +export * from "./utils/release_accouncement.js"; export * from "./utils/toasts.js"; export { populateLocalStorageWithCredentials } from "./fixtures/user.js"; diff --git a/packages/playwright-common/src/utils/release_accouncement.ts b/packages/playwright-common/src/utils/release_accouncement.ts new file mode 100644 index 0000000000..0435f58585 --- /dev/null +++ b/packages/playwright-common/src/utils/release_accouncement.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +import { Page } from "playwright-core"; + +/** + * Close the release announcement with the given name. + * @param page The Playwright page instance. + * @param name The name of the release announcement dialog. + */ +export async function closeReleaseAnnouncement(page: Page, name: string): Promise { + await page.getByRole("dialog", { name }).getByRole("button", { name: "OK" }).click(); +} diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx/display-section-release-announcement-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx/display-section-release-announcement-auto.png new file mode 100644 index 0000000000..3739c0881e Binary files /dev/null and b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx/display-section-release-announcement-auto.png differ diff --git a/packages/shared-components/src/i18n/strings/en_EN.json b/packages/shared-components/src/i18n/strings/en_EN.json index 80bed47b08..9bf59cc09b 100644 --- a/packages/shared-components/src/i18n/strings/en_EN.json +++ b/packages/shared-components/src/i18n/strings/en_EN.json @@ -67,6 +67,11 @@ "mentions_keywords": "Mentions and keywords", "mute_room": "Mute room" }, + "release_announcement": { + "room_list_section_close": "Ok", + "room_list_section_description": " Group your chats into sections to keep conversations organised and easy to navigate.", + "room_list_section_title": "Introducing Sections" + }, "room": { "context_menu": { "title": "Room options" diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx index 6fe8755910..bcf2e8efb2 100644 --- a/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx +++ b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx @@ -32,6 +32,7 @@ const RoomListHeaderViewWrapperImpl = ({ toggleMessagePreview, createSection, collapseOrExpandSections, + closeSectionReleaseAnnouncement, ...rest }: RoomListHeaderProps): JSX.Element => { const vm = useMockedViewModel(rest, { @@ -46,6 +47,7 @@ const RoomListHeaderViewWrapperImpl = ({ toggleMessagePreview, createSection, collapseOrExpandSections, + closeSectionReleaseAnnouncement, }); return ; }; @@ -68,6 +70,7 @@ const meta = { toggleMessagePreview: fn(), createSection: fn(), collapseOrExpandSections: fn(), + closeSectionReleaseAnnouncement: fn(), }, parameters: { design: { @@ -124,3 +127,30 @@ export const ExpandSections: Story = { collapseSections: "expand", }, }; + +export const DisplaySectionReleaseAnnouncement: Story = { + decorators: [ + (Story) => ( +
+ +
+ ), + ], + args: { + displaySectionReleaseAnnouncement: true, + }, + parameters: { + a11y: { + config: { + rules: [ + { + // compound-web's ReleaseAnnouncement renders its header as

, + // which jumps from RoomListHeaderView's

("Rooms"). + id: "heading-order", + enabled: false, + }, + ], + }, + }, + }, +}; diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.tsx b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.tsx index 3b17571d54..92711bd9be 100644 --- a/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.tsx +++ b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.tsx @@ -80,6 +80,10 @@ export interface RoomListHeaderViewSnapshot { * If undefined, no icon are shown. */ collapseSections?: CollapseSectionsOption; + /** + * Whether to display the section release announcement + */ + displaySectionReleaseAnnouncement: boolean; } export interface RoomListHeaderViewActions { @@ -127,6 +131,10 @@ export interface RoomListHeaderViewActions { * Collapse or expand all sections in the room list depending on the current state. */ collapseOrExpandSections: () => void; + /** + * Close the section release announcement + */ + closeSectionReleaseAnnouncement: () => void; } /** diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/default-snapshot.ts b/packages/shared-components/src/room-list/RoomListHeaderView/default-snapshot.ts index 41887e8ed4..a775a3c526 100644 --- a/packages/shared-components/src/room-list/RoomListHeaderView/default-snapshot.ts +++ b/packages/shared-components/src/room-list/RoomListHeaderView/default-snapshot.ts @@ -19,4 +19,5 @@ export const defaultSnapshot: RoomListHeaderViewSnapshot = { isMessagePreviewEnabled: true, useComposeIcon: true, canCreateSection: true, + displaySectionReleaseAnnouncement: false, }; diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/menu/ComposeMenuView.tsx b/packages/shared-components/src/room-list/RoomListHeaderView/menu/ComposeMenuView.tsx index 3803c23fcf..c31fa53e82 100644 --- a/packages/shared-components/src/room-list/RoomListHeaderView/menu/ComposeMenuView.tsx +++ b/packages/shared-components/src/room-list/RoomListHeaderView/menu/ComposeMenuView.tsx @@ -6,7 +6,7 @@ */ import React, { useState, type JSX } from "react"; -import { IconButton, Menu, MenuItem } from "@vector-im/compound-web"; +import { IconButton, Menu, MenuItem, ReleaseAnnouncement } from "@vector-im/compound-web"; import ComposeIcon from "@vector-im/compound-design-tokens/assets/web/icons/compose"; import VideoCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/video-call"; import ChatIcon from "@vector-im/compound-design-tokens/assets/web/icons/chat"; @@ -37,7 +37,33 @@ interface ComposeMenuViewProps { export function ComposeMenuView({ vm }: ComposeMenuViewProps): JSX.Element { const { translate: _t } = useI18n(); const [open, setOpen] = useState(false); - const { canCreateRoom, canCreateVideoRoom, canCreateSection, useComposeIcon } = useViewModel(vm); + const { canCreateRoom, canCreateVideoRoom, canCreateSection, useComposeIcon, displaySectionReleaseAnnouncement } = + useViewModel(vm); + + // 28px button with a 20px icon + const button = ( + + {useComposeIcon ? ( + + ) : ( + + )} + + ); + + if (displaySectionReleaseAnnouncement) { + return ( + + {button} + + ); + } return ( - {useComposeIcon ? ( - - ) : ( - - )} - - } + trigger={button} > {canCreateRoom && ( diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/test-utils.ts b/packages/shared-components/src/room-list/RoomListHeaderView/test-utils.ts index 70c51c45c8..4f534cc4ae 100644 --- a/packages/shared-components/src/room-list/RoomListHeaderView/test-utils.ts +++ b/packages/shared-components/src/room-list/RoomListHeaderView/test-utils.ts @@ -25,6 +25,7 @@ export class MockedViewModel extends MockViewModel i public toggleMessagePreview = vi.fn<() => void>(); public createSection = vi.fn<() => void>(); public collapseOrExpandSections = vi.fn<() => void>(); + public closeSectionReleaseAnnouncement = vi.fn<() => void>(); } export { defaultSnapshot } from "./default-snapshot";