Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Add a prefix to file, poll, image, video and audio in the pinned message banner #12950

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading