import React, { useCallback, useEffect, useRef } from 'react';
import {
Box,
Header,
Icon,
IconButton,
Icons,
Spinner,
Text,
Tooltip,
TooltipProvider,
} from 'folds';
import { Room, RoomEvent, ThreadEvent } from 'matrix-js-sdk';
import { isKeyHotkey } from 'is-hotkey';
import classNames from 'classnames';
import * as css from './ThreadPanel.css';
import { ContainerColor } from '../../../styles/ContainerColor.css';
import { ThreadTimeline } from './ThreadTimeline';
import { markThreadAsRead, useThreadInstance } from './useThread';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useEditor } from '../../../components/editor';
import { useKeyDown } from '../../../hooks/useKeyDown';
import { useSetting } from '../../../state/hooks/settings';
import { settingsAtom } from '../../../state/settings';
import { RoomInput } from '../RoomInput';
import {
getThreadNotificationModeIcon,
ThreadNotificationModeSwitcher,
} from '../../../components/ThreadNotificationModeSwitcher';
import { useThreadNotificationMode } from '../../../hooks/useThreadNotifications';
import { ThreadNotificationMode } from '../../../utils/threadNotifications';
type ThreadPanelHeaderProps = {
room: Room;
threadId: string;
requestClose: () => void;
};
function ThreadPanelHeader({ room, threadId, requestClose }: ThreadPanelHeaderProps) {
const mode = useThreadNotificationMode(room.roomId, threadId);
return (
Thread
{room.name}
{(handleOpen, opened) => (
Notifications
}
>
{(triggerRef) => (
)}
)}
Close
}
>
{(triggerRef) => (
)}
);
}
export type ThreadPanelProps = {
room: Room;
threadId: string;
requestClose: () => void;
};
export function ThreadPanel({ room, threadId, requestClose }: ThreadPanelProps) {
const mx = useMatrixClient();
const editor = useEditor();
const thread = useThreadInstance(room, threadId);
const [privateReadReceipts] = useSetting(settingsAtom, 'privateReadReceipts');
const fileDropContainerRef = useRef(null) as React.RefObject;
useKeyDown(
window,
useCallback(
(evt) => {
if (isKeyHotkey('escape', evt)) {
// The composer preventDefaults Escape when it consumes it (dismissing
// autocomplete / clearing a reply draft). Don't close the panel in
// that case — only when Escape wasn't already handled.
if (evt.defaultPrevented) return;
evt.preventDefault();
evt.stopPropagation();
requestClose();
}
},
[requestClose],
),
);
// Mark the thread read when the panel is open and on each new thread event.
// Deduped on the latest event id: RoomEvent.Timeline re-emits per event during
// backfill and for every edit/reaction, and sendReadReceipt POSTs
// unconditionally — without the guard, opening a thread with N replies would
// fire up to N receipt requests at the same event.
const lastReadEventIdRef = useRef(undefined);
useEffect(() => {
lastReadEventIdRef.current = undefined;
if (!thread) return undefined;
const markRead = () => {
const events = thread.liveTimeline.getEvents();
let latestId: string | undefined;
for (let i = events.length - 1; i >= 0; i -= 1) {
const evt = events[i];
if (evt && !evt.isSending()) {
latestId = evt.getId() ?? undefined;
break;
}
}
if (!latestId || latestId === lastReadEventIdRef.current) return;
lastReadEventIdRef.current = latestId;
markThreadAsRead(mx, thread, privateReadReceipts).catch(() => {
// Allow a retry on the next event if the receipt POST failed.
if (lastReadEventIdRef.current === latestId) {
lastReadEventIdRef.current = undefined;
}
});
};
markRead();
thread.on(ThreadEvent.NewReply, markRead);
thread.on(RoomEvent.Timeline, markRead);
return () => {
thread.off(ThreadEvent.NewReply, markRead);
thread.off(RoomEvent.Timeline, markRead);
};
}, [mx, thread, privateReadReceipts]);
return (
{!thread ? (
Loading thread…
) : (
<>
>
)}
);
}