Skip to content

Commit

Permalink
Add a prefix to file, poll, image, video and audio in the pinned mess…
Browse files Browse the repository at this point in the history
…age 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
  • Loading branch information
florianduros authored Sep 4, 2024
1 parent 9d8c5b6 commit 60fe70b
Show file tree
Hide file tree
Showing 5 changed files with 359 additions and 10 deletions.
4 changes: 4 additions & 0 deletions res/css/views/rooms/_PinnedMessageBanner.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
90 changes: 81 additions & 9 deletions src/components/views/rooms/PinnedMessageBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -116,7 +110,7 @@ export function PinnedMessageBanner({ room, permalinkCreator }: PinnedMessageBan
)}
</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 */}
{shouldUseMessageEvent && (
<div className="mx_PinnedMessageBanner_redactedMessage">
Expand All @@ -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;

/**
Expand Down
8 changes: 8 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<bold>%(prefix)s:</bold> %(preview)s",
"title": "<bold>%(index)s of %(length)s</bold> Pinned messages"
},
"read_topic": "Click to read topic",
Expand Down
28 changes: 27 additions & 1 deletion test/components/views/rooms/PinnedMessageBanner-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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", () => {
beforeEach(() => {
jest.spyOn(pinnedEventHooks, "usePinnedEvents").mockReturnValue([event1.getId()!, event2.getId()!]);
Expand Down
Loading

0 comments on commit 60fe70b

Please sign in to comment.