From 5815e70b7630538eb19f5d4bc047fa39b9ff9e75 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 8 Apr 2024 10:43:59 +0200 Subject: [PATCH] TAC: Release Announcement (#12380) * WIP * Store the release announcements in the account settings * Update TAC release announcement description * Fix settings content comparison * Add logging in case of failure * Watch settings changes * I add release announcement settings to disable it * Disable release announcement in e2e test * Add release announcement in e2e test * Add tests for ReleaseAnnouncementStore.ts * Update compound-web to `3.3.0` * Update TAC tests * Update Labs tests * Nits * Add test for ReleaseAnnouncement.tsx * Update `@vector-im/compound-web` * Add playwright snapshot * Delete false playwright screenshot * Wait for EW to be displayed after reload * Add screenshot * Clean util file * Renaming and comments fixing * Use second store instead of looking in the store. --------- Co-authored-by: R Midhun Suresh --- package.json | 2 +- playwright/e2e/release-announcement/index.ts | 77 ++++++++ .../releaseAnnouncement.spec.ts | 44 +++++ playwright/element-web-test.ts | 5 + ...uncement-Threads-Activity-Centre-linux.png | Bin 0 -> 62630 bytes .../structures/ReleaseAnnouncement.tsx | 54 ++++++ .../ThreadsActivityCentre.tsx | 77 +++++--- src/hooks/useIsReleaseAnnouncementOpen.ts | 32 ++++ src/i18n/strings/en_EN.json | 6 +- src/settings/Settings.tsx | 21 +++ .../handlers/AccountSettingsHandler.ts | 3 +- src/stores/ReleaseAnnouncementStore.ts | 176 ++++++++++++++++++ .../structures/ReleaseAnnouncement-test.tsx | 48 +++++ .../tabs/user/LabsUserSettingsTab-test.tsx | 2 +- .../spaces/ThreadsActivityCentre-test.tsx | 14 ++ .../ThreadsActivityCentre-test.tsx.snap | 138 +++++++++++++- test/stores/ReleaseAnnouncementStore-test.tsx | 125 +++++++++++++ yarn.lock | 26 ++- 18 files changed, 805 insertions(+), 45 deletions(-) create mode 100644 playwright/e2e/release-announcement/index.ts create mode 100644 playwright/e2e/release-announcement/releaseAnnouncement.spec.ts create mode 100644 playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-Threads-Activity-Centre-linux.png create mode 100644 src/components/structures/ReleaseAnnouncement.tsx create mode 100644 src/hooks/useIsReleaseAnnouncementOpen.ts create mode 100644 src/stores/ReleaseAnnouncementStore.ts create mode 100644 test/components/structures/ReleaseAnnouncement-test.tsx create mode 100644 test/stores/ReleaseAnnouncementStore-test.tsx diff --git a/package.json b/package.json index 6f2f894ac5..42e635d2ba 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@sentry/browser": "^7.0.0", "@testing-library/react-hooks": "^8.0.1", "@vector-im/compound-design-tokens": "^1.2.0", - "@vector-im/compound-web": "^3.1.1", + "@vector-im/compound-web": "^3.3.1", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", "@zxcvbn-ts/language-en": "^3.0.2", diff --git a/playwright/e2e/release-announcement/index.ts b/playwright/e2e/release-announcement/index.ts new file mode 100644 index 0000000000..d5ea4f2917 --- /dev/null +++ b/playwright/e2e/release-announcement/index.ts @@ -0,0 +1,77 @@ +/* + * + * Copyright 2024 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * / + */ + +import { Page } from "@playwright/test"; + +import { test as base, expect } from "../../element-web-test"; + +/** + * Set up for release announcement tests. + */ +export const test = base.extend<{ + util: Helpers; +}>({ + displayName: "Alice", + botCreateOpts: { displayName: "Other User" }, + + util: async ({ page, app, bot }, use) => { + await use(new Helpers(page)); + }, +}); + +export class Helpers { + constructor(private page: Page) {} + + /** + * Get the release announcement with the given name. + * @param name + * @private + */ + private getReleaseAnnouncement(name: string) { + return this.page.getByRole("dialog", { name }); + } + + /** + * Assert that the release announcement with the given name is visible. + * @param name + */ + async assertReleaseAnnouncementIsVisible(name: string) { + await expect(this.getReleaseAnnouncement(name)).toBeVisible(); + await expect(this.page).toMatchScreenshot(`release-announcement-${name}.png`); + } + + /** + * Assert that the release announcement with the given name is not visible. + * @param name + */ + assertReleaseAnnouncementIsNotVisible(name: string) { + return expect(this.getReleaseAnnouncement(name)).not.toBeVisible(); + } + + /** + * Mark the release announcement with the given name as read. + * If the release announcement is not visible, this will throw an error. + * @param name + */ + async markReleaseAnnouncementAsRead(name: string) { + const dialog = this.getReleaseAnnouncement(name); + await dialog.getByRole("button", { name: "Ok" }).click(); + } +} + +export { expect }; diff --git a/playwright/e2e/release-announcement/releaseAnnouncement.spec.ts b/playwright/e2e/release-announcement/releaseAnnouncement.spec.ts new file mode 100644 index 0000000000..24854560c8 --- /dev/null +++ b/playwright/e2e/release-announcement/releaseAnnouncement.spec.ts @@ -0,0 +1,44 @@ +/* + * + * Copyright 2024 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * / + */ + +import { test, expect } from "./"; + +test.describe("Release announcement", () => { + test.use({ + config: { + features: { + feature_release_announcement: true, + }, + }, + labsFlags: ["threadsActivityCentre"], + }); + + test("should display the release announcement process", async ({ page, app, util }) => { + // The TAC release announcement should be displayed + await util.assertReleaseAnnouncementIsVisible("Threads Activity Centre"); + // Hide the release announcement + await util.markReleaseAnnouncementAsRead("Threads Activity Centre"); + await util.assertReleaseAnnouncementIsNotVisible("Threads Activity Centre"); + + await page.reload(); + // Wait for EW to load + await expect(page.getByRole("navigation", { name: "Spaces" })).toBeVisible(); + // Check that once the release announcement has been marked as viewed, it does not appear again + await util.assertReleaseAnnouncementIsNotVisible("Threads Activity Centre"); + }); +}); diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts index a524c139f6..e67cca6ab8 100644 --- a/playwright/element-web-test.ts +++ b/playwright/element-web-test.ts @@ -52,6 +52,11 @@ const CONFIG_JSON: Partial = { // the location tests want a map style url. map_style_url: "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx", + + features: { + // We don't want to go through the feature announcement during the e2e test + feature_release_announcement: false, + }, }; export type TestOptions = { diff --git a/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-Threads-Activity-Centre-linux.png b/playwright/snapshots/release-announcement/releaseAnnouncement.spec.ts/release-announcement-Threads-Activity-Centre-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..6439fe305bb1412fb87686c414b84a23816c987f GIT binary patch literal 62630 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 r>5U2vol, "open" | "onClick"> { + feature: Feature; +} + +/** + * Display a release announcement component around the children + * Wrapper gluing the release announcement compound and the ReleaseAnnouncementStore + * @param feature - the feature to announce, should be listed in {@link Feature} + * @param children + * @param props + * @constructor + */ +export function ReleaseAnnouncement({ + feature, + children, + ...props +}: PropsWithChildren): JSX.Element { + const enabled = useIsReleaseAnnouncementOpen(feature); + + return ( + ReleaseAnnouncementStore.instance.nextReleaseAnnouncement()} + {...props} + > + {children} + + ); +} diff --git a/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentre.tsx b/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentre.tsx index ddb1dd98d3..2f8607fb13 100644 --- a/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentre.tsx +++ b/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentre.tsx @@ -34,6 +34,8 @@ import { NotificationLevel } from "../../../../stores/notifications/Notification import PosthogTrackers from "../../../../PosthogTrackers"; import { getKeyBindingsManager } from "../../../../KeyBindingsManager"; import { KeyBindingAction } from "../../../../accessibility/KeyboardShortcuts"; +import { ReleaseAnnouncement } from "../../../structures/ReleaseAnnouncement"; +import { useIsReleaseAnnouncementOpen } from "../../../../hooks/useIsReleaseAnnouncementOpen"; interface ThreadsActivityCentreProps { /** @@ -49,6 +51,7 @@ interface ThreadsActivityCentreProps { export function ThreadsActivityCentre({ displayButtonLabel }: ThreadsActivityCentreProps): JSX.Element { const [open, setOpen] = useState(false); const roomsAndNotifications = useUnreadThreadRooms(open); + const isReleaseAnnouncementOpen = useIsReleaseAnnouncementOpen("threadsActivityCentre"); return (

- { - // Track only when the Threads Activity Centre is opened - if (newOpen) PosthogTrackers.trackInteraction("WebThreadsActivityCentreButton"); - - setOpen(newOpen); - }} - side="right" - title={_t("threads_activity_centre|header")} - trigger={ + {isReleaseAnnouncementOpen ? ( + - } - > - {/* Make the content of the pop-up scrollable */} -
- {roomsAndNotifications.rooms.map(({ room, notificationLevel }) => ( - setOpen(false)} + + ) : ( + { + // Track only when the Threads Activity Centre is opened + if (newOpen) PosthogTrackers.trackInteraction("WebThreadsActivityCentreButton"); + + setOpen(newOpen); + }} + side="right" + title={_t("threads_activity_centre|header")} + trigger={ + - ))} - {roomsAndNotifications.rooms.length === 0 && ( -
- {_t("threads_activity_centre|no_rooms_with_unreads_threads")} -
- )} -
-
+ } + > + {/* Make the content of the pop-up scrollable */} +
+ {roomsAndNotifications.rooms.map(({ room, notificationLevel }) => ( + setOpen(false)} + /> + ))} + {roomsAndNotifications.rooms.length === 0 && ( +
+ {_t("threads_activity_centre|no_rooms_with_unreads_threads")} +
+ )} +
+ + )}
); } diff --git a/src/hooks/useIsReleaseAnnouncementOpen.ts b/src/hooks/useIsReleaseAnnouncementOpen.ts new file mode 100644 index 0000000000..ab8bf07c5e --- /dev/null +++ b/src/hooks/useIsReleaseAnnouncementOpen.ts @@ -0,0 +1,32 @@ +/* + * + * Copyright 2024 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * / + */ + +import { useTypedEventEmitterState } from "./useEventEmitter"; +import { Feature, ReleaseAnnouncementStore } from "../stores/ReleaseAnnouncementStore"; + +/** + * Return true if the release announcement of the given feature is enabled + * @param feature + */ +export function useIsReleaseAnnouncementOpen(feature: Feature): boolean { + return useTypedEventEmitterState( + ReleaseAnnouncementStore.instance, + "releaseAnnouncementChanged", + () => ReleaseAnnouncementStore.instance.getReleaseAnnouncement() === feature, + ); +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 07a85767e1..8067412064 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1417,6 +1417,7 @@ "group_spaces": "Spaces", "group_themes": "Themes", "group_threads": "Threads", + "group_ui": "User interface", "group_voip": "Voice & Video", "group_widgets": "Widgets", "hidebold": "Hide notification dot (only display counters badges)", @@ -1440,6 +1441,7 @@ "oidc_native_flow": "OIDC native authentication", "oidc_native_flow_description": "⚠ WARNING: Experimental. Use OIDC native authentication when supported by the server.", "pinning": "Message Pinning", + "release_announcement": "Release announcement", "render_reaction_images": "Render custom images in reactions", "render_reaction_images_description": "Sometimes referred to as \"custom emojis\".", "report_to_moderators": "Report to moderators", @@ -3161,7 +3163,9 @@ }, "threads_activity_centre": { "header": "Threads activity", - "no_rooms_with_unreads_threads": "You don't have rooms with unread threads yet." + "no_rooms_with_unreads_threads": "You don't have rooms with unread threads yet.", + "release_announcement_description": "Threads notifications have moved, find them here from now on.", + "release_announcement_header": "Threads Activity Centre" }, "time": { "about_day_ago": "about a day ago", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 3dc842945e..2297437479 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -89,6 +89,7 @@ export enum LabGroup { Encryption, Experimental, Developer, + Ui, } export enum Features { @@ -98,6 +99,7 @@ export enum Features { OidcNativeFlow = "feature_oidc_native_flow", // If true, every new login will use the new rust crypto implementation RustCrypto = "feature_rust_crypto", + ReleaseAnnouncement = "feature_release_announcement", } export const labGroupNames: Record = { @@ -114,6 +116,7 @@ export const labGroupNames: Record = { [LabGroup.Encryption]: _td("labs|group_encryption"), [LabGroup.Experimental]: _td("labs|group_experimental"), [LabGroup.Developer]: _td("labs|group_developer"), + [LabGroup.Ui]: _td("labs|group_ui"), }; export type SettingValueType = @@ -1145,6 +1148,24 @@ export const SETTINGS: { [setting: string]: ISetting } = { default: false, isFeature: true, }, + /** + * Enable or disable the release announcement feature + */ + [Features.ReleaseAnnouncement]: { + isFeature: true, + labsGroup: LabGroup.Ui, + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, + default: true, + displayName: _td("labs|release_announcement"), + }, + /** + * Managed by the {@link ReleaseAnnouncementStore} + * Store the release announcement data + */ + "releaseAnnouncementData": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + default: {}, + }, [UIFeature.RoomHistorySettings]: { supportedLevels: LEVELS_UI_FEATURE, default: true, diff --git a/src/settings/handlers/AccountSettingsHandler.ts b/src/settings/handlers/AccountSettingsHandler.ts index e931a92600..639bef628c 100644 --- a/src/settings/handlers/AccountSettingsHandler.ts +++ b/src/settings/handlers/AccountSettingsHandler.ts @@ -17,6 +17,7 @@ limitations under the License. import { ClientEvent, MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { defer } from "matrix-js-sdk/src/utils"; +import { isEqual } from "lodash"; import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler"; import { objectClone, objectKeyChanges } from "../../utils/objects"; @@ -168,7 +169,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa // which race between different lines. const deferred = defer(); const handler = (event: MatrixEvent): void => { - if (event.getType() !== eventType || event.getContent()[field] !== value) return; + if (event.getType() !== eventType || !isEqual(event.getContent()[field], value)) return; this.client.off(ClientEvent.AccountData, handler); deferred.resolve(); }; diff --git a/src/stores/ReleaseAnnouncementStore.ts b/src/stores/ReleaseAnnouncementStore.ts new file mode 100644 index 0000000000..9beeed4f70 --- /dev/null +++ b/src/stores/ReleaseAnnouncementStore.ts @@ -0,0 +1,176 @@ +/* + * + * Copyright 2024 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * / + */ + +import { TypedEventEmitter } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; + +import SettingsStore from "../settings/SettingsStore"; +import { SettingLevel } from "../settings/SettingLevel"; +import { Features } from "../settings/Settings"; + +/** + * The features are shown in the array order. + */ +const FEATURES = ["threadsActivityCentre"] as const; +/** + * All the features that can be shown in the release announcements. + */ +export type Feature = (typeof FEATURES)[number]; +/** + * The stored settings for the release announcements. + * The boolean is at true when the user has viewed the feature + */ +type StoredSettings = Record; + +/** + * The events emitted by the ReleaseAnnouncementStore. + */ +type ReleaseAnnouncementStoreEvents = "releaseAnnouncementChanged"; +/** + * The handlers for the ReleaseAnnouncementStore events. + */ +type HandlerMap = { + releaseAnnouncementChanged: (newFeature: Feature | null) => void; +}; + +/** + * The ReleaseAnnouncementStore is responsible for managing the release announcements. + * It keeps track of the viewed release announcements and emits events when the release announcement changes. + */ +export class ReleaseAnnouncementStore extends TypedEventEmitter { + /** + * The singleton instance of the ReleaseAnnouncementStore. + * @private + */ + private static internalInstance: ReleaseAnnouncementStore; + /** + * The index of the feature to show. + * @private + */ + private index = 0; + + /** + * The singleton instance of the ReleaseAnnouncementStore. + */ + public static get instance(): ReleaseAnnouncementStore { + if (!ReleaseAnnouncementStore.internalInstance) { + ReleaseAnnouncementStore.internalInstance = new ReleaseAnnouncementStore(); + } + return ReleaseAnnouncementStore.internalInstance; + } + + /** + * Should be used only for testing purposes. + * @internal + */ + public constructor() { + super(); + SettingsStore.watchSetting("releaseAnnouncementData", null, () => { + this.emit("releaseAnnouncementChanged", this.getReleaseAnnouncement()); + }); + } + + /** + * Get the viewed release announcements from the settings. + * @private + */ + private getViewedReleaseAnnouncements(): StoredSettings { + return SettingsStore.getValue("releaseAnnouncementData"); + } + + /** + * Check if the release announcement is enabled. + * @private + */ + private isReleaseAnnouncementEnabled(): boolean { + return SettingsStore.getValue(Features.ReleaseAnnouncement); + } + + /** + * Get the release announcement that should be displayed + * @returns The feature to announce or null if there is no feature to announce + */ + public getReleaseAnnouncement(): Feature | null { + // Do nothing if the release announcement is disabled + const isReleaseAnnouncementEnabled = this.isReleaseAnnouncementEnabled(); + if (!isReleaseAnnouncementEnabled) return null; + + const viewedReleaseAnnouncements = this.getViewedReleaseAnnouncements(); + + // Find the first feature that has not been viewed + for (let i = this.index; i < FEATURES.length; i++) { + if (!viewedReleaseAnnouncements[FEATURES[i]]) { + this.index = i; + return FEATURES[this.index]; + } + } + + // All features have been viewed + return null; + } + + /** + * Mark the current release announcement as viewed. + * This will update the account settings + * @private + */ + private async markReleaseAnnouncementAsViewed(): Promise { + // Do nothing if the release announcement is disabled + const isReleaseAnnouncementEnabled = this.isReleaseAnnouncementEnabled(); + if (!isReleaseAnnouncementEnabled) return; + + const viewedReleaseAnnouncements = this.getViewedReleaseAnnouncements(); + + // If the index is out of bounds, do nothing + // Normally it shouldn't happen, but it's better to be safe + const feature = FEATURES[this.index]; + if (!feature) return; + + // Mark the feature as viewed + viewedReleaseAnnouncements[FEATURES[this.index]] = true; + this.index++; + + // Do sanity check if we can store the new value in the settings + const isSupported = SettingsStore.isLevelSupported(SettingLevel.ACCOUNT); + if (!isSupported) return; + + const canSetValue = SettingsStore.canSetValue("releaseAnnouncementData", null, SettingLevel.ACCOUNT); + if (canSetValue) { + try { + await SettingsStore.setValue( + "releaseAnnouncementData", + null, + SettingLevel.ACCOUNT, + viewedReleaseAnnouncements, + ); + } catch (e) { + logger.log("Failed to set release announcement settings", e); + } + } + } + + /** + * Mark the current release announcement as viewed and move to the next release announcement. + * This will update the account settings and emit the `releaseAnnouncementChanged` event + */ + public async nextReleaseAnnouncement(): Promise { + await this.markReleaseAnnouncementAsViewed(); + + this.emit("releaseAnnouncementChanged", this.getReleaseAnnouncement()); + } +} diff --git a/test/components/structures/ReleaseAnnouncement-test.tsx b/test/components/structures/ReleaseAnnouncement-test.tsx new file mode 100644 index 0000000000..3477e54d4b --- /dev/null +++ b/test/components/structures/ReleaseAnnouncement-test.tsx @@ -0,0 +1,48 @@ +/* + * + * Copyright 2024 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * / + */ + +import React from "react"; +import { render, screen, waitFor } from "@testing-library/react"; + +import { ReleaseAnnouncement } from "../../../src/components/structures/ReleaseAnnouncement"; + +describe("ReleaseAnnouncement", () => { + function renderReleaseAnnouncement() { + return render( + +
content
+
, + ); + } + + test("render the release announcement and close it", async () => { + renderReleaseAnnouncement(); + + // The release announcement is displayed + expect(screen.queryByRole("dialog", { name: "header" })).toBeDefined(); + // Click on the close button in the release announcement + screen.getByRole("button", { name: "close" }).click(); + // The release announcement should be hidden after the close button is clicked + await waitFor(() => expect(screen.queryByRole("dialog", { name: "header" })).toBeNull()); + }); +}); diff --git a/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx b/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx index 3239c0f875..fd9a92a225 100644 --- a/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/user/LabsUserSettingsTab-test.tsx @@ -60,7 +60,7 @@ describe("", () => { // non-beta labs section expect(screen.getByText("Early previews")).toBeInTheDocument(); const labsSections = container.getElementsByClassName("mx_SettingsSubsection"); - expect(labsSections).toHaveLength(10); + expect(labsSections).toHaveLength(11); }); describe("Rust crypto setting", () => { diff --git a/test/components/views/spaces/ThreadsActivityCentre-test.tsx b/test/components/views/spaces/ThreadsActivityCentre-test.tsx index 8deb27ec7e..9cc47c93f1 100644 --- a/test/components/views/spaces/ThreadsActivityCentre-test.tsx +++ b/test/components/views/spaces/ThreadsActivityCentre-test.tsx @@ -28,6 +28,8 @@ import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; import { stubClient } from "../../../test-utils"; import { populateThread } from "../../../test-utils/threads"; import DMRoomMap from "../../../../src/utils/DMRoomMap"; +import SettingsStore from "../../../../src/settings/SettingsStore"; +import { SettingLevel } from "../../../../src/settings/SettingLevel"; describe("ThreadsActivityCentre", () => { const getTACButton = () => { @@ -101,11 +103,23 @@ describe("ThreadsActivityCentre", () => { ); }); + beforeEach(async () => { + await SettingsStore.setValue("feature_release_announcement", null, SettingLevel.DEVICE, false); + }); + it("should render the threads activity centre button", async () => { renderTAC(); expect(getTACButton()).toBeInTheDocument(); }); + it("should render the release announcement", async () => { + // Enable release announcement + await SettingsStore.setValue("feature_release_announcement", null, SettingLevel.DEVICE, true); + + renderTAC(); + expect(document.body).toMatchSnapshot(); + }); + it("should render the threads activity centre button and the display label", async () => { renderTAC({ displayButtonLabel: true }); expect(getTACButton()).toBeInTheDocument(); diff --git a/test/components/views/spaces/__snapshots__/ThreadsActivityCentre-test.tsx.snap b/test/components/views/spaces/__snapshots__/ThreadsActivityCentre-test.tsx.snap index 0d2841c614..3146a3c80a 100644 --- a/test/components/views/spaces/__snapshots__/ThreadsActivityCentre-test.tsx.snap +++ b/test/components/views/spaces/__snapshots__/ThreadsActivityCentre-test.tsx.snap @@ -2,7 +2,7 @@ exports[`ThreadsActivityCentre renders notifications matching the snapshot 1`] = ` `; + +exports[`ThreadsActivityCentre should render the release announcement 1`] = ` + +
+
+ + + + +
+
+
+ + + +
+ +`; diff --git a/test/stores/ReleaseAnnouncementStore-test.tsx b/test/stores/ReleaseAnnouncementStore-test.tsx new file mode 100644 index 0000000000..77e79cd545 --- /dev/null +++ b/test/stores/ReleaseAnnouncementStore-test.tsx @@ -0,0 +1,125 @@ +/* + * + * Copyright 2024 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * / + */ + +import { mocked } from "jest-mock"; + +import SettingsStore, { CallbackFn } from "../../src/settings/SettingsStore"; +import { Feature, ReleaseAnnouncementStore } from "../../src/stores/ReleaseAnnouncementStore"; +import { SettingLevel } from "../../src/settings/SettingLevel"; + +jest.mock("../../src/settings/SettingsStore"); + +describe("ReleaseAnnouncementStore", () => { + let releaseAnnouncementStore: ReleaseAnnouncementStore; + // Local settings + // Instead of using the real SettingsStore, we use a local settings object + // to avoid side effects between tests + let settings: Record = {}; + + beforeEach(() => { + // Default settings + settings = { + feature_release_announcement: true, + releaseAnnouncementData: {}, + }; + const watchCallbacks: Array = []; + + mocked(SettingsStore.getValue).mockImplementation((setting: string) => { + return settings[setting]; + }); + mocked(SettingsStore.setValue).mockImplementation( + (settingName: string, roomId: string | null, level: SettingLevel, value: any): Promise => { + settings[settingName] = value; + // we don't care about the parameters, just call the callbacks + // @ts-ignore + watchCallbacks.forEach((cb) => cb()); + return Promise.resolve(); + }, + ); + mocked(SettingsStore.isLevelSupported).mockReturnValue(true); + mocked(SettingsStore.canSetValue).mockReturnValue(true); + mocked(SettingsStore.watchSetting).mockImplementation((settingName: string, roomId: null, callback: any) => { + watchCallbacks.push(callback); + return "watcherId"; + }); + + releaseAnnouncementStore = new ReleaseAnnouncementStore(); + }); + + /** + * Disables the release announcement feature. + */ + function disableReleaseAnnouncement() { + settings["feature_release_announcement"] = false; + } + + /** + * Listens to the next release announcement change event. + */ + function listenReleaseAnnouncementChanged() { + return new Promise((resolve) => + releaseAnnouncementStore.once("releaseAnnouncementChanged", resolve), + ); + } + + it("should be a singleton", () => { + expect(ReleaseAnnouncementStore.instance).toBeDefined(); + }); + + it("should return null when the release announcement is disabled", async () => { + disableReleaseAnnouncement(); + + expect(releaseAnnouncementStore.getReleaseAnnouncement()).toBeNull(); + + // Wait for the next release announcement change event + const promise = listenReleaseAnnouncementChanged(); + // Call the next release announcement + // because the release announcement is disabled, the next release announcement should be null + await releaseAnnouncementStore.nextReleaseAnnouncement(); + expect(await promise).toBeNull(); + expect(releaseAnnouncementStore.getReleaseAnnouncement()).toBeNull(); + }); + + it("should return the next feature when the next release announcement is called", async () => { + // Sanity check + expect(releaseAnnouncementStore.getReleaseAnnouncement()).toBe("threadsActivityCentre"); + + const promise = listenReleaseAnnouncementChanged(); + await releaseAnnouncementStore.nextReleaseAnnouncement(); + // Currently there is only one feature, so the next feature should be null + expect(await promise).toBeNull(); + expect(releaseAnnouncementStore.getReleaseAnnouncement()).toBeNull(); + + const secondStore = new ReleaseAnnouncementStore(); + // The TAC release announcement has been viewed, so it should be updated in the store account + // The release announcement viewing states should be share among all instances (devices in the same account) + expect(secondStore.getReleaseAnnouncement()).toBeNull(); + }); + + it("should listen to release announcement data changes in the store", async () => { + const secondStore = new ReleaseAnnouncementStore(); + expect(secondStore.getReleaseAnnouncement()).toBe("threadsActivityCentre"); + + const promise = listenReleaseAnnouncementChanged(); + await secondStore.nextReleaseAnnouncement(); + + // Currently there is only one feature, so the next feature should be null + expect(await promise).toBeNull(); + expect(releaseAnnouncementStore.getReleaseAnnouncement()).toBeNull(); + }); +}); diff --git a/yarn.lock b/yarn.lock index 15f8a7ae09..3fbabea662 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1491,13 +1491,22 @@ "@floating-ui/core" "^1.0.0" "@floating-ui/utils" "^0.2.0" -"@floating-ui/react-dom@^2.0.0": +"@floating-ui/react-dom@^2.0.0", "@floating-ui/react-dom@^2.0.8": version "2.0.8" resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.8.tgz#afc24f9756d1b433e1fe0d047c24bd4d9cefaa5d" integrity sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw== dependencies: "@floating-ui/dom" "^1.6.1" +"@floating-ui/react@^0.26.9": + version "0.26.10" + resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.26.10.tgz#d4a4878bcfaed70963ec0eaa67a71bead5924ee5" + integrity sha512-sh6f9gVvWQdEzLObrWbJ97c0clJObiALsFe0LiR/kb3tDRKwEhObASEH2QyfdoO/ZBPzwxa9j+nYFo+sqgbioA== + dependencies: + "@floating-ui/react-dom" "^2.0.0" + "@floating-ui/utils" "^0.2.0" + tabbable "^6.0.0" + "@floating-ui/utils@^0.2.0", "@floating-ui/utils@^0.2.1": version "0.2.1" resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2" @@ -3047,11 +3056,13 @@ dependencies: svg2vectordrawable "^2.9.1" -"@vector-im/compound-web@^3.1.1": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-3.1.3.tgz#bd23b4b2067b5ff0035b7c5f11bf6c57f98eb6be" - integrity sha512-h1uEKxMrZXUlEA2b8sd57WbxDy9LV8E0MYbz1vdKbU0n3lJb8neUbCAJE7PdQUoOSCi91jw8H+xH8XRLxTYYYw== +"@vector-im/compound-web@^3.3.1": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-3.3.1.tgz#f5d69255fa62472626e0ed71b7176b09f21cbcaf" + integrity sha512-V9CQfaMyKdsWxC1D4Wz08Xh0ge3SnaOBf5SSIp1+uwoJTPyfEFHKgqbZl536SHBvVBc9M9IYg+3+lPB8xkFRFA== dependencies: + "@floating-ui/react" "^0.26.9" + "@floating-ui/react-dom" "^2.0.8" "@radix-ui/react-context-menu" "^2.1.5" "@radix-ui/react-dropdown-menu" "^2.0.6" "@radix-ui/react-form" "^0.0.3" @@ -8940,6 +8951,11 @@ symbol-tree@^3.2.4: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== +tabbable@^6.0.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97" + integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew== + table@^6.8.1: version "6.8.2" resolved "https://registry.yarnpkg.com/table/-/table-6.8.2.tgz#c5504ccf201213fa227248bdc8c5569716ac6c58"