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