diff --git a/desktop/src/features/channels/ui/ChannelPane.tsx b/desktop/src/features/channels/ui/ChannelPane.tsx index a721e847e..83c4a4d5a 100644 --- a/desktop/src/features/channels/ui/ChannelPane.tsx +++ b/desktop/src/features/channels/ui/ChannelPane.tsx @@ -90,7 +90,6 @@ type ChannelPaneProps = { threadHeadMessage: TimelineMessage | null; threadMessages: MainTimelineEntry[]; threadTypingPubkeys: string[]; - threadTotalReplyCount: number; threadReplyTargetId: string | null; threadReplyTargetMessage: TimelineMessage | null; threadScrollTargetId: string | null; @@ -130,7 +129,6 @@ export const ChannelPane = React.memo(function ChannelPane({ threadMessages, threadScrollTargetId, threadTypingPubkeys, - threadTotalReplyCount, threadReplyTargetId, threadReplyTargetMessage, typingPubkeys, @@ -286,7 +284,6 @@ export const ChannelPane = React.memo(function ChannelPane({ widthPx={threadPanelWidthPx} threadReplies={threadMessages} threadTypingPubkeys={threadTypingPubkeys} - totalReplyCount={threadTotalReplyCount} /> ) : null} diff --git a/desktop/src/features/channels/ui/ChannelScreen.tsx b/desktop/src/features/channels/ui/ChannelScreen.tsx index a122b22ea..d174764b1 100644 --- a/desktop/src/features/channels/ui/ChannelScreen.tsx +++ b/desktop/src/features/channels/ui/ChannelScreen.tsx @@ -274,7 +274,7 @@ export function ChannelScreen({ const openThreadHeadMessage = threadPanelData.threadHead; const threadMessages = threadPanelData.visibleReplies; const threadReplyTargetMessage = threadPanelData.replyTargetMessage; - const threadTotalReplyCount = threadPanelData.totalReplyCount; + const editTargetMessage = React.useMemo( () => timelineMessages.find((message) => message.id === editTargetId) ?? null, @@ -466,7 +466,6 @@ export function ChannelScreen({ threadHeadMessage={openThreadHeadMessage} threadMessages={threadMessages} threadTypingPubkeys={threadTypingPubkeys} - threadTotalReplyCount={threadTotalReplyCount} threadReplyTargetId={threadReplyTargetId} threadReplyTargetMessage={threadReplyTargetMessage} threadScrollTargetId={threadScrollTargetId} diff --git a/desktop/src/features/messages/ui/MessageThreadPanel.tsx b/desktop/src/features/messages/ui/MessageThreadPanel.tsx index f0bd4ff28..b0bec5efb 100644 --- a/desktop/src/features/messages/ui/MessageThreadPanel.tsx +++ b/desktop/src/features/messages/ui/MessageThreadPanel.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { X } from "lucide-react"; +import { ArrowDown, X } from "lucide-react"; import type { MainTimelineEntry } from "@/features/messages/lib/threadPanel"; import type { TimelineMessage } from "@/features/messages/types"; @@ -10,6 +10,7 @@ import { MessageComposer } from "./MessageComposer"; import { MessageRow } from "./MessageRow"; import { MessageThreadSummaryRow } from "./MessageThreadSummaryRow"; import { TypingIndicatorRow } from "./TypingIndicatorRow"; +import { useTimelineScrollManager } from "./useTimelineScrollManager"; type MessageThreadPanelProps = { canResetWidth: boolean; @@ -44,7 +45,6 @@ type MessageThreadPanelProps = { threadHead: TimelineMessage | null; threadReplies: MainTimelineEntry[]; threadTypingPubkeys: string[]; - totalReplyCount: number; widthPx: number; }; @@ -98,59 +98,26 @@ export function MessageThreadPanel({ } : null; - React.useEffect(() => { - if (!threadHeadId) { - return; - } - - const threadBody = threadBodyRef.current; - if (!threadBody) { - return; - } - - const scrollToBottom = () => { - threadBody.scrollTop = threadBody.scrollHeight; - }; - const frame = requestAnimationFrame(() => { - scrollToBottom(); - }); - const timeoutId = window.setTimeout(scrollToBottom, 300); - - return () => { - cancelAnimationFrame(frame); - window.clearTimeout(timeoutId); - }; - }, [threadHeadId]); - - React.useEffect(() => { - if (!scrollTargetId) { - return; - } - - const threadBody = threadBodyRef.current; - if (!threadBody) { - return; - } - - const target = threadBody.querySelector( - `[data-message-id="${scrollTargetId}"]`, - ); - if (!target) { - return; - } - - const frame = requestAnimationFrame(() => { - target.scrollIntoView({ - behavior: "smooth", - block: "start", - }); - }); - onScrollTargetResolved(); + const threadMessages = React.useMemo( + () => threadReplies.map((entry) => entry.message), + [threadReplies], + ); - return () => { - cancelAnimationFrame(frame); - }; - }, [onScrollTargetResolved, scrollTargetId]); + const { + bottomAnchorRef, + contentRef, + isAtBottom, + newMessageCount, + scrollToBottom, + syncScrollState, + } = useTimelineScrollManager({ + channelId: threadHeadId, + isLoading: false, + messages: threadMessages, + onTargetReached: onScrollTargetResolved, + scrollContainerRef: threadBodyRef, + targetMessageId: scrollTargetId, + }); if (!threadHead) { return null; @@ -197,73 +164,95 @@ export function MessageThreadPanel({
-
-
- +
+
+
+ +
-
-
- {threadReplies.length > 0 ? ( -
- {threadReplies.map((entry, index) => { - const nextDepth = threadReplies[index + 1]?.message.depth ?? -1; - const isExpanded = nextDepth > entry.message.depth; +
+ {threadReplies.length > 0 ? ( +
+ {threadReplies.map((entry, index) => { + const nextDepth = + threadReplies[index + 1]?.message.depth ?? -1; + const isExpanded = nextDepth > entry.message.depth; - return ( -
- - {entry.summary && !isExpanded ? ( - + - ) : null} -
- ); - })} -
- ) : ( -
-

- No replies in this branch yet -

-

- Reply in the thread to continue this branch. -

-
- )} + {entry.summary && !isExpanded ? ( + + ) : null} +
+ ); + })} +
+ ) : ( +
+

+ No replies in this branch yet +

+

+ Reply in the thread to continue this branch. +

+
+ )} +
+
+ {!isAtBottom ? ( +
+ +
+ ) : null} +