@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
JavaScript
"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