From 679b170bc5ae5eeb9c04c47fc5eeb663cb45b30c Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 29 May 2024 09:22:50 +0200 Subject: [PATCH] Close the release announcement when a dialog is opened (#12559) * Fire `ModalManagerEvent.Closed` when a dialog is closed * Listen to modal events in the RA * Fix first RA test --- src/Modal.tsx | 11 +++++++ src/hooks/useIsReleaseAnnouncementOpen.ts | 21 ++++++++++-- src/stores/ReleaseAnnouncementStore.ts | 4 ++- .../structures/ReleaseAnnouncement-test.tsx | 32 +++++++++++++++++-- 4 files changed, 63 insertions(+), 5 deletions(-) diff --git a/src/Modal.tsx b/src/Modal.tsx index 2ac12d280f..f39372d532 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -65,10 +65,12 @@ interface IOptions { export enum ModalManagerEvent { Opened = "opened", + Closed = "closed", } type HandlerMap = { [ModalManagerEvent.Opened]: () => void; + [ModalManagerEvent.Closed]: () => void; }; export class ModalManager extends TypedEventEmitter { @@ -232,6 +234,7 @@ export class ModalManager extends TypedEventEmitter { const modal = this.getCurrentModal(); if (!modal) { diff --git a/src/hooks/useIsReleaseAnnouncementOpen.ts b/src/hooks/useIsReleaseAnnouncementOpen.ts index ab8bf07c5e..f48df3fccf 100644 --- a/src/hooks/useIsReleaseAnnouncementOpen.ts +++ b/src/hooks/useIsReleaseAnnouncementOpen.ts @@ -16,17 +16,34 @@ * / */ -import { useTypedEventEmitterState } from "./useEventEmitter"; +import { useState } from "react"; + +import { useTypedEventEmitter, useTypedEventEmitterState } from "./useEventEmitter"; import { Feature, ReleaseAnnouncementStore } from "../stores/ReleaseAnnouncementStore"; +import Modal, { ModalManagerEvent } from "../Modal"; + +/** + * Hook to return true if a modal is opened + */ +function useModalOpened(): boolean { + const [opened, setOpened] = useState(false); + useTypedEventEmitter(Modal, ModalManagerEvent.Opened, () => setOpened(true)); + // Modal can be stacked, we need to check if all dialogs are closed + useTypedEventEmitter(Modal, ModalManagerEvent.Closed, () => !Modal.hasDialogs() && setOpened(false)); + return opened; +} /** * Return true if the release announcement of the given feature is enabled * @param feature */ export function useIsReleaseAnnouncementOpen(feature: Feature): boolean { - return useTypedEventEmitterState( + const modalOpened = useModalOpened(); + const releaseAnnouncementOpened = useTypedEventEmitterState( ReleaseAnnouncementStore.instance, "releaseAnnouncementChanged", () => ReleaseAnnouncementStore.instance.getReleaseAnnouncement() === feature, ); + + return !modalOpened && releaseAnnouncementOpened; } diff --git a/src/stores/ReleaseAnnouncementStore.ts b/src/stores/ReleaseAnnouncementStore.ts index 9beeed4f70..604c13fc49 100644 --- a/src/stores/ReleaseAnnouncementStore.ts +++ b/src/stores/ReleaseAnnouncementStore.ts @@ -18,6 +18,7 @@ import { TypedEventEmitter } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; +import { cloneDeep } from "lodash"; import SettingsStore from "../settings/SettingsStore"; import { SettingLevel } from "../settings/SettingLevel"; @@ -90,7 +91,8 @@ export class ReleaseAnnouncementStore extends TypedEventEmitter("releaseAnnouncementData"); + // Clone the settings to avoid to mutate the internal stored value in the SettingsStore + return cloneDeep(SettingsStore.getValue("releaseAnnouncementData")); } /** diff --git a/test/components/structures/ReleaseAnnouncement-test.tsx b/test/components/structures/ReleaseAnnouncement-test.tsx index 3477e54d4b..799aa017e9 100644 --- a/test/components/structures/ReleaseAnnouncement-test.tsx +++ b/test/components/structures/ReleaseAnnouncement-test.tsx @@ -17,11 +17,19 @@ */ import React from "react"; -import { render, screen, waitFor } from "@testing-library/react"; +import { act, render, screen, waitFor } from "@testing-library/react"; import { ReleaseAnnouncement } from "../../../src/components/structures/ReleaseAnnouncement"; +import Modal, { ModalManagerEvent } from "../../../src/Modal"; +import { ReleaseAnnouncementStore } from "../../../src/stores/ReleaseAnnouncementStore"; describe("ReleaseAnnouncement", () => { + beforeEach(async () => { + // Reset the singleton instance of the ReleaseAnnouncementStore + // @ts-ignore + ReleaseAnnouncementStore.internalInstance = new ReleaseAnnouncementStore(); + }); + function renderReleaseAnnouncement() { return render( { renderReleaseAnnouncement(); // The release announcement is displayed - expect(screen.queryByRole("dialog", { name: "header" })).toBeDefined(); + expect(screen.queryByRole("dialog", { name: "header" })).toBeVisible(); // 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()); }); + + test("when a dialog is opened, the release announcement should not be displayed", async () => { + renderReleaseAnnouncement(); + // The release announcement is displayed + expect(screen.queryByRole("dialog", { name: "header" })).toBeVisible(); + + // Open a dialog + act(() => { + Modal.emit(ModalManagerEvent.Opened); + }); + // The release announcement should be hidden after the dialog is opened + expect(screen.queryByRole("dialog", { name: "header" })).toBeNull(); + + // Close the dialog + act(() => { + Modal.emit(ModalManagerEvent.Closed); + }); + // The release announcement should be displayed after the dialog is closed + expect(screen.queryByRole("dialog", { name: "header" })).toBeVisible(); + }); });