element-portable/src/components/views/rooms/PinnedMessageBanner.tsx
Michael Telatynski aeabf3b188
Show message type prefix in thread root & reply previews (#28361)
* Extract EventPreview from PinnedMessageBanner

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Show message type prefix in thread root previews

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Show message type prefix in thread reply preview

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-11-05 11:34:42 +00:00

244 lines
9.3 KiB
TypeScript

/*
* Copyright 2024 New Vector Ltd.
* Copyright 2024 The Matrix.org Foundation C.I.C.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
* Please see LICENSE files in the repository root for full details.
*/
import React, { JSX, useEffect, 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 classNames from "classnames";
import { usePinnedEvents, useSortedFetchedPinnedEvents } from "../../../hooks/usePinnedEvents";
import { _t } from "../../../languageHandler";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
import { useEventEmitter } from "../../../hooks/useEventEmitter";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import dis from "../../../dispatcher/dispatcher";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { Action } from "../../../dispatcher/actions";
import MessageEvent from "../messages/MessageEvent";
import PosthogTrackers from "../../../PosthogTrackers.ts";
import { EventPreview } from "./EventPreview.tsx";
/**
* The props for the {@link PinnedMessageBanner} component.
*/
interface PinnedMessageBannerProps {
/**
* The permalink creator to use.
*/
permalinkCreator: RoomPermalinkCreator;
/**
* The room where the banner is displayed
*/
room: Room;
}
/**
* A banner that displays the pinned messages in a room.
*/
export function PinnedMessageBanner({ room, permalinkCreator }: PinnedMessageBannerProps): JSX.Element | null {
const pinnedEventIds = usePinnedEvents(room);
const pinnedEvents = useSortedFetchedPinnedEvents(room, pinnedEventIds);
const eventCount = pinnedEvents.length;
const isSinglePinnedEvent = eventCount === 1;
const [currentEventIndex, setCurrentEventIndex] = useState(eventCount - 1);
// When the number of pinned messages changes, we want to display the last message
useEffect(() => {
setCurrentEventIndex(() => eventCount - 1);
}, [eventCount]);
const pinnedEvent = pinnedEvents[currentEventIndex];
if (!pinnedEvent) return null;
const shouldUseMessageEvent = pinnedEvent.isRedacted() || pinnedEvent.isDecryptionFailure();
const onBannerClick = (): void => {
PosthogTrackers.trackInteraction("PinnedMessageBannerClick");
// Scroll to the pinned message
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
event_id: pinnedEvent.getId(),
highlighted: true,
room_id: room.roomId,
metricsTrigger: undefined, // room doesn't change
});
// Cycle through the pinned messages
// When we reach the first message, we go back to the last message
setCurrentEventIndex((currentEventIndex) => (--currentEventIndex === -1 ? eventCount - 1 : currentEventIndex));
};
return (
<div
className="mx_PinnedMessageBanner"
data-single-message={isSinglePinnedEvent}
aria-label={_t("room|pinned_message_banner|description")}
data-testid="pinned-message-banner"
>
<button
aria-label={_t("room|pinned_message_banner|go_to_message")}
type="button"
className="mx_PinnedMessageBanner_main"
onClick={onBannerClick}
>
<div className="mx_PinnedMessageBanner_content">
<Indicators count={eventCount} currentIndex={currentEventIndex} />
<PinIcon width="20px" height="20px" className="mx_PinnedMessageBanner_PinIcon" />
{!isSinglePinnedEvent && (
<div className="mx_PinnedMessageBanner_title" data-testid="banner-counter">
{_t(
"room|pinned_message_banner|title",
{
index: currentEventIndex + 1,
length: eventCount,
},
{ bold: (sub) => <span className="mx_PinnedMessageBanner_title_counter">{sub}</span> },
)}
</div>
)}
<EventPreview
mxEvent={pinnedEvent}
className="mx_PinnedMessageBanner_message"
data-testid="banner-message"
/>
{/* 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">
<MessageEvent
mxEvent={pinnedEvent}
maxImageHeight={20}
permalinkCreator={permalinkCreator}
replacingEventId={pinnedEvent.replacingEventId()}
/>
</div>
)}
</div>
</button>
{!isSinglePinnedEvent && <BannerButton room={room} />}
</div>
);
}
const MAX_INDICATORS = 3;
/**
* The props for the {@link IndicatorsProps} component.
*/
interface IndicatorsProps {
/**
* The number of messages pinned
*/
count: number;
/**
* The current index of the pinned message
*/
currentIndex: number;
}
/**
* A component that displays vertical indicators for the pinned messages.
*/
function Indicators({ count, currentIndex }: IndicatorsProps): JSX.Element {
// We only display a maximum of 3 indicators at one time.
// When there is more than 3 messages pinned, we will cycle through the indicators
// If there is only 2 messages pinned, we will display 2 indicators
// In case of 1 message pinned, the indicators are not displayed, see {@link PinnedMessageBanner} logic.
const numberOfIndicators = Math.min(count, MAX_INDICATORS);
// The index of the active indicator
const index = currentIndex % numberOfIndicators;
// We hide the indicators when we are on the last cycle and there are less than 3 remaining messages pinned
const numberOfCycles = Math.ceil(count / numberOfIndicators);
// If the current index is greater than the last cycle index, we are on the last cycle
const isLastCycle = currentIndex >= (numberOfCycles - 1) * MAX_INDICATORS;
// The index of the last message in the last cycle
const lastCycleIndex = numberOfIndicators - (numberOfCycles * numberOfIndicators - count);
return (
<div className="mx_PinnedMessageBanner_Indicators">
{Array.from({ length: numberOfIndicators }).map((_, i) => (
<Indicator key={i} active={i === index} hidden={isLastCycle && lastCycleIndex <= i} />
))}
</div>
);
}
/**
* The props for the {@link Indicator} component.
*/
interface IndicatorProps {
/**
* Whether the indicator is active
*/
active: boolean;
/**
* Whether the indicator is hidden
*/
hidden: boolean;
}
/**
* A component that displays a vertical indicator for a pinned message.
*/
function Indicator({ active, hidden }: IndicatorProps): JSX.Element {
return (
<div
data-testid="banner-indicator"
className={classNames("mx_PinnedMessageBanner_Indicator", {
"mx_PinnedMessageBanner_Indicator--active": active,
"mx_PinnedMessageBanner_Indicator--hidden": hidden,
})}
/>
);
}
function getRightPanelPhase(roomId: string): RightPanelPhases | null {
if (!RightPanelStore.instance.isOpenForRoom(roomId)) return null;
return RightPanelStore.instance.currentCard.phase;
}
/**
* The props for the {@link BannerButton} component.
*/
interface BannerButtonProps {
/**
* The room where the banner is displayed
*/
room: Room;
}
/**
* A button that allows the user to view or close the list of pinned messages.
*/
function BannerButton({ room }: BannerButtonProps): JSX.Element {
const [currentPhase, setCurrentPhase] = useState<RightPanelPhases | null>(getRightPanelPhase(room.roomId));
useEventEmitter(RightPanelStore.instance, UPDATE_EVENT, () => setCurrentPhase(getRightPanelPhase(room.roomId)));
const isPinnedMessagesPhase = currentPhase === RightPanelPhases.PinnedMessages;
return (
<Button
className="mx_PinnedMessageBanner_actions"
kind="tertiary"
onClick={() => {
if (isPinnedMessagesPhase) PosthogTrackers.trackInteraction("PinnedMessageBannerCloseListButton");
else PosthogTrackers.trackInteraction("PinnedMessageBannerViewAllButton");
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.PinnedMessages);
}}
>
{isPinnedMessagesPhase
? _t("room|pinned_message_banner|button_close_list")
: _t("room|pinned_message_banner|button_view_all")}
</Button>
);
}