fix(desktop): keep thread-open affordance in archived channels (#4012)

## 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
<img width="811" height="794" alt="Screenshot 2026-07-31 at 20 26 00"
src="https://github.com/user-attachments/assets/670d9db4-30da-4c6d-97dc-275b5dbebca8"
/>

### After
<img width="873" height="791" alt="Screenshot 2026-07-31 at 20 28 04"
src="https://github.com/user-attachments/assets/88525231-2539-4eb3-8117-8e58a0cb3855"
/>

## 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 <treyw@squareup.com>
Co-authored-by: npub14h0tw3uj7jm77qfxcwn6um2s5h55l0klrt2w9srzp3m3yvjc0mpsjsuk6e <addeb74792f4b7ef0126c3a7ae6d50a5e94fbedf1ad4e2c0620c771232587ec3@buzz.block.builderlab.xyz>
This commit is contained in:
Trey Wood
2026-08-01 08:41:42 -04:00
committed by GitHub
co-authored by npub14h0tw3uj7jm77qfxcwn6um2s5h55l0klrt2w9srzp3m3yvjc0mpsjsuk6e
parent 19d57b0d46
commit 8e81afa431
5 changed files with 18 additions and 3 deletions
@@ -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}
@@ -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<void>;
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(
<HuddleAttachment
channelId={channelId}
message={message}
onOpenThread={onReply}
onOpenThread={onOpenThread}
/>
);
default:
@@ -755,6 +755,7 @@ export function MessageThreadPanel({
onMarkUnread={onMarkUnread}
onMarkRead={onMarkRead}
onReply={onSelectReplyTarget}
onOpenThread={onExpandReplies}
onToggleReaction={onToggleReaction}
profiles={profiles}
showDepthGuides={shouldShowThreadBranchGuides}
@@ -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}
@@ -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 (
<div
@@ -790,6 +796,7 @@ function MessageRowItem({
onMarkUnread={onMarkUnread}
onToggleReaction={onToggleReaction}
onReply={onReply}
onOpenThread={onOpenThread}
onUnfollowThread={
unfollowThreadById
? () => unfollowThreadById(message.id)
@@ -802,7 +809,7 @@ function MessageRowItem({
<MessageThreadSummaryRow
depth={message.depth}
message={message}
onOpenThread={onReply}
onOpenThread={onOpenThread}
showDepthGuides={false}
summary={summary}
summaryIndentOffsetRem={-THREAD_REPLY_ROW_MARGIN_INLINE_REM}
@@ -839,6 +846,7 @@ function MessageRowItem({
onMarkUnread={onMarkUnread}
onToggleReaction={onToggleReaction}
onReply={onReply}
onOpenThread={onOpenThread}
profiles={profiles}
searchQuery={isSearchMatch ? searchQuery : undefined}
showDepthGuides={false}