From 8e81afa431deecd172f1ad6aab6f022f31cd812c Mon Sep 17 00:00:00 2001 From: Trey Wood Date: Sat, 1 Aug 2026 06:41:42 -0600 Subject: [PATCH] fix(desktop): keep thread-open affordance in archived channels (#4012) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Threaded replies "disappeared" from archived Buzz channels: the **"N replies →"** summary row and the huddle-started **"View thread"** button vanished, so existing threads were unreachable from the channel timeline. The thread data was intact — this was a UI gate, not data loss. ## Root cause A single `onReply` prop drove two distinct affordances: - the **compose** affordances (hover "Reply" button, inline reply target), and - the **view** affordances ("N replies →" summary row, huddle "View thread"). `ChannelPane` nulls `onReply` on archived channels to keep them read-only. That correctly hid composing — but also hid the view affordances, since they keyed off the same prop. ## Fix Two independent props, one per concern: - **`onReply`** drives the compose affordances and is gated on `archivedAt` — nulled on archived channels, so no new replies can be started. - **`onOpenThread`** drives the view affordances and is passed regardless of archived state, threaded `ChannelPane → MessageTimeline → TimelineMessageList → MessageRow`. Opening a thread on an archived channel is read-only: the thread panel's composer is independently gated via `isComposerDisabled` (includes `archivedAt !== null`, `ChannelPane.tsx:318`). ### Before Screenshot 2026-07-31 at 20 26 00 ### After Screenshot 2026-07-31 at 20 28 04 ## Validation - `pnpm typecheck` clean - biome lint clean on touched files - full `pnpm test` suite green (3885 tests) - pre-push `branch-skew` / `desktop-check` / `desktop-test` hooks passed Signed-off-by: Trey Wood Co-authored-by: npub14h0tw3uj7jm77qfxcwn6um2s5h55l0klrt2w9srzp3m3yvjc0mpsjsuk6e --- 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 20eff07ea..770c45e44 100644 --- a/desktop/src/features/channels/ui/ChannelPane.tsx +++ b/desktop/src/features/channels/ui/ChannelPane.tsx @@ -681,6 +681,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 688b5d5f0..286526b65 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 6234af22d..85a6f6b93 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 cc5fb1e3d..954da08b0 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 b724d995e..89183d085 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({