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.

754 lines (747 loc) 28.2 kB
"use client"; 'use strict'; var jsxRuntime = require('react/jsx-runtime'); var core = require('@liveblocks/core'); var _private = require('@liveblocks/react/_private'); var TogglePrimitive = require('@radix-ui/react-toggle'); var react = require('react'); var components = require('../components.cjs'); var Check = require('../icons/Check.cjs'); var Cross = require('../icons/Cross.cjs'); var Delete = require('../icons/Delete.cjs'); var Edit = require('../icons/Edit.cjs'); var Ellipsis = require('../icons/Ellipsis.cjs'); var EmojiPlus = require('../icons/EmojiPlus.cjs'); var overrides = require('../overrides.cjs'); var index = require('../primitives/Comment/index.cjs'); var index$1 = require('../primitives/Composer/index.cjs'); var Timestamp = require('../primitives/Timestamp.cjs'); var shared = require('../shared.cjs'); var cn = require('../utils/cn.cjs'); var download = require('../utils/download.cjs'); var useGroupMention = require('../utils/use-group-mention.cjs'); var useRefs = require('../utils/use-refs.cjs'); var useVisible = require('../utils/use-visible.cjs'); var useWindowFocus = require('../utils/use-window-focus.cjs'); var Composer = require('./Composer.cjs'); var Attachment = require('./internal/Attachment.cjs'); var Avatar = require('./internal/Avatar.cjs'); var Button = require('./internal/Button.cjs'); var Dropdown = require('./internal/Dropdown.cjs'); var Emoji = require('./internal/Emoji.cjs'); var EmojiPicker = require('./internal/EmojiPicker.cjs'); var Group = require('./internal/Group.cjs'); var List = require('./internal/List.cjs'); var Tooltip = require('./internal/Tooltip.cjs'); var User = require('./internal/User.cjs'); var TooltipPrimitive = require('@radix-ui/react-tooltip'); var PopoverPrimitive = require('@radix-ui/react-popover'); var DropdownMenuPrimitive = require('@radix-ui/react-dropdown-menu'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var TogglePrimitive__namespace = /*#__PURE__*/_interopNamespaceDefault(TogglePrimitive); const REACTIONS_TRUNCATE = 5; function CommentUserMention({ mention, className, ...props }) { const currentId = shared.useCurrentUserId(); return /* @__PURE__ */ jsxRuntime.jsxs( index.Mention, { className: cn.cn("lb-mention lb-comment-mention", className), "data-self": mention.id === currentId ? "" : void 0, ...props, children: [ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lb-mention-symbol", children: core.MENTION_CHARACTER }), /* @__PURE__ */ jsxRuntime.jsx(User.User, { userId: mention.id }) ] } ); } function CommentGroupMention({ mention, className, ...props }) { const isMember = useGroupMention.useIsGroupMentionMember(mention); return /* @__PURE__ */ jsxRuntime.jsxs( index.Mention, { className: cn.cn("lb-mention lb-comment-mention", className), "data-self": isMember ? "" : void 0, ...props, children: [ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lb-mention-symbol", children: core.MENTION_CHARACTER }), /* @__PURE__ */ jsxRuntime.jsx(Group.Group, { groupId: mention.id }) ] } ); } function CommentMention({ mention, ...props }) { switch (mention.kind) { case "user": return /* @__PURE__ */ jsxRuntime.jsx(CommentUserMention, { mention, ...props }); case "group": return /* @__PURE__ */ jsxRuntime.jsx(CommentGroupMention, { mention, ...props }); default: return core.assertNever(mention, "Unhandled mention kind"); } } function CommentLink({ href, children, className, ...props }) { const { Anchor } = components.useComponents(); return /* @__PURE__ */ jsxRuntime.jsx( index.Link, { className: cn.cn("lb-comment-link", className), href, ...props, asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(Anchor, { ...props, children }) } ); } function CommentNonInteractiveLink({ href: _href, children, className, ...props }) { return /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn.cn("lb-comment-link", className), ...props, children }); } const CommentReactionButton = react.forwardRef(({ reaction, overrides: overrides$1, className, ...props }, forwardedRef) => { const $ = overrides.useOverrides(overrides$1); return /* @__PURE__ */ jsxRuntime.jsxs( Button.CustomButton, { className: cn.cn("lb-comment-reaction", className), variant: "outline", "aria-label": $.COMMENT_REACTION_DESCRIPTION( reaction.emoji, reaction.users.length ), ...props, ref: forwardedRef, children: [ /* @__PURE__ */ jsxRuntime.jsx(Emoji.Emoji, { className: "lb-comment-reaction-emoji", emoji: reaction.emoji }), /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lb-comment-reaction-count", children: reaction.users.length }) ] } ); }); const CommentReaction = react.forwardRef(({ comment, reaction, overrides: overrides$1, disabled, ...props }, forwardedRef) => { const addReaction = _private.useAddRoomCommentReaction(comment.roomId); const removeReaction = _private.useRemoveRoomCommentReaction(comment.roomId); const currentId = shared.useCurrentUserId(); const isActive = react.useMemo(() => { return reaction.users.some((users) => users.id === currentId); }, [currentId, reaction]); const $ = overrides.useOverrides(overrides$1); const tooltipContent = react.useMemo( () => /* @__PURE__ */ jsxRuntime.jsx("span", { children: $.COMMENT_REACTION_LIST( /* @__PURE__ */ jsxRuntime.jsx( List.List, { values: reaction.users.map((users) => /* @__PURE__ */ jsxRuntime.jsx(User.User, { userId: users.id, replaceSelf: true }, users.id)), formatRemaining: $.LIST_REMAINING_USERS, truncate: REACTIONS_TRUNCATE, locale: $.locale } ), reaction.emoji, reaction.users.length ) }), [$, reaction] ); const stopPropagation = react.useCallback((event) => { event.stopPropagation(); }, []); const handlePressedChange = react.useCallback( (isPressed) => { if (isPressed) { addReaction({ threadId: comment.threadId, commentId: comment.id, emoji: reaction.emoji }); } else { removeReaction({ threadId: comment.threadId, commentId: comment.id, emoji: reaction.emoji }); } }, [addReaction, comment.threadId, comment.id, reaction.emoji, removeReaction] ); return /* @__PURE__ */ jsxRuntime.jsx( Tooltip.Tooltip, { content: tooltipContent, multiline: true, className: "lb-comment-reaction-tooltip", children: /* @__PURE__ */ jsxRuntime.jsx( TogglePrimitive__namespace.Root, { asChild: true, pressed: isActive, onPressedChange: handlePressedChange, onClick: stopPropagation, disabled, ref: forwardedRef, children: /* @__PURE__ */ jsxRuntime.jsx( CommentReactionButton, { "data-self": isActive ? "" : void 0, reaction, overrides: overrides$1, ...props } ) } ) } ); }); const CommentNonInteractiveReaction = react.forwardRef(({ reaction, overrides, ...props }, forwardedRef) => { const currentId = shared.useCurrentUserId(); const isActive = react.useMemo(() => { return reaction.users.some((users) => users.id === currentId); }, [currentId, reaction]); return /* @__PURE__ */ jsxRuntime.jsx( CommentReactionButton, { disableable: false, "data-self": isActive ? "" : void 0, reaction, overrides, ...props, ref: forwardedRef } ); }); function openAttachment({ attachment, url }) { if (attachment.mimeType === "application/pdf" || attachment.mimeType.startsWith("image/") || attachment.mimeType.startsWith("video/") || attachment.mimeType.startsWith("audio/")) { window.open(url, "_blank"); } else { download.download(url, attachment.name); } } function CommentMediaAttachment({ attachment, onAttachmentClick, roomId, className, overrides, ...props }) { const { url } = _private.useRoomAttachmentUrl(attachment.id, roomId); const handleClick = react.useCallback( (event) => { if (!url) { return; } const args = { attachment, url }; onAttachmentClick?.(args, event); if (event.isDefaultPrevented()) { return; } openAttachment(args); }, [attachment, onAttachmentClick, url] ); return /* @__PURE__ */ jsxRuntime.jsx( Attachment.MediaAttachment, { className: cn.cn("lb-comment-attachment", className), ...props, attachment, overrides, onClick: url ? handleClick : void 0, roomId } ); } function CommentFileAttachment({ attachment, onAttachmentClick, roomId, className, overrides, ...props }) { const { url } = _private.useRoomAttachmentUrl(attachment.id, roomId); const handleClick = react.useCallback( (event) => { if (!url) { return; } const args = { attachment, url }; onAttachmentClick?.(args, event); if (event.isDefaultPrevented()) { return; } openAttachment(args); }, [attachment, onAttachmentClick, url] ); return /* @__PURE__ */ jsxRuntime.jsx( Attachment.FileAttachment, { className: cn.cn("lb-comment-attachment", className), ...props, attachment, overrides, onClick: url ? handleClick : void 0, roomId } ); } function CommentNonInteractiveFileAttachment({ className, ...props }) { return /* @__PURE__ */ jsxRuntime.jsx( Attachment.FileAttachment, { className: cn.cn("lb-comment-attachment", className), allowMediaPreview: false, ...props } ); } function AutoMarkReadThreadIdHandler({ threadId, roomId, commentRef }) { const markThreadAsRead = _private.useMarkRoomThreadAsRead(roomId); const isWindowFocused = useWindowFocus.useWindowFocus(); useVisible.useIntersectionCallback( commentRef, (isIntersecting) => { if (isIntersecting) { markThreadAsRead(threadId); } }, { // The underlying IntersectionObserver is only enabled when the window is focused enabled: isWindowFocused } ); return null; } const CommentDropdownItem = react.forwardRef(({ children, icon, onSelect, onClick, ...props }, forwardedRef) => { const handleClick = react.useCallback( (event) => { onClick?.(event); if (!event.isDefaultPrevented()) { event.stopPropagation(); } }, [onClick] ); return /* @__PURE__ */ jsxRuntime.jsx( Dropdown.DropdownItem, { icon, onSelect, onClick: handleClick, ...props, ref: forwardedRef, children } ); }); const Comment = Object.assign( react.forwardRef( ({ comment, indentContent = true, showDeleted, showActions = "hover", showReactions = true, showAttachments = true, showComposerFormattingControls = true, onAuthorClick, onMentionClick, onAttachmentClick, onCommentEdit, onCommentDelete, dropdownItems, overrides: overrides$1, components: components$1, className, actions, actionsClassName, autoMarkReadThreadId, ...props }, forwardedRef) => { const ref = react.useRef(null); const mergedRefs = useRefs.useRefs(forwardedRef, ref); const currentUserId = shared.useCurrentUserId(); const deleteComment = _private.useDeleteRoomComment(comment.roomId); const editComment = _private.useEditRoomComment(comment.roomId); const addReaction = _private.useAddRoomCommentReaction(comment.roomId); const removeReaction = _private.useRemoveRoomCommentReaction(comment.roomId); const $ = overrides.useOverrides(overrides$1); const [isEditing, setEditing] = react.useState(false); const [isTarget, setTarget] = react.useState(false); const [isMoreActionOpen, setMoreActionOpen] = react.useState(false); const [isReactionActionOpen, setReactionActionOpen] = react.useState(false); const { mediaAttachments, fileAttachments } = react.useMemo(() => { return Attachment.separateMediaAttachments(comment.attachments); }, [comment.attachments]); const permissions = _private.useRoomPermissions(comment.roomId); const canComment = permissions.size > 0 ? permissions.has(core.Permission.CommentsWrite) || permissions.has(core.Permission.Write) : true; const stopPropagation = react.useCallback((event) => { event.stopPropagation(); }, []); const handleEdit = react.useCallback(() => { setEditing(true); }, []); const handleEditCancel = react.useCallback( (event) => { event.stopPropagation(); setEditing(false); }, [] ); const handleEditSubmit = react.useCallback( ({ body, attachments }, event) => { onCommentEdit?.(comment); if (event.isDefaultPrevented()) { return; } event.stopPropagation(); event.preventDefault(); setEditing(false); editComment({ commentId: comment.id, threadId: comment.threadId, body, attachments }); }, [comment, editComment, onCommentEdit] ); const handleDelete = react.useCallback(() => { onCommentDelete?.(comment); deleteComment({ commentId: comment.id, threadId: comment.threadId }); }, [comment, deleteComment, onCommentDelete]); const handleAuthorClick = react.useCallback( (event) => { onAuthorClick?.(comment.userId, event); }, [comment.userId, onAuthorClick] ); const handleReactionSelect = react.useCallback( (emoji) => { const reactionIndex = comment.reactions.findIndex( (reaction) => reaction.emoji === emoji ); if (reactionIndex >= 0 && currentUserId && comment.reactions[reactionIndex]?.users.some( (user) => user.id === currentUserId )) { removeReaction({ threadId: comment.threadId, commentId: comment.id, emoji }); } else { addReaction({ threadId: comment.threadId, commentId: comment.id, emoji }); } }, [ addReaction, comment.id, comment.reactions, comment.threadId, removeReaction, currentUserId ] ); react.useEffect(() => { const isWindowDefined = typeof window !== "undefined"; if (!isWindowDefined) return; const hash = window.location.hash; const commentId = hash.slice(1); if (commentId === comment.id) { setTarget(true); } }, []); if (!showDeleted && !comment.body) { return null; } const defaultDropdownItems = comment.userId === currentUserId ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [ /* @__PURE__ */ jsxRuntime.jsx(CommentDropdownItem, { onSelect: handleEdit, icon: /* @__PURE__ */ jsxRuntime.jsx(Edit.EditIcon, {}), children: $.COMMENT_EDIT }), /* @__PURE__ */ jsxRuntime.jsx(CommentDropdownItem, { onSelect: handleDelete, icon: /* @__PURE__ */ jsxRuntime.jsx(Delete.DeleteIcon, {}), children: $.COMMENT_DELETE }) ] }) : null; const dropdownContent = typeof dropdownItems === "function" ? dropdownItems({ children: defaultDropdownItems, comment }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [ defaultDropdownItems, dropdownItems ] }); return /* @__PURE__ */ jsxRuntime.jsx(TooltipPrimitive.TooltipProvider, { children: /* @__PURE__ */ jsxRuntime.jsxs(components.ComponentsProvider, { components: components$1, children: [ autoMarkReadThreadId && /* @__PURE__ */ jsxRuntime.jsx( AutoMarkReadThreadIdHandler, { commentRef: ref, threadId: autoMarkReadThreadId, roomId: comment.roomId } ), /* @__PURE__ */ jsxRuntime.jsxs( "div", { id: comment.id, className: cn.cn( "lb-root lb-comment", indentContent && "lb-comment:indent-content", showActions === "hover" && "lb-comment:show-actions-hover", (isMoreActionOpen || isReactionActionOpen) && "lb-comment:action-open", className ), "data-deleted": !comment.body ? "" : void 0, "data-editing": isEditing ? "" : void 0, "data-target": isTarget ? "" : void 0, dir: $.dir, ...props, ref: mergedRefs, children: [ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lb-comment-header", children: [ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lb-comment-details", children: [ /* @__PURE__ */ jsxRuntime.jsx( Avatar.Avatar, { className: "lb-comment-avatar", userId: comment.userId, onClick: handleAuthorClick } ), /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "lb-comment-details-labels", children: [ /* @__PURE__ */ jsxRuntime.jsx( User.User, { className: "lb-comment-author", userId: comment.userId, onClick: handleAuthorClick } ), /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "lb-comment-date", children: [ /* @__PURE__ */ jsxRuntime.jsx( Timestamp.Timestamp, { locale: $.locale, date: comment.createdAt, className: "lb-date lb-comment-date-created" } ), comment.editedAt && comment.body && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [ " ", /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lb-comment-date-edited", children: $.COMMENT_EDITED }) ] }) ] }) ] }) ] }), showActions && !isEditing && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn.cn("lb-comment-actions", actionsClassName), children: [ actions ?? null, showReactions && canComment ? /* @__PURE__ */ jsxRuntime.jsx( EmojiPicker.EmojiPicker, { onEmojiSelect: handleReactionSelect, onOpenChange: setReactionActionOpen, children: /* @__PURE__ */ jsxRuntime.jsx(Tooltip.Tooltip, { content: $.COMMENT_ADD_REACTION, children: /* @__PURE__ */ jsxRuntime.jsx(PopoverPrimitive.PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx( Button.Button, { className: "lb-comment-action", onClick: stopPropagation, "aria-label": $.COMMENT_ADD_REACTION, icon: /* @__PURE__ */ jsxRuntime.jsx(EmojiPlus.EmojiPlusIcon, {}) } ) }) }) } ) : null, dropdownContent ? /* @__PURE__ */ jsxRuntime.jsx( Dropdown.Dropdown, { open: isMoreActionOpen, onOpenChange: setMoreActionOpen, align: "end", content: dropdownContent, children: /* @__PURE__ */ jsxRuntime.jsx(Tooltip.Tooltip, { content: $.COMMENT_MORE, children: /* @__PURE__ */ jsxRuntime.jsx(DropdownMenuPrimitive.DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx( Button.Button, { className: "lb-comment-action", disabled: !comment.body, onClick: stopPropagation, "aria-label": $.COMMENT_MORE, icon: /* @__PURE__ */ jsxRuntime.jsx(Ellipsis.EllipsisIcon, {}) } ) }) }) } ) : null ] }) ] }), /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lb-comment-content", children: isEditing ? /* @__PURE__ */ jsxRuntime.jsx( Composer.Composer, { className: "lb-comment-composer", onComposerSubmit: handleEditSubmit, defaultValue: comment.body, defaultAttachments: comment.attachments, autoFocus: true, showAttribution: false, showAttachments, showFormattingControls: showComposerFormattingControls, actions: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [ /* @__PURE__ */ jsxRuntime.jsx( Tooltip.Tooltip, { content: $.COMMENT_EDIT_COMPOSER_CANCEL, "aria-label": $.COMMENT_EDIT_COMPOSER_CANCEL, children: /* @__PURE__ */ jsxRuntime.jsx( Button.Button, { className: "lb-composer-action", onClick: handleEditCancel, icon: /* @__PURE__ */ jsxRuntime.jsx(Cross.CrossIcon, {}) } ) } ), /* @__PURE__ */ jsxRuntime.jsx( Tooltip.ShortcutTooltip, { content: $.COMMENT_EDIT_COMPOSER_SAVE, shortcut: "Enter", children: /* @__PURE__ */ jsxRuntime.jsx(index$1.Submit, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx( Button.Button, { variant: "primary", className: "lb-composer-action", onClick: stopPropagation, "aria-label": $.COMMENT_EDIT_COMPOSER_SAVE, icon: /* @__PURE__ */ jsxRuntime.jsx(Check.CheckIcon, {}) } ) }) } ) ] }), overrides: { COMPOSER_PLACEHOLDER: $.COMMENT_EDIT_COMPOSER_PLACEHOLDER }, roomId: comment.roomId } ) : comment.body ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [ /* @__PURE__ */ jsxRuntime.jsx( index.Body, { className: "lb-comment-body", body: comment.body, components: { Mention: ({ mention }) => /* @__PURE__ */ jsxRuntime.jsx( CommentMention, { mention, onClick: (event) => onMentionClick?.(mention, event), overrides: overrides$1 } ), Link: CommentLink } } ), showAttachments && (mediaAttachments.length > 0 || fileAttachments.length > 0) ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lb-comment-attachments", children: [ mediaAttachments.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lb-attachments", children: mediaAttachments.map((attachment) => /* @__PURE__ */ jsxRuntime.jsx( CommentMediaAttachment, { attachment, overrides: overrides$1, onAttachmentClick, roomId: comment.roomId }, attachment.id )) }) : null, fileAttachments.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lb-attachments", children: fileAttachments.map((attachment) => /* @__PURE__ */ jsxRuntime.jsx( CommentFileAttachment, { attachment, overrides: overrides$1, onAttachmentClick, roomId: comment.roomId }, attachment.id )) }) : null ] }) : null, showReactions && comment.reactions.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lb-comment-reactions", children: [ comment.reactions.map((reaction) => /* @__PURE__ */ jsxRuntime.jsx( CommentReaction, { comment, reaction, overrides: overrides$1, disabled: !canComment }, reaction.emoji )), canComment ? /* @__PURE__ */ jsxRuntime.jsx(EmojiPicker.EmojiPicker, { onEmojiSelect: handleReactionSelect, children: /* @__PURE__ */ jsxRuntime.jsx(Tooltip.Tooltip, { content: $.COMMENT_ADD_REACTION, children: /* @__PURE__ */ jsxRuntime.jsx(PopoverPrimitive.PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx( Button.Button, { className: "lb-comment-reaction lb-comment-reaction-add", variant: "outline", onClick: stopPropagation, "aria-label": $.COMMENT_ADD_REACTION, icon: /* @__PURE__ */ jsxRuntime.jsx(EmojiPlus.EmojiPlusIcon, {}) } ) }) }) }) : null ] }) ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lb-comment-body", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "lb-comment-deleted", children: $.COMMENT_DELETED }) }) }) ] } ) ] }) }); } ), { /** * Displays a dropdown item in the comment's dropdown. */ DropdownItem: CommentDropdownItem } ); exports.Comment = Comment; exports.CommentLink = CommentLink; exports.CommentMention = CommentMention; exports.CommentNonInteractiveFileAttachment = CommentNonInteractiveFileAttachment; exports.CommentNonInteractiveLink = CommentNonInteractiveLink; exports.CommentNonInteractiveReaction = CommentNonInteractiveReaction; exports.CommentReaction = CommentReaction; //# sourceMappingURL=Comment.cjs.map