Files
ThreadNet-Web/src/components/structures/ThreadPanel.tsx
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

257 lines
10 KiB
TypeScript
Raw Normal View History

2021-08-17 10:38:09 +01:00
/*
Copyright 2021 - 2023 The Matrix.org Foundation C.I.C.
2021-08-17 10:38:09 +01:00
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Optional } from "matrix-events-sdk";
2022-01-21 10:03:08 +00:00
import React, { useContext, useEffect, useRef, useState } from "react";
import { EventTimelineSet, Room, Thread } from "matrix-js-sdk/src/matrix";
2024-03-28 17:38:21 +00:00
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";
2021-08-17 10:38:09 +01:00
2024-03-28 17:38:21 +00:00
import { Icon as MarkAllThreadsReadIcon } from "../../../res/img/element-icons/check-all.svg";
2021-08-17 10:38:09 +01:00
import BaseCard from "../views/right_panel/BaseCard";
import ResizeNotifier from "../../utils/ResizeNotifier";
2024-03-28 17:38:21 +00:00
import MatrixClientContext, { useMatrixClientContext } from "../../contexts/MatrixClientContext";
2021-10-14 15:27:35 +02:00
import { _t } from "../../languageHandler";
import { ContextMenuButton } from "../../accessibility/context_menu/ContextMenuButton";
import ContextMenu, { ChevronFace, MenuItemRadio, useContextMenu } from "./ContextMenu";
2024-03-28 17:38:21 +00:00
import RoomContext, { TimelineRenderingType, useRoomContext } from "../../contexts/RoomContext";
2021-10-14 15:27:35 +02:00
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";
2024-03-28 17:38:21 +00:00
import { clearRoomNotification } from "../../utils/notifications";
import EmptyState from "../views/right_panel/EmptyState";
2021-08-17 10:38:09 +01:00
interface IProps {
roomId: string;
onClose: () => void;
resizeNotifier: ResizeNotifier;
permalinkCreator: RoomPermalinkCreator;
2021-08-17 10:38:09 +01:00
}
2021-10-14 15:27:35 +02:00
export enum ThreadFilterType {
"My",
"All",
2021-08-17 10:38:09 +01:00
}
2021-10-14 15:27:35 +02:00
type ThreadPanelHeaderOption = {
label: string;
description: string;
key: ThreadFilterType;
};
2021-08-17 10:38:09 +01:00
export const ThreadPanelHeaderFilterOptionItem: React.FC<
ThreadPanelHeaderOption & {
onClick: () => void;
isSelected: boolean;
}
> = ({ label, description, onClick, isSelected }) => {
return (
2021-10-14 15:27:35 +02:00
<MenuItemRadio active={isSelected} className="mx_ThreadPanel_Header_FilterOptionItem" onClick={onClick}>
<span>{label}</span>
<span>{description}</span>
</MenuItemRadio>
);
2021-10-14 15:27:35 +02:00
};
2021-08-17 10:38:09 +01:00
export const ThreadPanelHeader: React.FC<{
2021-10-14 15:27:35 +02:00
filterOption: ThreadFilterType;
setFilterOption: (filterOption: ThreadFilterType) => void;
}> = ({ filterOption, setFilterOption }) => {
2024-03-28 17:38:21 +00:00
const mxClient = useMatrixClientContext();
const roomContext = useRoomContext();
2021-10-14 15:27:35 +02:00
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu<HTMLElement>();
const options: readonly ThreadPanelHeaderOption[] = [
{
label: _t("threads|all_threads"),
description: _t("threads|all_threads_description"),
2021-10-14 15:27:35 +02:00
key: ThreadFilterType.All,
},
{
label: _t("threads|my_threads"),
description: _t("threads|my_threads_description"),
key: ThreadFilterType.My,
},
2021-10-14 15:27:35 +02:00
];
2021-08-17 10:38:09 +01:00
2021-10-14 15:27:35 +02:00
const value = options.find((option) => option.key === filterOption);
const contextMenuOptions = options.map((opt) => (
<ThreadPanelHeaderFilterOptionItem
key={opt.key}
label={opt.label}
description={opt.description}
onClick={() => {
setFilterOption(opt.key);
closeMenu();
}}
isSelected={opt === value}
/>
));
const contextMenu = menuDisplayed ? (
<ContextMenu
top={108}
right={33}
onFinished={closeMenu}
chevronFace={ChevronFace.Top}
wrapperClassName="mx_BaseCard_header_title"
>
2021-10-14 15:27:35 +02:00
{contextMenuOptions}
</ContextMenu>
) : null;
2024-03-28 17:38:21 +00:00
const onMarkAllThreadsReadClick = React.useCallback(
(e) => {
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],
);
2024-03-28 17:38:21 +00:00
return (
<div className="mx_BaseCard_header_title">
<Tooltip label={_t("threads|mark_all_read")}>
<IconButton onClick={onMarkAllThreadsReadClick} aria-label={_t("threads|mark_all_read")} size="24px">
<MarkAllThreadsReadIcon />
</IconButton>
</Tooltip>
<div className="mx_ThreadPanel_vertical_separator" />
<ContextMenuButton
className="mx_ThreadPanel_dropdown"
ref={button}
isExpanded={menuDisplayed}
onClick={(ev: ButtonEvent) => {
openMenu();
PosthogTrackers.trackInteraction("WebRightPanelThreadPanelFilterDropdown", ev);
}}
>
{`${_t("threads|show_thread_filter")} ${value?.label}`}
</ContextMenuButton>
{contextMenu}
</div>
2021-11-11 13:56:44 +00:00
);
};
const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) => {
2021-10-14 15:27:35 +02:00
const mxClient = useContext(MatrixClientContext);
const roomContext = useContext(RoomContext);
const timelinePanel = useRef<TimelinePanel | null>(null);
const card = useRef<HTMLDivElement | null>(null);
const closeButonRef = useRef<HTMLButtonElement | null>(null);
2021-10-14 15:27:35 +02:00
const [filterOption, setFilterOption] = useState<ThreadFilterType>(ThreadFilterType.All);
2022-03-22 21:34:16 +00:00
const [room, setRoom] = useState<Room | null>(null);
const [narrow, setNarrow] = useState<boolean>(false);
const timelineSet: Optional<EventTimelineSet> =
filterOption === ThreadFilterType.My ? room?.threadsTimelineSets[1] : room?.threadsTimelineSets[0];
const hasThreads = Boolean(room?.threadsTimelineSets?.[0]?.getLiveTimeline()?.getEvents()?.length);
useEffect(() => {
2022-03-22 21:34:16 +00:00
const room = mxClient.getRoom(roomId);
2024-01-02 18:56:39 +00:00
room
?.createThreadsTimelineSets()
.then(() => room.fetchRoomThreads())
.then(() => {
2022-03-22 21:34:16 +00:00
setFilterOption(ThreadFilterType.All);
setRoom(room);
2022-03-22 21:34:16 +00:00
});
}, [mxClient, roomId]);
2022-01-21 10:03:08 +00:00
useEffect(() => {
if (timelineSet && !Thread.hasServerSideSupport) {
timelinePanel.current?.refreshTimeline();
}
}, [timelineSet, timelinePanel]);
2021-10-14 15:27:35 +02:00
return (
<RoomContext.Provider
value={{
...roomContext,
timelineRenderingType: TimelineRenderingType.ThreadsList,
showHiddenEvents: true,
narrow,
2021-10-14 15:27:35 +02:00
}}
>
2021-08-17 10:38:09 +01:00
<BaseCard
2024-07-09 17:06:50 +05:30
hideHeaderButtons
header={
hasThreads && <ThreadPanelHeader filterOption={filterOption} setFilterOption={setFilterOption} />
2022-12-12 12:24:14 +01:00
}
2024-07-09 17:06:50 +05:30
id="thread-panel"
2021-08-17 10:38:09 +01:00
className="mx_ThreadPanel"
2024-07-09 17:06:50 +05:30
ariaLabelledBy="thread-panel-tab"
role="tabpanel"
2021-10-14 15:27:35 +02:00
onClose={onClose}
withoutScrollContainer={true}
ref={card}
closeButtonRef={closeButonRef}
2021-08-17 10:38:09 +01:00
>
{card.current && <Measured sensor={card.current} onMeasurement={setNarrow} />}
{timelineSet ? (
<TimelinePanel
key={filterOption + ":" + (timelineSet.getFilter()?.filterId ?? roomId)}
ref={timelinePanel}
2022-09-21 10:13:33 +01:00
showReadReceipts={false} // No RR support in thread's list
manageReadReceipts={false} // No RR support in thread's list
manageReadMarkers={false} // No RM support in thread's list
sendReadReceiptOnLoad={false} // No RR support in thread's list
2021-12-22 14:08:05 +00:00
timelineSet={timelineSet}
2022-03-15 14:08:34 +00:00
showUrlPreview={false} // No URL previews at the threads list level
2021-12-22 14:08:05 +00:00
empty={
<EmptyState
Icon={ThreadsIcon}
title={_t("threads|empty_title")}
description={_t("threads|empty_description", {
replyInThread: _t("action|reply_in_thread"),
})}
2021-12-22 14:08:05 +00:00
/>
}
alwaysShowTimestamps={true}
layout={Layout.Group}
hideThreadedMessages={false}
hidden={false}
showReactions={false}
2022-06-25 11:51:07 +00:00
className="mx_RoomView_messagePanel"
2021-12-22 14:08:05 +00:00
membersLoaded={true}
permalinkCreator={permalinkCreator}
disableGrouping={true}
/>
) : (
<div className="mx_AutoHideScrollbar">
<Spinner />
</div>
)}
2021-08-17 10:38:09 +01:00
</BaseCard>
2021-10-14 15:27:35 +02:00
</RoomContext.Provider>
);
};
export default ThreadPanel;