/* * 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({ 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 (
{!isSinglePinnedEvent && }
); } 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 (
{Array.from({ length: numberOfIndicators }).map((_, i) => (
); } /** * 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 (
); } 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(getRightPanelPhase(room.roomId)); useEventEmitter(RightPanelStore.instance, UPDATE_EVENT, () => setCurrentPhase(getRightPanelPhase(room.roomId))); const isPinnedMessagesPhase = currentPhase === RightPanelPhases.PinnedMessages; return ( ); }