feat(threads): "View Thread (N replies)" on a thread's root message (#165)
CI / Build & Quality Checks (push) Successful in 1m34s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 9s
CI / Trigger Desktop Build (push) Successful in 6s
CI / Playwright smoke (e2e) (push) Successful in 12m7s
CI / Build & Quality Checks (push) Successful in 1m34s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 9s
CI / Trigger Desktop Build (push) Successful in 6s
CI / Playwright smoke (e2e) (push) Successful in 12m7s
The SDK gives a thread root its own id as threadRootId, so the `!isThreadedMessage` guard hid the thread action on exactly the message that has a thread; the "N replies" chip was the only way in. Roots now get the action in both the hover bar and the ⋮ menu, labelled "View thread" / "View Thread (14 replies)" with the thread icon; other messages keep "Reply in Thread". Same handler, which opens the thread panel. Verified in Chromium: on a 14-reply root both entries show and open the panel; an ordinary message still shows "Reply in thread". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b71f9c95c6
commit
2b1f4ee5bc
@@ -1176,6 +1176,18 @@ export const Message = React.memo(
|
||||
};
|
||||
|
||||
const isThreadedMessage = mEvent.threadRootId !== undefined;
|
||||
// [Gitea #165] A root that already has a thread: the same action opens it,
|
||||
// so say "View Thread (N replies)" rather than "Reply in Thread".
|
||||
// The SDK gives a thread root its own id as threadRootId, so without this
|
||||
// a root (the one message that HAS a thread) got no thread action at all.
|
||||
const isThreadRoot = !!mEvent.getId() && mEvent.threadRootId === mEvent.getId();
|
||||
const showThreadAction = !isThreadedMessage || isThreadRoot;
|
||||
const existingThread = mEvent.getId() ? room.getThread(mEvent.getId()!) : null;
|
||||
const threadReplies = existingThread?.length ?? 0;
|
||||
const threadActionLabel =
|
||||
threadReplies > 0
|
||||
? `View Thread (${threadReplies} ${threadReplies === 1 ? 'reply' : 'replies'})`
|
||||
: 'Reply in Thread';
|
||||
|
||||
// The full action menu, shared by the desktop PopOut and the touch
|
||||
// bottom sheet (#166).
|
||||
@@ -1273,10 +1285,12 @@ export const Message = React.memo(
|
||||
</Text>
|
||||
</MenuItem>
|
||||
)}
|
||||
{!isThreadedMessage && (
|
||||
{showThreadAction && (
|
||||
<MenuItem
|
||||
size="300"
|
||||
after={<Icon src={Icons.ThreadPlus} size="100" />}
|
||||
after={
|
||||
<Icon src={threadReplies > 0 ? Icons.Thread : Icons.ThreadPlus} size="100" />
|
||||
}
|
||||
radii="300"
|
||||
data-event-id={mEvent.getId()}
|
||||
onClick={(evt: React.MouseEvent<HTMLButtonElement>) => {
|
||||
@@ -1285,7 +1299,7 @@ export const Message = React.memo(
|
||||
}}
|
||||
>
|
||||
<Text className={css.MessageMenuItemText} as="span" size="T300" truncate>
|
||||
Reply in Thread
|
||||
{threadActionLabel}
|
||||
</Text>
|
||||
</MenuItem>
|
||||
)}
|
||||
@@ -1465,16 +1479,16 @@ export const Message = React.memo(
|
||||
>
|
||||
<Icon src={Icons.ReplyArrow} size="100" />
|
||||
</IconButton>
|
||||
{!isThreadedMessage && (
|
||||
{showThreadAction && (
|
||||
<IconButton
|
||||
onClick={(ev) => onReplyClick(ev, true)}
|
||||
data-event-id={mEvent.getId()}
|
||||
variant="SurfaceVariant"
|
||||
size="300"
|
||||
radii="300"
|
||||
aria-label="Reply in thread"
|
||||
aria-label={threadReplies > 0 ? 'View thread' : 'Reply in thread'}
|
||||
>
|
||||
<Icon src={Icons.ThreadPlus} size="100" />
|
||||
<Icon src={threadReplies > 0 ? Icons.Thread : Icons.ThreadPlus} size="100" />
|
||||
</IconButton>
|
||||
)}
|
||||
{canEditEventOrCaption(mx, mEvent) && onEditId && (
|
||||
|
||||
Reference in New Issue
Block a user