diff --git a/apps/main/package.json b/apps/main/package.json
index 3775a5c..3ffcf32 100644
--- a/apps/main/package.json
+++ b/apps/main/package.json
@@ -75,6 +75,8 @@
"@blocknote/core": "^0.41.1",
"@blocknote/mantine": "^0.41.1",
"@blocknote/react": "^0.41.1",
+ "@chatscope/chat-ui-kit-react": "^2.1.1",
+ "@chatscope/chat-ui-kit-styles": "^1.4.0",
"@datadog/browser-rum": "^6.13.0",
"@datadog/browser-rum-react": "^6.13.0",
"@hookform/resolvers": "^5.2.2",
diff --git a/apps/main/src/components/ChatChannelPreview.tsx b/apps/main/src/components/ChatChannelPreview.tsx
new file mode 100644
index 0000000..f647967
--- /dev/null
+++ b/apps/main/src/components/ChatChannelPreview.tsx
@@ -0,0 +1,90 @@
+import { ChannelBadge } from "@ui/components/ChannelBadge";
+import type { UserTablo } from "@xtablo/shared/types/tablos.types";
+import { Badge } from "@xtablo/ui/components/badge";
+import { twMerge } from "tailwind-merge";
+
+interface ChatChannelPreviewProps {
+ tablo: UserTablo;
+ isActive: boolean;
+ onClick: () => void;
+ unreadCount: number;
+ lastMessage?: string;
+ lastMessageTime?: string;
+ isOnline: boolean;
+}
+
+function formatTimestamp(timestamp: string | Date): string {
+ const date = new Date(timestamp);
+ const now = new Date();
+ const diff = now.getTime() - date.getTime();
+ const minutes = Math.floor(diff / 60000);
+ const hours = Math.floor(diff / 3600000);
+ const days = Math.floor(diff / 86400000);
+
+ if (minutes < 1) return "now";
+ if (minutes < 60) return `${minutes}m`;
+ if (hours < 24) return `${hours}h`;
+ if (days < 7) return `${days}d`;
+ return date.toLocaleDateString();
+}
+
+export function ChatChannelPreview({
+ tablo,
+ isActive,
+ onClick,
+ unreadCount,
+ lastMessage,
+ lastMessageTime,
+ isOnline,
+}: ChatChannelPreviewProps) {
+ return (
+
+
+
+
+
+
+ {tablo.name}
+
+ {lastMessageTime && (
+
+ {formatTimestamp(lastMessageTime)}
+
+ )}
+
+
+
+
+ {lastMessage ?? "No messages yet"}
+
+
+ {unreadCount > 0 && (
+
+
+ {unreadCount > 99 ? "99+" : unreadCount}
+
+
+ )}
+
+
+
+ {isActive && (
+
+ )}
+
+ );
+}
diff --git a/apps/main/src/components/ChatHeader.tsx b/apps/main/src/components/ChatHeader.tsx
new file mode 100644
index 0000000..da3664e
--- /dev/null
+++ b/apps/main/src/components/ChatHeader.tsx
@@ -0,0 +1,54 @@
+import { ChannelBadge } from "@ui/components/ChannelBadge";
+import type { UserTablo } from "@xtablo/shared/types/tablos.types";
+
+interface ChatHeaderProps {
+ tablo: UserTablo | null;
+ onToggleChannelList?: () => void;
+ isChannelListExpanded?: boolean;
+ onlineUsers: string[];
+}
+
+export function ChatHeader({
+ tablo,
+ onToggleChannelList,
+ isChannelListExpanded = false,
+ onlineUsers,
+}: ChatHeaderProps) {
+ const memberCount = onlineUsers.length;
+
+ return (
+
+ {onToggleChannelList && (
+
+ )}
+ {tablo && (
+ <>
+
0} />
+
+
{tablo.name}
+ {memberCount > 0 && (
+
+ {memberCount} online
+
+ )}
+
+ >
+ )}
+
+ );
+}
diff --git a/apps/main/src/lib/routes.tsx b/apps/main/src/lib/routes.tsx
index 412bbc8..a6a2175 100644
--- a/apps/main/src/lib/routes.tsx
+++ b/apps/main/src/lib/routes.tsx
@@ -28,7 +28,6 @@ import { TabloDetailsPage } from "../pages/tablo-details";
import { TablosPage } from "../pages/tablos";
import { TasksPage } from "../pages/tasks";
import { UpdatePasswordPage } from "../pages/update-password";
-import ChatProvider from "../providers/ChatProvider";
export const routes: RouteObject[] = [
// Protected routes
@@ -75,11 +74,7 @@ export const routes: RouteObject[] = [
},
{
path: "chat",
- element: (
-
-
-
- ),
+ element: ,
children: [{ index: true }, { path: ":channelId" }],
},
// Notes feature temporarily hidden
diff --git a/apps/main/src/pages/chat.tsx b/apps/main/src/pages/chat.tsx
index 7b299cf..726b9ba 100644
--- a/apps/main/src/pages/chat.tsx
+++ b/apps/main/src/pages/chat.tsx
@@ -1,39 +1,58 @@
-import { ChannelPreview } from "@ui/components/ChannelPreview";
-import { CustomChannelHeader } from "@ui/components/CustomChannelHeader";
-import { useEffect, useState } from "react";
+import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
import {
- Channel,
- ChannelList,
- MessageInput,
+ ChatContainer,
MessageList,
- useChatContext,
- Window,
-} from "stream-chat-react";
-import { useChannelFromUrl } from "../hooks/channel";
+ Message,
+ MessageInput,
+ TypingIndicator,
+} from "@chatscope/chat-ui-kit-react";
+import { useEffect, useState } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+import { ChatChannelPreview } from "../components/ChatChannelPreview";
+import { ChatHeader } from "../components/ChatHeader";
+import { useChat } from "../hooks/useChat";
+import { useChatUnread } from "../hooks/useChatUnread";
import { useTablosList } from "../hooks/tablos";
import { useUser } from "../providers/UserStoreProvider";
export function ChatPage() {
const user = useUser();
- const filters = { members: { $in: [user.id] }, type: "messaging" };
-
- const { client, channel, setActiveChannel } = useChatContext();
- const { channel: channelFromUrl, isChannelInUrl } = useChannelFromUrl(client);
-
+ const { channelId } = useParams();
+ const navigate = useNavigate();
const { data: tablos } = useTablosList();
- const [isChannelListExpanded, setIsChannelListExpanded] = useState(false);
+ const { getUnreadCount } = useChatUnread();
+ const [isChannelListExpanded, setIsChannelListExpanded] = useState(!channelId);
- const toggleChannelList = () => {
- setIsChannelListExpanded(!isChannelListExpanded);
+ const {
+ messages,
+ sendMessage,
+ sendTyping,
+ isConnected,
+ typingUsers,
+ onlineUsers,
+ loadMoreMessages,
+ hasMoreMessages,
+ markAsRead,
+ } = useChat(channelId);
+
+ const activeTablo = tablos?.find((t) => t.id === channelId) ?? null;
+
+ // Mark as read when channel is focused
+ useEffect(() => {
+ if (channelId && messages.length > 0) {
+ markAsRead();
+ }
+ }, [channelId, messages.length, markAsRead]);
+
+ const handleSend = (innerHtml: string, textContent: string) => {
+ const text = textContent.trim();
+ if (!text) return;
+ sendMessage(text);
};
- useEffect(() => {
- if (channelFromUrl) {
- setActiveChannel(channelFromUrl);
- } else {
- setIsChannelListExpanded(true);
- }
- }, [channelFromUrl]);
+ const handleChannelSelect = (tabloId: string) => {
+ navigate(`/chat/${tabloId}`);
+ };
return (
@@ -41,46 +60,72 @@ export function ChatPage() {
Discussions
+ {/* Channel list sidebar */}
-
(
- t.id === channel.id) ?? null}
- activeChannel={activeChannel}
- setActiveChannel={setActiveChannel}
- unreadCount={unread}
- latestMessagePreview={latestMessagePreview}
+
+ {tablos?.map((tablo) => (
+ handleChannelSelect(tablo.id)}
+ unreadCount={getUnreadCount(tablo.id)}
+ isOnline={onlineUsers.some((uid) => uid !== user.id)}
/>
- )}
- />
+ ))}
+
-
-
-
-
+ {channelId && activeTablo ? (
+ <>
+ setIsChannelListExpanded(!isChannelListExpanded)}
isChannelListExpanded={isChannelListExpanded}
+ onlineUsers={onlineUsers}
/>
-
-
-
-
+
+
+ 0 ? (
+
+ ) : undefined
+ }
+ >
+ {messages.map((msg) => (
+
+ ))}
+
+ sendTyping()}
+ attachButton={false}
+ />
+
+
+ >
+ ) : (
+
+ Select a conversation to start chatting
+
+ )}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8825c92..dcd9b8d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -124,6 +124,25 @@ importers:
specifier: ^4.0.8
version: 4.0.8(@types/debug@4.1.12)(@types/node@20.19.23)(@vitest/ui@4.0.8)(happy-dom@20.0.7)(jiti@2.6.1)(jsdom@20.0.3)(lightningcss@1.30.2)(terser@5.46.1)(tsx@4.20.6)
+ apps/chat-worker:
+ dependencies:
+ hono:
+ specifier: ^4.7.7
+ version: 4.10.4
+ jose:
+ specifier: ^6.0.0
+ version: 6.2.2
+ devDependencies:
+ '@cloudflare/workers-types':
+ specifier: ^4.20250410.0
+ version: 4.20260411.1
+ typescript:
+ specifier: ^5.8.3
+ version: 5.9.3
+ wrangler:
+ specifier: ^4.14.0
+ version: 4.44.0(@cloudflare/workers-types@4.20260411.1)
+
apps/external:
dependencies:
'@tanstack/react-query':
@@ -174,7 +193,7 @@ importers:
version: 2.2.5
'@cloudflare/vite-plugin':
specifier: ^1.9.4
- version: 1.13.14(vite@6.4.1(@types/node@22.18.12)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.1)(tsx@4.20.6))(workerd@1.20251011.0)(wrangler@4.44.0)
+ version: 1.13.14(vite@6.4.1(@types/node@22.18.12)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.1)(tsx@4.20.6))(workerd@1.20251011.0)(wrangler@4.44.0(@cloudflare/workers-types@4.20260411.1))
'@tailwindcss/vite':
specifier: ^4.0.14
version: 4.1.15(vite@6.4.1(@types/node@22.18.12)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.1)(tsx@4.20.6))
@@ -204,7 +223,7 @@ importers:
version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.18.12)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.1)(tsx@4.20.6))
wrangler:
specifier: ^4.24.3
- version: 4.44.0
+ version: 4.44.0(@cloudflare/workers-types@4.20260411.1)
apps/main:
dependencies:
@@ -217,6 +236,12 @@ importers:
'@blocknote/react':
specifier: ^0.41.1
version: 0.41.1(@floating-ui/dom@1.7.4)(@tiptap/extensions@3.8.0(@tiptap/core@3.8.0(@tiptap/pm@3.8.0))(@tiptap/pm@3.8.0))(@types/hast@3.0.4)(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ '@chatscope/chat-ui-kit-react':
+ specifier: ^2.1.1
+ version: 2.1.1(prop-types@15.8.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ '@chatscope/chat-ui-kit-styles':
+ specifier: ^1.4.0
+ version: 1.4.0
'@datadog/browser-rum':
specifier: ^6.13.0
version: 6.22.0
@@ -373,7 +398,7 @@ importers:
version: 2.2.5
'@cloudflare/vite-plugin':
specifier: ^1.9.4
- version: 1.13.14(vite@6.4.1(@types/node@22.18.12)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.1)(tsx@4.20.6))(workerd@1.20251011.0)(wrangler@4.44.0)
+ version: 1.13.14(vite@6.4.1(@types/node@22.18.12)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.1)(tsx@4.20.6))(workerd@1.20251011.0)(wrangler@4.44.0(@cloudflare/workers-types@4.20260411.1))
'@esbuild-plugins/node-globals-polyfill':
specifier: ^0.2.3
version: 0.2.3(esbuild@0.25.11)
@@ -511,7 +536,7 @@ importers:
version: 7.4.0
wrangler:
specifier: ^4.24.3
- version: 4.44.0
+ version: 4.44.0(@cloudflare/workers-types@4.20260411.1)
packages/shared:
dependencies:
@@ -1623,6 +1648,16 @@ packages:
'@braintree/sanitize-url@6.0.4':
resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==}
+ '@chatscope/chat-ui-kit-react@2.1.1':
+ resolution: {integrity: sha512-rCtE9abdmAbBDkAAUYBC1TDTBMZHquqFIZhADptAfHcJ8z8W3XH/z/ZuwBSJXtzi6h1mwCNc3tBmm1A2NLGhNg==}
+ peerDependencies:
+ prop-types: ^15.7.2
+ react: ^16.12.0 || ^17.0.0 || ^18.2.0 || ^19.0.0
+ react-dom: ^16.12.0 || ^17.0.0 || ^18.2.0 || ^19.0.0
+
+ '@chatscope/chat-ui-kit-styles@1.4.0':
+ resolution: {integrity: sha512-016mBJD3DESw7Nh+lkKcPd22xG92ghA0VpIXIbjQtmXhC7Ve6wRazTy8z1Ahut+Tbv179+JxrftuMngsj/yV8Q==}
+
'@cloudflare/kv-asset-handler@0.4.0':
resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==}
engines: {node: '>=18.0.0'}
@@ -1672,6 +1707,9 @@ packages:
cpu: [x64]
os: [win32]
+ '@cloudflare/workers-types@4.20260411.1':
+ resolution: {integrity: sha512-SsntcTanLz+LmgJC8yB7sGCtpC8HxboVDmwrOH1hp1SHZwuKnhfmhUfeiwy7O/cE3iVN1cxe1E17stxP5DJXDw==}
+
'@cspotcode/source-map-support@0.8.1':
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
@@ -2180,6 +2218,29 @@ packages:
'@formatjs/intl-localematcher@0.6.2':
resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==}
+ '@fortawesome/fontawesome-common-types@6.7.2':
+ resolution: {integrity: sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==}
+ engines: {node: '>=6'}
+
+ '@fortawesome/fontawesome-free@6.7.2':
+ resolution: {integrity: sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==}
+ engines: {node: '>=6'}
+
+ '@fortawesome/fontawesome-svg-core@6.7.2':
+ resolution: {integrity: sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==}
+ engines: {node: '>=6'}
+
+ '@fortawesome/free-solid-svg-icons@6.7.2':
+ resolution: {integrity: sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==}
+ engines: {node: '>=6'}
+
+ '@fortawesome/react-fontawesome@0.2.6':
+ resolution: {integrity: sha512-mtBFIi1UsYQo7rYonYFkjgYKGoL8T+fEH6NGUpvuqtY3ytMsAoDaPo5rk25KuMtKDipY4bGYM/CkmCHA1N3FUg==}
+ deprecated: v0.2.x is no longer supported. Unless you are still using FontAwesome 5, please update to v3.1.1 or greater.
+ peerDependencies:
+ '@fortawesome/fontawesome-svg-core': ~1 || ~6 || ~7
+ react: ^16.3 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
'@google-cloud/common@5.0.2':
resolution: {integrity: sha512-V7bmBKYQyu0eVG2BFejuUjlBt+zrya6vtsKdY+JxMM/dNntPF41vZ9+LhOshEUH01zOHEqBSvI7Dad7ZS6aUeA==}
engines: {node: '>=14.0.0'}
@@ -5480,6 +5541,9 @@ packages:
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
+ classnames@2.5.1:
+ resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
+
cli-cursor@3.1.0:
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
engines: {node: '>=8'}
@@ -6936,6 +7000,9 @@ packages:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
+ jose@6.2.2:
+ resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==}
+
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -11231,6 +11298,20 @@ snapshots:
'@braintree/sanitize-url@6.0.4': {}
+ '@chatscope/chat-ui-kit-react@2.1.1(prop-types@15.8.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+ dependencies:
+ '@chatscope/chat-ui-kit-styles': 1.4.0
+ '@fortawesome/fontawesome-free': 6.7.2
+ '@fortawesome/fontawesome-svg-core': 6.7.2
+ '@fortawesome/free-solid-svg-icons': 6.7.2
+ '@fortawesome/react-fontawesome': 0.2.6(@fortawesome/fontawesome-svg-core@6.7.2)(react@19.0.0)
+ classnames: 2.5.1
+ prop-types: 15.8.1
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+
+ '@chatscope/chat-ui-kit-styles@1.4.0': {}
+
'@cloudflare/kv-asset-handler@0.4.0':
dependencies:
mime: 3.0.0
@@ -11241,7 +11322,7 @@ snapshots:
optionalDependencies:
workerd: 1.20251011.0
- '@cloudflare/vite-plugin@1.13.14(vite@6.4.1(@types/node@22.18.12)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.1)(tsx@4.20.6))(workerd@1.20251011.0)(wrangler@4.44.0)':
+ '@cloudflare/vite-plugin@1.13.14(vite@6.4.1(@types/node@22.18.12)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.1)(tsx@4.20.6))(workerd@1.20251011.0)(wrangler@4.44.0(@cloudflare/workers-types@4.20260411.1))':
dependencies:
'@cloudflare/unenv-preset': 2.7.8(unenv@2.0.0-rc.21)(workerd@1.20251011.0)
'@remix-run/node-fetch-server': 0.8.1
@@ -11251,7 +11332,7 @@ snapshots:
tinyglobby: 0.2.15
unenv: 2.0.0-rc.21
vite: 6.4.1(@types/node@22.18.12)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.1)(tsx@4.20.6)
- wrangler: 4.44.0
+ wrangler: 4.44.0(@cloudflare/workers-types@4.20260411.1)
ws: 8.18.0
transitivePeerDependencies:
- bufferutil
@@ -11273,6 +11354,8 @@ snapshots:
'@cloudflare/workerd-windows-64@1.20251011.0':
optional: true
+ '@cloudflare/workers-types@4.20260411.1': {}
+
'@cspotcode/source-map-support@0.8.1':
dependencies:
'@jridgewell/trace-mapping': 0.3.9
@@ -11651,6 +11734,24 @@ snapshots:
dependencies:
tslib: 2.8.1
+ '@fortawesome/fontawesome-common-types@6.7.2': {}
+
+ '@fortawesome/fontawesome-free@6.7.2': {}
+
+ '@fortawesome/fontawesome-svg-core@6.7.2':
+ dependencies:
+ '@fortawesome/fontawesome-common-types': 6.7.2
+
+ '@fortawesome/free-solid-svg-icons@6.7.2':
+ dependencies:
+ '@fortawesome/fontawesome-common-types': 6.7.2
+
+ '@fortawesome/react-fontawesome@0.2.6(@fortawesome/fontawesome-svg-core@6.7.2)(react@19.0.0)':
+ dependencies:
+ '@fortawesome/fontawesome-svg-core': 6.7.2
+ prop-types: 15.8.1
+ react: 19.0.0
+
'@google-cloud/common@5.0.2':
dependencies:
'@google-cloud/projectify': 4.0.0
@@ -15697,6 +15798,8 @@ snapshots:
dependencies:
clsx: 2.1.1
+ classnames@2.5.1: {}
+
cli-cursor@3.1.0:
dependencies:
restore-cursor: 3.1.0
@@ -17687,6 +17790,8 @@ snapshots:
jiti@2.6.1: {}
+ jose@6.2.2: {}
+
js-tokens@4.0.0: {}
js-tokens@9.0.1: {}
@@ -20926,7 +21031,7 @@ snapshots:
'@cloudflare/workerd-linux-arm64': 1.20251011.0
'@cloudflare/workerd-windows-64': 1.20251011.0
- wrangler@4.44.0:
+ wrangler@4.44.0(@cloudflare/workers-types@4.20260411.1):
dependencies:
'@cloudflare/kv-asset-handler': 0.4.0
'@cloudflare/unenv-preset': 2.7.8(unenv@2.0.0-rc.21)(workerd@1.20251011.0)
@@ -20937,6 +21042,7 @@ snapshots:
unenv: 2.0.0-rc.21
workerd: 1.20251011.0
optionalDependencies:
+ '@cloudflare/workers-types': 4.20260411.1
fsevents: 2.3.3
transitivePeerDependencies:
- bufferutil