/* Copyright 2024 New Vector Ltd. Copyright 2021-2023 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 { Optional } from "matrix-events-sdk"; import React, { useContext, useEffect, useRef, useState } from "react"; import { EventTimelineSet, Room, Thread } from "matrix-js-sdk/src/matrix"; import { IconButton, Tooltip } from "@vector-im/compound-web"; import { logger } from "matrix-js-sdk/src/logger"; import ThreadsIcon from "@vector-im/compound-design-tokens/assets/web/icons/threads"; import { Icon as MarkAllThreadsReadIcon } from "../../../res/img/element-icons/check-all.svg"; import BaseCard from "../views/right_panel/BaseCard"; import ResizeNotifier from "../../utils/ResizeNotifier"; import MatrixClientContext, { useMatrixClientContext } from "../../contexts/MatrixClientContext"; import { _t } from "../../languageHandler"; import { ContextMenuButton } from "../../accessibility/context_menu/ContextMenuButton"; import ContextMenu, { ChevronFace, MenuItemRadio, useContextMenu } from "./ContextMenu"; import RoomContext, { TimelineRenderingType, useRoomContext } from "../../contexts/RoomContext"; import TimelinePanel from "./TimelinePanel"; import { Layout } from "../../settings/enums/Layout"; import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; import Measured from "../views/elements/Measured"; import PosthogTrackers from "../../PosthogTrackers"; import { ButtonEvent } from "../views/elements/AccessibleButton"; import Spinner from "../views/elements/Spinner"; import { clearRoomNotification } from "../../utils/notifications"; import EmptyState from "../views/right_panel/EmptyState"; interface IProps { roomId: string; onClose: () => void; resizeNotifier: ResizeNotifier; permalinkCreator: RoomPermalinkCreator; } export enum ThreadFilterType { "My", "All", } type ThreadPanelHeaderOption = { label: string; description: string; key: ThreadFilterType; }; export const ThreadPanelHeaderFilterOptionItem: React.FC< ThreadPanelHeaderOption & { onClick: () => void; isSelected: boolean; } > = ({ label, description, onClick, isSelected }) => { return ( {label} {description} ); }; export const ThreadPanelHeader: React.FC<{ filterOption: ThreadFilterType; setFilterOption: (filterOption: ThreadFilterType) => void; }> = ({ filterOption, setFilterOption }) => { const mxClient = useMatrixClientContext(); const roomContext = useRoomContext(); const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); const options: readonly ThreadPanelHeaderOption[] = [ { label: _t("threads|all_threads"), description: _t("threads|all_threads_description"), key: ThreadFilterType.All, }, { label: _t("threads|my_threads"), description: _t("threads|my_threads_description"), key: ThreadFilterType.My, }, ]; const value = options.find((option) => option.key === filterOption); const contextMenuOptions = options.map((opt) => ( { setFilterOption(opt.key); closeMenu(); }} isSelected={opt === value} /> )); const contextMenu = menuDisplayed ? ( {contextMenuOptions} ) : null; const onMarkAllThreadsReadClick = React.useCallback( (e: React.MouseEvent) => { PosthogTrackers.trackInteraction("WebThreadsMarkAllReadButton", e); if (!roomContext.room) { logger.error("No room in context to mark all threads read"); return; } // This actually clears all room notifications by sending an unthreaded read receipt. // We'd have to loop over all unread threads (pagninating back to find any we don't // know about yet) and send threaded receipts for all of them... or implement a // specific API for it. In practice, the user will have to be viewing the room to // see this button, so will have marked the room itself read anyway. clearRoomNotification(roomContext.room, mxClient).catch((e) => { logger.error("Failed to mark all threads read", e); }); }, [roomContext.room, mxClient], ); return (
{ openMenu(); PosthogTrackers.trackInteraction("WebRightPanelThreadPanelFilterDropdown", ev); }} > {`${_t("threads|show_thread_filter")} ${value?.label}`} {contextMenu}
); }; const ThreadPanel: React.FC = ({ roomId, onClose, permalinkCreator }) => { const mxClient = useContext(MatrixClientContext); const roomContext = useContext(RoomContext); const timelinePanel = useRef(null); const card = useRef(null); const closeButonRef = useRef(null); const [filterOption, setFilterOption] = useState(ThreadFilterType.All); const [room, setRoom] = useState(null); const [narrow, setNarrow] = useState(false); const timelineSet: Optional = filterOption === ThreadFilterType.My ? room?.threadsTimelineSets[1] : room?.threadsTimelineSets[0]; const hasThreads = Boolean(room?.threadsTimelineSets?.[0]?.getLiveTimeline()?.getEvents()?.length); useEffect(() => { const room = mxClient.getRoom(roomId); room ?.createThreadsTimelineSets() .then(() => room.fetchRoomThreads()) .then(() => { setFilterOption(ThreadFilterType.All); setRoom(room); }); }, [mxClient, roomId]); useEffect(() => { if (timelineSet && !Thread.hasServerSideSupport) { timelinePanel.current?.refreshTimeline(); } }, [timelineSet, timelinePanel]); return ( } id="thread-panel" className="mx_ThreadPanel" ariaLabelledBy="thread-panel-tab" role="tabpanel" onClose={onClose} withoutScrollContainer={true} ref={card} closeButtonRef={closeButonRef} > {card.current && } {timelineSet ? ( } alwaysShowTimestamps={true} layout={Layout.Group} hideThreadedMessages={false} hidden={false} showReactions={false} className="mx_RoomView_messagePanel" membersLoaded={true} permalinkCreator={permalinkCreator} disableGrouping={true} /> ) : (
)}
); }; export default ThreadPanel;