@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.
580 lines (576 loc) • 20 kB
JavaScript
"use client";
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
import { assertNever, generateUrl, sanitizeUrl, warnOnce } from '@liveblocks/core';
import { useMarkInboxNotificationAsRead, useDeleteInboxNotification, useClient, useInboxNotificationThread, useRoomInfo } from '@liveblocks/react';
import { useRoomThreadSubscription } from '@liveblocks/react/_private';
import { Slot } from '@radix-ui/react-slot';
import { TooltipProvider } from '@radix-ui/react-tooltip';
import { forwardRef, useState, useCallback, useMemo } from 'react';
import { useComponents } from '../components.js';
import { BellIcon } from '../icons/Bell.js';
import { BellCrossedIcon } from '../icons/BellCrossed.js';
import { CheckIcon } from '../icons/Check.js';
import { DeleteIcon } from '../icons/Delete.js';
import { EllipsisIcon } from '../icons/Ellipsis.js';
import { WarningIcon } from '../icons/Warning.js';
import { useOverrides } from '../overrides.js';
import { Timestamp } from '../primitives/Timestamp.js';
import { useCurrentUserId } from '../shared.js';
import { cn } from '../utils/cn.js';
import { Avatar } from './internal/Avatar.js';
import { Button } from './internal/Button.js';
import { CodeBlock } from './internal/CodeBlock.js';
import { Dropdown, DropdownItem } from './internal/Dropdown.js';
import { generateInboxNotificationThreadContents, InboxNotificationComment, INBOX_NOTIFICATION_THREAD_MAX_COMMENTS } from './internal/InboxNotificationThread.js';
import { List } from './internal/List.js';
import { Room } from './internal/Room.js';
import { Tooltip } from './internal/Tooltip.js';
import { User } from './internal/User.js';
import { DropdownMenuTrigger } from '@radix-ui/react-dropdown-menu';
const InboxNotificationLayout = forwardRef(
({
inboxNotification,
children,
aside,
title,
date,
unread,
markAsReadOnClick,
onClick,
href,
showActions,
overrides,
components,
className,
asChild,
additionalDropdownItemsBefore,
additionalDropdownItemsAfter,
...props
}, forwardedRef) => {
const $ = useOverrides(overrides);
const { Anchor } = useComponents(components);
const Component = asChild ? Slot : Anchor;
const [isMoreActionOpen, setMoreActionOpen] = useState(false);
const markInboxNotificationAsRead = useMarkInboxNotificationAsRead();
const deleteInboxNotification = useDeleteInboxNotification();
const handleClick = useCallback(
(event) => {
onClick?.(event);
const shouldMarkAsReadOnClick = markAsReadOnClick ?? Boolean(href);
if (unread && shouldMarkAsReadOnClick) {
markInboxNotificationAsRead(inboxNotification.id);
}
},
[
href,
inboxNotification.id,
markAsReadOnClick,
markInboxNotificationAsRead,
onClick,
unread
]
);
const stopPropagation = useCallback((event) => {
event.stopPropagation();
}, []);
const preventDefaultAndStopPropagation = useCallback(
(event) => {
event.preventDefault();
event.stopPropagation();
},
[]
);
const handleMoreClick = useCallback((event) => {
event.preventDefault();
event.stopPropagation();
setMoreActionOpen((open) => !open);
}, []);
const handleMarkAsRead = useCallback(() => {
markInboxNotificationAsRead(inboxNotification.id);
}, [inboxNotification.id, markInboxNotificationAsRead]);
const handleDelete = useCallback(() => {
deleteInboxNotification(inboxNotification.id);
}, [inboxNotification.id, deleteInboxNotification]);
return /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(
Component,
{
className: cn(
"lb-root lb-inbox-notification",
showActions === "hover" && "lb-inbox-notification:show-actions-hover",
isMoreActionOpen && "lb-inbox-notification:action-open",
className
),
dir: $.dir,
"data-unread": unread ? "" : void 0,
"data-kind": inboxNotification.kind,
onClick: handleClick,
href,
...props,
ref: forwardedRef,
children: [
aside && /* @__PURE__ */ jsx("div", { className: "lb-inbox-notification-aside", children: aside }),
/* @__PURE__ */ jsxs("div", { className: "lb-inbox-notification-content", children: [
/* @__PURE__ */ jsxs("div", { className: "lb-inbox-notification-header", children: [
/* @__PURE__ */ jsx("span", { className: "lb-inbox-notification-title", children: title }),
/* @__PURE__ */ jsx("div", { className: "lb-inbox-notification-details", children: /* @__PURE__ */ jsxs("span", { className: "lb-inbox-notification-details-labels", children: [
/* @__PURE__ */ jsx(
Timestamp,
{
locale: $.locale,
date,
className: "lb-date lb-inbox-notification-date"
}
),
unread && /* @__PURE__ */ jsx(
"span",
{
className: "lb-inbox-notification-unread-indicator",
role: "presentation"
}
)
] }) }),
showActions && /* @__PURE__ */ jsx("div", { className: "lb-inbox-notification-actions", children: /* @__PURE__ */ jsx(
Dropdown,
{
open: isMoreActionOpen,
onOpenChange: setMoreActionOpen,
align: "end",
content: /* @__PURE__ */ jsxs(Fragment, { children: [
additionalDropdownItemsBefore,
unread ? /* @__PURE__ */ jsx(
DropdownItem,
{
onSelect: handleMarkAsRead,
onClick: stopPropagation,
icon: /* @__PURE__ */ jsx(CheckIcon, {}),
children: $.INBOX_NOTIFICATION_MARK_AS_READ
}
) : null,
/* @__PURE__ */ jsx(
DropdownItem,
{
onSelect: handleDelete,
onClick: stopPropagation,
icon: /* @__PURE__ */ jsx(DeleteIcon, {}),
children: $.INBOX_NOTIFICATION_DELETE
}
),
additionalDropdownItemsAfter
] }),
children: /* @__PURE__ */ jsx(Tooltip, { content: $.INBOX_NOTIFICATION_MORE, children: /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
Button,
{
className: "lb-inbox-notification-action",
onClick: handleMoreClick,
onPointerDown: preventDefaultAndStopPropagation,
onPointerUp: preventDefaultAndStopPropagation,
"aria-label": $.INBOX_NOTIFICATION_MORE,
icon: /* @__PURE__ */ jsx(EllipsisIcon, {})
}
) }) })
}
) })
] }),
/* @__PURE__ */ jsx("div", { className: "lb-inbox-notification-body", children })
] })
]
}
) });
}
);
function InboxNotificationIcon({
className,
...props
}) {
return /* @__PURE__ */ jsx("div", { className: cn("lb-inbox-notification-icon", className), ...props });
}
function InboxNotificationAvatar({
className,
...props
}) {
return /* @__PURE__ */ jsx(
Avatar,
{
className: cn("lb-inbox-notification-avatar", className),
...props
}
);
}
const InboxNotificationThread = forwardRef(
({
inboxNotification,
href,
showRoomName = true,
showReactions = true,
showAttachments = true,
showActions = "hover",
overrides,
...props
}, forwardedRef) => {
const $ = useOverrides(overrides);
const client = useClient();
const thread = useInboxNotificationThread(inboxNotification.id);
const {
status: subscriptionStatus,
subscribe,
unsubscribe
} = useRoomThreadSubscription(thread.roomId, thread.id);
const currentUserId = useCurrentUserId();
const { info } = useRoomInfo(inboxNotification.roomId);
const contents = useMemo(() => {
const contents2 = generateInboxNotificationThreadContents(
client,
inboxNotification,
thread,
currentUserId ?? ""
);
if (contents2.comments.length === 0 || contents2.userIds.length === 0) {
return null;
}
switch (contents2.type) {
case "comments": {
const reversedUserIds = [...contents2.userIds].reverse();
const firstUserId = reversedUserIds[0];
const aside2 = /* @__PURE__ */ jsx(InboxNotificationAvatar, { userId: firstUserId });
const title2 = $.INBOX_NOTIFICATION_THREAD_COMMENTS_LIST(
/* @__PURE__ */ jsx(
List,
{
values: reversedUserIds.map((userId) => /* @__PURE__ */ jsx(User, { userId, replaceSelf: true }, userId)),
formatRemaining: $.LIST_REMAINING_USERS,
truncate: INBOX_NOTIFICATION_THREAD_MAX_COMMENTS - 1,
locale: $.locale
}
),
showRoomName ? /* @__PURE__ */ jsx(Room, { roomId: thread.roomId }) : void 0,
reversedUserIds.length
);
const content2 = /* @__PURE__ */ jsx("div", { className: "lb-inbox-notification-comments", children: contents2.comments.map((comment) => /* @__PURE__ */ jsx(
InboxNotificationComment,
{
comment,
showHeader: contents2.comments.length > 1,
showAttachments,
showReactions,
overrides
},
comment.id
)) });
return {
unread: contents2.unread,
date: contents2.date,
aside: aside2,
title: title2,
content: content2,
threadId: thread.id,
commentId: contents2.comments[contents2.comments.length - 1].id
};
}
case "mention": {
const mentionCreatedBy = contents2.userIds[0];
const mentionComment = contents2.comments[0];
const aside2 = /* @__PURE__ */ jsx(InboxNotificationAvatar, { userId: mentionCreatedBy });
const title2 = $.INBOX_NOTIFICATION_THREAD_MENTION(
/* @__PURE__ */ jsx(User, { userId: mentionCreatedBy }, mentionCreatedBy),
showRoomName ? /* @__PURE__ */ jsx(Room, { roomId: thread.roomId }) : void 0
);
const content2 = /* @__PURE__ */ jsx("div", { className: "lb-inbox-notification-comments", children: /* @__PURE__ */ jsx(
InboxNotificationComment,
{
comment: mentionComment,
showHeader: false,
showAttachments,
showReactions,
overrides
},
mentionComment.id
) });
return {
unread: contents2.unread,
date: contents2.date,
aside: aside2,
title: title2,
content: content2,
threadId: thread.id,
commentId: mentionComment.id
};
}
default:
return assertNever(
contents2,
"Unexpected thread inbox notification type"
);
}
}, [
$,
client,
currentUserId,
inboxNotification,
overrides,
showRoomName,
showAttachments,
showReactions,
thread
]);
const resolvedHref = useMemo(() => {
const resolvedHref2 = href ?? info?.url;
return resolvedHref2 ? (
// Set the comment ID as the URL hash.
generateUrl(resolvedHref2, void 0, contents?.commentId)
) : void 0;
}, [contents?.commentId, href, info?.url]);
const handleSubscribeChange = useCallback(() => {
if (subscriptionStatus === "subscribed") {
unsubscribe();
} else {
subscribe();
}
}, [subscriptionStatus, subscribe, unsubscribe]);
const stopPropagation = useCallback((event) => {
event.stopPropagation();
}, []);
if (!contents) {
return null;
}
const { aside, title, content, date, unread } = contents;
return /* @__PURE__ */ jsx(
InboxNotificationLayout,
{
inboxNotification,
aside,
title,
date,
unread,
overrides,
href: resolvedHref,
showActions,
markAsReadOnClick: false,
additionalDropdownItemsBefore: /* @__PURE__ */ jsx(
DropdownItem,
{
onSelect: handleSubscribeChange,
onClick: stopPropagation,
icon: subscriptionStatus === "subscribed" ? /* @__PURE__ */ jsx(BellCrossedIcon, {}) : /* @__PURE__ */ jsx(BellIcon, {}),
children: subscriptionStatus === "subscribed" ? $.THREAD_UNSUBSCRIBE : $.THREAD_SUBSCRIBE
}
),
...props,
ref: forwardedRef,
children: content
}
);
}
);
const InboxNotificationTextMention = forwardRef(
({
inboxNotification,
showActions = "hover",
showRoomName = true,
href,
overrides,
...props
}, ref) => {
const $ = useOverrides(overrides);
const { info } = useRoomInfo(inboxNotification.roomId);
const resolvedHref = useMemo(() => {
const resolvedHref2 = href ?? info?.url;
return resolvedHref2 ? sanitizeUrl(resolvedHref2) ?? void 0 : void 0;
}, [href, info?.url]);
const unread = useMemo(() => {
return !inboxNotification.readAt || inboxNotification.notifiedAt > inboxNotification.readAt;
}, [inboxNotification.notifiedAt, inboxNotification.readAt]);
return /* @__PURE__ */ jsx(
InboxNotificationLayout,
{
inboxNotification,
aside: /* @__PURE__ */ jsx(InboxNotificationAvatar, { userId: inboxNotification.createdBy }),
title: $.INBOX_NOTIFICATION_TEXT_MENTION(
/* @__PURE__ */ jsx(
User,
{
userId: inboxNotification.createdBy
},
inboxNotification.createdBy
),
showRoomName ? /* @__PURE__ */ jsx(Room, { roomId: inboxNotification.roomId }) : void 0
),
date: inboxNotification.notifiedAt,
unread,
overrides,
showActions,
href: resolvedHref,
...props,
ref
}
);
}
);
const InboxNotificationCustom = forwardRef(
({
inboxNotification,
showActions = "hover",
title,
aside,
children,
overrides,
...props
}, forwardedRef) => {
const unread = useMemo(() => {
return !inboxNotification.readAt || inboxNotification.notifiedAt > inboxNotification.readAt;
}, [inboxNotification.notifiedAt, inboxNotification.readAt]);
return /* @__PURE__ */ jsx(
InboxNotificationLayout,
{
inboxNotification,
aside,
title,
date: inboxNotification.notifiedAt,
unread,
overrides,
showActions,
...props,
ref: forwardedRef,
children
}
);
}
);
const InboxNotificationInspector = forwardRef(
({ inboxNotification, showActions = "hover", overrides, ...props }, forwardedRef) => {
const unread = useMemo(() => {
return !inboxNotification.readAt || inboxNotification.notifiedAt > inboxNotification.readAt;
}, [inboxNotification.notifiedAt, inboxNotification.readAt]);
return /* @__PURE__ */ jsx(
InboxNotificationLayout,
{
inboxNotification,
title: /* @__PURE__ */ jsx("code", { children: inboxNotification.id }),
date: inboxNotification.notifiedAt,
unread,
overrides,
showActions,
...props,
ref: forwardedRef,
"data-inspector": "",
children: /* @__PURE__ */ jsx(
CodeBlock,
{
title: "Data",
code: JSON.stringify(inboxNotification, null, 2)
}
)
}
);
}
);
const InboxNotificationCustomMissing = forwardRef(({ inboxNotification, ...props }, forwardedRef) => {
return /* @__PURE__ */ jsxs(
InboxNotificationCustom,
{
inboxNotification,
...props,
title: /* @__PURE__ */ jsxs(Fragment, { children: [
"Custom notification kind ",
/* @__PURE__ */ jsx("code", { children: inboxNotification.kind }),
" is not handled"
] }),
aside: /* @__PURE__ */ jsx(InboxNotificationIcon, { children: /* @__PURE__ */ jsx(WarningIcon, {}) }),
ref: forwardedRef,
"data-missing": "",
children: [
"Notifications of this kind won\u2019t be displayed in production. Use the",
" ",
/* @__PURE__ */ jsx("code", { children: "kinds" }),
" prop to define how they should be rendered, learn more in the console."
]
}
);
});
const InboxNotification = Object.assign(
forwardRef(
({ inboxNotification, kinds, ...props }, forwardedRef) => {
switch (inboxNotification.kind) {
case "thread": {
const ResolvedInboxNotificationThread = kinds?.thread ?? InboxNotificationThread;
return /* @__PURE__ */ jsx(
ResolvedInboxNotificationThread,
{
inboxNotification,
...props,
ref: forwardedRef
}
);
}
case "textMention": {
const ResolvedInboxNotificationTextMention = kinds?.textMention ?? InboxNotificationTextMention;
return /* @__PURE__ */ jsx(
ResolvedInboxNotificationTextMention,
{
inboxNotification,
...props,
ref: forwardedRef
}
);
}
default: {
const ResolvedInboxNotificationCustom = kinds?.[inboxNotification.kind];
if (!ResolvedInboxNotificationCustom) {
if (process.env.NODE_ENV !== "production") {
warnOnce(
`Custom notification kind "${inboxNotification.kind}" is not handled so notifications of this kind will not be displayed in production. Use the kinds prop to define how they should be rendered. Learn more: https://liveblocks.io/docs/api-reference/liveblocks-react-ui#Rendering-notification-kinds-differently.`
);
return /* @__PURE__ */ jsx(
InboxNotificationCustomMissing,
{
inboxNotification,
...props,
ref: forwardedRef
}
);
} else {
return null;
}
}
return /* @__PURE__ */ jsx(
ResolvedInboxNotificationCustom,
{
inboxNotification,
...props,
ref: forwardedRef
}
);
}
}
}
),
{
/**
* Displays a thread inbox notification kind.
*/
Thread: InboxNotificationThread,
/**
* Displays a text mention inbox notification kind.
*/
TextMention: InboxNotificationTextMention,
/**
* Displays a custom inbox notification kind.
*/
Custom: InboxNotificationCustom,
/**
* Display the inbox notification's data, which can be useful during development.
*
* @example
* <InboxNotification
* inboxNotification={inboxNotification}
* kinds={{
* $custom: InboxNotification.Inspector,
* }}
* />
*/
Inspector: InboxNotificationInspector,
Icon: InboxNotificationIcon,
Avatar: InboxNotificationAvatar
}
);
export { InboxNotification };
//# sourceMappingURL=InboxNotification.js.map