From 40e29a9e35e567598c55df6920e37f7057aa9db9 Mon Sep 17 00:00:00 2001 From: npub14h0tw3uj7jm77qfxcwn6um2s5h55l0klrt2w9srzp3m3yvjc0mpsjsuk6e Date: Fri, 31 Jul 2026 15:23:39 -0600 Subject: [PATCH] fix(desktop): keep thread-open affordances in archived channels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Archived channels hid every thread-open affordance in the channel timeline — the "N replies →" summary row and the huddle-started "View thread" button — so existing threads were unreachable from the channel (though still reachable via the inbox "view in thread" entry point). The thread data was intact; this was a UI gate, not data loss. Root cause: one prop did double duty. `onReply` drove both the compose affordances (the hover "Reply" button, inline reply target) and the view affordances (summary row, huddle "View thread"). ChannelPane nulls `onReply` on archived channels to enforce read-only — which correctly hid composing, but inadvertently hid the view affordances too since they keyed off the same prop. Fully separate the two concerns into distinct props: - `onReply` drives only the compose affordances, and stays gated on archived (nulled) so no new replies can be started. - `onOpenThread` drives only the view affordances (summary row, huddle "View thread"), and is passed regardless of archived state, threaded `ChannelPane → MessageTimeline → TimelineMessageList → MessageRow`. The two props are now independent — no fallback coupling. The one caller that previously relied on the shared prop, MessageThreadPanel's reply rows, now passes `onOpenThread={onExpandReplies}` explicitly. Opening a thread on an archived channel is safe: the thread panel's composer is independently gated via `isComposerDisabled`, which includes `archivedAt !== null` (ChannelPane.tsx:318), so it opens read-only. Co-authored-by: Trey Wood Signed-off-by: Trey Wood --- desktop/src/features/channels/ui/ChannelPane.tsx | 1 + desktop/src/features/messages/ui/MessageRow.tsx | 4 +++- .../src/features/messages/ui/MessageThreadPanel.tsx | 1 + desktop/src/features/messages/ui/MessageTimeline.tsx | 3 +++ .../src/features/messages/ui/TimelineMessageList.tsx | 12 ++++++++++-- 5 files changed, 18 insertions(+), 3 deletions(-) diff --git a/desktop/src/features/channels/ui/ChannelPane.tsx b/desktop/src/features/channels/ui/ChannelPane.tsx index 70877875f7..4ab3010ed2 100644 --- a/desktop/src/features/channels/ui/ChannelPane.tsx +++ b/desktop/src/features/channels/ui/ChannelPane.tsx @@ -676,6 +676,7 @@ export const ChannelPane = React.memo(function ChannelPane({ onMarkUnread={onMarkUnread} onMarkRead={onMarkRead} onReply={activeChannel?.archivedAt ? undefined : onOpenThread} + onOpenThread={onOpenThread} channelName={activeChannel?.name} channelType={activeChannel?.channelType ?? null} isSendingVideoReviewComment={isSending} diff --git a/desktop/src/features/messages/ui/MessageRow.tsx b/desktop/src/features/messages/ui/MessageRow.tsx index 688b5d5f0d..286526b658 100644 --- a/desktop/src/features/messages/ui/MessageRow.tsx +++ b/desktop/src/features/messages/ui/MessageRow.tsx @@ -88,6 +88,7 @@ export const MessageRow = React.memo( onMarkRead, onToggleReaction, onReply, + onOpenThread, onEntranceComplete, playEntrance = false, onUnfollowThread, @@ -135,6 +136,7 @@ export const MessageRow = React.memo( remove: boolean, ) => Promise; onReply?: (message: TimelineMessage) => void; + onOpenThread?: (message: TimelineMessage) => void; onUnfollowThread?: (message: TimelineMessage) => void; onEntranceComplete?: (messageId: string) => void; playEntrance?: boolean; @@ -335,7 +337,7 @@ export const MessageRow = React.memo( ); default: diff --git a/desktop/src/features/messages/ui/MessageThreadPanel.tsx b/desktop/src/features/messages/ui/MessageThreadPanel.tsx index 6234af22d1..85a6f6b930 100644 --- a/desktop/src/features/messages/ui/MessageThreadPanel.tsx +++ b/desktop/src/features/messages/ui/MessageThreadPanel.tsx @@ -755,6 +755,7 @@ export function MessageThreadPanel({ onMarkUnread={onMarkUnread} onMarkRead={onMarkRead} onReply={onSelectReplyTarget} + onOpenThread={onExpandReplies} onToggleReaction={onToggleReaction} profiles={profiles} showDepthGuides={shouldShowThreadBranchGuides} diff --git a/desktop/src/features/messages/ui/MessageTimeline.tsx b/desktop/src/features/messages/ui/MessageTimeline.tsx index cc5fb1e3dc..954da08b01 100644 --- a/desktop/src/features/messages/ui/MessageTimeline.tsx +++ b/desktop/src/features/messages/ui/MessageTimeline.tsx @@ -84,6 +84,7 @@ type MessageTimelineProps = { onMarkUnread?: (message: TimelineMessage) => void; onMarkRead?: (message: TimelineMessage) => void; onReply?: (message: TimelineMessage) => void; + onOpenThread?: (message: TimelineMessage) => void; isSendingVideoReviewComment?: boolean; onSendVideoReviewComment?: ( message: TimelineMessage, @@ -178,6 +179,7 @@ const MessageTimelineBase = React.forwardRef< onMarkUnread, onMarkRead, onReply, + onOpenThread, channelName, channelType, isSendingVideoReviewComment = false, @@ -635,6 +637,7 @@ const MessageTimelineBase = React.forwardRef< onMarkUnread={onMarkUnread} onMarkRead={onMarkRead} onReply={onReply} + onOpenThread={onOpenThread} isSendingVideoReviewComment={isSendingVideoReviewComment} onSendVideoReviewComment={onSendVideoReviewComment} onStartReached={loadOlderViaVirtualizer} diff --git a/desktop/src/features/messages/ui/TimelineMessageList.tsx b/desktop/src/features/messages/ui/TimelineMessageList.tsx index b724d995eb..89183d0856 100644 --- a/desktop/src/features/messages/ui/TimelineMessageList.tsx +++ b/desktop/src/features/messages/ui/TimelineMessageList.tsx @@ -77,6 +77,7 @@ type TimelineMessageListProps = { onMarkUnread?: (message: TimelineMessage) => void; onMarkRead?: (message: TimelineMessage) => void; onReply?: (message: TimelineMessage) => void; + onOpenThread?: (message: TimelineMessage) => void; isSendingVideoReviewComment?: boolean; onSendVideoReviewComment?: ( message: TimelineMessage, @@ -142,6 +143,7 @@ export const TimelineMessageList = React.memo(function TimelineMessageList({ onMarkUnread, onMarkRead, onReply, + onOpenThread, isSendingVideoReviewComment = false, onSendVideoReviewComment, onToggleReaction, @@ -255,6 +257,7 @@ export const TimelineMessageList = React.memo(function TimelineMessageList({ onMarkRead={onMarkRead} onMarkUnread={onMarkUnread} onReply={onReply} + onOpenThread={onOpenThread} onToggleReaction={onToggleReaction} profiles={profiles} searchActiveMessageId={searchActiveMessageId} @@ -286,6 +289,7 @@ export const TimelineMessageList = React.memo(function TimelineMessageList({ onMarkRead, onMarkUnread, onReply, + onOpenThread, onToggleReaction, profiles, ownerProfiles, @@ -699,6 +703,7 @@ type MessageRowItemProps = Pick< | "onMarkUnread" | "onMarkRead" | "onReply" + | "onOpenThread" | "onToggleReaction" | "profiles" | "searchActiveMessageId" @@ -737,6 +742,7 @@ function MessageRowItem({ onMarkUnread, onMarkRead, onReply, + onOpenThread, onToggleReaction, profiles, searchActiveMessageId, @@ -755,7 +761,7 @@ function MessageRowItem({ const canDelete = canManage && onDelete ? onDelete : undefined; const canEdit = canManage && onEdit ? onEdit : undefined; - if (summary && onReply) { + if (summary && onOpenThread) { const isHighlighted = message.id === highlightedMessageId; return (
unfollowThreadById(message.id) @@ -802,7 +809,7 @@ function MessageRowItem({