@liveblocks/react-ui 3.21.0-private1 → 3.21.0-private3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"Thread.cjs","sources":["../../src/components/Thread.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n type BaseMetadata,\n type CommentData,\n type DCM,\n type DTM,\n findLastIndex,\n type ThreadData,\n} from \"@liveblocks/core\";\nimport {\n useHasPermissionAccess,\n useMarkRoomThreadAsRead,\n useMarkRoomThreadAsResolved,\n useMarkRoomThreadAsUnresolved,\n useRoomThreadSubscription,\n} from \"@liveblocks/react/_private\";\nimport { Toggle as TogglePrimitive } from \"radix-ui\";\nimport {\n type ComponentPropsWithoutRef,\n type ComponentType,\n type ForwardedRef,\n forwardRef,\n Fragment,\n type PropsWithChildren,\n type ReactNode,\n type RefAttributes,\n type SyntheticEvent,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nimport type { GlobalComponents } from \"../components\";\nimport { ArrowDownIcon } from \"../icons/ArrowDown\";\nimport { BellIcon } from \"../icons/Bell\";\nimport { BellCrossedIcon } from \"../icons/BellCrossed\";\nimport { CheckCircleIcon } from \"../icons/CheckCircle\";\nimport { CheckCircleFillIcon } from \"../icons/CheckCircleFill\";\nimport type {\n CommentOverrides,\n ComposerOverrides,\n GlobalOverrides,\n ThreadOverrides,\n} from \"../overrides\";\nimport { useOverrides } from \"../overrides\";\nimport { cn } from \"../utils/cn\";\nimport { useStableComponent } from \"../utils/use-stable-component\";\nimport { useIntersectionCallback } from \"../utils/use-visible\";\nimport { useWindowFocus } from \"../utils/use-window-focus\";\nimport { Comment as DefaultComment, type CommentProps } from \"./Comment\";\nimport { Composer, type ComposerProps } from \"./Composer\";\nimport { Button } from \"./internal/Button\";\nimport { Tooltip, TooltipProvider } from \"./internal/Tooltip\";\n\n// An invisible marker used to mark a thread as read only when it becomes visible.\nfunction MarkThreadAsReadMarker({ thread }: { thread: ThreadData }) {\n const { id: threadId, roomId } = thread;\n const ref = useRef<HTMLDivElement>(null);\n const markThreadAsRead = useMarkRoomThreadAsRead(roomId);\n const isWindowFocused = useWindowFocus();\n\n useIntersectionCallback(\n ref,\n (isIntersecting) => {\n if (isIntersecting) {\n markThreadAsRead(threadId);\n }\n },\n {\n // The underlying IntersectionObserver is only enabled when the window is focused\n enabled: isWindowFocused,\n }\n );\n\n return (\n <div\n ref={ref}\n style={{ height: 0 }}\n aria-hidden\n data-mark-as-read-marker=\"\"\n />\n );\n}\n\nexport interface ThreadComponents<\n // Future components might reference thread metadata so we declare it\n // already to respect the `TM` → `CM` order used everywhere else.\n _TM extends BaseMetadata = DTM,\n CM extends BaseMetadata = DCM,\n> {\n /**\n * The component used to display comments.\n */\n Comment: ComponentType<CommentProps<CM>>;\n}\n\nexport interface ThreadProps<\n TM extends BaseMetadata = DTM,\n CM extends BaseMetadata = DCM,\n> extends ComponentPropsWithoutRef<\"div\"> {\n /**\n * The thread to display.\n */\n thread: ThreadData<TM, CM>;\n\n /**\n * How to show or hide the composer to reply to the thread.\n */\n showComposer?: boolean | \"collapsed\";\n\n /**\n * Whether to show the action to resolve the thread.\n */\n showResolveAction?: boolean;\n\n /**\n * How to show or hide the actions.\n */\n showActions?: CommentProps[\"showActions\"];\n\n /**\n * Whether to show reactions.\n */\n showReactions?: CommentProps[\"showReactions\"];\n\n /**\n * Whether to show the composer's formatting controls.\n */\n showComposerFormattingControls?: ComposerProps[\"showFormattingControls\"];\n\n /**\n * Add (or change) items to display in a comment's dropdown.\n */\n commentDropdownItems?:\n | ReactNode\n | ((props: PropsWithChildren<{ comment: CommentData<CM> }>) => ReactNode);\n\n /**\n * The maximum number of comments to show.\n *\n * The first and last comments are always shown and by default if some comments\n * are hidden, only the first comment will be shown before the \"show more\" button\n * and after it will be shown all the newest comments to fit the limit set.\n *\n * It's possible to customize this by setting `maxVisibleComments` to an object:\n *\n * @example\n * // Only show the last comment, and all the older ones to fit the limit.\n * <Thread maxVisibleComments={{ max: 5, show: \"oldest\" }} />\n *\n * @example\n * // Show as many old comments as new ones to fit the limit.\n * <Thread maxVisibleComments={{ max: 5, show: \"both\" }} />\n */\n maxVisibleComments?:\n | number\n | { max: number; show: \"oldest\" | \"both\" | \"newest\" };\n\n /**\n * Whether to blur the composer editor when the composer is submitted.\n */\n blurComposerOnSubmit?: ComposerProps[\"blurOnSubmit\"];\n\n /**\n * Whether to indent the comments' content.\n */\n indentCommentContent?: CommentProps[\"indentContent\"];\n\n /**\n * Whether to show deleted comments.\n */\n showDeletedComments?: CommentProps[\"showDeleted\"];\n\n /**\n * Whether to show the thread's subscription status.\n */\n showSubscription?: boolean;\n\n /**\n * Whether to show attachments.\n */\n showAttachments?: boolean;\n\n /**\n * The event handler called when changing the resolved status.\n */\n onResolvedChange?: (resolved: boolean) => void;\n\n /**\n * The event handler called when a comment is edited.\n */\n onCommentEdit?: CommentProps[\"onCommentEdit\"];\n\n /**\n * The event handler called when a comment is deleted.\n */\n onCommentDelete?: CommentProps[\"onCommentDelete\"];\n\n /**\n * The event handler called when the thread is deleted.\n * A thread is deleted when all its comments are deleted.\n */\n onThreadDelete?: (thread: ThreadData<TM, CM>) => void;\n\n /**\n * The event handler called when clicking on a comment's author.\n */\n onAuthorClick?: CommentProps[\"onAuthorClick\"];\n\n /**\n * The event handler called when clicking on a mention.\n */\n onMentionClick?: CommentProps[\"onMentionClick\"];\n\n /**\n * The event handler called when clicking on a comment's attachment.\n */\n onAttachmentClick?: CommentProps[\"onAttachmentClick\"];\n\n /**\n * The event handler called when the composer is submitted.\n */\n onComposerSubmit?: ComposerProps[\"onComposerSubmit\"];\n\n /**\n * Whether to focus the composer on mount.\n */\n autoFocus?: ComposerProps[\"autoFocus\"];\n\n /**\n * Override the component's strings.\n */\n overrides?: Partial<\n GlobalOverrides & ThreadOverrides & CommentOverrides & ComposerOverrides\n >;\n\n /**\n * Override the component's components.\n */\n components?: Partial<GlobalComponents & ThreadComponents<TM, CM>>;\n}\n\n/**\n * Displays a thread of comments, with a composer to reply\n * to it.\n *\n * @example\n * <>\n * {threads.map((thread) => (\n * <Thread key={thread.id} thread={thread} />\n * ))}\n * </>\n */\nexport const Thread = forwardRef(\n <TM extends BaseMetadata = DTM, CM extends BaseMetadata = DCM>(\n {\n thread,\n indentCommentContent = true,\n showActions = \"hover\",\n showDeletedComments,\n showResolveAction = true,\n showReactions = true,\n showComposer = \"collapsed\",\n showAttachments = true,\n showSubscription = true,\n showComposerFormattingControls = true,\n maxVisibleComments,\n commentDropdownItems,\n onResolvedChange,\n onCommentEdit,\n onCommentDelete,\n onThreadDelete,\n onAuthorClick,\n onMentionClick,\n onAttachmentClick,\n onComposerSubmit,\n blurComposerOnSubmit,\n autoFocus,\n overrides,\n components,\n className,\n ...props\n }: ThreadProps<TM, CM>,\n forwardedRef: ForwardedRef<HTMLDivElement>\n ) => {\n const Comment = useStableComponent(components?.Comment, DefaultComment);\n const markThreadAsResolved = useMarkRoomThreadAsResolved(thread.roomId);\n const markThreadAsUnresolved = useMarkRoomThreadAsUnresolved(thread.roomId);\n const $ = useOverrides(overrides);\n const [showAllComments, setShowAllComments] = useState(false);\n const firstCommentIndex = useMemo(() => {\n return showDeletedComments\n ? 0\n : thread.comments.findIndex((comment) => comment.body);\n }, [showDeletedComments, thread.comments]);\n const lastCommentIndex = useMemo(() => {\n return showDeletedComments\n ? thread.comments.length - 1\n : findLastIndex(thread.comments, (comment) => Boolean(comment.body));\n }, [showDeletedComments, thread.comments]);\n const commentsCount = useMemo(() => {\n return showDeletedComments\n ? thread.comments.length\n : thread.comments.filter((comment) => comment.body).length;\n }, [showDeletedComments, thread.comments]);\n const hiddenComments = useMemo(() => {\n const maxVisibleCommentsCount =\n typeof maxVisibleComments === \"number\"\n ? maxVisibleComments\n : maxVisibleComments?.max;\n const visibleCommentsShow =\n (typeof maxVisibleComments === \"object\"\n ? maxVisibleComments?.show\n : undefined) ?? \"newest\";\n\n // If we explicitly want to show all comments or there's no limit set,\n // no need to hide any comments.\n if (showAllComments || maxVisibleCommentsCount === undefined) {\n return;\n }\n\n const comments = thread.comments\n .map((comment, index) => ({ comment, index }))\n .filter(({ comment }) => showDeletedComments || comment.body);\n\n // There aren't enough comments so no need to hide any.\n if (comments.length <= Math.max(maxVisibleCommentsCount, 2)) {\n return;\n }\n\n const firstVisibleComment = comments[0]!;\n const lastVisibleComment = comments[comments.length - 1]!;\n\n // Always show the first and last comments even if the limit is set to lower than 2.\n if (maxVisibleCommentsCount <= 2) {\n const firstHiddenCommentIndex =\n comments[1]?.index ?? firstVisibleComment.index;\n const lastHiddenCommentIndex =\n comments[comments.length - 2]?.index ?? lastVisibleComment.index;\n\n return {\n firstIndex: firstHiddenCommentIndex,\n lastIndex: lastHiddenCommentIndex,\n count: comments.slice(1, comments.length - 1).length,\n };\n }\n\n const remainingVisibleCommentsCount = maxVisibleCommentsCount - 2;\n\n // Split the remaining visible comments before, after, or equally.\n const beforeVisibleCommentsCount =\n visibleCommentsShow === \"oldest\"\n ? remainingVisibleCommentsCount\n : visibleCommentsShow === \"newest\"\n ? 0\n : Math.floor(remainingVisibleCommentsCount / 2);\n const afterVisibleCommentsCount =\n visibleCommentsShow === \"oldest\"\n ? 0\n : visibleCommentsShow === \"newest\"\n ? remainingVisibleCommentsCount\n : Math.ceil(remainingVisibleCommentsCount / 2);\n\n // The first comment is always visible so `+ 1` to skip it.\n const firstHiddenComment = comments[1 + beforeVisibleCommentsCount];\n // The last comment is always visible so `- 2` to skip it.\n const lastHiddenComment =\n comments[comments.length - 2 - afterVisibleCommentsCount];\n\n // There aren't any comments to hide besides the first and last ones.\n if (\n !firstHiddenComment ||\n !lastHiddenComment ||\n firstHiddenComment.index > lastHiddenComment.index\n ) {\n return;\n }\n\n return {\n firstIndex: firstHiddenComment.index,\n lastIndex: lastHiddenComment.index,\n count: thread.comments\n .slice(firstHiddenComment.index, lastHiddenComment.index + 1)\n .filter((comment) => showDeletedComments || comment.body).length,\n };\n }, [\n maxVisibleComments,\n showAllComments,\n showDeletedComments,\n thread.comments,\n ]);\n const {\n status: subscriptionStatus,\n unreadSince,\n subscribe,\n unsubscribe,\n } = useRoomThreadSubscription(thread.roomId, thread.id);\n const unreadIndex = useMemo(() => {\n // The user is not subscribed to this thread.\n if (subscriptionStatus !== \"subscribed\") {\n return;\n }\n\n // The user hasn't read the thread yet, so all comments are unread.\n if (unreadSince === null) {\n return firstCommentIndex;\n }\n\n // The user has read the thread, so we find the first unread comment.\n const unreadIndex = thread.comments.findIndex(\n (comment) =>\n (showDeletedComments ? true : comment.body) &&\n comment.createdAt > unreadSince\n );\n\n return unreadIndex >= 0 && unreadIndex < thread.comments.length\n ? unreadIndex\n : undefined;\n }, [\n firstCommentIndex,\n showDeletedComments,\n subscriptionStatus,\n thread.comments,\n unreadSince,\n ]);\n const [newIndex, setNewIndex] = useState<number>();\n const newIndicatorIndex = newIndex === undefined ? unreadIndex : newIndex;\n\n useEffect(() => {\n if (unreadIndex) {\n // Keep the \"new\" indicator at the lowest unread index.\n setNewIndex((persistedUnreadIndex) =>\n Math.min(persistedUnreadIndex ?? Infinity, unreadIndex)\n );\n }\n }, [unreadIndex]);\n\n const canComment = useHasPermissionAccess(\n thread.roomId,\n \"comments\",\n \"write\"\n );\n\n const stopPropagation = useCallback((event: SyntheticEvent) => {\n event.stopPropagation();\n }, []);\n\n const handleResolvedChange = useCallback(\n (resolved: boolean) => {\n onResolvedChange?.(resolved);\n\n if (resolved) {\n markThreadAsResolved(thread.id);\n } else {\n markThreadAsUnresolved(thread.id);\n }\n },\n [\n markThreadAsResolved,\n markThreadAsUnresolved,\n onResolvedChange,\n thread.id,\n ]\n );\n\n const handleCommentDelete = useCallback(\n (comment: CommentData) => {\n onCommentDelete?.(comment);\n\n const filteredComments = thread.comments.filter(\n (comment) => comment.body\n );\n\n if (filteredComments.length <= 1) {\n onThreadDelete?.(thread);\n }\n },\n [onCommentDelete, onThreadDelete, thread]\n );\n\n const handleSubscribeChange = useCallback(() => {\n if (subscriptionStatus === \"subscribed\") {\n unsubscribe();\n } else {\n subscribe();\n }\n }, [subscriptionStatus, subscribe, unsubscribe]);\n\n let currentCommentIndex = 0;\n\n return (\n <TooltipProvider>\n <div\n className={cn(\n \"lb-root lb-thread\",\n showActions === \"hover\" && \"lb-thread:show-actions-hover\",\n className\n )}\n data-resolved={thread.resolved ? \"\" : undefined}\n data-unread={unreadIndex !== undefined ? \"\" : undefined}\n dir={$.dir}\n {...props}\n ref={forwardedRef}\n >\n <div className=\"lb-thread-comments\" role=\"feed\">\n {thread.comments.map((comment, index) => {\n const isNonDeletedComment = showDeletedComments || comment.body;\n const isFirstComment = index === firstCommentIndex;\n const isUnread =\n unreadIndex !== undefined && index >= unreadIndex;\n const isHidden =\n hiddenComments &&\n index >= hiddenComments.firstIndex &&\n index <= hiddenComments.lastIndex;\n const isHiddenBecauseDeleted =\n !showDeletedComments && !comment.body;\n const isFirstHiddenComment =\n isHidden && index === hiddenComments.firstIndex;\n\n if (isFirstHiddenComment) {\n return (\n <div\n key={`${comment.id}:show-more`}\n className=\"lb-thread-show-more\"\n >\n <Button\n variant=\"ghost\"\n className=\"lb-thread-show-more-button\"\n onClick={() => setShowAllComments(true)}\n >\n {$.THREAD_SHOW_MORE_COMMENTS(hiddenComments.count)}\n </Button>\n </div>\n );\n }\n\n if (isHidden || isHiddenBecauseDeleted) {\n return null;\n }\n\n if (isNonDeletedComment) {\n currentCommentIndex++;\n }\n\n const children = (\n <Comment\n key={comment.id}\n tabIndex={0}\n aria-posinset={currentCommentIndex}\n aria-setsize={commentsCount}\n overrides={overrides}\n className=\"lb-thread-comment\"\n data-unread={isUnread ? \"\" : undefined}\n comment={comment}\n indentContent={indentCommentContent}\n showDeleted={showDeletedComments}\n showActions={showActions}\n showReactions={showReactions}\n showAttachments={showAttachments}\n showComposerFormattingControls={\n showComposerFormattingControls\n }\n onCommentEdit={onCommentEdit}\n onCommentDelete={handleCommentDelete}\n onAuthorClick={onAuthorClick}\n onMentionClick={onMentionClick}\n onAttachmentClick={onAttachmentClick}\n components={components}\n actionsClassName={\n isFirstComment ? \"lb-thread-actions\" : undefined\n }\n actions={\n isFirstComment && showResolveAction ? (\n <Tooltip\n content={\n thread.resolved\n ? $.THREAD_UNRESOLVE\n : $.THREAD_RESOLVE\n }\n >\n <TogglePrimitive.Root\n pressed={thread.resolved}\n onPressedChange={handleResolvedChange}\n asChild\n >\n <Button\n className=\"lb-comment-action\"\n onClick={stopPropagation}\n aria-label={\n thread.resolved\n ? $.THREAD_UNRESOLVE\n : $.THREAD_RESOLVE\n }\n icon={\n thread.resolved ? (\n <CheckCircleFillIcon />\n ) : (\n <CheckCircleIcon />\n )\n }\n disabled={!canComment}\n />\n </TogglePrimitive.Root>\n </Tooltip>\n ) : null\n }\n internalDropdownItems={\n isFirstComment && showSubscription ? (\n <DefaultComment.DropdownItem\n onSelect={handleSubscribeChange}\n icon={\n subscriptionStatus === \"subscribed\" ? (\n <BellCrossedIcon />\n ) : (\n <BellIcon />\n )\n }\n >\n {subscriptionStatus === \"subscribed\"\n ? $.THREAD_UNSUBSCRIBE\n : $.THREAD_SUBSCRIBE}\n </DefaultComment.DropdownItem>\n ) : undefined\n }\n dropdownItems={\n commentDropdownItems as CommentProps[\"dropdownItems\"]\n }\n />\n );\n\n return index === newIndicatorIndex &&\n newIndicatorIndex !== firstCommentIndex &&\n newIndicatorIndex <= lastCommentIndex ? (\n <Fragment key={comment.id}>\n <div\n className=\"lb-thread-new-indicator\"\n aria-label={$.THREAD_NEW_INDICATOR_DESCRIPTION}\n >\n <span className=\"lb-thread-new-indicator-label\">\n <ArrowDownIcon className=\"lb-thread-new-indicator-label-icon\" />\n {$.THREAD_NEW_INDICATOR}\n </span>\n </div>\n {children}\n </Fragment>\n ) : (\n children\n );\n })}\n </div>\n {unreadIndex !== undefined && (\n <MarkThreadAsReadMarker thread={thread} />\n )}\n {showComposer && (\n <Composer\n className=\"lb-thread-composer\"\n threadId={thread.id}\n defaultCollapsed={showComposer === \"collapsed\" ? true : undefined}\n showAttachments={showAttachments}\n showFormattingControls={showComposerFormattingControls}\n onComposerSubmit={onComposerSubmit}\n blurOnSubmit={blurComposerOnSubmit}\n overrides={{\n COMPOSER_PLACEHOLDER: $.THREAD_COMPOSER_PLACEHOLDER,\n COMPOSER_SEND: $.THREAD_COMPOSER_SEND,\n ...overrides,\n }}\n roomId={thread.roomId}\n autoFocus={autoFocus}\n />\n )}\n </div>\n </TooltipProvider>\n );\n }\n) as <TM extends BaseMetadata = DTM, CM extends BaseMetadata = DCM>(\n props: ThreadProps<TM, CM> & RefAttributes<HTMLDivElement>\n) => JSX.Element;\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA0DA;AACE;AACA;AACA;AACA;AAEA;AAAA;AACE;AAEE;AACE;AAAyB;AAC3B;AACF;AACA;AAAA;AAEW;AACX;AAGF;AACE;AAAC;AAAA;AACC;AACmB;AACR;AACc;AAAA;AAG/B;AA2KO;AAAe;AAElB;AACE;AACuB;AACT;AACd;AACoB;AACJ;AACD;AACG;AACC;AACc;AACjC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACG;AAIL;AACA;AACA;AACA;AACA;AACA;AACE;AAEuD;AAEzD;AACE;AAEqE;AAEvE;AACE;AAEsD;AAExD;AACE;AAIA;AAOA;AACE;AAAA;AAGF;AAKA;AACE;AAAA;AAGF;AACA;AAGA;AACE;AAEA;AAGA;AAAO;AACO;AACD;AACmC;AAChD;AAGF;AAGA;AAMA;AAQA;AAEA;AAIA;AAKE;AAAA;AAGF;AAAO;AAC0B;AACF;AAG+B;AAC9D;AACC;AACD;AACA;AACA;AACO;AAET;AAAM;AACI;AACR;AACA;AACA;AAEF;AAEE;AACE;AAAA;AAIF;AACE;AAAO;AAIT;AAAoC;AAGZ;AAGxB;AAEI;AACH;AACD;AACA;AACA;AACO;AACP;AAEF;AACA;AAEA;AACE;AAEE;AAAA;AACwD;AACxD;AACF;AAGF;AAAmB;AACV;AACP;AACA;AAGF;AACE;AAAsB;AAGxB;AAA6B;AAEzB;AAEA;AACE;AAA8B;AAE9B;AAAgC;AAClC;AACF;AACA;AACE;AACA;AACA;AACO;AACT;AAGF;AAA4B;AAExB;AAEA;AAAyC;AAClB;AAGvB;AACE;AAAuB;AACzB;AACF;AACwC;AAG1C;AACE;AACE;AAAY;AAEZ;AAAU;AACZ;AAGF;AAEA;AAEI;AAAC;AAAA;AACY;AACT;AAC2B;AAC3B;AACF;AACsC;AACQ;AACvC;AACH;AACC;AAEL;AAEI;AACA;AACA;AAEA;AAIA;AAEA;AAGA;AACE;AACE;AAAC;AAAA;AAEW;AAEV;AAAC;AAAA;AACS;AACE;AAC4B;AAEW;AAAA;AACnD;AAAA;AATkB;AAUpB;AAIJ;AACE;AAAO;AAGT;AACE;AAAA;AAGF;AACE;AAAC;AAAA;AAEW;AACK;AACD;AACd;AACU;AACmB;AAC7B;AACe;AACF;AACb;AACA;AACA;AACA;AAGA;AACiB;AACjB;AACA;AACA;AACA;AAEyC;AAIrC;AAAC;AAAA;AAIS;AAGR;AAAiB;AAAhB;AACiB;AACC;AACV;AAEP;AAAC;AAAA;AACW;AACD;AAID;AAMa;AAGV;AAAA;AACb;AAAA;AACF;AAAA;AAEA;AAIF;AAAgB;AAAf;AACW;AAKI;AAMR;AAAA;AAEN;AAGJ;AAAA;AA/EW;AAoFjB;AAII;AAAA;AAAC;AAAA;AACW;AACI;AAGZ;AAA8D;AAC3D;AACL;AAAA;AACF;AACC;AAGH;AAGN;AAE0C;AAGxC;AAAC;AAAA;AACW;AACO;AACuC;AACxD;AACwB;AACxB;AACc;AACH;AACe;AACP;AACd;AACL;AACe;AACf;AAAA;AACF;AAAA;AAAA;AAGN;AAGN;;"}
1
+ {"version":3,"file":"Thread.cjs","sources":["../../src/components/Thread.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n type BaseMetadata,\n type CommentData,\n type DCM,\n type DTM,\n findLastIndex,\n type ThreadData,\n} from \"@liveblocks/core\";\nimport {\n useHasPermissionAccess,\n useMarkRoomThreadAsRead,\n useMarkRoomThreadAsResolved,\n useMarkRoomThreadAsUnresolved,\n useRoomThreadSubscription,\n} from \"@liveblocks/react/_private\";\nimport { Toggle as TogglePrimitive } from \"radix-ui\";\nimport {\n type ComponentPropsWithoutRef,\n type ComponentType,\n type ForwardedRef,\n forwardRef,\n Fragment,\n type PropsWithChildren,\n type ReactNode,\n type RefAttributes,\n type SyntheticEvent,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nimport type { GlobalComponents } from \"../components\";\nimport { ArrowDownIcon } from \"../icons/ArrowDown\";\nimport { BellIcon } from \"../icons/Bell\";\nimport { BellCrossedIcon } from \"../icons/BellCrossed\";\nimport { CheckCircleIcon } from \"../icons/CheckCircle\";\nimport { CheckCircleFillIcon } from \"../icons/CheckCircleFill\";\nimport type {\n CommentOverrides,\n ComposerOverrides,\n GlobalOverrides,\n ThreadOverrides,\n} from \"../overrides\";\nimport { useOverrides } from \"../overrides\";\nimport { commentsResourceForVisibility } from \"../shared\";\nimport { cn } from \"../utils/cn\";\nimport { useStableComponent } from \"../utils/use-stable-component\";\nimport { useIntersectionCallback } from \"../utils/use-visible\";\nimport { useWindowFocus } from \"../utils/use-window-focus\";\nimport { Comment as DefaultComment, type CommentProps } from \"./Comment\";\nimport { Composer, type ComposerProps } from \"./Composer\";\nimport { Button } from \"./internal/Button\";\nimport { Tooltip, TooltipProvider } from \"./internal/Tooltip\";\n\n// An invisible marker used to mark a thread as read only when it becomes visible.\nfunction MarkThreadAsReadMarker({ thread }: { thread: ThreadData }) {\n const { id: threadId, roomId } = thread;\n const ref = useRef<HTMLDivElement>(null);\n const markThreadAsRead = useMarkRoomThreadAsRead(roomId);\n const isWindowFocused = useWindowFocus();\n\n useIntersectionCallback(\n ref,\n (isIntersecting) => {\n if (isIntersecting) {\n markThreadAsRead(threadId);\n }\n },\n {\n // The underlying IntersectionObserver is only enabled when the window is focused\n enabled: isWindowFocused,\n }\n );\n\n return (\n <div\n ref={ref}\n style={{ height: 0 }}\n aria-hidden\n data-mark-as-read-marker=\"\"\n />\n );\n}\n\nexport interface ThreadComponents<\n // Future components might reference thread metadata so we declare it\n // already to respect the `TM` → `CM` order used everywhere else.\n _TM extends BaseMetadata = DTM,\n CM extends BaseMetadata = DCM,\n> {\n /**\n * The component used to display comments.\n */\n Comment: ComponentType<CommentProps<CM>>;\n}\n\nexport interface ThreadProps<\n TM extends BaseMetadata = DTM,\n CM extends BaseMetadata = DCM,\n> extends ComponentPropsWithoutRef<\"div\"> {\n /**\n * The thread to display.\n */\n thread: ThreadData<TM, CM>;\n\n /**\n * How to show or hide the composer to reply to the thread.\n */\n showComposer?: boolean | \"collapsed\";\n\n /**\n * Whether to show the action to resolve the thread.\n */\n showResolveAction?: boolean;\n\n /**\n * How to show or hide the actions.\n */\n showActions?: CommentProps[\"showActions\"];\n\n /**\n * Whether to show reactions.\n */\n showReactions?: CommentProps[\"showReactions\"];\n\n /**\n * Whether to show the composer's formatting controls.\n */\n showComposerFormattingControls?: ComposerProps[\"showFormattingControls\"];\n\n /**\n * Add (or change) items to display in a comment's dropdown.\n */\n commentDropdownItems?:\n | ReactNode\n | ((props: PropsWithChildren<{ comment: CommentData<CM> }>) => ReactNode);\n\n /**\n * The maximum number of comments to show.\n *\n * The first and last comments are always shown and by default if some comments\n * are hidden, only the first comment will be shown before the \"show more\" button\n * and after it will be shown all the newest comments to fit the limit set.\n *\n * It's possible to customize this by setting `maxVisibleComments` to an object:\n *\n * @example\n * // Only show the last comment, and all the older ones to fit the limit.\n * <Thread maxVisibleComments={{ max: 5, show: \"oldest\" }} />\n *\n * @example\n * // Show as many old comments as new ones to fit the limit.\n * <Thread maxVisibleComments={{ max: 5, show: \"both\" }} />\n */\n maxVisibleComments?:\n | number\n | { max: number; show: \"oldest\" | \"both\" | \"newest\" };\n\n /**\n * Whether to blur the composer editor when the composer is submitted.\n */\n blurComposerOnSubmit?: ComposerProps[\"blurOnSubmit\"];\n\n /**\n * Whether to indent the comments' content.\n */\n indentCommentContent?: CommentProps[\"indentContent\"];\n\n /**\n * Whether to show deleted comments.\n */\n showDeletedComments?: CommentProps[\"showDeleted\"];\n\n /**\n * Whether to show the thread's subscription status.\n */\n showSubscription?: boolean;\n\n /**\n * Whether to show attachments.\n */\n showAttachments?: boolean;\n\n /**\n * The event handler called when changing the resolved status.\n */\n onResolvedChange?: (resolved: boolean) => void;\n\n /**\n * The event handler called when a comment is edited.\n */\n onCommentEdit?: CommentProps[\"onCommentEdit\"];\n\n /**\n * The event handler called when a comment is deleted.\n */\n onCommentDelete?: CommentProps[\"onCommentDelete\"];\n\n /**\n * The event handler called when the thread is deleted.\n * A thread is deleted when all its comments are deleted.\n */\n onThreadDelete?: (thread: ThreadData<TM, CM>) => void;\n\n /**\n * The event handler called when clicking on a comment's author.\n */\n onAuthorClick?: CommentProps[\"onAuthorClick\"];\n\n /**\n * The event handler called when clicking on a mention.\n */\n onMentionClick?: CommentProps[\"onMentionClick\"];\n\n /**\n * The event handler called when clicking on a comment's attachment.\n */\n onAttachmentClick?: CommentProps[\"onAttachmentClick\"];\n\n /**\n * The event handler called when the composer is submitted.\n */\n onComposerSubmit?: ComposerProps[\"onComposerSubmit\"];\n\n /**\n * Whether to focus the composer on mount.\n */\n autoFocus?: ComposerProps[\"autoFocus\"];\n\n /**\n * Override the component's strings.\n */\n overrides?: Partial<\n GlobalOverrides & ThreadOverrides & CommentOverrides & ComposerOverrides\n >;\n\n /**\n * Override the component's components.\n */\n components?: Partial<GlobalComponents & ThreadComponents<TM, CM>>;\n}\n\n/**\n * Displays a thread of comments, with a composer to reply\n * to it.\n *\n * @example\n * <>\n * {threads.map((thread) => (\n * <Thread key={thread.id} thread={thread} />\n * ))}\n * </>\n */\nexport const Thread = forwardRef(\n <TM extends BaseMetadata = DTM, CM extends BaseMetadata = DCM>(\n {\n thread,\n indentCommentContent = true,\n showActions = \"hover\",\n showDeletedComments,\n showResolveAction = true,\n showReactions = true,\n showComposer = \"collapsed\",\n showAttachments = true,\n showSubscription = true,\n showComposerFormattingControls = true,\n maxVisibleComments,\n commentDropdownItems,\n onResolvedChange,\n onCommentEdit,\n onCommentDelete,\n onThreadDelete,\n onAuthorClick,\n onMentionClick,\n onAttachmentClick,\n onComposerSubmit,\n blurComposerOnSubmit,\n autoFocus,\n overrides,\n components,\n className,\n ...props\n }: ThreadProps<TM, CM>,\n forwardedRef: ForwardedRef<HTMLDivElement>\n ) => {\n const Comment = useStableComponent(components?.Comment, DefaultComment);\n const markThreadAsResolved = useMarkRoomThreadAsResolved(thread.roomId);\n const markThreadAsUnresolved = useMarkRoomThreadAsUnresolved(thread.roomId);\n const $ = useOverrides(overrides);\n const [showAllComments, setShowAllComments] = useState(false);\n const firstCommentIndex = useMemo(() => {\n return showDeletedComments\n ? 0\n : thread.comments.findIndex((comment) => comment.body);\n }, [showDeletedComments, thread.comments]);\n const lastCommentIndex = useMemo(() => {\n return showDeletedComments\n ? thread.comments.length - 1\n : findLastIndex(thread.comments, (comment) => Boolean(comment.body));\n }, [showDeletedComments, thread.comments]);\n const commentsCount = useMemo(() => {\n return showDeletedComments\n ? thread.comments.length\n : thread.comments.filter((comment) => comment.body).length;\n }, [showDeletedComments, thread.comments]);\n const hiddenComments = useMemo(() => {\n const maxVisibleCommentsCount =\n typeof maxVisibleComments === \"number\"\n ? maxVisibleComments\n : maxVisibleComments?.max;\n const visibleCommentsShow =\n (typeof maxVisibleComments === \"object\"\n ? maxVisibleComments?.show\n : undefined) ?? \"newest\";\n\n // If we explicitly want to show all comments or there's no limit set,\n // no need to hide any comments.\n if (showAllComments || maxVisibleCommentsCount === undefined) {\n return;\n }\n\n const comments = thread.comments\n .map((comment, index) => ({ comment, index }))\n .filter(({ comment }) => showDeletedComments || comment.body);\n\n // There aren't enough comments so no need to hide any.\n if (comments.length <= Math.max(maxVisibleCommentsCount, 2)) {\n return;\n }\n\n const firstVisibleComment = comments[0]!;\n const lastVisibleComment = comments[comments.length - 1]!;\n\n // Always show the first and last comments even if the limit is set to lower than 2.\n if (maxVisibleCommentsCount <= 2) {\n const firstHiddenCommentIndex =\n comments[1]?.index ?? firstVisibleComment.index;\n const lastHiddenCommentIndex =\n comments[comments.length - 2]?.index ?? lastVisibleComment.index;\n\n return {\n firstIndex: firstHiddenCommentIndex,\n lastIndex: lastHiddenCommentIndex,\n count: comments.slice(1, comments.length - 1).length,\n };\n }\n\n const remainingVisibleCommentsCount = maxVisibleCommentsCount - 2;\n\n // Split the remaining visible comments before, after, or equally.\n const beforeVisibleCommentsCount =\n visibleCommentsShow === \"oldest\"\n ? remainingVisibleCommentsCount\n : visibleCommentsShow === \"newest\"\n ? 0\n : Math.floor(remainingVisibleCommentsCount / 2);\n const afterVisibleCommentsCount =\n visibleCommentsShow === \"oldest\"\n ? 0\n : visibleCommentsShow === \"newest\"\n ? remainingVisibleCommentsCount\n : Math.ceil(remainingVisibleCommentsCount / 2);\n\n // The first comment is always visible so `+ 1` to skip it.\n const firstHiddenComment = comments[1 + beforeVisibleCommentsCount];\n // The last comment is always visible so `- 2` to skip it.\n const lastHiddenComment =\n comments[comments.length - 2 - afterVisibleCommentsCount];\n\n // There aren't any comments to hide besides the first and last ones.\n if (\n !firstHiddenComment ||\n !lastHiddenComment ||\n firstHiddenComment.index > lastHiddenComment.index\n ) {\n return;\n }\n\n return {\n firstIndex: firstHiddenComment.index,\n lastIndex: lastHiddenComment.index,\n count: thread.comments\n .slice(firstHiddenComment.index, lastHiddenComment.index + 1)\n .filter((comment) => showDeletedComments || comment.body).length,\n };\n }, [\n maxVisibleComments,\n showAllComments,\n showDeletedComments,\n thread.comments,\n ]);\n const {\n status: subscriptionStatus,\n unreadSince,\n subscribe,\n unsubscribe,\n } = useRoomThreadSubscription(thread.roomId, thread.id);\n const unreadIndex = useMemo(() => {\n // The user is not subscribed to this thread.\n if (subscriptionStatus !== \"subscribed\") {\n return;\n }\n\n // The user hasn't read the thread yet, so all comments are unread.\n if (unreadSince === null) {\n return firstCommentIndex;\n }\n\n // The user has read the thread, so we find the first unread comment.\n const unreadIndex = thread.comments.findIndex(\n (comment) =>\n (showDeletedComments ? true : comment.body) &&\n comment.createdAt > unreadSince\n );\n\n return unreadIndex >= 0 && unreadIndex < thread.comments.length\n ? unreadIndex\n : undefined;\n }, [\n firstCommentIndex,\n showDeletedComments,\n subscriptionStatus,\n thread.comments,\n unreadSince,\n ]);\n const [newIndex, setNewIndex] = useState<number>();\n const newIndicatorIndex = newIndex === undefined ? unreadIndex : newIndex;\n\n useEffect(() => {\n if (unreadIndex) {\n // Keep the \"new\" indicator at the lowest unread index.\n setNewIndex((persistedUnreadIndex) =>\n Math.min(persistedUnreadIndex ?? Infinity, unreadIndex)\n );\n }\n }, [unreadIndex]);\n\n const canComment = useHasPermissionAccess(\n thread.roomId,\n commentsResourceForVisibility(thread.visibility),\n \"write\"\n );\n\n const stopPropagation = useCallback((event: SyntheticEvent) => {\n event.stopPropagation();\n }, []);\n\n const handleResolvedChange = useCallback(\n (resolved: boolean) => {\n onResolvedChange?.(resolved);\n\n if (resolved) {\n markThreadAsResolved(thread.id);\n } else {\n markThreadAsUnresolved(thread.id);\n }\n },\n [\n markThreadAsResolved,\n markThreadAsUnresolved,\n onResolvedChange,\n thread.id,\n ]\n );\n\n const handleCommentDelete = useCallback(\n (comment: CommentData) => {\n onCommentDelete?.(comment);\n\n const filteredComments = thread.comments.filter(\n (comment) => comment.body\n );\n\n if (filteredComments.length <= 1) {\n onThreadDelete?.(thread);\n }\n },\n [onCommentDelete, onThreadDelete, thread]\n );\n\n const handleSubscribeChange = useCallback(() => {\n if (subscriptionStatus === \"subscribed\") {\n unsubscribe();\n } else {\n subscribe();\n }\n }, [subscriptionStatus, subscribe, unsubscribe]);\n\n let currentCommentIndex = 0;\n\n return (\n <TooltipProvider>\n <div\n className={cn(\n \"lb-root lb-thread\",\n showActions === \"hover\" && \"lb-thread:show-actions-hover\",\n className\n )}\n data-resolved={thread.resolved ? \"\" : undefined}\n data-unread={unreadIndex !== undefined ? \"\" : undefined}\n dir={$.dir}\n {...props}\n ref={forwardedRef}\n >\n <div className=\"lb-thread-comments\" role=\"feed\">\n {thread.comments.map((comment, index) => {\n const isNonDeletedComment = showDeletedComments || comment.body;\n const isFirstComment = index === firstCommentIndex;\n const isUnread =\n unreadIndex !== undefined && index >= unreadIndex;\n const isHidden =\n hiddenComments &&\n index >= hiddenComments.firstIndex &&\n index <= hiddenComments.lastIndex;\n const isHiddenBecauseDeleted =\n !showDeletedComments && !comment.body;\n const isFirstHiddenComment =\n isHidden && index === hiddenComments.firstIndex;\n\n if (isFirstHiddenComment) {\n return (\n <div\n key={`${comment.id}:show-more`}\n className=\"lb-thread-show-more\"\n >\n <Button\n variant=\"ghost\"\n className=\"lb-thread-show-more-button\"\n onClick={() => setShowAllComments(true)}\n >\n {$.THREAD_SHOW_MORE_COMMENTS(hiddenComments.count)}\n </Button>\n </div>\n );\n }\n\n if (isHidden || isHiddenBecauseDeleted) {\n return null;\n }\n\n if (isNonDeletedComment) {\n currentCommentIndex++;\n }\n\n const children = (\n <Comment\n key={comment.id}\n tabIndex={0}\n aria-posinset={currentCommentIndex}\n aria-setsize={commentsCount}\n overrides={overrides}\n className=\"lb-thread-comment\"\n data-unread={isUnread ? \"\" : undefined}\n comment={comment}\n visibility={thread.visibility}\n indentContent={indentCommentContent}\n showDeleted={showDeletedComments}\n showActions={showActions}\n showReactions={showReactions}\n showAttachments={showAttachments}\n showComposerFormattingControls={\n showComposerFormattingControls\n }\n onCommentEdit={onCommentEdit}\n onCommentDelete={handleCommentDelete}\n onAuthorClick={onAuthorClick}\n onMentionClick={onMentionClick}\n onAttachmentClick={onAttachmentClick}\n components={components}\n actionsClassName={\n isFirstComment ? \"lb-thread-actions\" : undefined\n }\n actions={\n isFirstComment && showResolveAction ? (\n <Tooltip\n content={\n thread.resolved\n ? $.THREAD_UNRESOLVE\n : $.THREAD_RESOLVE\n }\n >\n <TogglePrimitive.Root\n pressed={thread.resolved}\n onPressedChange={handleResolvedChange}\n asChild\n >\n <Button\n className=\"lb-comment-action\"\n onClick={stopPropagation}\n aria-label={\n thread.resolved\n ? $.THREAD_UNRESOLVE\n : $.THREAD_RESOLVE\n }\n icon={\n thread.resolved ? (\n <CheckCircleFillIcon />\n ) : (\n <CheckCircleIcon />\n )\n }\n disabled={!canComment}\n />\n </TogglePrimitive.Root>\n </Tooltip>\n ) : null\n }\n internalDropdownItems={\n isFirstComment && showSubscription ? (\n <DefaultComment.DropdownItem\n onSelect={handleSubscribeChange}\n icon={\n subscriptionStatus === \"subscribed\" ? (\n <BellCrossedIcon />\n ) : (\n <BellIcon />\n )\n }\n >\n {subscriptionStatus === \"subscribed\"\n ? $.THREAD_UNSUBSCRIBE\n : $.THREAD_SUBSCRIBE}\n </DefaultComment.DropdownItem>\n ) : undefined\n }\n dropdownItems={\n commentDropdownItems as CommentProps[\"dropdownItems\"]\n }\n />\n );\n\n return index === newIndicatorIndex &&\n newIndicatorIndex !== firstCommentIndex &&\n newIndicatorIndex <= lastCommentIndex ? (\n <Fragment key={comment.id}>\n <div\n className=\"lb-thread-new-indicator\"\n aria-label={$.THREAD_NEW_INDICATOR_DESCRIPTION}\n >\n <span className=\"lb-thread-new-indicator-label\">\n <ArrowDownIcon className=\"lb-thread-new-indicator-label-icon\" />\n {$.THREAD_NEW_INDICATOR}\n </span>\n </div>\n {children}\n </Fragment>\n ) : (\n children\n );\n })}\n </div>\n {unreadIndex !== undefined && (\n <MarkThreadAsReadMarker thread={thread} />\n )}\n {showComposer && (\n <Composer\n className=\"lb-thread-composer\"\n threadId={thread.id}\n visibility={thread.visibility}\n defaultCollapsed={showComposer === \"collapsed\" ? true : undefined}\n showAttachments={showAttachments}\n showFormattingControls={showComposerFormattingControls}\n onComposerSubmit={onComposerSubmit}\n blurOnSubmit={blurComposerOnSubmit}\n overrides={{\n COMPOSER_PLACEHOLDER: $.THREAD_COMPOSER_PLACEHOLDER,\n COMPOSER_SEND: $.THREAD_COMPOSER_SEND,\n ...overrides,\n }}\n roomId={thread.roomId}\n autoFocus={autoFocus}\n />\n )}\n </div>\n </TooltipProvider>\n );\n }\n) as <TM extends BaseMetadata = DTM, CM extends BaseMetadata = DCM>(\n props: ThreadProps<TM, CM> & RefAttributes<HTMLDivElement>\n) => JSX.Element;\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AA2DA;AACE;AACA;AACA;AACA;AAEA;AAAA;AACE;AAEE;AACE;AAAyB;AAC3B;AACF;AACA;AAAA;AAEW;AACX;AAGF;AACE;AAAC;AAAA;AACC;AACmB;AACR;AACc;AAAA;AAG/B;AA2KO;AAAe;AAElB;AACE;AACuB;AACT;AACd;AACoB;AACJ;AACD;AACG;AACC;AACc;AACjC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACG;AAIL;AACA;AACA;AACA;AACA;AACA;AACE;AAEuD;AAEzD;AACE;AAEqE;AAEvE;AACE;AAEsD;AAExD;AACE;AAIA;AAOA;AACE;AAAA;AAGF;AAKA;AACE;AAAA;AAGF;AACA;AAGA;AACE;AAEA;AAGA;AAAO;AACO;AACD;AACmC;AAChD;AAGF;AAGA;AAMA;AAQA;AAEA;AAIA;AAKE;AAAA;AAGF;AAAO;AAC0B;AACF;AAG+B;AAC9D;AACC;AACD;AACA;AACA;AACO;AAET;AAAM;AACI;AACR;AACA;AACA;AAEF;AAEE;AACE;AAAA;AAIF;AACE;AAAO;AAIT;AAAoC;AAGZ;AAGxB;AAEI;AACH;AACD;AACA;AACA;AACO;AACP;AAEF;AACA;AAEA;AACE;AAEE;AAAA;AACwD;AACxD;AACF;AAGF;AAAmB;AACV;AACwC;AAC/C;AAGF;AACE;AAAsB;AAGxB;AAA6B;AAEzB;AAEA;AACE;AAA8B;AAE9B;AAAgC;AAClC;AACF;AACA;AACE;AACA;AACA;AACO;AACT;AAGF;AAA4B;AAExB;AAEA;AAAyC;AAClB;AAGvB;AACE;AAAuB;AACzB;AACF;AACwC;AAG1C;AACE;AACE;AAAY;AAEZ;AAAU;AACZ;AAGF;AAEA;AAEI;AAAC;AAAA;AACY;AACT;AAC2B;AAC3B;AACF;AACsC;AACQ;AACvC;AACH;AACC;AAEL;AAEI;AACA;AACA;AAEA;AAIA;AAEA;AAGA;AACE;AACE;AAAC;AAAA;AAEW;AAEV;AAAC;AAAA;AACS;AACE;AAC4B;AAEW;AAAA;AACnD;AAAA;AATkB;AAUpB;AAIJ;AACE;AAAO;AAGT;AACE;AAAA;AAGF;AACE;AAAC;AAAA;AAEW;AACK;AACD;AACd;AACU;AACmB;AAC7B;AACmB;AACJ;AACF;AACb;AACA;AACA;AACA;AAGA;AACiB;AACjB;AACA;AACA;AACA;AAEyC;AAIrC;AAAC;AAAA;AAIS;AAGR;AAAiB;AAAhB;AACiB;AACC;AACV;AAEP;AAAC;AAAA;AACW;AACD;AAID;AAMa;AAGV;AAAA;AACb;AAAA;AACF;AAAA;AAEA;AAIF;AAAgB;AAAf;AACW;AAKI;AAMR;AAAA;AAEN;AAGJ;AAAA;AAhFW;AAqFjB;AAII;AAAA;AAAC;AAAA;AACW;AACI;AAGZ;AAA8D;AAC3D;AACL;AAAA;AACF;AACC;AAGH;AAGN;AAE0C;AAGxC;AAAC;AAAA;AACW;AACO;AACE;AACqC;AACxD;AACwB;AACxB;AACc;AACH;AACe;AACP;AACd;AACL;AACe;AACf;AAAA;AACF;AAAA;AAAA;AAGN;AAGN;;"}
@@ -10,6 +10,7 @@ import { BellCrossedIcon } from '../icons/BellCrossed.js';
10
10
  import { CheckCircleIcon } from '../icons/CheckCircle.js';
11
11
  import { CheckCircleFillIcon } from '../icons/CheckCircleFill.js';
12
12
  import { useOverrides } from '../overrides.js';
13
+ import { commentsResourceForVisibility } from '../shared.js';
13
14
  import { cn } from '../utils/cn.js';
14
15
  import { useStableComponent } from '../utils/use-stable-component.js';
15
16
  import { useIntersectionCallback } from '../utils/use-visible.js';
@@ -165,7 +166,7 @@ const Thread = forwardRef(
165
166
  }, [unreadIndex]);
166
167
  const canComment = useHasPermissionAccess(
167
168
  thread.roomId,
168
- "comments",
169
+ commentsResourceForVisibility(thread.visibility),
169
170
  "write"
170
171
  );
171
172
  const stopPropagation = useCallback((event) => {
@@ -262,6 +263,7 @@ const Thread = forwardRef(
262
263
  className: "lb-thread-comment",
263
264
  "data-unread": isUnread ? "" : void 0,
264
265
  comment,
266
+ visibility: thread.visibility,
265
267
  indentContent: indentCommentContent,
266
268
  showDeleted: showDeletedComments,
267
269
  showActions,
@@ -332,6 +334,7 @@ const Thread = forwardRef(
332
334
  {
333
335
  className: "lb-thread-composer",
334
336
  threadId: thread.id,
337
+ visibility: thread.visibility,
335
338
  defaultCollapsed: showComposer === "collapsed" ? true : void 0,
336
339
  showAttachments,
337
340
  showFormattingControls: showComposerFormattingControls,
@@ -1 +1 @@
1
- {"version":3,"file":"Thread.js","sources":["../../src/components/Thread.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n type BaseMetadata,\n type CommentData,\n type DCM,\n type DTM,\n findLastIndex,\n type ThreadData,\n} from \"@liveblocks/core\";\nimport {\n useHasPermissionAccess,\n useMarkRoomThreadAsRead,\n useMarkRoomThreadAsResolved,\n useMarkRoomThreadAsUnresolved,\n useRoomThreadSubscription,\n} from \"@liveblocks/react/_private\";\nimport { Toggle as TogglePrimitive } from \"radix-ui\";\nimport {\n type ComponentPropsWithoutRef,\n type ComponentType,\n type ForwardedRef,\n forwardRef,\n Fragment,\n type PropsWithChildren,\n type ReactNode,\n type RefAttributes,\n type SyntheticEvent,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nimport type { GlobalComponents } from \"../components\";\nimport { ArrowDownIcon } from \"../icons/ArrowDown\";\nimport { BellIcon } from \"../icons/Bell\";\nimport { BellCrossedIcon } from \"../icons/BellCrossed\";\nimport { CheckCircleIcon } from \"../icons/CheckCircle\";\nimport { CheckCircleFillIcon } from \"../icons/CheckCircleFill\";\nimport type {\n CommentOverrides,\n ComposerOverrides,\n GlobalOverrides,\n ThreadOverrides,\n} from \"../overrides\";\nimport { useOverrides } from \"../overrides\";\nimport { cn } from \"../utils/cn\";\nimport { useStableComponent } from \"../utils/use-stable-component\";\nimport { useIntersectionCallback } from \"../utils/use-visible\";\nimport { useWindowFocus } from \"../utils/use-window-focus\";\nimport { Comment as DefaultComment, type CommentProps } from \"./Comment\";\nimport { Composer, type ComposerProps } from \"./Composer\";\nimport { Button } from \"./internal/Button\";\nimport { Tooltip, TooltipProvider } from \"./internal/Tooltip\";\n\n// An invisible marker used to mark a thread as read only when it becomes visible.\nfunction MarkThreadAsReadMarker({ thread }: { thread: ThreadData }) {\n const { id: threadId, roomId } = thread;\n const ref = useRef<HTMLDivElement>(null);\n const markThreadAsRead = useMarkRoomThreadAsRead(roomId);\n const isWindowFocused = useWindowFocus();\n\n useIntersectionCallback(\n ref,\n (isIntersecting) => {\n if (isIntersecting) {\n markThreadAsRead(threadId);\n }\n },\n {\n // The underlying IntersectionObserver is only enabled when the window is focused\n enabled: isWindowFocused,\n }\n );\n\n return (\n <div\n ref={ref}\n style={{ height: 0 }}\n aria-hidden\n data-mark-as-read-marker=\"\"\n />\n );\n}\n\nexport interface ThreadComponents<\n // Future components might reference thread metadata so we declare it\n // already to respect the `TM` → `CM` order used everywhere else.\n _TM extends BaseMetadata = DTM,\n CM extends BaseMetadata = DCM,\n> {\n /**\n * The component used to display comments.\n */\n Comment: ComponentType<CommentProps<CM>>;\n}\n\nexport interface ThreadProps<\n TM extends BaseMetadata = DTM,\n CM extends BaseMetadata = DCM,\n> extends ComponentPropsWithoutRef<\"div\"> {\n /**\n * The thread to display.\n */\n thread: ThreadData<TM, CM>;\n\n /**\n * How to show or hide the composer to reply to the thread.\n */\n showComposer?: boolean | \"collapsed\";\n\n /**\n * Whether to show the action to resolve the thread.\n */\n showResolveAction?: boolean;\n\n /**\n * How to show or hide the actions.\n */\n showActions?: CommentProps[\"showActions\"];\n\n /**\n * Whether to show reactions.\n */\n showReactions?: CommentProps[\"showReactions\"];\n\n /**\n * Whether to show the composer's formatting controls.\n */\n showComposerFormattingControls?: ComposerProps[\"showFormattingControls\"];\n\n /**\n * Add (or change) items to display in a comment's dropdown.\n */\n commentDropdownItems?:\n | ReactNode\n | ((props: PropsWithChildren<{ comment: CommentData<CM> }>) => ReactNode);\n\n /**\n * The maximum number of comments to show.\n *\n * The first and last comments are always shown and by default if some comments\n * are hidden, only the first comment will be shown before the \"show more\" button\n * and after it will be shown all the newest comments to fit the limit set.\n *\n * It's possible to customize this by setting `maxVisibleComments` to an object:\n *\n * @example\n * // Only show the last comment, and all the older ones to fit the limit.\n * <Thread maxVisibleComments={{ max: 5, show: \"oldest\" }} />\n *\n * @example\n * // Show as many old comments as new ones to fit the limit.\n * <Thread maxVisibleComments={{ max: 5, show: \"both\" }} />\n */\n maxVisibleComments?:\n | number\n | { max: number; show: \"oldest\" | \"both\" | \"newest\" };\n\n /**\n * Whether to blur the composer editor when the composer is submitted.\n */\n blurComposerOnSubmit?: ComposerProps[\"blurOnSubmit\"];\n\n /**\n * Whether to indent the comments' content.\n */\n indentCommentContent?: CommentProps[\"indentContent\"];\n\n /**\n * Whether to show deleted comments.\n */\n showDeletedComments?: CommentProps[\"showDeleted\"];\n\n /**\n * Whether to show the thread's subscription status.\n */\n showSubscription?: boolean;\n\n /**\n * Whether to show attachments.\n */\n showAttachments?: boolean;\n\n /**\n * The event handler called when changing the resolved status.\n */\n onResolvedChange?: (resolved: boolean) => void;\n\n /**\n * The event handler called when a comment is edited.\n */\n onCommentEdit?: CommentProps[\"onCommentEdit\"];\n\n /**\n * The event handler called when a comment is deleted.\n */\n onCommentDelete?: CommentProps[\"onCommentDelete\"];\n\n /**\n * The event handler called when the thread is deleted.\n * A thread is deleted when all its comments are deleted.\n */\n onThreadDelete?: (thread: ThreadData<TM, CM>) => void;\n\n /**\n * The event handler called when clicking on a comment's author.\n */\n onAuthorClick?: CommentProps[\"onAuthorClick\"];\n\n /**\n * The event handler called when clicking on a mention.\n */\n onMentionClick?: CommentProps[\"onMentionClick\"];\n\n /**\n * The event handler called when clicking on a comment's attachment.\n */\n onAttachmentClick?: CommentProps[\"onAttachmentClick\"];\n\n /**\n * The event handler called when the composer is submitted.\n */\n onComposerSubmit?: ComposerProps[\"onComposerSubmit\"];\n\n /**\n * Whether to focus the composer on mount.\n */\n autoFocus?: ComposerProps[\"autoFocus\"];\n\n /**\n * Override the component's strings.\n */\n overrides?: Partial<\n GlobalOverrides & ThreadOverrides & CommentOverrides & ComposerOverrides\n >;\n\n /**\n * Override the component's components.\n */\n components?: Partial<GlobalComponents & ThreadComponents<TM, CM>>;\n}\n\n/**\n * Displays a thread of comments, with a composer to reply\n * to it.\n *\n * @example\n * <>\n * {threads.map((thread) => (\n * <Thread key={thread.id} thread={thread} />\n * ))}\n * </>\n */\nexport const Thread = forwardRef(\n <TM extends BaseMetadata = DTM, CM extends BaseMetadata = DCM>(\n {\n thread,\n indentCommentContent = true,\n showActions = \"hover\",\n showDeletedComments,\n showResolveAction = true,\n showReactions = true,\n showComposer = \"collapsed\",\n showAttachments = true,\n showSubscription = true,\n showComposerFormattingControls = true,\n maxVisibleComments,\n commentDropdownItems,\n onResolvedChange,\n onCommentEdit,\n onCommentDelete,\n onThreadDelete,\n onAuthorClick,\n onMentionClick,\n onAttachmentClick,\n onComposerSubmit,\n blurComposerOnSubmit,\n autoFocus,\n overrides,\n components,\n className,\n ...props\n }: ThreadProps<TM, CM>,\n forwardedRef: ForwardedRef<HTMLDivElement>\n ) => {\n const Comment = useStableComponent(components?.Comment, DefaultComment);\n const markThreadAsResolved = useMarkRoomThreadAsResolved(thread.roomId);\n const markThreadAsUnresolved = useMarkRoomThreadAsUnresolved(thread.roomId);\n const $ = useOverrides(overrides);\n const [showAllComments, setShowAllComments] = useState(false);\n const firstCommentIndex = useMemo(() => {\n return showDeletedComments\n ? 0\n : thread.comments.findIndex((comment) => comment.body);\n }, [showDeletedComments, thread.comments]);\n const lastCommentIndex = useMemo(() => {\n return showDeletedComments\n ? thread.comments.length - 1\n : findLastIndex(thread.comments, (comment) => Boolean(comment.body));\n }, [showDeletedComments, thread.comments]);\n const commentsCount = useMemo(() => {\n return showDeletedComments\n ? thread.comments.length\n : thread.comments.filter((comment) => comment.body).length;\n }, [showDeletedComments, thread.comments]);\n const hiddenComments = useMemo(() => {\n const maxVisibleCommentsCount =\n typeof maxVisibleComments === \"number\"\n ? maxVisibleComments\n : maxVisibleComments?.max;\n const visibleCommentsShow =\n (typeof maxVisibleComments === \"object\"\n ? maxVisibleComments?.show\n : undefined) ?? \"newest\";\n\n // If we explicitly want to show all comments or there's no limit set,\n // no need to hide any comments.\n if (showAllComments || maxVisibleCommentsCount === undefined) {\n return;\n }\n\n const comments = thread.comments\n .map((comment, index) => ({ comment, index }))\n .filter(({ comment }) => showDeletedComments || comment.body);\n\n // There aren't enough comments so no need to hide any.\n if (comments.length <= Math.max(maxVisibleCommentsCount, 2)) {\n return;\n }\n\n const firstVisibleComment = comments[0]!;\n const lastVisibleComment = comments[comments.length - 1]!;\n\n // Always show the first and last comments even if the limit is set to lower than 2.\n if (maxVisibleCommentsCount <= 2) {\n const firstHiddenCommentIndex =\n comments[1]?.index ?? firstVisibleComment.index;\n const lastHiddenCommentIndex =\n comments[comments.length - 2]?.index ?? lastVisibleComment.index;\n\n return {\n firstIndex: firstHiddenCommentIndex,\n lastIndex: lastHiddenCommentIndex,\n count: comments.slice(1, comments.length - 1).length,\n };\n }\n\n const remainingVisibleCommentsCount = maxVisibleCommentsCount - 2;\n\n // Split the remaining visible comments before, after, or equally.\n const beforeVisibleCommentsCount =\n visibleCommentsShow === \"oldest\"\n ? remainingVisibleCommentsCount\n : visibleCommentsShow === \"newest\"\n ? 0\n : Math.floor(remainingVisibleCommentsCount / 2);\n const afterVisibleCommentsCount =\n visibleCommentsShow === \"oldest\"\n ? 0\n : visibleCommentsShow === \"newest\"\n ? remainingVisibleCommentsCount\n : Math.ceil(remainingVisibleCommentsCount / 2);\n\n // The first comment is always visible so `+ 1` to skip it.\n const firstHiddenComment = comments[1 + beforeVisibleCommentsCount];\n // The last comment is always visible so `- 2` to skip it.\n const lastHiddenComment =\n comments[comments.length - 2 - afterVisibleCommentsCount];\n\n // There aren't any comments to hide besides the first and last ones.\n if (\n !firstHiddenComment ||\n !lastHiddenComment ||\n firstHiddenComment.index > lastHiddenComment.index\n ) {\n return;\n }\n\n return {\n firstIndex: firstHiddenComment.index,\n lastIndex: lastHiddenComment.index,\n count: thread.comments\n .slice(firstHiddenComment.index, lastHiddenComment.index + 1)\n .filter((comment) => showDeletedComments || comment.body).length,\n };\n }, [\n maxVisibleComments,\n showAllComments,\n showDeletedComments,\n thread.comments,\n ]);\n const {\n status: subscriptionStatus,\n unreadSince,\n subscribe,\n unsubscribe,\n } = useRoomThreadSubscription(thread.roomId, thread.id);\n const unreadIndex = useMemo(() => {\n // The user is not subscribed to this thread.\n if (subscriptionStatus !== \"subscribed\") {\n return;\n }\n\n // The user hasn't read the thread yet, so all comments are unread.\n if (unreadSince === null) {\n return firstCommentIndex;\n }\n\n // The user has read the thread, so we find the first unread comment.\n const unreadIndex = thread.comments.findIndex(\n (comment) =>\n (showDeletedComments ? true : comment.body) &&\n comment.createdAt > unreadSince\n );\n\n return unreadIndex >= 0 && unreadIndex < thread.comments.length\n ? unreadIndex\n : undefined;\n }, [\n firstCommentIndex,\n showDeletedComments,\n subscriptionStatus,\n thread.comments,\n unreadSince,\n ]);\n const [newIndex, setNewIndex] = useState<number>();\n const newIndicatorIndex = newIndex === undefined ? unreadIndex : newIndex;\n\n useEffect(() => {\n if (unreadIndex) {\n // Keep the \"new\" indicator at the lowest unread index.\n setNewIndex((persistedUnreadIndex) =>\n Math.min(persistedUnreadIndex ?? Infinity, unreadIndex)\n );\n }\n }, [unreadIndex]);\n\n const canComment = useHasPermissionAccess(\n thread.roomId,\n \"comments\",\n \"write\"\n );\n\n const stopPropagation = useCallback((event: SyntheticEvent) => {\n event.stopPropagation();\n }, []);\n\n const handleResolvedChange = useCallback(\n (resolved: boolean) => {\n onResolvedChange?.(resolved);\n\n if (resolved) {\n markThreadAsResolved(thread.id);\n } else {\n markThreadAsUnresolved(thread.id);\n }\n },\n [\n markThreadAsResolved,\n markThreadAsUnresolved,\n onResolvedChange,\n thread.id,\n ]\n );\n\n const handleCommentDelete = useCallback(\n (comment: CommentData) => {\n onCommentDelete?.(comment);\n\n const filteredComments = thread.comments.filter(\n (comment) => comment.body\n );\n\n if (filteredComments.length <= 1) {\n onThreadDelete?.(thread);\n }\n },\n [onCommentDelete, onThreadDelete, thread]\n );\n\n const handleSubscribeChange = useCallback(() => {\n if (subscriptionStatus === \"subscribed\") {\n unsubscribe();\n } else {\n subscribe();\n }\n }, [subscriptionStatus, subscribe, unsubscribe]);\n\n let currentCommentIndex = 0;\n\n return (\n <TooltipProvider>\n <div\n className={cn(\n \"lb-root lb-thread\",\n showActions === \"hover\" && \"lb-thread:show-actions-hover\",\n className\n )}\n data-resolved={thread.resolved ? \"\" : undefined}\n data-unread={unreadIndex !== undefined ? \"\" : undefined}\n dir={$.dir}\n {...props}\n ref={forwardedRef}\n >\n <div className=\"lb-thread-comments\" role=\"feed\">\n {thread.comments.map((comment, index) => {\n const isNonDeletedComment = showDeletedComments || comment.body;\n const isFirstComment = index === firstCommentIndex;\n const isUnread =\n unreadIndex !== undefined && index >= unreadIndex;\n const isHidden =\n hiddenComments &&\n index >= hiddenComments.firstIndex &&\n index <= hiddenComments.lastIndex;\n const isHiddenBecauseDeleted =\n !showDeletedComments && !comment.body;\n const isFirstHiddenComment =\n isHidden && index === hiddenComments.firstIndex;\n\n if (isFirstHiddenComment) {\n return (\n <div\n key={`${comment.id}:show-more`}\n className=\"lb-thread-show-more\"\n >\n <Button\n variant=\"ghost\"\n className=\"lb-thread-show-more-button\"\n onClick={() => setShowAllComments(true)}\n >\n {$.THREAD_SHOW_MORE_COMMENTS(hiddenComments.count)}\n </Button>\n </div>\n );\n }\n\n if (isHidden || isHiddenBecauseDeleted) {\n return null;\n }\n\n if (isNonDeletedComment) {\n currentCommentIndex++;\n }\n\n const children = (\n <Comment\n key={comment.id}\n tabIndex={0}\n aria-posinset={currentCommentIndex}\n aria-setsize={commentsCount}\n overrides={overrides}\n className=\"lb-thread-comment\"\n data-unread={isUnread ? \"\" : undefined}\n comment={comment}\n indentContent={indentCommentContent}\n showDeleted={showDeletedComments}\n showActions={showActions}\n showReactions={showReactions}\n showAttachments={showAttachments}\n showComposerFormattingControls={\n showComposerFormattingControls\n }\n onCommentEdit={onCommentEdit}\n onCommentDelete={handleCommentDelete}\n onAuthorClick={onAuthorClick}\n onMentionClick={onMentionClick}\n onAttachmentClick={onAttachmentClick}\n components={components}\n actionsClassName={\n isFirstComment ? \"lb-thread-actions\" : undefined\n }\n actions={\n isFirstComment && showResolveAction ? (\n <Tooltip\n content={\n thread.resolved\n ? $.THREAD_UNRESOLVE\n : $.THREAD_RESOLVE\n }\n >\n <TogglePrimitive.Root\n pressed={thread.resolved}\n onPressedChange={handleResolvedChange}\n asChild\n >\n <Button\n className=\"lb-comment-action\"\n onClick={stopPropagation}\n aria-label={\n thread.resolved\n ? $.THREAD_UNRESOLVE\n : $.THREAD_RESOLVE\n }\n icon={\n thread.resolved ? (\n <CheckCircleFillIcon />\n ) : (\n <CheckCircleIcon />\n )\n }\n disabled={!canComment}\n />\n </TogglePrimitive.Root>\n </Tooltip>\n ) : null\n }\n internalDropdownItems={\n isFirstComment && showSubscription ? (\n <DefaultComment.DropdownItem\n onSelect={handleSubscribeChange}\n icon={\n subscriptionStatus === \"subscribed\" ? (\n <BellCrossedIcon />\n ) : (\n <BellIcon />\n )\n }\n >\n {subscriptionStatus === \"subscribed\"\n ? $.THREAD_UNSUBSCRIBE\n : $.THREAD_SUBSCRIBE}\n </DefaultComment.DropdownItem>\n ) : undefined\n }\n dropdownItems={\n commentDropdownItems as CommentProps[\"dropdownItems\"]\n }\n />\n );\n\n return index === newIndicatorIndex &&\n newIndicatorIndex !== firstCommentIndex &&\n newIndicatorIndex <= lastCommentIndex ? (\n <Fragment key={comment.id}>\n <div\n className=\"lb-thread-new-indicator\"\n aria-label={$.THREAD_NEW_INDICATOR_DESCRIPTION}\n >\n <span className=\"lb-thread-new-indicator-label\">\n <ArrowDownIcon className=\"lb-thread-new-indicator-label-icon\" />\n {$.THREAD_NEW_INDICATOR}\n </span>\n </div>\n {children}\n </Fragment>\n ) : (\n children\n );\n })}\n </div>\n {unreadIndex !== undefined && (\n <MarkThreadAsReadMarker thread={thread} />\n )}\n {showComposer && (\n <Composer\n className=\"lb-thread-composer\"\n threadId={thread.id}\n defaultCollapsed={showComposer === \"collapsed\" ? true : undefined}\n showAttachments={showAttachments}\n showFormattingControls={showComposerFormattingControls}\n onComposerSubmit={onComposerSubmit}\n blurOnSubmit={blurComposerOnSubmit}\n overrides={{\n COMPOSER_PLACEHOLDER: $.THREAD_COMPOSER_PLACEHOLDER,\n COMPOSER_SEND: $.THREAD_COMPOSER_SEND,\n ...overrides,\n }}\n roomId={thread.roomId}\n autoFocus={autoFocus}\n />\n )}\n </div>\n </TooltipProvider>\n );\n }\n) as <TM extends BaseMetadata = DTM, CM extends BaseMetadata = DCM>(\n props: ThreadProps<TM, CM> & RefAttributes<HTMLDivElement>\n) => JSX.Element;\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA0DA;AACE;AACA;AACA;AACA;AAEA;AAAA;AACE;AAEE;AACE;AAAyB;AAC3B;AACF;AACA;AAAA;AAEW;AACX;AAGF;AACE;AAAC;AAAA;AACC;AACmB;AACR;AACc;AAAA;AAG/B;AA2KO;AAAe;AAElB;AACE;AACuB;AACT;AACd;AACoB;AACJ;AACD;AACG;AACC;AACc;AACjC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACG;AAIL;AACA;AACA;AACA;AACA;AACA;AACE;AAEuD;AAEzD;AACE;AAEqE;AAEvE;AACE;AAEsD;AAExD;AACE;AAIA;AAOA;AACE;AAAA;AAGF;AAKA;AACE;AAAA;AAGF;AACA;AAGA;AACE;AAEA;AAGA;AAAO;AACO;AACD;AACmC;AAChD;AAGF;AAGA;AAMA;AAQA;AAEA;AAIA;AAKE;AAAA;AAGF;AAAO;AAC0B;AACF;AAG+B;AAC9D;AACC;AACD;AACA;AACA;AACO;AAET;AAAM;AACI;AACR;AACA;AACA;AAEF;AAEE;AACE;AAAA;AAIF;AACE;AAAO;AAIT;AAAoC;AAGZ;AAGxB;AAEI;AACH;AACD;AACA;AACA;AACO;AACP;AAEF;AACA;AAEA;AACE;AAEE;AAAA;AACwD;AACxD;AACF;AAGF;AAAmB;AACV;AACP;AACA;AAGF;AACE;AAAsB;AAGxB;AAA6B;AAEzB;AAEA;AACE;AAA8B;AAE9B;AAAgC;AAClC;AACF;AACA;AACE;AACA;AACA;AACO;AACT;AAGF;AAA4B;AAExB;AAEA;AAAyC;AAClB;AAGvB;AACE;AAAuB;AACzB;AACF;AACwC;AAG1C;AACE;AACE;AAAY;AAEZ;AAAU;AACZ;AAGF;AAEA;AAEI;AAAC;AAAA;AACY;AACT;AAC2B;AAC3B;AACF;AACsC;AACQ;AACvC;AACH;AACC;AAEL;AAEI;AACA;AACA;AAEA;AAIA;AAEA;AAGA;AACE;AACE;AAAC;AAAA;AAEW;AAEV;AAAC;AAAA;AACS;AACE;AAC4B;AAEW;AAAA;AACnD;AAAA;AATkB;AAUpB;AAIJ;AACE;AAAO;AAGT;AACE;AAAA;AAGF;AACE;AAAC;AAAA;AAEW;AACK;AACD;AACd;AACU;AACmB;AAC7B;AACe;AACF;AACb;AACA;AACA;AACA;AAGA;AACiB;AACjB;AACA;AACA;AACA;AAEyC;AAIrC;AAAC;AAAA;AAIS;AAGR;AAAiB;AAAhB;AACiB;AACC;AACV;AAEP;AAAC;AAAA;AACW;AACD;AAID;AAMa;AAGV;AAAA;AACb;AAAA;AACF;AAAA;AAEA;AAIF;AAAgB;AAAf;AACW;AAKI;AAMR;AAAA;AAEN;AAGJ;AAAA;AA/EW;AAoFjB;AAII;AAAA;AAAC;AAAA;AACW;AACI;AAGZ;AAA8D;AAC3D;AACL;AAAA;AACF;AACC;AAGH;AAGN;AAE0C;AAGxC;AAAC;AAAA;AACW;AACO;AACuC;AACxD;AACwB;AACxB;AACc;AACH;AACe;AACP;AACd;AACL;AACe;AACf;AAAA;AACF;AAAA;AAAA;AAGN;AAGN;;"}
1
+ {"version":3,"file":"Thread.js","sources":["../../src/components/Thread.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n type BaseMetadata,\n type CommentData,\n type DCM,\n type DTM,\n findLastIndex,\n type ThreadData,\n} from \"@liveblocks/core\";\nimport {\n useHasPermissionAccess,\n useMarkRoomThreadAsRead,\n useMarkRoomThreadAsResolved,\n useMarkRoomThreadAsUnresolved,\n useRoomThreadSubscription,\n} from \"@liveblocks/react/_private\";\nimport { Toggle as TogglePrimitive } from \"radix-ui\";\nimport {\n type ComponentPropsWithoutRef,\n type ComponentType,\n type ForwardedRef,\n forwardRef,\n Fragment,\n type PropsWithChildren,\n type ReactNode,\n type RefAttributes,\n type SyntheticEvent,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nimport type { GlobalComponents } from \"../components\";\nimport { ArrowDownIcon } from \"../icons/ArrowDown\";\nimport { BellIcon } from \"../icons/Bell\";\nimport { BellCrossedIcon } from \"../icons/BellCrossed\";\nimport { CheckCircleIcon } from \"../icons/CheckCircle\";\nimport { CheckCircleFillIcon } from \"../icons/CheckCircleFill\";\nimport type {\n CommentOverrides,\n ComposerOverrides,\n GlobalOverrides,\n ThreadOverrides,\n} from \"../overrides\";\nimport { useOverrides } from \"../overrides\";\nimport { commentsResourceForVisibility } from \"../shared\";\nimport { cn } from \"../utils/cn\";\nimport { useStableComponent } from \"../utils/use-stable-component\";\nimport { useIntersectionCallback } from \"../utils/use-visible\";\nimport { useWindowFocus } from \"../utils/use-window-focus\";\nimport { Comment as DefaultComment, type CommentProps } from \"./Comment\";\nimport { Composer, type ComposerProps } from \"./Composer\";\nimport { Button } from \"./internal/Button\";\nimport { Tooltip, TooltipProvider } from \"./internal/Tooltip\";\n\n// An invisible marker used to mark a thread as read only when it becomes visible.\nfunction MarkThreadAsReadMarker({ thread }: { thread: ThreadData }) {\n const { id: threadId, roomId } = thread;\n const ref = useRef<HTMLDivElement>(null);\n const markThreadAsRead = useMarkRoomThreadAsRead(roomId);\n const isWindowFocused = useWindowFocus();\n\n useIntersectionCallback(\n ref,\n (isIntersecting) => {\n if (isIntersecting) {\n markThreadAsRead(threadId);\n }\n },\n {\n // The underlying IntersectionObserver is only enabled when the window is focused\n enabled: isWindowFocused,\n }\n );\n\n return (\n <div\n ref={ref}\n style={{ height: 0 }}\n aria-hidden\n data-mark-as-read-marker=\"\"\n />\n );\n}\n\nexport interface ThreadComponents<\n // Future components might reference thread metadata so we declare it\n // already to respect the `TM` → `CM` order used everywhere else.\n _TM extends BaseMetadata = DTM,\n CM extends BaseMetadata = DCM,\n> {\n /**\n * The component used to display comments.\n */\n Comment: ComponentType<CommentProps<CM>>;\n}\n\nexport interface ThreadProps<\n TM extends BaseMetadata = DTM,\n CM extends BaseMetadata = DCM,\n> extends ComponentPropsWithoutRef<\"div\"> {\n /**\n * The thread to display.\n */\n thread: ThreadData<TM, CM>;\n\n /**\n * How to show or hide the composer to reply to the thread.\n */\n showComposer?: boolean | \"collapsed\";\n\n /**\n * Whether to show the action to resolve the thread.\n */\n showResolveAction?: boolean;\n\n /**\n * How to show or hide the actions.\n */\n showActions?: CommentProps[\"showActions\"];\n\n /**\n * Whether to show reactions.\n */\n showReactions?: CommentProps[\"showReactions\"];\n\n /**\n * Whether to show the composer's formatting controls.\n */\n showComposerFormattingControls?: ComposerProps[\"showFormattingControls\"];\n\n /**\n * Add (or change) items to display in a comment's dropdown.\n */\n commentDropdownItems?:\n | ReactNode\n | ((props: PropsWithChildren<{ comment: CommentData<CM> }>) => ReactNode);\n\n /**\n * The maximum number of comments to show.\n *\n * The first and last comments are always shown and by default if some comments\n * are hidden, only the first comment will be shown before the \"show more\" button\n * and after it will be shown all the newest comments to fit the limit set.\n *\n * It's possible to customize this by setting `maxVisibleComments` to an object:\n *\n * @example\n * // Only show the last comment, and all the older ones to fit the limit.\n * <Thread maxVisibleComments={{ max: 5, show: \"oldest\" }} />\n *\n * @example\n * // Show as many old comments as new ones to fit the limit.\n * <Thread maxVisibleComments={{ max: 5, show: \"both\" }} />\n */\n maxVisibleComments?:\n | number\n | { max: number; show: \"oldest\" | \"both\" | \"newest\" };\n\n /**\n * Whether to blur the composer editor when the composer is submitted.\n */\n blurComposerOnSubmit?: ComposerProps[\"blurOnSubmit\"];\n\n /**\n * Whether to indent the comments' content.\n */\n indentCommentContent?: CommentProps[\"indentContent\"];\n\n /**\n * Whether to show deleted comments.\n */\n showDeletedComments?: CommentProps[\"showDeleted\"];\n\n /**\n * Whether to show the thread's subscription status.\n */\n showSubscription?: boolean;\n\n /**\n * Whether to show attachments.\n */\n showAttachments?: boolean;\n\n /**\n * The event handler called when changing the resolved status.\n */\n onResolvedChange?: (resolved: boolean) => void;\n\n /**\n * The event handler called when a comment is edited.\n */\n onCommentEdit?: CommentProps[\"onCommentEdit\"];\n\n /**\n * The event handler called when a comment is deleted.\n */\n onCommentDelete?: CommentProps[\"onCommentDelete\"];\n\n /**\n * The event handler called when the thread is deleted.\n * A thread is deleted when all its comments are deleted.\n */\n onThreadDelete?: (thread: ThreadData<TM, CM>) => void;\n\n /**\n * The event handler called when clicking on a comment's author.\n */\n onAuthorClick?: CommentProps[\"onAuthorClick\"];\n\n /**\n * The event handler called when clicking on a mention.\n */\n onMentionClick?: CommentProps[\"onMentionClick\"];\n\n /**\n * The event handler called when clicking on a comment's attachment.\n */\n onAttachmentClick?: CommentProps[\"onAttachmentClick\"];\n\n /**\n * The event handler called when the composer is submitted.\n */\n onComposerSubmit?: ComposerProps[\"onComposerSubmit\"];\n\n /**\n * Whether to focus the composer on mount.\n */\n autoFocus?: ComposerProps[\"autoFocus\"];\n\n /**\n * Override the component's strings.\n */\n overrides?: Partial<\n GlobalOverrides & ThreadOverrides & CommentOverrides & ComposerOverrides\n >;\n\n /**\n * Override the component's components.\n */\n components?: Partial<GlobalComponents & ThreadComponents<TM, CM>>;\n}\n\n/**\n * Displays a thread of comments, with a composer to reply\n * to it.\n *\n * @example\n * <>\n * {threads.map((thread) => (\n * <Thread key={thread.id} thread={thread} />\n * ))}\n * </>\n */\nexport const Thread = forwardRef(\n <TM extends BaseMetadata = DTM, CM extends BaseMetadata = DCM>(\n {\n thread,\n indentCommentContent = true,\n showActions = \"hover\",\n showDeletedComments,\n showResolveAction = true,\n showReactions = true,\n showComposer = \"collapsed\",\n showAttachments = true,\n showSubscription = true,\n showComposerFormattingControls = true,\n maxVisibleComments,\n commentDropdownItems,\n onResolvedChange,\n onCommentEdit,\n onCommentDelete,\n onThreadDelete,\n onAuthorClick,\n onMentionClick,\n onAttachmentClick,\n onComposerSubmit,\n blurComposerOnSubmit,\n autoFocus,\n overrides,\n components,\n className,\n ...props\n }: ThreadProps<TM, CM>,\n forwardedRef: ForwardedRef<HTMLDivElement>\n ) => {\n const Comment = useStableComponent(components?.Comment, DefaultComment);\n const markThreadAsResolved = useMarkRoomThreadAsResolved(thread.roomId);\n const markThreadAsUnresolved = useMarkRoomThreadAsUnresolved(thread.roomId);\n const $ = useOverrides(overrides);\n const [showAllComments, setShowAllComments] = useState(false);\n const firstCommentIndex = useMemo(() => {\n return showDeletedComments\n ? 0\n : thread.comments.findIndex((comment) => comment.body);\n }, [showDeletedComments, thread.comments]);\n const lastCommentIndex = useMemo(() => {\n return showDeletedComments\n ? thread.comments.length - 1\n : findLastIndex(thread.comments, (comment) => Boolean(comment.body));\n }, [showDeletedComments, thread.comments]);\n const commentsCount = useMemo(() => {\n return showDeletedComments\n ? thread.comments.length\n : thread.comments.filter((comment) => comment.body).length;\n }, [showDeletedComments, thread.comments]);\n const hiddenComments = useMemo(() => {\n const maxVisibleCommentsCount =\n typeof maxVisibleComments === \"number\"\n ? maxVisibleComments\n : maxVisibleComments?.max;\n const visibleCommentsShow =\n (typeof maxVisibleComments === \"object\"\n ? maxVisibleComments?.show\n : undefined) ?? \"newest\";\n\n // If we explicitly want to show all comments or there's no limit set,\n // no need to hide any comments.\n if (showAllComments || maxVisibleCommentsCount === undefined) {\n return;\n }\n\n const comments = thread.comments\n .map((comment, index) => ({ comment, index }))\n .filter(({ comment }) => showDeletedComments || comment.body);\n\n // There aren't enough comments so no need to hide any.\n if (comments.length <= Math.max(maxVisibleCommentsCount, 2)) {\n return;\n }\n\n const firstVisibleComment = comments[0]!;\n const lastVisibleComment = comments[comments.length - 1]!;\n\n // Always show the first and last comments even if the limit is set to lower than 2.\n if (maxVisibleCommentsCount <= 2) {\n const firstHiddenCommentIndex =\n comments[1]?.index ?? firstVisibleComment.index;\n const lastHiddenCommentIndex =\n comments[comments.length - 2]?.index ?? lastVisibleComment.index;\n\n return {\n firstIndex: firstHiddenCommentIndex,\n lastIndex: lastHiddenCommentIndex,\n count: comments.slice(1, comments.length - 1).length,\n };\n }\n\n const remainingVisibleCommentsCount = maxVisibleCommentsCount - 2;\n\n // Split the remaining visible comments before, after, or equally.\n const beforeVisibleCommentsCount =\n visibleCommentsShow === \"oldest\"\n ? remainingVisibleCommentsCount\n : visibleCommentsShow === \"newest\"\n ? 0\n : Math.floor(remainingVisibleCommentsCount / 2);\n const afterVisibleCommentsCount =\n visibleCommentsShow === \"oldest\"\n ? 0\n : visibleCommentsShow === \"newest\"\n ? remainingVisibleCommentsCount\n : Math.ceil(remainingVisibleCommentsCount / 2);\n\n // The first comment is always visible so `+ 1` to skip it.\n const firstHiddenComment = comments[1 + beforeVisibleCommentsCount];\n // The last comment is always visible so `- 2` to skip it.\n const lastHiddenComment =\n comments[comments.length - 2 - afterVisibleCommentsCount];\n\n // There aren't any comments to hide besides the first and last ones.\n if (\n !firstHiddenComment ||\n !lastHiddenComment ||\n firstHiddenComment.index > lastHiddenComment.index\n ) {\n return;\n }\n\n return {\n firstIndex: firstHiddenComment.index,\n lastIndex: lastHiddenComment.index,\n count: thread.comments\n .slice(firstHiddenComment.index, lastHiddenComment.index + 1)\n .filter((comment) => showDeletedComments || comment.body).length,\n };\n }, [\n maxVisibleComments,\n showAllComments,\n showDeletedComments,\n thread.comments,\n ]);\n const {\n status: subscriptionStatus,\n unreadSince,\n subscribe,\n unsubscribe,\n } = useRoomThreadSubscription(thread.roomId, thread.id);\n const unreadIndex = useMemo(() => {\n // The user is not subscribed to this thread.\n if (subscriptionStatus !== \"subscribed\") {\n return;\n }\n\n // The user hasn't read the thread yet, so all comments are unread.\n if (unreadSince === null) {\n return firstCommentIndex;\n }\n\n // The user has read the thread, so we find the first unread comment.\n const unreadIndex = thread.comments.findIndex(\n (comment) =>\n (showDeletedComments ? true : comment.body) &&\n comment.createdAt > unreadSince\n );\n\n return unreadIndex >= 0 && unreadIndex < thread.comments.length\n ? unreadIndex\n : undefined;\n }, [\n firstCommentIndex,\n showDeletedComments,\n subscriptionStatus,\n thread.comments,\n unreadSince,\n ]);\n const [newIndex, setNewIndex] = useState<number>();\n const newIndicatorIndex = newIndex === undefined ? unreadIndex : newIndex;\n\n useEffect(() => {\n if (unreadIndex) {\n // Keep the \"new\" indicator at the lowest unread index.\n setNewIndex((persistedUnreadIndex) =>\n Math.min(persistedUnreadIndex ?? Infinity, unreadIndex)\n );\n }\n }, [unreadIndex]);\n\n const canComment = useHasPermissionAccess(\n thread.roomId,\n commentsResourceForVisibility(thread.visibility),\n \"write\"\n );\n\n const stopPropagation = useCallback((event: SyntheticEvent) => {\n event.stopPropagation();\n }, []);\n\n const handleResolvedChange = useCallback(\n (resolved: boolean) => {\n onResolvedChange?.(resolved);\n\n if (resolved) {\n markThreadAsResolved(thread.id);\n } else {\n markThreadAsUnresolved(thread.id);\n }\n },\n [\n markThreadAsResolved,\n markThreadAsUnresolved,\n onResolvedChange,\n thread.id,\n ]\n );\n\n const handleCommentDelete = useCallback(\n (comment: CommentData) => {\n onCommentDelete?.(comment);\n\n const filteredComments = thread.comments.filter(\n (comment) => comment.body\n );\n\n if (filteredComments.length <= 1) {\n onThreadDelete?.(thread);\n }\n },\n [onCommentDelete, onThreadDelete, thread]\n );\n\n const handleSubscribeChange = useCallback(() => {\n if (subscriptionStatus === \"subscribed\") {\n unsubscribe();\n } else {\n subscribe();\n }\n }, [subscriptionStatus, subscribe, unsubscribe]);\n\n let currentCommentIndex = 0;\n\n return (\n <TooltipProvider>\n <div\n className={cn(\n \"lb-root lb-thread\",\n showActions === \"hover\" && \"lb-thread:show-actions-hover\",\n className\n )}\n data-resolved={thread.resolved ? \"\" : undefined}\n data-unread={unreadIndex !== undefined ? \"\" : undefined}\n dir={$.dir}\n {...props}\n ref={forwardedRef}\n >\n <div className=\"lb-thread-comments\" role=\"feed\">\n {thread.comments.map((comment, index) => {\n const isNonDeletedComment = showDeletedComments || comment.body;\n const isFirstComment = index === firstCommentIndex;\n const isUnread =\n unreadIndex !== undefined && index >= unreadIndex;\n const isHidden =\n hiddenComments &&\n index >= hiddenComments.firstIndex &&\n index <= hiddenComments.lastIndex;\n const isHiddenBecauseDeleted =\n !showDeletedComments && !comment.body;\n const isFirstHiddenComment =\n isHidden && index === hiddenComments.firstIndex;\n\n if (isFirstHiddenComment) {\n return (\n <div\n key={`${comment.id}:show-more`}\n className=\"lb-thread-show-more\"\n >\n <Button\n variant=\"ghost\"\n className=\"lb-thread-show-more-button\"\n onClick={() => setShowAllComments(true)}\n >\n {$.THREAD_SHOW_MORE_COMMENTS(hiddenComments.count)}\n </Button>\n </div>\n );\n }\n\n if (isHidden || isHiddenBecauseDeleted) {\n return null;\n }\n\n if (isNonDeletedComment) {\n currentCommentIndex++;\n }\n\n const children = (\n <Comment\n key={comment.id}\n tabIndex={0}\n aria-posinset={currentCommentIndex}\n aria-setsize={commentsCount}\n overrides={overrides}\n className=\"lb-thread-comment\"\n data-unread={isUnread ? \"\" : undefined}\n comment={comment}\n visibility={thread.visibility}\n indentContent={indentCommentContent}\n showDeleted={showDeletedComments}\n showActions={showActions}\n showReactions={showReactions}\n showAttachments={showAttachments}\n showComposerFormattingControls={\n showComposerFormattingControls\n }\n onCommentEdit={onCommentEdit}\n onCommentDelete={handleCommentDelete}\n onAuthorClick={onAuthorClick}\n onMentionClick={onMentionClick}\n onAttachmentClick={onAttachmentClick}\n components={components}\n actionsClassName={\n isFirstComment ? \"lb-thread-actions\" : undefined\n }\n actions={\n isFirstComment && showResolveAction ? (\n <Tooltip\n content={\n thread.resolved\n ? $.THREAD_UNRESOLVE\n : $.THREAD_RESOLVE\n }\n >\n <TogglePrimitive.Root\n pressed={thread.resolved}\n onPressedChange={handleResolvedChange}\n asChild\n >\n <Button\n className=\"lb-comment-action\"\n onClick={stopPropagation}\n aria-label={\n thread.resolved\n ? $.THREAD_UNRESOLVE\n : $.THREAD_RESOLVE\n }\n icon={\n thread.resolved ? (\n <CheckCircleFillIcon />\n ) : (\n <CheckCircleIcon />\n )\n }\n disabled={!canComment}\n />\n </TogglePrimitive.Root>\n </Tooltip>\n ) : null\n }\n internalDropdownItems={\n isFirstComment && showSubscription ? (\n <DefaultComment.DropdownItem\n onSelect={handleSubscribeChange}\n icon={\n subscriptionStatus === \"subscribed\" ? (\n <BellCrossedIcon />\n ) : (\n <BellIcon />\n )\n }\n >\n {subscriptionStatus === \"subscribed\"\n ? $.THREAD_UNSUBSCRIBE\n : $.THREAD_SUBSCRIBE}\n </DefaultComment.DropdownItem>\n ) : undefined\n }\n dropdownItems={\n commentDropdownItems as CommentProps[\"dropdownItems\"]\n }\n />\n );\n\n return index === newIndicatorIndex &&\n newIndicatorIndex !== firstCommentIndex &&\n newIndicatorIndex <= lastCommentIndex ? (\n <Fragment key={comment.id}>\n <div\n className=\"lb-thread-new-indicator\"\n aria-label={$.THREAD_NEW_INDICATOR_DESCRIPTION}\n >\n <span className=\"lb-thread-new-indicator-label\">\n <ArrowDownIcon className=\"lb-thread-new-indicator-label-icon\" />\n {$.THREAD_NEW_INDICATOR}\n </span>\n </div>\n {children}\n </Fragment>\n ) : (\n children\n );\n })}\n </div>\n {unreadIndex !== undefined && (\n <MarkThreadAsReadMarker thread={thread} />\n )}\n {showComposer && (\n <Composer\n className=\"lb-thread-composer\"\n threadId={thread.id}\n visibility={thread.visibility}\n defaultCollapsed={showComposer === \"collapsed\" ? true : undefined}\n showAttachments={showAttachments}\n showFormattingControls={showComposerFormattingControls}\n onComposerSubmit={onComposerSubmit}\n blurOnSubmit={blurComposerOnSubmit}\n overrides={{\n COMPOSER_PLACEHOLDER: $.THREAD_COMPOSER_PLACEHOLDER,\n COMPOSER_SEND: $.THREAD_COMPOSER_SEND,\n ...overrides,\n }}\n roomId={thread.roomId}\n autoFocus={autoFocus}\n />\n )}\n </div>\n </TooltipProvider>\n );\n }\n) as <TM extends BaseMetadata = DTM, CM extends BaseMetadata = DCM>(\n props: ThreadProps<TM, CM> & RefAttributes<HTMLDivElement>\n) => JSX.Element;\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AA2DA;AACE;AACA;AACA;AACA;AAEA;AAAA;AACE;AAEE;AACE;AAAyB;AAC3B;AACF;AACA;AAAA;AAEW;AACX;AAGF;AACE;AAAC;AAAA;AACC;AACmB;AACR;AACc;AAAA;AAG/B;AA2KO;AAAe;AAElB;AACE;AACuB;AACT;AACd;AACoB;AACJ;AACD;AACG;AACC;AACc;AACjC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACG;AAIL;AACA;AACA;AACA;AACA;AACA;AACE;AAEuD;AAEzD;AACE;AAEqE;AAEvE;AACE;AAEsD;AAExD;AACE;AAIA;AAOA;AACE;AAAA;AAGF;AAKA;AACE;AAAA;AAGF;AACA;AAGA;AACE;AAEA;AAGA;AAAO;AACO;AACD;AACmC;AAChD;AAGF;AAGA;AAMA;AAQA;AAEA;AAIA;AAKE;AAAA;AAGF;AAAO;AAC0B;AACF;AAG+B;AAC9D;AACC;AACD;AACA;AACA;AACO;AAET;AAAM;AACI;AACR;AACA;AACA;AAEF;AAEE;AACE;AAAA;AAIF;AACE;AAAO;AAIT;AAAoC;AAGZ;AAGxB;AAEI;AACH;AACD;AACA;AACA;AACO;AACP;AAEF;AACA;AAEA;AACE;AAEE;AAAA;AACwD;AACxD;AACF;AAGF;AAAmB;AACV;AACwC;AAC/C;AAGF;AACE;AAAsB;AAGxB;AAA6B;AAEzB;AAEA;AACE;AAA8B;AAE9B;AAAgC;AAClC;AACF;AACA;AACE;AACA;AACA;AACO;AACT;AAGF;AAA4B;AAExB;AAEA;AAAyC;AAClB;AAGvB;AACE;AAAuB;AACzB;AACF;AACwC;AAG1C;AACE;AACE;AAAY;AAEZ;AAAU;AACZ;AAGF;AAEA;AAEI;AAAC;AAAA;AACY;AACT;AAC2B;AAC3B;AACF;AACsC;AACQ;AACvC;AACH;AACC;AAEL;AAEI;AACA;AACA;AAEA;AAIA;AAEA;AAGA;AACE;AACE;AAAC;AAAA;AAEW;AAEV;AAAC;AAAA;AACS;AACE;AAC4B;AAEW;AAAA;AACnD;AAAA;AATkB;AAUpB;AAIJ;AACE;AAAO;AAGT;AACE;AAAA;AAGF;AACE;AAAC;AAAA;AAEW;AACK;AACD;AACd;AACU;AACmB;AAC7B;AACmB;AACJ;AACF;AACb;AACA;AACA;AACA;AAGA;AACiB;AACjB;AACA;AACA;AACA;AAEyC;AAIrC;AAAC;AAAA;AAIS;AAGR;AAAiB;AAAhB;AACiB;AACC;AACV;AAEP;AAAC;AAAA;AACW;AACD;AAID;AAMa;AAGV;AAAA;AACb;AAAA;AACF;AAAA;AAEA;AAIF;AAAgB;AAAf;AACW;AAKI;AAMR;AAAA;AAEN;AAGJ;AAAA;AAhFW;AAqFjB;AAII;AAAA;AAAC;AAAA;AACW;AACI;AAGZ;AAA8D;AAC3D;AACL;AAAA;AACF;AACC;AAGH;AAGN;AAE0C;AAGxC;AAAC;AAAA;AACW;AACO;AACE;AACqC;AACxD;AACwB;AACxB;AACc;AACH;AACe;AACP;AACd;AACL;AACe;AACf;AAAA;AACF;AAAA;AAAA;AAGN;AAGN;;"}
package/dist/index.d.cts CHANGED
@@ -1018,7 +1018,6 @@ type ComposerCreateCommentProps<CM extends BaseMetadata> = {
1018
1018
  threadId: string;
1019
1019
  commentId?: never;
1020
1020
  metadata?: never;
1021
- visibility?: never;
1022
1021
  /**
1023
1022
  * The metadata of the comment to create.
1024
1023
  */
@@ -1034,7 +1033,6 @@ type ComposerEditCommentProps<CM extends BaseMetadata> = {
1034
1033
  */
1035
1034
  commentId: string;
1036
1035
  metadata?: never;
1037
- visibility?: never;
1038
1036
  /**
1039
1037
  * The metadata of the comment to edit.
1040
1038
  */
@@ -1103,6 +1101,10 @@ interface CommentProps<CM extends BaseMetadata = DCM> extends Omit<ComponentProp
1103
1101
  * The comment to display.
1104
1102
  */
1105
1103
  comment: CommentData<CM>;
1104
+ /**
1105
+ * The visibility of the thread containing the comment.
1106
+ */
1107
+ visibility?: ThreadVisibility;
1106
1108
  /**
1107
1109
  * The comment's avatar.
1108
1110
  * Can be combined with `Comment.Avatar` to easily follow default styles.
package/dist/index.d.ts CHANGED
@@ -1018,7 +1018,6 @@ type ComposerCreateCommentProps<CM extends BaseMetadata> = {
1018
1018
  threadId: string;
1019
1019
  commentId?: never;
1020
1020
  metadata?: never;
1021
- visibility?: never;
1022
1021
  /**
1023
1022
  * The metadata of the comment to create.
1024
1023
  */
@@ -1034,7 +1033,6 @@ type ComposerEditCommentProps<CM extends BaseMetadata> = {
1034
1033
  */
1035
1034
  commentId: string;
1036
1035
  metadata?: never;
1037
- visibility?: never;
1038
1036
  /**
1039
1037
  * The metadata of the comment to edit.
1040
1038
  */
@@ -1103,6 +1101,10 @@ interface CommentProps<CM extends BaseMetadata = DCM> extends Omit<ComponentProp
1103
1101
  * The comment to display.
1104
1102
  */
1105
1103
  comment: CommentData<CM>;
1104
+ /**
1105
+ * The visibility of the thread containing the comment.
1106
+ */
1107
+ visibility?: ThreadVisibility;
1106
1108
  /**
1107
1109
  * The comment's avatar.
1108
1110
  * Can be combined with `Comment.Avatar` to easily follow default styles.
package/dist/shared.cjs CHANGED
@@ -8,6 +8,16 @@ function useCurrentUserId() {
8
8
  const client = react.useClient();
9
9
  return _private.useSignal(client[core.kInternal].currentUserId) ?? null;
10
10
  }
11
+ function commentsResourceForVisibility(visibility) {
12
+ if (visibility === "private") {
13
+ return "comments:private";
14
+ }
15
+ if (visibility === "public") {
16
+ return "comments:public";
17
+ }
18
+ return "comments";
19
+ }
11
20
 
21
+ exports.commentsResourceForVisibility = commentsResourceForVisibility;
12
22
  exports.useCurrentUserId = useCurrentUserId;
13
23
  //# sourceMappingURL=shared.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"shared.cjs","sources":["../src/shared.ts"],"sourcesContent":["import { kInternal } from \"@liveblocks/core\";\nimport { useClient } from \"@liveblocks/react\";\nimport { useSignal } from \"@liveblocks/react/_private\";\n\nexport function useCurrentUserId(): string | null {\n const client = useClient();\n return useSignal(client[kInternal].currentUserId) ?? null;\n}\n"],"names":["useClient","useSignal","kInternal"],"mappings":";;;;;;AAIO,SAAS,gBAAkC,GAAA;AAChD,EAAA,MAAM,SAASA,eAAU,EAAA,CAAA;AACzB,EAAA,OAAOC,kBAAU,CAAA,MAAA,CAAOC,cAAS,CAAA,CAAE,aAAa,CAAK,IAAA,IAAA,CAAA;AACvD;;;;"}
1
+ {"version":3,"file":"shared.cjs","sources":["../src/shared.ts"],"sourcesContent":["import {\n kInternal,\n type PermissionResources,\n type ThreadVisibility,\n} from \"@liveblocks/core\";\nimport { useClient } from \"@liveblocks/react\";\nimport { useSignal } from \"@liveblocks/react/_private\";\n\nexport function useCurrentUserId(): string | null {\n const client = useClient();\n return useSignal(client[kInternal].currentUserId) ?? null;\n}\n\nexport function commentsResourceForVisibility(\n visibility: ThreadVisibility | undefined\n): PermissionResources {\n if (visibility === \"private\") {\n return \"comments:private\";\n }\n\n if (visibility === \"public\") {\n return \"comments:public\";\n }\n\n return \"comments\";\n}\n"],"names":["useClient","useSignal","kInternal"],"mappings":";;;;;;AAQO,SAAS,gBAAkC,GAAA;AAChD,EAAA,MAAM,SAASA,eAAU,EAAA,CAAA;AACzB,EAAA,OAAOC,kBAAU,CAAA,MAAA,CAAOC,cAAS,CAAA,CAAE,aAAa,CAAK,IAAA,IAAA,CAAA;AACvD,CAAA;AAEO,SAAS,8BACd,UACqB,EAAA;AACrB,EAAA,IAAI,eAAe,SAAW,EAAA;AAC5B,IAAO,OAAA,kBAAA,CAAA;AAAA,GACT;AAEA,EAAA,IAAI,eAAe,QAAU,EAAA;AAC3B,IAAO,OAAA,iBAAA,CAAA;AAAA,GACT;AAEA,EAAO,OAAA,UAAA,CAAA;AACT;;;;;"}
package/dist/shared.js CHANGED
@@ -6,6 +6,15 @@ function useCurrentUserId() {
6
6
  const client = useClient();
7
7
  return useSignal(client[kInternal].currentUserId) ?? null;
8
8
  }
9
+ function commentsResourceForVisibility(visibility) {
10
+ if (visibility === "private") {
11
+ return "comments:private";
12
+ }
13
+ if (visibility === "public") {
14
+ return "comments:public";
15
+ }
16
+ return "comments";
17
+ }
9
18
 
10
- export { useCurrentUserId };
19
+ export { commentsResourceForVisibility, useCurrentUserId };
11
20
  //# sourceMappingURL=shared.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"shared.js","sources":["../src/shared.ts"],"sourcesContent":["import { kInternal } from \"@liveblocks/core\";\nimport { useClient } from \"@liveblocks/react\";\nimport { useSignal } from \"@liveblocks/react/_private\";\n\nexport function useCurrentUserId(): string | null {\n const client = useClient();\n return useSignal(client[kInternal].currentUserId) ?? null;\n}\n"],"names":[],"mappings":";;;;AAIO,SAAS,gBAAkC,GAAA;AAChD,EAAA,MAAM,SAAS,SAAU,EAAA,CAAA;AACzB,EAAA,OAAO,SAAU,CAAA,MAAA,CAAO,SAAS,CAAA,CAAE,aAAa,CAAK,IAAA,IAAA,CAAA;AACvD;;;;"}
1
+ {"version":3,"file":"shared.js","sources":["../src/shared.ts"],"sourcesContent":["import {\n kInternal,\n type PermissionResources,\n type ThreadVisibility,\n} from \"@liveblocks/core\";\nimport { useClient } from \"@liveblocks/react\";\nimport { useSignal } from \"@liveblocks/react/_private\";\n\nexport function useCurrentUserId(): string | null {\n const client = useClient();\n return useSignal(client[kInternal].currentUserId) ?? null;\n}\n\nexport function commentsResourceForVisibility(\n visibility: ThreadVisibility | undefined\n): PermissionResources {\n if (visibility === \"private\") {\n return \"comments:private\";\n }\n\n if (visibility === \"public\") {\n return \"comments:public\";\n }\n\n return \"comments\";\n}\n"],"names":[],"mappings":";;;;AAQO,SAAS,gBAAkC,GAAA;AAChD,EAAA,MAAM,SAAS,SAAU,EAAA,CAAA;AACzB,EAAA,OAAO,SAAU,CAAA,MAAA,CAAO,SAAS,CAAA,CAAE,aAAa,CAAK,IAAA,IAAA,CAAA;AACvD,CAAA;AAEO,SAAS,8BACd,UACqB,EAAA;AACrB,EAAA,IAAI,eAAe,SAAW,EAAA;AAC5B,IAAO,OAAA,kBAAA,CAAA;AAAA,GACT;AAEA,EAAA,IAAI,eAAe,QAAU,EAAA;AAC3B,IAAO,OAAA,iBAAA,CAAA;AAAA,GACT;AAEA,EAAO,OAAA,UAAA,CAAA;AACT;;;;"}
package/dist/version.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const PKG_NAME = "@liveblocks/react-ui";
4
- const PKG_VERSION = typeof "3.21.0-private1" === "string" && "3.21.0-private1";
4
+ const PKG_VERSION = typeof "3.21.0-private3" === "string" && "3.21.0-private3";
5
5
  const PKG_FORMAT = typeof "cjs" === "string" && "cjs";
6
6
 
7
7
  exports.PKG_FORMAT = PKG_FORMAT;
package/dist/version.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const PKG_NAME = "@liveblocks/react-ui";
2
- const PKG_VERSION = typeof "3.21.0-private1" === "string" && "3.21.0-private1";
2
+ const PKG_VERSION = typeof "3.21.0-private3" === "string" && "3.21.0-private3";
3
3
  const PKG_FORMAT = typeof "esm" === "string" && "esm";
4
4
 
5
5
  export { PKG_FORMAT, PKG_NAME, PKG_VERSION };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liveblocks/react-ui",
3
- "version": "3.21.0-private1",
3
+ "version": "3.21.0-private3",
4
4
  "description": "A set of React pre-built components for the Liveblocks products. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Liveblocks Inc.",
@@ -73,9 +73,9 @@
73
73
  "slate-history": "^0.110.3",
74
74
  "slate-hyperscript": "^0.100.0",
75
75
  "slate-react": "^0.110.3",
76
- "@liveblocks/core": "3.21.0-private1",
77
- "@liveblocks/client": "3.21.0-private1",
78
- "@liveblocks/react": "3.21.0-private1"
76
+ "@liveblocks/client": "3.21.0-private3",
77
+ "@liveblocks/react": "3.21.0-private3",
78
+ "@liveblocks/core": "3.21.0-private3"
79
79
  },
80
80
  "peerDependencies": {
81
81
  "@types/react": "^18 || ^19",