(`[data-cmt-more-for="${comment.id}"]`)
: active instanceof HTMLElement && active !== container && active !== document.body
? () => (active.isConnected ? active : null)
: null
setEditingId(comment.id)
setEditText(comment.body)
}
const saveEdit = () => {
const comment = comments.find((c) => c.id === editingId)
if (!comment || isCommentEmpty(editText)) return
editComment(editor, comment, editText)
setEditingId(null)
}
// Swap a comment for a pre-filled composer while it's being edited; otherwise show the card,
// with the affordances the viewer is allowed on it.
const renderComment = (card: CommentCardProps, index: number): ReactNode => {
const comment = comments[index]
const permissions = commentPermissions.get(comment.id)
if (editingId === comment.id) {
return (
{
if (e.key === 'Escape') {
setEditingId(null)
e.stopPropagation()
}
}}
>
)
}
return (
}
actions={
canComment && (
<>
{(permissions?.edit || permissions?.delete) && (
)}
{/* Last so react is always the rightmost pill. */}
>
)
}
/>
)
}
// Resolve and delete are commenting writes: behind `canComment`, plus the `currentUserId` a
// resolve stamps into `resolved.by`.
const resolveLabel = msg(thread.resolved ? 'comments.reopen' : 'comments.resolve')
const headerActions = (
<>
{/* Host verbs — assign, link a ticket — sit ahead of the built-in actions. */}
{ThreadActions && }
{(threadHref !== undefined || canDeleteThread) && (
)}
{canComment && currentUserId && (
)}
openThreadId.set(editor, null)}
>
>
)
return (
{
setReply(value)
saveCommentDraft(replyDraftSlot(thread.id), value)
},
onSubmit: postReply,
// Up in the empty reply box edits the comment directly above it, chat-style —
// only when you're allowed to edit it (the same gate as the Edit link).
onArrowUpWhenEmpty: () => {
const last = comments[comments.length - 1]
if (last && commentPermissions.get(last.id)?.edit) startEdit(last)
},
// No user, no author for the record — dead send button.
disabled: isCommentEmpty(reply) || !currentUserId,
getMentionSuggestions,
renderMentionSuggestion,
// Reuses the composer's own focus path, so the caret lands at the end of a
// restored draft rather than in front of it.
autoFocus: focusReply,
}
: undefined
}
footer={
!canComment && !thread.resolved && ComposerFallback ? (
) : undefined
}
/>
)
})