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
This commit is contained in:
Florian Duros 2024-09-04 11:07:19 +02:00 committed by GitHub
parent 9d8c5b6a1c
commit 60fe70b3cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 359 additions and 10 deletions

View file

@ -94,6 +94,10 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
.mx_PinnedMessageBanner_prefix {
font: var(--cpd-font-body-sm-semibold);
}
} }
.mx_PinnedMessageBanner_redactedMessage { .mx_PinnedMessageBanner_redactedMessage {

View file

@ -17,7 +17,7 @@
import React, { JSX, useEffect, useMemo, useState } from "react"; import React, { JSX, useEffect, useMemo, useState } from "react";
import { Icon as PinIcon } from "@vector-im/compound-design-tokens/icons/pin-solid.svg"; import { Icon as PinIcon } from "@vector-im/compound-design-tokens/icons/pin-solid.svg";
import { Button } from "@vector-im/compound-web"; 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 classNames from "classnames";
import { usePinnedEvents, useSortedFetchedPinnedEvents } from "../../../hooks/usePinnedEvents"; import { usePinnedEvents, useSortedFetchedPinnedEvents } from "../../../hooks/usePinnedEvents";
@ -59,16 +59,10 @@ export function PinnedMessageBanner({ room, permalinkCreator }: PinnedMessageBan
const [currentEventIndex, setCurrentEventIndex] = useState(eventCount - 1); const [currentEventIndex, setCurrentEventIndex] = useState(eventCount - 1);
// When the number of pinned messages changes, we want to display the last message // When the number of pinned messages changes, we want to display the last message
useEffect(() => { useEffect(() => {
setCurrentEventIndex((currentEventIndex) => eventCount - 1); setCurrentEventIndex(() => eventCount - 1);
}, [eventCount]); }, [eventCount]);
const pinnedEvent = pinnedEvents[currentEventIndex]; 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; if (!pinnedEvent) return null;
const shouldUseMessageEvent = pinnedEvent.isRedacted() || pinnedEvent.isDecryptionFailure(); const shouldUseMessageEvent = pinnedEvent.isRedacted() || pinnedEvent.isDecryptionFailure();
@ -116,7 +110,7 @@ export function PinnedMessageBanner({ room, permalinkCreator }: PinnedMessageBan
)} )}
</div> </div>
)} )}
{eventPreview && <span className="mx_PinnedMessageBanner_message">{eventPreview}</span>} <EventPreview pinnedEvent={pinnedEvent} />
{/* 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 */} {/* 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 && ( {shouldUseMessageEvent && (
<div className="mx_PinnedMessageBanner_redactedMessage"> <div className="mx_PinnedMessageBanner_redactedMessage">
@ -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 (
<span className="mx_PinnedMessageBanner_message" data-testid="banner-message">
{preview}
</span>
);
return (
<span className="mx_PinnedMessageBanner_message" data-testid="banner-message">
{_t(
"room|pinned_message_banner|preview",
{
prefix,
preview,
},
{
bold: (sub) => <span className="mx_PinnedMessageBanner_prefix">{sub}</span>,
},
)}
</span>
);
}
/**
* 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; const MAX_INDICATORS = 3;
/** /**

View file

@ -2053,6 +2053,14 @@
"button_view_all": "View all", "button_view_all": "View all",
"description": "This room has pinned messages. Click to view them.", "description": "This room has pinned messages. Click to view them.",
"go_to_message": "View the pinned message in the timeline.", "go_to_message": "View the pinned message in the timeline.",
"prefix": {
"audio": "Audio",
"file": "File",
"image": "Image",
"poll": "Poll",
"video": "Video"
},
"preview": "<bold>%(prefix)s:</bold> %(preview)s",
"title": "<bold>%(index)s of %(length)s</bold> Pinned messages" "title": "<bold>%(index)s of %(length)s</bold> Pinned messages"
}, },
"read_topic": "Click to read topic", "read_topic": "Click to read topic",

View file

@ -22,7 +22,7 @@ import userEvent from "@testing-library/user-event";
import * as pinnedEventHooks from "../../../../src/hooks/usePinnedEvents"; import * as pinnedEventHooks from "../../../../src/hooks/usePinnedEvents";
import { PinnedMessageBanner } from "../../../../src/components/views/rooms/PinnedMessageBanner"; import { PinnedMessageBanner } from "../../../../src/components/views/rooms/PinnedMessageBanner";
import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; 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 dis from "../../../../src/dispatcher/dispatcher";
import RightPanelStore from "../../../../src/stores/right-panel/RightPanelStore"; import RightPanelStore from "../../../../src/stores/right-panel/RightPanelStore";
import { RightPanelPhases } from "../../../../src/stores/right-panel/RightPanelStorePhases"; import { RightPanelPhases } from "../../../../src/stores/right-panel/RightPanelStorePhases";
@ -185,6 +185,32 @@ describe("<PinnedMessageBanner />", () => {
}); });
}); });
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", () => { describe("Right button", () => {
beforeEach(() => { beforeEach(() => {
jest.spyOn(pinnedEventHooks, "usePinnedEvents").mockReturnValue([event1.getId()!, event2.getId()!]); jest.spyOn(pinnedEventHooks, "usePinnedEvents").mockReturnValue([event1.getId()!, event2.getId()!]);

View file

@ -1,5 +1,52 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<PinnedMessageBanner /> should display display a poll event 1`] = `
<DocumentFragment>
<div
aria-label="This room has pinned messages. Click to view them."
class="mx_PinnedMessageBanner"
data-single-message="true"
data-testid="pinned-message-banner"
>
<button
aria-label="View the pinned message in the timeline."
class="mx_PinnedMessageBanner_main"
type="button"
>
<div
class="mx_PinnedMessageBanner_content"
>
<div
class="mx_PinnedMessageBanner_Indicators"
>
<div
class="mx_PinnedMessageBanner_Indicator mx_PinnedMessageBanner_Indicator--active"
data-testid="banner-indicator"
/>
</div>
<div
class="mx_PinnedMessageBanner_PinIcon"
width="20"
/>
<span
class="mx_PinnedMessageBanner_message"
data-testid="banner-message"
>
<span>
<span
class="mx_PinnedMessageBanner_prefix"
>
Poll:
</span>
Alice?
</span>
</span>
</div>
</button>
</div>
</DocumentFragment>
`;
exports[`<PinnedMessageBanner /> should display the last message when the pinned event array changed 1`] = ` exports[`<PinnedMessageBanner /> should display the last message when the pinned event array changed 1`] = `
<DocumentFragment> <DocumentFragment>
<div <div
@ -51,6 +98,7 @@ exports[`<PinnedMessageBanner /> should display the last message when the pinned
</div> </div>
<span <span
class="mx_PinnedMessageBanner_message" class="mx_PinnedMessageBanner_message"
data-testid="banner-message"
> >
Third pinned message Third pinned message
</span> </span>
@ -69,6 +117,194 @@ exports[`<PinnedMessageBanner /> should display the last message when the pinned
</DocumentFragment> </DocumentFragment>
`; `;
exports[`<PinnedMessageBanner /> should display the m.audio event type 1`] = `
<DocumentFragment>
<div
aria-label="This room has pinned messages. Click to view them."
class="mx_PinnedMessageBanner"
data-single-message="true"
data-testid="pinned-message-banner"
>
<button
aria-label="View the pinned message in the timeline."
class="mx_PinnedMessageBanner_main"
type="button"
>
<div
class="mx_PinnedMessageBanner_content"
>
<div
class="mx_PinnedMessageBanner_Indicators"
>
<div
class="mx_PinnedMessageBanner_Indicator mx_PinnedMessageBanner_Indicator--active"
data-testid="banner-indicator"
/>
</div>
<div
class="mx_PinnedMessageBanner_PinIcon"
width="20"
/>
<span
class="mx_PinnedMessageBanner_message"
data-testid="banner-message"
>
<span>
<span
class="mx_PinnedMessageBanner_prefix"
>
Audio:
</span>
Message with m.audio type
</span>
</span>
</div>
</button>
</div>
</DocumentFragment>
`;
exports[`<PinnedMessageBanner /> should display the m.file event type 1`] = `
<DocumentFragment>
<div
aria-label="This room has pinned messages. Click to view them."
class="mx_PinnedMessageBanner"
data-single-message="true"
data-testid="pinned-message-banner"
>
<button
aria-label="View the pinned message in the timeline."
class="mx_PinnedMessageBanner_main"
type="button"
>
<div
class="mx_PinnedMessageBanner_content"
>
<div
class="mx_PinnedMessageBanner_Indicators"
>
<div
class="mx_PinnedMessageBanner_Indicator mx_PinnedMessageBanner_Indicator--active"
data-testid="banner-indicator"
/>
</div>
<div
class="mx_PinnedMessageBanner_PinIcon"
width="20"
/>
<span
class="mx_PinnedMessageBanner_message"
data-testid="banner-message"
>
<span>
<span
class="mx_PinnedMessageBanner_prefix"
>
File:
</span>
Message with m.file type
</span>
</span>
</div>
</button>
</div>
</DocumentFragment>
`;
exports[`<PinnedMessageBanner /> should display the m.image event type 1`] = `
<DocumentFragment>
<div
aria-label="This room has pinned messages. Click to view them."
class="mx_PinnedMessageBanner"
data-single-message="true"
data-testid="pinned-message-banner"
>
<button
aria-label="View the pinned message in the timeline."
class="mx_PinnedMessageBanner_main"
type="button"
>
<div
class="mx_PinnedMessageBanner_content"
>
<div
class="mx_PinnedMessageBanner_Indicators"
>
<div
class="mx_PinnedMessageBanner_Indicator mx_PinnedMessageBanner_Indicator--active"
data-testid="banner-indicator"
/>
</div>
<div
class="mx_PinnedMessageBanner_PinIcon"
width="20"
/>
<span
class="mx_PinnedMessageBanner_message"
data-testid="banner-message"
>
<span>
<span
class="mx_PinnedMessageBanner_prefix"
>
Image:
</span>
Message with m.image type
</span>
</span>
</div>
</button>
</div>
</DocumentFragment>
`;
exports[`<PinnedMessageBanner /> should display the m.video event type 1`] = `
<DocumentFragment>
<div
aria-label="This room has pinned messages. Click to view them."
class="mx_PinnedMessageBanner"
data-single-message="true"
data-testid="pinned-message-banner"
>
<button
aria-label="View the pinned message in the timeline."
class="mx_PinnedMessageBanner_main"
type="button"
>
<div
class="mx_PinnedMessageBanner_content"
>
<div
class="mx_PinnedMessageBanner_Indicators"
>
<div
class="mx_PinnedMessageBanner_Indicator mx_PinnedMessageBanner_Indicator--active"
data-testid="banner-indicator"
/>
</div>
<div
class="mx_PinnedMessageBanner_PinIcon"
width="20"
/>
<span
class="mx_PinnedMessageBanner_message"
data-testid="banner-message"
>
<span>
<span
class="mx_PinnedMessageBanner_prefix"
>
Video:
</span>
Message with m.video type
</span>
</span>
</div>
</button>
</div>
</DocumentFragment>
`;
exports[`<PinnedMessageBanner /> should render 2 pinned event 1`] = ` exports[`<PinnedMessageBanner /> should render 2 pinned event 1`] = `
<DocumentFragment> <DocumentFragment>
<div <div
@ -116,6 +352,7 @@ exports[`<PinnedMessageBanner /> should render 2 pinned event 1`] = `
</div> </div>
<span <span
class="mx_PinnedMessageBanner_message" class="mx_PinnedMessageBanner_message"
data-testid="banner-message"
> >
Second pinned message Second pinned message
</span> </span>
@ -185,6 +422,7 @@ exports[`<PinnedMessageBanner /> should render 4 pinned event 1`] = `
</div> </div>
<span <span
class="mx_PinnedMessageBanner_message" class="mx_PinnedMessageBanner_message"
data-testid="banner-message"
> >
Fourth pinned message Fourth pinned message
</span> </span>
@ -233,6 +471,7 @@ exports[`<PinnedMessageBanner /> should render a single pinned event 1`] = `
/> />
<span <span
class="mx_PinnedMessageBanner_message" class="mx_PinnedMessageBanner_message"
data-testid="banner-message"
> >
First pinned message First pinned message
</span> </span>