From 60fe70b3cc3a9c6d36c659223db53a3d8aca5448 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Wed, 4 Sep 2024 11:07:19 +0200 Subject: [PATCH] Add a prefix to file, poll, image, video and audio in the pinned message banner (#12950) * Move event preview to its own component * Remove unused parameter * Add prefix to file, audio, video and image in the pinned message banner * Add prefix to poll in the pinned message banner * Add tests --- res/css/views/rooms/_PinnedMessageBanner.pcss | 4 + .../views/rooms/PinnedMessageBanner.tsx | 90 ++++++- src/i18n/strings/en_EN.json | 8 + .../views/rooms/PinnedMessageBanner-test.tsx | 28 +- .../PinnedMessageBanner-test.tsx.snap | 239 ++++++++++++++++++ 5 files changed, 359 insertions(+), 10 deletions(-) diff --git a/res/css/views/rooms/_PinnedMessageBanner.pcss b/res/css/views/rooms/_PinnedMessageBanner.pcss index 31cf3ece8a..838b356ac4 100644 --- a/res/css/views/rooms/_PinnedMessageBanner.pcss +++ b/res/css/views/rooms/_PinnedMessageBanner.pcss @@ -94,6 +94,10 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + + .mx_PinnedMessageBanner_prefix { + font: var(--cpd-font-body-sm-semibold); + } } .mx_PinnedMessageBanner_redactedMessage { diff --git a/src/components/views/rooms/PinnedMessageBanner.tsx b/src/components/views/rooms/PinnedMessageBanner.tsx index 0479e30ce9..3f6a1b14a6 100644 --- a/src/components/views/rooms/PinnedMessageBanner.tsx +++ b/src/components/views/rooms/PinnedMessageBanner.tsx @@ -17,7 +17,7 @@ import React, { JSX, useEffect, useMemo, useState } from "react"; import { Icon as PinIcon } from "@vector-im/compound-design-tokens/icons/pin-solid.svg"; import { Button } from "@vector-im/compound-web"; -import { Room } from "matrix-js-sdk/src/matrix"; +import { M_POLL_START, MatrixEvent, MsgType, Room } from "matrix-js-sdk/src/matrix"; import classNames from "classnames"; import { usePinnedEvents, useSortedFetchedPinnedEvents } from "../../../hooks/usePinnedEvents"; @@ -59,16 +59,10 @@ export function PinnedMessageBanner({ room, permalinkCreator }: PinnedMessageBan const [currentEventIndex, setCurrentEventIndex] = useState(eventCount - 1); // When the number of pinned messages changes, we want to display the last message useEffect(() => { - setCurrentEventIndex((currentEventIndex) => eventCount - 1); + setCurrentEventIndex(() => eventCount - 1); }, [eventCount]); const pinnedEvent = pinnedEvents[currentEventIndex]; - // Generate a preview for the pinned event - const eventPreview = useMemo(() => { - if (!pinnedEvent || pinnedEvent.isRedacted() || pinnedEvent.isDecryptionFailure()) return null; - return MessagePreviewStore.instance.generatePreviewForEvent(pinnedEvent); - }, [pinnedEvent]); - if (!pinnedEvent) return null; const shouldUseMessageEvent = pinnedEvent.isRedacted() || pinnedEvent.isDecryptionFailure(); @@ -116,7 +110,7 @@ export function PinnedMessageBanner({ room, permalinkCreator }: PinnedMessageBan )} )} - {eventPreview && {eventPreview}} + {/* In case of redacted event, we want to display the nice sentence of the message event like in the timeline or in the pinned message list */} {shouldUseMessageEvent && (
@@ -135,6 +129,84 @@ export function PinnedMessageBanner({ room, permalinkCreator }: PinnedMessageBan ); } +/** + * The props for the {@link EventPreview} component. + */ +interface EventPreviewProps { + /** + * The pinned event to display the preview for + */ + pinnedEvent: MatrixEvent; +} + +/** + * A component that displays a preview for the pinned event. + */ +function EventPreview({ pinnedEvent }: EventPreviewProps): JSX.Element | null { + const preview = useEventPreview(pinnedEvent); + if (!preview) return null; + + const prefix = getPreviewPrefix(pinnedEvent.getType(), pinnedEvent.getContent().msgtype as MsgType); + if (!prefix) + return ( + + {preview} + + ); + + return ( + + {_t( + "room|pinned_message_banner|preview", + { + prefix, + preview, + }, + { + bold: (sub) => {sub}, + }, + )} + + ); +} + +/** + * Hooks to generate a preview for the pinned event. + * @param pinnedEvent + */ +function useEventPreview(pinnedEvent: MatrixEvent | null): string | null { + return useMemo(() => { + if (!pinnedEvent || pinnedEvent.isRedacted() || pinnedEvent.isDecryptionFailure()) return null; + return MessagePreviewStore.instance.generatePreviewForEvent(pinnedEvent); + }, [pinnedEvent]); +} + +/** + * Get the prefix for the preview based on the type and the message type. + * @param type + * @param msgType + */ +function getPreviewPrefix(type: string, msgType: MsgType): string | null { + switch (type) { + case M_POLL_START.name: + return _t("room|pinned_message_banner|prefix|poll"); + default: + } + + switch (msgType) { + case MsgType.Audio: + return _t("room|pinned_message_banner|prefix|audio"); + case MsgType.Image: + return _t("room|pinned_message_banner|prefix|image"); + case MsgType.Video: + return _t("room|pinned_message_banner|prefix|video"); + case MsgType.File: + return _t("room|pinned_message_banner|prefix|file"); + default: + return null; + } +} + const MAX_INDICATORS = 3; /** diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e0b85cc4e1..b1f4fb4607 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2053,6 +2053,14 @@ "button_view_all": "View all", "description": "This room has pinned messages. Click to view them.", "go_to_message": "View the pinned message in the timeline.", + "prefix": { + "audio": "Audio", + "file": "File", + "image": "Image", + "poll": "Poll", + "video": "Video" + }, + "preview": "%(prefix)s: %(preview)s", "title": "%(index)s of %(length)s Pinned messages" }, "read_topic": "Click to read topic", diff --git a/test/components/views/rooms/PinnedMessageBanner-test.tsx b/test/components/views/rooms/PinnedMessageBanner-test.tsx index 0febf21a91..0231d18ffc 100644 --- a/test/components/views/rooms/PinnedMessageBanner-test.tsx +++ b/test/components/views/rooms/PinnedMessageBanner-test.tsx @@ -22,7 +22,7 @@ import userEvent from "@testing-library/user-event"; import * as pinnedEventHooks from "../../../../src/hooks/usePinnedEvents"; import { PinnedMessageBanner } from "../../../../src/components/views/rooms/PinnedMessageBanner"; import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; -import { stubClient } from "../../../test-utils"; +import { makePollStartEvent, stubClient } from "../../../test-utils"; import dis from "../../../../src/dispatcher/dispatcher"; import RightPanelStore from "../../../../src/stores/right-panel/RightPanelStore"; import { RightPanelPhases } from "../../../../src/stores/right-panel/RightPanelStorePhases"; @@ -185,6 +185,32 @@ describe("", () => { }); }); + it.each([ + ["m.file", "File"], + ["m.audio", "Audio"], + ["m.video", "Video"], + ["m.image", "Image"], + ])("should display the %s event type", (msgType, label) => { + const body = `Message with ${msgType} type`; + const event = makePinEvent({ content: { body, msgtype: msgType } }); + jest.spyOn(pinnedEventHooks, "usePinnedEvents").mockReturnValue([event.getId()!]); + jest.spyOn(pinnedEventHooks, "useSortedFetchedPinnedEvents").mockReturnValue([event]); + + const { asFragment } = renderBanner(); + expect(screen.getByTestId("banner-message")).toHaveTextContent(`${label}: ${body}`); + expect(asFragment()).toMatchSnapshot(); + }); + + it("should display display a poll event", async () => { + const event = makePollStartEvent("Alice?", userId); + jest.spyOn(pinnedEventHooks, "usePinnedEvents").mockReturnValue([event.getId()!]); + jest.spyOn(pinnedEventHooks, "useSortedFetchedPinnedEvents").mockReturnValue([event]); + + const { asFragment } = renderBanner(); + expect(screen.getByTestId("banner-message")).toHaveTextContent("Poll: Alice?"); + expect(asFragment()).toMatchSnapshot(); + }); + describe("Right button", () => { beforeEach(() => { jest.spyOn(pinnedEventHooks, "usePinnedEvents").mockReturnValue([event1.getId()!, event2.getId()!]); diff --git a/test/components/views/rooms/__snapshots__/PinnedMessageBanner-test.tsx.snap b/test/components/views/rooms/__snapshots__/PinnedMessageBanner-test.tsx.snap index 90b1efb635..bc733146ba 100644 --- a/test/components/views/rooms/__snapshots__/PinnedMessageBanner-test.tsx.snap +++ b/test/components/views/rooms/__snapshots__/PinnedMessageBanner-test.tsx.snap @@ -1,5 +1,52 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[` should display display a poll event 1`] = ` + +
+ +
+
+`; + exports[` should display the last message when the pinned event array changed 1`] = `
should display the last message when the pinned
Third pinned message @@ -69,6 +117,194 @@ exports[` should display the last message when the pinned
`; +exports[` should display the m.audio event type 1`] = ` + +
+ +
+
+`; + +exports[` should display the m.file event type 1`] = ` + +
+ +
+
+`; + +exports[` should display the m.image event type 1`] = ` + +
+ +
+
+`; + +exports[` should display the m.video event type 1`] = ` + +
+ +
+
+`; + exports[` should render 2 pinned event 1`] = `
should render 2 pinned event 1`] = `
Second pinned message @@ -185,6 +422,7 @@ exports[` should render 4 pinned event 1`] = `
Fourth pinned message @@ -233,6 +471,7 @@ exports[` should render a single pinned event 1`] = ` /> First pinned message