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.

656 lines (652 loc) 23.7 kB
"use client"; import { jsxs, jsx, Fragment } from 'react/jsx-runtime'; import { useAddRoomCommentReaction, useRemoveRoomCommentReaction, useRoomAttachmentUrl, useMarkRoomThreadAsRead, useDeleteRoomComment, useEditRoomComment } from '@liveblocks/react/_private'; import * as TogglePrimitive from '@radix-ui/react-toggle'; import { forwardRef, useMemo, useCallback, useRef, useState, useEffect } from 'react'; import { CheckIcon } from '../icons/Check.js'; import { CrossIcon } from '../icons/Cross.js'; import { DeleteIcon } from '../icons/Delete.js'; import { EditIcon } from '../icons/Edit.js'; import { EllipsisIcon } from '../icons/Ellipsis.js'; import { EmojiPlusIcon } from '../icons/EmojiPlus.js'; import { useOverrides } from '../overrides.js'; import { Mention as CommentMention$1, Link as CommentLink$1, Body as CommentBody } from '../primitives/Comment/index.js'; import { Submit as ComposerSubmit } from '../primitives/Composer/index.js'; import { Timestamp } from '../primitives/Timestamp.js'; import { useCurrentUserId } from '../shared.js'; import { MENTION_CHARACTER } from '../slate/plugins/mentions.js'; import { classNames } from '../utils/class-names.js'; import { download } from '../utils/download.js'; import { useRefs } from '../utils/use-refs.js'; import { useVisibleCallback } from '../utils/use-visible.js'; import { useWindowFocus } from '../utils/use-window-focus.js'; import { Composer } from './Composer.js'; import { MediaAttachment, FileAttachment, separateMediaAttachments } from './internal/Attachment.js'; import { Avatar } from './internal/Avatar.js'; import { CustomButton, Button } from './internal/Button.js'; import { Dropdown, DropdownItem } from './internal/Dropdown.js'; import { Emoji } from './internal/Emoji.js'; import { EmojiPicker } from './internal/EmojiPicker.js'; import { List } from './internal/List.js'; import { Tooltip, ShortcutTooltip } from './internal/Tooltip.js'; import { User } from './internal/User.js'; import { TooltipProvider } from '@radix-ui/react-tooltip'; import { PopoverTrigger } from '@radix-ui/react-popover'; import { DropdownMenuTrigger } from '@radix-ui/react-dropdown-menu'; const REACTIONS_TRUNCATE = 5; function CommentMention({ userId, className, ...props }) { const currentId = useCurrentUserId(); return /* @__PURE__ */ jsxs(CommentMention$1, { className: classNames("lb-comment-mention", className), "data-self": userId === currentId ? "" : void 0, ...props, children: [ MENTION_CHARACTER, /* @__PURE__ */ jsx(User, { userId }) ] }); } function CommentLink({ href, children, className, ...props }) { return /* @__PURE__ */ jsx(CommentLink$1, { className: classNames("lb-comment-link", className), href, ...props, children }); } function CommentNonInteractiveLink({ href: _href, children, className, ...props }) { return /* @__PURE__ */ jsx("span", { className: classNames("lb-comment-link", className), ...props, children }); } const CommentReactionButton = forwardRef(({ reaction, overrides, className, ...props }, forwardedRef) => { const $ = useOverrides(overrides); return /* @__PURE__ */ jsxs(CustomButton, { className: classNames("lb-comment-reaction", className), variant: "outline", "aria-label": $.COMMENT_REACTION_DESCRIPTION( reaction.emoji, reaction.users.length ), ...props, ref: forwardedRef, children: [ /* @__PURE__ */ jsx(Emoji, { className: "lb-comment-reaction-emoji", emoji: reaction.emoji }), /* @__PURE__ */ jsx("span", { className: "lb-comment-reaction-count", children: reaction.users.length }) ] }); }); const CommentReaction = forwardRef(({ comment, reaction, overrides, disabled, ...props }, forwardedRef) => { const addReaction = useAddRoomCommentReaction(comment.roomId); const removeReaction = useRemoveRoomCommentReaction(comment.roomId); const currentId = useCurrentUserId(); const isActive = useMemo(() => { return reaction.users.some((users) => users.id === currentId); }, [currentId, reaction]); const $ = useOverrides(overrides); const tooltipContent = useMemo( () => /* @__PURE__ */ jsx("span", { children: $.COMMENT_REACTION_LIST( /* @__PURE__ */ jsx(List, { values: reaction.users.map((users) => /* @__PURE__ */ jsx(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 = useCallback((event) => { event.stopPropagation(); }, []); const handlePressedChange = 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__ */ jsx(Tooltip, { content: tooltipContent, multiline: true, className: "lb-comment-reaction-tooltip", children: /* @__PURE__ */ jsx(TogglePrimitive.Root, { asChild: true, pressed: isActive, onPressedChange: handlePressedChange, onClick: stopPropagation, disabled, ref: forwardedRef, children: /* @__PURE__ */ jsx(CommentReactionButton, { "data-self": isActive ? "" : void 0, reaction, overrides, ...props }) }) }); }); const CommentNonInteractiveReaction = forwardRef(({ reaction, overrides, ...props }, forwardedRef) => { const currentId = useCurrentUserId(); const isActive = useMemo(() => { return reaction.users.some((users) => users.id === currentId); }, [currentId, reaction]); return /* @__PURE__ */ 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(url, attachment.name); } } function CommentMediaAttachment({ attachment, onAttachmentClick, roomId, className, overrides, ...props }) { const { url } = useRoomAttachmentUrl(attachment.id, roomId); const handleClick = useCallback( (event) => { if (!url) { return; } const args = { attachment, url }; onAttachmentClick?.(args, event); if (event.isDefaultPrevented()) { return; } openAttachment(args); }, [attachment, onAttachmentClick, url] ); return /* @__PURE__ */ jsx(MediaAttachment, { className: classNames("lb-comment-attachment", className), ...props, attachment, overrides, onClick: url ? handleClick : void 0, roomId }); } function CommentFileAttachment({ attachment, onAttachmentClick, roomId, className, overrides, ...props }) { const { url } = useRoomAttachmentUrl(attachment.id, roomId); const handleClick = useCallback( (event) => { if (!url) { return; } const args = { attachment, url }; onAttachmentClick?.(args, event); if (event.isDefaultPrevented()) { return; } openAttachment(args); }, [attachment, onAttachmentClick, url] ); return /* @__PURE__ */ jsx(FileAttachment, { className: classNames("lb-comment-attachment", className), ...props, attachment, overrides, onClick: url ? handleClick : void 0, roomId }); } function CommentNonInteractiveFileAttachment({ className, ...props }) { return /* @__PURE__ */ jsx(FileAttachment, { className: classNames("lb-comment-attachment", className), allowMediaPreview: false, ...props }); } function AutoMarkReadThreadIdHandler({ threadId, roomId, commentRef }) { const markThreadAsRead = useMarkRoomThreadAsRead(roomId); const isWindowFocused = useWindowFocus(); useVisibleCallback( commentRef, () => { markThreadAsRead(threadId); }, { enabled: isWindowFocused } ); return null; } const Comment = forwardRef( ({ comment, indentContent = true, showDeleted, showActions = "hover", showReactions = true, showAttachments = true, showComposerFormattingControls = true, onAuthorClick, onMentionClick, onAttachmentClick, onCommentEdit, onCommentDelete, overrides, className, additionalActions, additionalActionsClassName, autoMarkReadThreadId, ...props }, forwardedRef) => { const ref = useRef(null); const mergedRefs = useRefs(forwardedRef, ref); const currentUserId = useCurrentUserId(); const deleteComment = useDeleteRoomComment(comment.roomId); const editComment = useEditRoomComment(comment.roomId); const addReaction = useAddRoomCommentReaction(comment.roomId); const removeReaction = useRemoveRoomCommentReaction(comment.roomId); const $ = useOverrides(overrides); const [isEditing, setEditing] = useState(false); const [isTarget, setTarget] = useState(false); const [isMoreActionOpen, setMoreActionOpen] = useState(false); const [isReactionActionOpen, setReactionActionOpen] = useState(false); const { mediaAttachments, fileAttachments } = useMemo(() => { return separateMediaAttachments(comment.attachments); }, [comment.attachments]); const stopPropagation = useCallback((event) => { event.stopPropagation(); }, []); const handleEdit = useCallback(() => { setEditing(true); }, []); const handleEditCancel = useCallback( (event) => { event.stopPropagation(); setEditing(false); }, [] ); const handleEditSubmit = 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 = useCallback(() => { onCommentDelete?.(comment); deleteComment({ commentId: comment.id, threadId: comment.threadId }); }, [comment, deleteComment, onCommentDelete]); const handleAuthorClick = useCallback( (event) => { onAuthorClick?.(comment.userId, event); }, [comment.userId, onAuthorClick] ); const handleReactionSelect = 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 ] ); 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; } return /* @__PURE__ */ jsxs(TooltipProvider, { children: [ autoMarkReadThreadId && /* @__PURE__ */ jsx(AutoMarkReadThreadIdHandler, { commentRef: ref, threadId: autoMarkReadThreadId, roomId: comment.roomId }), /* @__PURE__ */ jsxs("div", { id: comment.id, className: classNames( "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__ */ jsxs("div", { className: "lb-comment-header", children: [ /* @__PURE__ */ jsxs("div", { className: "lb-comment-details", children: [ /* @__PURE__ */ jsx(Avatar, { className: "lb-comment-avatar", userId: comment.userId, onClick: handleAuthorClick }), /* @__PURE__ */ jsxs("span", { className: "lb-comment-details-labels", children: [ /* @__PURE__ */ jsx(User, { className: "lb-comment-author", userId: comment.userId, onClick: handleAuthorClick }), /* @__PURE__ */ jsxs("span", { className: "lb-comment-date", children: [ /* @__PURE__ */ jsx(Timestamp, { locale: $.locale, date: comment.createdAt, className: "lb-date lb-comment-date-created" }), comment.editedAt && comment.body && /* @__PURE__ */ jsxs(Fragment, { children: [ " ", /* @__PURE__ */ jsx("span", { className: "lb-comment-date-edited", children: $.COMMENT_EDITED }) ] }) ] }) ] }) ] }), showActions && !isEditing && /* @__PURE__ */ jsxs("div", { className: classNames( "lb-comment-actions", additionalActionsClassName ), children: [ additionalActions ?? null, showReactions && /* @__PURE__ */ jsx(EmojiPicker, { onEmojiSelect: handleReactionSelect, onOpenChange: setReactionActionOpen, children: /* @__PURE__ */ jsx(Tooltip, { content: $.COMMENT_ADD_REACTION, children: /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { className: "lb-comment-action", onClick: stopPropagation, "aria-label": $.COMMENT_ADD_REACTION, icon: /* @__PURE__ */ jsx(EmojiPlusIcon, {}) }) }) }) }), comment.userId === currentUserId && /* @__PURE__ */ jsx(Dropdown, { open: isMoreActionOpen, onOpenChange: setMoreActionOpen, align: "end", content: /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx(DropdownItem, { onSelect: handleEdit, onClick: stopPropagation, icon: /* @__PURE__ */ jsx(EditIcon, {}), children: $.COMMENT_EDIT }), /* @__PURE__ */ jsx(DropdownItem, { onSelect: handleDelete, onClick: stopPropagation, icon: /* @__PURE__ */ jsx(DeleteIcon, {}), children: $.COMMENT_DELETE }) ] }), children: /* @__PURE__ */ jsx(Tooltip, { content: $.COMMENT_MORE, children: /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { className: "lb-comment-action", disabled: !comment.body, onClick: stopPropagation, "aria-label": $.COMMENT_MORE, icon: /* @__PURE__ */ jsx(EllipsisIcon, {}) }) }) }) }) ] }) ] }), /* @__PURE__ */ jsx("div", { className: "lb-comment-content", children: isEditing ? /* @__PURE__ */ jsx(Composer, { className: "lb-comment-composer", onComposerSubmit: handleEditSubmit, defaultValue: comment.body, defaultAttachments: comment.attachments, autoFocus: true, showAttribution: false, showAttachments, showFormattingControls: showComposerFormattingControls, actions: /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx(Tooltip, { content: $.COMMENT_EDIT_COMPOSER_CANCEL, "aria-label": $.COMMENT_EDIT_COMPOSER_CANCEL, children: /* @__PURE__ */ jsx(Button, { className: "lb-composer-action", onClick: handleEditCancel, icon: /* @__PURE__ */ jsx(CrossIcon, {}) }) }), /* @__PURE__ */ jsx(ShortcutTooltip, { content: $.COMMENT_EDIT_COMPOSER_SAVE, shortcut: "Enter", children: /* @__PURE__ */ jsx(ComposerSubmit, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "primary", className: "lb-composer-action", onClick: stopPropagation, "aria-label": $.COMMENT_EDIT_COMPOSER_SAVE, icon: /* @__PURE__ */ jsx(CheckIcon, {}) }) }) }) ] }), overrides: { COMPOSER_PLACEHOLDER: $.COMMENT_EDIT_COMPOSER_PLACEHOLDER }, roomId: comment.roomId }) : comment.body ? /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx(CommentBody, { className: "lb-comment-body", body: comment.body, components: { Mention: ({ userId }) => /* @__PURE__ */ jsx(CommentMention, { userId, onClick: (event) => onMentionClick?.(userId, event) }), Link: CommentLink } }), showAttachments && (mediaAttachments.length > 0 || fileAttachments.length > 0) ? /* @__PURE__ */ jsxs("div", { className: "lb-comment-attachments", children: [ mediaAttachments.length > 0 ? /* @__PURE__ */ jsx("div", { className: "lb-attachments", children: mediaAttachments.map((attachment) => /* @__PURE__ */ jsx(CommentMediaAttachment, { attachment, overrides, onAttachmentClick, roomId: comment.roomId }, attachment.id)) }) : null, fileAttachments.length > 0 ? /* @__PURE__ */ jsx("div", { className: "lb-attachments", children: fileAttachments.map((attachment) => /* @__PURE__ */ jsx(CommentFileAttachment, { attachment, overrides, onAttachmentClick, roomId: comment.roomId }, attachment.id)) }) : null ] }) : null, showReactions && comment.reactions.length > 0 && /* @__PURE__ */ jsxs("div", { className: "lb-comment-reactions", children: [ comment.reactions.map((reaction) => /* @__PURE__ */ jsx(CommentReaction, { comment, reaction, overrides }, reaction.emoji)), /* @__PURE__ */ jsx(EmojiPicker, { onEmojiSelect: handleReactionSelect, children: /* @__PURE__ */ jsx(Tooltip, { content: $.COMMENT_ADD_REACTION, children: /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { className: "lb-comment-reaction lb-comment-reaction-add", variant: "outline", onClick: stopPropagation, "aria-label": $.COMMENT_ADD_REACTION, icon: /* @__PURE__ */ jsx(EmojiPlusIcon, {}) }) }) }) }) ] }) ] }) : /* @__PURE__ */ jsx("div", { className: "lb-comment-body", children: /* @__PURE__ */ jsx("p", { className: "lb-comment-deleted", children: $.COMMENT_DELETED }) }) }) ] }) ] }); } ); export { Comment, CommentLink, CommentMention, CommentNonInteractiveFileAttachment, CommentNonInteractiveLink, CommentNonInteractiveReaction, CommentReaction }; //# sourceMappingURL=Comment.js.map