UNPKG

@liveblocks/react-ui

Version:

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.

324 lines (320 loc) 13.6 kB
"use client"; import { jsx, jsxs, Fragment } from 'react/jsx-runtime'; import { findLastIndex, Permission } from '@liveblocks/core'; import { useMarkRoomThreadAsResolved, useMarkRoomThreadAsUnresolved, useRoomThreadSubscription, useRoomPermissions } from '@liveblocks/react/_private'; import * as TogglePrimitive from '@radix-ui/react-toggle'; import { forwardRef, useState, useMemo, useEffect, useCallback, Fragment as Fragment$1 } from 'react'; import { ArrowDownIcon } from '../icons/ArrowDown.js'; import { BellIcon } from '../icons/Bell.js'; import { BellCrossedIcon } from '../icons/BellCrossed.js'; import { CheckCircleIcon } from '../icons/CheckCircle.js'; import { CheckCircleFillIcon } from '../icons/CheckCircleFill.js'; import { useOverrides } from '../overrides.js'; import { cn } from '../utils/cn.js'; import { Comment } from './Comment.js'; import { Composer } from './Composer.js'; import { Button } from './internal/Button.js'; import { Tooltip } from './internal/Tooltip.js'; import { TooltipProvider } from '@radix-ui/react-tooltip'; const Thread = forwardRef( ({ thread, indentCommentContent = true, showActions = "hover", showDeletedComments, showResolveAction = true, showReactions = true, showComposer = "collapsed", showAttachments = true, showComposerFormattingControls = true, maxVisibleComments, commentDropdownItems, onResolvedChange, onCommentEdit, onCommentDelete, onThreadDelete, onAuthorClick, onMentionClick, onAttachmentClick, onComposerSubmit, blurComposerOnSubmit, overrides, components, className, ...props }, forwardedRef) => { const markThreadAsResolved = useMarkRoomThreadAsResolved(thread.roomId); const markThreadAsUnresolved = useMarkRoomThreadAsUnresolved(thread.roomId); const $ = useOverrides(overrides); const [showAllComments, setShowAllComments] = useState(false); const firstCommentIndex = useMemo(() => { return showDeletedComments ? 0 : thread.comments.findIndex((comment) => comment.body); }, [showDeletedComments, thread.comments]); const lastCommentIndex = useMemo(() => { return showDeletedComments ? thread.comments.length - 1 : findLastIndex(thread.comments, (comment) => Boolean(comment.body)); }, [showDeletedComments, thread.comments]); const hiddenComments = useMemo(() => { const maxVisibleCommentsCount = typeof maxVisibleComments === "number" ? maxVisibleComments : maxVisibleComments?.max; const visibleCommentsShow = (typeof maxVisibleComments === "object" ? maxVisibleComments?.show : void 0) ?? "newest"; if (showAllComments || maxVisibleCommentsCount === void 0) { return; } const comments = thread.comments.map((comment, index) => ({ comment, index })).filter(({ comment }) => showDeletedComments || comment.body); if (comments.length <= Math.max(maxVisibleCommentsCount, 2)) { return; } const firstVisibleComment = comments[0]; const lastVisibleComment = comments[comments.length - 1]; if (maxVisibleCommentsCount <= 2) { const firstHiddenCommentIndex = comments[1]?.index ?? firstVisibleComment.index; const lastHiddenCommentIndex = comments[comments.length - 2]?.index ?? lastVisibleComment.index; return { firstIndex: firstHiddenCommentIndex, lastIndex: lastHiddenCommentIndex, count: comments.slice(1, comments.length - 1).length }; } const remainingVisibleCommentsCount = maxVisibleCommentsCount - 2; const beforeVisibleCommentsCount = visibleCommentsShow === "oldest" ? remainingVisibleCommentsCount : visibleCommentsShow === "newest" ? 0 : Math.floor(remainingVisibleCommentsCount / 2); const afterVisibleCommentsCount = visibleCommentsShow === "oldest" ? 0 : visibleCommentsShow === "newest" ? remainingVisibleCommentsCount : Math.ceil(remainingVisibleCommentsCount / 2); const firstHiddenComment = comments[1 + beforeVisibleCommentsCount]; const lastHiddenComment = comments[comments.length - 2 - afterVisibleCommentsCount]; if (!firstHiddenComment || !lastHiddenComment || firstHiddenComment.index > lastHiddenComment.index) { return; } return { firstIndex: firstHiddenComment.index, lastIndex: lastHiddenComment.index, count: thread.comments.slice(firstHiddenComment.index, lastHiddenComment.index + 1).filter((comment) => showDeletedComments || comment.body).length }; }, [ maxVisibleComments, showAllComments, showDeletedComments, thread.comments ]); const { status: subscriptionStatus, unreadSince, subscribe, unsubscribe } = useRoomThreadSubscription(thread.roomId, thread.id); const unreadIndex = useMemo(() => { if (subscriptionStatus !== "subscribed") { return; } if (unreadSince === null) { return firstCommentIndex; } const unreadIndex2 = thread.comments.findIndex( (comment) => (showDeletedComments ? true : comment.body) && comment.createdAt > unreadSince ); return unreadIndex2 >= 0 && unreadIndex2 < thread.comments.length ? unreadIndex2 : void 0; }, [ firstCommentIndex, showDeletedComments, subscriptionStatus, thread.comments, unreadSince ]); const [newIndex, setNewIndex] = useState(); const newIndicatorIndex = newIndex === void 0 ? unreadIndex : newIndex; useEffect(() => { if (unreadIndex) { setNewIndex( (persistedUnreadIndex) => Math.min(persistedUnreadIndex ?? Infinity, unreadIndex) ); } }, [unreadIndex]); const permissions = useRoomPermissions(thread.roomId); const canComment = permissions.size > 0 ? permissions.has(Permission.CommentsWrite) || permissions.has(Permission.Write) : true; const stopPropagation = useCallback((event) => { event.stopPropagation(); }, []); const handleResolvedChange = useCallback( (resolved) => { onResolvedChange?.(resolved); if (resolved) { markThreadAsResolved(thread.id); } else { markThreadAsUnresolved(thread.id); } }, [ markThreadAsResolved, markThreadAsUnresolved, onResolvedChange, thread.id ] ); const handleCommentDelete = useCallback( (comment) => { onCommentDelete?.(comment); const filteredComments = thread.comments.filter( (comment2) => comment2.body ); if (filteredComments.length <= 1) { onThreadDelete?.(thread); } }, [onCommentDelete, onThreadDelete, thread] ); const handleSubscribeChange = useCallback(() => { if (subscriptionStatus === "subscribed") { unsubscribe(); } else { subscribe(); } }, [subscriptionStatus, subscribe, unsubscribe]); return /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs( "div", { className: cn( "lb-root lb-thread", showActions === "hover" && "lb-thread:show-actions-hover", className ), "data-resolved": thread.resolved ? "" : void 0, "data-unread": unreadIndex !== void 0 ? "" : void 0, dir: $.dir, ...props, ref: forwardedRef, children: [ /* @__PURE__ */ jsx("div", { className: "lb-thread-comments", children: thread.comments.map((comment, index) => { const isFirstComment = index === firstCommentIndex; const isUnread = unreadIndex !== void 0 && index >= unreadIndex; const isHidden = hiddenComments && index >= hiddenComments.firstIndex && index <= hiddenComments.lastIndex; const isFirstHiddenComment = isHidden && index === hiddenComments.firstIndex; if (isFirstHiddenComment) { return /* @__PURE__ */ jsx( "div", { className: "lb-thread-show-more", children: /* @__PURE__ */ jsx( Button, { variant: "ghost", className: "lb-thread-show-more-button", onClick: () => setShowAllComments(true), children: $.THREAD_SHOW_MORE_COMMENTS(hiddenComments.count) } ) }, `${comment.id}-show-more` ); } if (isHidden) { return null; } const children = /* @__PURE__ */ jsx( Comment, { overrides, className: "lb-thread-comment", "data-unread": isUnread ? "" : void 0, comment, indentContent: indentCommentContent, showDeleted: showDeletedComments, showActions, showReactions, showAttachments, showComposerFormattingControls, onCommentEdit, onCommentDelete: handleCommentDelete, onAuthorClick, onMentionClick, onAttachmentClick, components, autoMarkReadThreadId: index === lastCommentIndex && isUnread ? thread.id : void 0, actionsClassName: isFirstComment ? "lb-thread-actions" : void 0, actions: isFirstComment && showResolveAction ? /* @__PURE__ */ jsx( Tooltip, { content: thread.resolved ? $.THREAD_UNRESOLVE : $.THREAD_RESOLVE, children: /* @__PURE__ */ jsx( TogglePrimitive.Root, { pressed: thread.resolved, onPressedChange: handleResolvedChange, asChild: true, children: /* @__PURE__ */ jsx( Button, { className: "lb-comment-action", onClick: stopPropagation, "aria-label": thread.resolved ? $.THREAD_UNRESOLVE : $.THREAD_RESOLVE, icon: thread.resolved ? /* @__PURE__ */ jsx(CheckCircleFillIcon, {}) : /* @__PURE__ */ jsx(CheckCircleIcon, {}), disabled: !canComment } ) } ) } ) : null, dropdownItems: ({ children: children2 }) => { const threadDropdownItems = isFirstComment ? /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx( Comment.DropdownItem, { onSelect: handleSubscribeChange, icon: subscriptionStatus === "subscribed" ? /* @__PURE__ */ jsx(BellCrossedIcon, {}) : /* @__PURE__ */ jsx(BellIcon, {}), children: subscriptionStatus === "subscribed" ? $.THREAD_UNSUBSCRIBE : $.THREAD_SUBSCRIBE } ) }) : null; if (typeof commentDropdownItems === "function") { return commentDropdownItems({ children: /* @__PURE__ */ jsxs(Fragment, { children: [ threadDropdownItems, children2 ] }), comment }); } return threadDropdownItems || commentDropdownItems || children2 ? /* @__PURE__ */ jsxs(Fragment, { children: [ threadDropdownItems, children2, commentDropdownItems ] }) : null; } }, comment.id ); return index === newIndicatorIndex && newIndicatorIndex !== firstCommentIndex && newIndicatorIndex <= lastCommentIndex ? /* @__PURE__ */ jsxs(Fragment$1, { children: [ /* @__PURE__ */ jsx( "div", { className: "lb-thread-new-indicator", "aria-label": $.THREAD_NEW_INDICATOR_DESCRIPTION, children: /* @__PURE__ */ jsxs("span", { className: "lb-thread-new-indicator-label", children: [ /* @__PURE__ */ jsx(ArrowDownIcon, { className: "lb-thread-new-indicator-label-icon" }), $.THREAD_NEW_INDICATOR ] }) } ), children ] }, comment.id) : children; }) }), showComposer && /* @__PURE__ */ jsx( Composer, { className: "lb-thread-composer", threadId: thread.id, defaultCollapsed: showComposer === "collapsed" ? true : void 0, showAttachments, showFormattingControls: showComposerFormattingControls, onComposerSubmit, blurOnSubmit: blurComposerOnSubmit, overrides: { COMPOSER_PLACEHOLDER: $.THREAD_COMPOSER_PLACEHOLDER, COMPOSER_SEND: $.THREAD_COMPOSER_SEND, ...overrides }, roomId: thread.roomId } ) ] } ) }); } ); export { Thread }; //# sourceMappingURL=Thread.js.map