From dbdb23f6bc2669021b5b9191b4aa59f462944749 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 5 Dec 2024 17:11:40 +0100 Subject: [PATCH] Notify to resize the timeline when the pinned message banner is displayed or hidden (#28654) --- src/components/structures/RoomView.tsx | 6 ++- .../views/rooms/PinnedMessageBanner.tsx | 34 ++++++++++-- .../views/rooms/PinnedMessageBanner-test.tsx | 52 ++++++++++++++++++- 3 files changed, 86 insertions(+), 6 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 891e6b97f42..772d5698a30 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -2372,7 +2372,11 @@ export class RoomView extends React.Component { ); const pinnedMessageBanner = ( - + ); let messageComposer; diff --git a/src/components/views/rooms/PinnedMessageBanner.tsx b/src/components/views/rooms/PinnedMessageBanner.tsx index f44b4417c99..32000d57925 100644 --- a/src/components/views/rooms/PinnedMessageBanner.tsx +++ b/src/components/views/rooms/PinnedMessageBanner.tsx @@ -6,10 +6,10 @@ * Please see LICENSE files in the repository root for full details. */ -import React, { JSX, useEffect, useState } from "react"; +import React, { JSX, useEffect, useRef, useState } from "react"; import PinIcon from "@vector-im/compound-design-tokens/assets/web/icons/pin-solid"; import { Button } from "@vector-im/compound-web"; -import { Room } from "matrix-js-sdk/src/matrix"; +import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import classNames from "classnames"; import { usePinnedEvents, useSortedFetchedPinnedEvents } from "../../../hooks/usePinnedEvents"; @@ -25,6 +25,7 @@ import { Action } from "../../../dispatcher/actions"; import MessageEvent from "../messages/MessageEvent"; import PosthogTrackers from "../../../PosthogTrackers.ts"; import { EventPreview } from "./EventPreview.tsx"; +import ResizeNotifier from "../../../utils/ResizeNotifier"; /** * The props for the {@link PinnedMessageBanner} component. @@ -38,12 +39,20 @@ interface PinnedMessageBannerProps { * The room where the banner is displayed */ room: Room; + /** + * The resize notifier to notify the timeline to resize itself when the banner is displayed or hidden. + */ + resizeNotifier: ResizeNotifier; } /** * A banner that displays the pinned messages in a room. */ -export function PinnedMessageBanner({ room, permalinkCreator }: PinnedMessageBannerProps): JSX.Element | null { +export function PinnedMessageBanner({ + room, + permalinkCreator, + resizeNotifier, +}: PinnedMessageBannerProps): JSX.Element | null { const pinnedEventIds = usePinnedEvents(room); const pinnedEvents = useSortedFetchedPinnedEvents(room, pinnedEventIds); const eventCount = pinnedEvents.length; @@ -56,6 +65,8 @@ export function PinnedMessageBanner({ room, permalinkCreator }: PinnedMessageBan }, [eventCount]); const pinnedEvent = pinnedEvents[currentEventIndex]; + useNotifyTimeline(pinnedEvent, resizeNotifier); + if (!pinnedEvent) return null; const shouldUseMessageEvent = pinnedEvent.isRedacted() || pinnedEvent.isDecryptionFailure(); @@ -128,6 +139,23 @@ export function PinnedMessageBanner({ room, permalinkCreator }: PinnedMessageBan ); } +/** + * When the banner is displayed or hidden, we want to notify the timeline to resize itself. + * @param pinnedEvent + * @param resizeNotifier + */ +function useNotifyTimeline(pinnedEvent: MatrixEvent | null, resizeNotifier: ResizeNotifier): void { + const previousEvent = useRef(null); + useEffect(() => { + // If we switch from a pinned message to no pinned message or the opposite, we want to resize the timeline + if ((previousEvent.current && !pinnedEvent) || (!previousEvent.current && pinnedEvent)) { + resizeNotifier.notifyTimelineHeightChanged(); + } + + previousEvent.current = pinnedEvent; + }, [pinnedEvent, resizeNotifier]); +} + const MAX_INDICATORS = 3; /** diff --git a/test/unit-tests/components/views/rooms/PinnedMessageBanner-test.tsx b/test/unit-tests/components/views/rooms/PinnedMessageBanner-test.tsx index 8d380c76bb5..8717762e0da 100644 --- a/test/unit-tests/components/views/rooms/PinnedMessageBanner-test.tsx +++ b/test/unit-tests/components/views/rooms/PinnedMessageBanner-test.tsx @@ -20,6 +20,7 @@ import RightPanelStore from "../../../../../src/stores/right-panel/RightPanelSto import { RightPanelPhases } from "../../../../../src/stores/right-panel/RightPanelStorePhases"; import { UPDATE_EVENT } from "../../../../../src/stores/AsyncStore"; import { Action } from "../../../../../src/dispatcher/actions"; +import ResizeNotifier from "../../../../../src/utils/ResizeNotifier.ts"; describe("", () => { const userId = "@alice:server.org"; @@ -28,10 +29,12 @@ describe("", () => { let mockClient: MatrixClient; let room: Room; let permalinkCreator: RoomPermalinkCreator; + let resizeNotifier: ResizeNotifier; beforeEach(() => { mockClient = stubClient(); room = new Room(roomId, mockClient, userId); permalinkCreator = new RoomPermalinkCreator(room); + resizeNotifier = new ResizeNotifier(); jest.spyOn(dis, "dispatch").mockReturnValue(undefined); }); @@ -77,7 +80,7 @@ describe("", () => { */ function renderBanner() { return render( - , + , withClientContextRenderOptions(mockClient), ); } @@ -145,7 +148,9 @@ describe("", () => { event3.getId()!, ]); jest.spyOn(pinnedEventHooks, "useSortedFetchedPinnedEvents").mockReturnValue([event1, event2, event3]); - rerender(); + rerender( + , + ); await expect(screen.findByText("Third pinned message")).resolves.toBeVisible(); expect(asFragment()).toMatchSnapshot(); }); @@ -206,6 +211,42 @@ describe("", () => { expect(asFragment()).toMatchSnapshot(); }); + describe("Notify the timeline to resize", () => { + beforeEach(() => { + jest.spyOn(resizeNotifier, "notifyTimelineHeightChanged"); + jest.spyOn(pinnedEventHooks, "usePinnedEvents").mockReturnValue([event1.getId()!, event2.getId()!]); + jest.spyOn(pinnedEventHooks, "useSortedFetchedPinnedEvents").mockReturnValue([event1, event2]); + }); + + it("should notify the timeline to resize when we display the banner", async () => { + renderBanner(); + await expect(screen.findByText("Second pinned message")).resolves.toBeVisible(); + // The banner is displayed, so we need to resize the timeline + expect(resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalledTimes(1); + + await userEvent.click(screen.getByRole("button", { name: "View the pinned message in the timeline." })); + await expect(screen.findByText("First pinned message")).resolves.toBeVisible(); + // The banner is already displayed, so we don't need to resize the timeline + expect(resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalledTimes(1); + }); + + it("should notify the timeline to resize when we hide the banner", async () => { + const { rerender } = renderBanner(); + await expect(screen.findByText("Second pinned message")).resolves.toBeVisible(); + // The banner is displayed, so we need to resize the timeline + expect(resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalledTimes(1); + + // The banner has no event to display and is hidden + jest.spyOn(pinnedEventHooks, "usePinnedEvents").mockReturnValue([]); + jest.spyOn(pinnedEventHooks, "useSortedFetchedPinnedEvents").mockReturnValue([]); + rerender( + , + ); + // The timeline should be resized + expect(resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalledTimes(2); + }); + }); + describe("Right button", () => { beforeEach(() => { jest.spyOn(pinnedEventHooks, "usePinnedEvents").mockReturnValue([event1.getId()!, event2.getId()!]); @@ -217,6 +258,8 @@ describe("", () => { jest.spyOn(RightPanelStore.instance, "isOpenForRoom").mockReturnValue(false); renderBanner(); + await expect(screen.findByText("Second pinned message")).resolves.toBeVisible(); + expect(screen.getByRole("button", { name: "View all" })).toBeVisible(); }); @@ -228,6 +271,8 @@ describe("", () => { }); renderBanner(); + await expect(screen.findByText("Second pinned message")).resolves.toBeVisible(); + expect(screen.getByRole("button", { name: "View all" })).toBeVisible(); }); @@ -239,6 +284,8 @@ describe("", () => { }); renderBanner(); + await expect(screen.findByText("Second pinned message")).resolves.toBeVisible(); + expect(screen.getByRole("button", { name: "Close list" })).toBeVisible(); }); @@ -263,6 +310,7 @@ describe("", () => { }); renderBanner(); + await expect(screen.findByText("Second pinned message")).resolves.toBeVisible(); expect(screen.getByRole("button", { name: "Close list" })).toBeVisible(); jest.spyOn(RightPanelStore.instance, "isOpenForRoom").mockReturnValue(false);