{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "realtime-chat-react", "title": "Realtime Chat (React)", "description": "A realtime chat component powered by Convex live queries. Messages sync instantly across all connected clients. Includes full Convex backend with demo mode support.", "dependencies": [ "lucide-react", "convex@latest", "@auth/core", "@convex-dev/auth@latest" ], "registryDependencies": ["input", "button"], "files": [ { "path": "src/registry/convex/blocks/realtime-chat-react/components/chat-message.tsx", "content": "import { cn } from \"@/lib/utils\";\n\ninterface ChatMessageProps {\n message: {\n id: string;\n content: string;\n user: {\n name: string;\n };\n createdAt: string;\n };\n isOwnMessage?: boolean;\n}\n\nexport function ChatMessage({\n message,\n isOwnMessage = false,\n}: ChatMessageProps) {\n const formattedTime = new Date(message.createdAt).toLocaleTimeString([], {\n hour: \"2-digit\",\n minute: \"2-digit\",\n });\n\n return (\n \n
\n {message.user.name}\n {formattedTime}\n
\n \n {message.content}\n \n \n );\n}\n", "type": "registry:component", "target": "components/chat-message.tsx" }, { "path": "src/registry/convex/blocks/realtime-chat-react/components/realtime-chat.tsx", "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Card,\n CardContent,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\";\nimport { Input } from \"@/components/ui/input\";\nimport { Send } from \"lucide-react\";\nimport { useChatScroll } from \"../hooks/use-chat-scroll\";\nimport {\n useRealtimeChat,\n type ChatMessage as ChatMessageType,\n} from \"../hooks/use-realtime-chat\";\nimport { ChatMessage } from \"./chat-message\";\n\ninterface RealtimeChatProps {\n roomName: string;\n username: string;\n className?: string;\n}\n\nexport function RealtimeChat({\n roomName,\n username,\n className,\n}: RealtimeChatProps) {\n const [inputValue, setInputValue] = useState(\"\");\n const { messages, sendMessage, isConnected } = useRealtimeChat({\n roomName,\n username,\n });\n const { containerRef, scrollToBottom, handleScroll, shouldAutoScroll } =\n useChatScroll();\n\n useEffect(() => {\n if (shouldAutoScroll()) {\n scrollToBottom();\n }\n }, [messages, scrollToBottom, shouldAutoScroll]);\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n if (!inputValue.trim()) return;\n\n await sendMessage(inputValue.trim());\n setInputValue(\"\");\n };\n\n return (\n \n \n {roomName}\n \n
\n {isConnected ? \"Connected\" : \"Disconnected\"}\n \n \n\n \n {messages.length === 0 ? (\n
\n No messages yet. Start the conversation!\n
\n ) : (\n messages.map((message) => (\n \n ))\n )}\n \n\n \n
\n
\n setInputValue(e.target.value)}\n placeholder=\"Type a message...\"\n disabled={!isConnected}\n className=\"flex-1\"\n />\n \n \n \n
\n
\n
\n \n );\n}\n", "type": "registry:component", "target": "components/realtime-chat.tsx" }, { "path": "src/registry/convex/blocks/realtime-chat-react/hooks/use-realtime-chat.tsx", "content": "\"use client\";\n\nimport { useQuery, useMutation } from \"convex/react\";\nimport { api } from \"@/convex/_generated/api\";\nimport { useCallback, useEffect, useState } from \"react\";\n\ninterface UseRealtimeChatProps {\n roomName: string;\n username: string;\n}\n\nexport interface ChatMessage {\n id: string;\n content: string;\n user: {\n name: string;\n };\n createdAt: string;\n}\n\n// Get or create a session ID for demo mode\nfunction getSessionId(): string {\n if (typeof window === \"undefined\") return \"\";\n let id = localStorage.getItem(\"demo-session-id\");\n if (!id) {\n id = `demo-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;\n localStorage.setItem(\"demo-session-id\", id);\n }\n return id;\n}\n\nexport function useRealtimeChat({ roomName, username }: UseRealtimeChatProps) {\n const [sessionId, setSessionId] = useState(\"\");\n const rawMessages = useQuery(api.messages.list, { roomId: roomName });\n const sendMutation = useMutation(api.messages.send);\n\n useEffect(() => {\n setSessionId(getSessionId());\n }, []);\n\n const messages: ChatMessage[] = (rawMessages ?? []).map((msg: any) => ({\n id: msg._id,\n content: msg.content,\n user: {\n name: msg.userName,\n },\n createdAt: new Date(msg._creationTime).toISOString(),\n }));\n\n const sendMessage = useCallback(\n async (content: string) => {\n await sendMutation({\n roomId: roomName,\n content,\n userName: username,\n sessionId,\n });\n },\n [sendMutation, roomName, username, sessionId],\n );\n\n const isConnected = rawMessages !== undefined;\n\n return { messages, sendMessage, isConnected };\n}\n", "type": "registry:hook", "target": "hooks/use-realtime-chat.tsx" }, { "path": "src/registry/convex/blocks/realtime-chat-react/hooks/use-chat-scroll.tsx", "content": "\"use client\";\n\nimport { useCallback, useRef } from \"react\";\n\ninterface UseChatScrollOptions {\n scrollThreshold?: number;\n}\n\nexport function useChatScroll({\n scrollThreshold = 100,\n}: UseChatScrollOptions = {}) {\n const containerRef = useRef(null);\n const isAtBottomRef = useRef(true);\n\n const scrollToBottom = useCallback((behavior: ScrollBehavior = \"smooth\") => {\n if (containerRef.current) {\n containerRef.current.scrollTo({\n top: containerRef.current.scrollHeight,\n behavior,\n });\n }\n }, []);\n\n const handleScroll = useCallback(() => {\n if (containerRef.current) {\n const { scrollTop, scrollHeight, clientHeight } = containerRef.current;\n isAtBottomRef.current =\n scrollHeight - scrollTop - clientHeight < scrollThreshold;\n }\n }, [scrollThreshold]);\n\n const shouldAutoScroll = useCallback(() => {\n return isAtBottomRef.current;\n }, []);\n\n return {\n containerRef,\n scrollToBottom,\n handleScroll,\n shouldAutoScroll,\n };\n}\n", "type": "registry:hook", "target": "hooks/use-chat-scroll.tsx" }, { "path": "src/registry/convex/blocks/realtime-chat-react/convex/schema.ts", "content": "import { defineSchema, defineTable } from \"convex/server\";\nimport { v } from \"convex/values\";\n\n/**\n * Schema for realtime chat functionality.\n *\n * This schema supports both authenticated and demo modes:\n * - Authenticated: userId links messages to the user\n * - Demo mode: sessionId tracks messages without authentication\n *\n * Security considerations:\n * - Messages are scoped to rooms for isolation\n * - sessionId enables demo mode without requiring auth\n * - Index on roomId for efficient message retrieval\n */\nexport default defineSchema({\n // Chat messages table\n messages: defineTable({\n // Room identifier for message isolation\n roomId: v.string(),\n\n // User reference (optional for demo mode)\n userId: v.optional(v.id(\"users\")),\n\n // Message content\n content: v.string(),\n\n // Display name (denormalized for performance)\n userName: v.string(),\n\n // Session identifier for demo mode tracking\n sessionId: v.optional(v.string()),\n }).index(\"by_room\", [\"roomId\"]),\n\n // Optional: Users table if you want to track users\n // Uncomment if using with authentication\n // users: defineTable({\n // name: v.optional(v.string()),\n // email: v.optional(v.string()),\n // image: v.optional(v.string()),\n // }),\n});\n", "type": "registry:file", "target": "convex/schema.ts" }, { "path": "src/registry/convex/blocks/realtime-chat-react/convex/messages.ts", "content": "import { query, mutation } from \"./_generated/server\";\nimport { v } from \"convex/values\";\nimport { ConvexError } from \"convex/values\";\n\n/**\n * Message validator for return types.\n */\nconst messageValidator = v.object({\n _id: v.id(\"messages\"),\n _creationTime: v.number(),\n roomId: v.string(),\n userId: v.optional(v.id(\"users\")),\n content: v.string(),\n userName: v.string(),\n sessionId: v.optional(v.string()),\n});\n\n/**\n * Maximum message length to prevent abuse.\n */\nconst MAX_MESSAGE_LENGTH = 2000;\n\n/**\n * Maximum room ID length.\n */\nconst MAX_ROOM_ID_LENGTH = 100;\n\n/**\n * List all messages in a room, ordered by creation time.\n *\n * This query is reactive - it automatically updates when messages change.\n * Uses index for efficient retrieval.\n */\nexport const list = query({\n args: { roomId: v.string() },\n returns: v.array(messageValidator),\n handler: async (ctx: any, args: { roomId: string }) => {\n // Validate room ID\n if (!args.roomId || args.roomId.length > MAX_ROOM_ID_LENGTH) {\n return [];\n }\n\n return await ctx.db\n .query(\"messages\")\n .withIndex(\"by_room\", (q: any) => q.eq(\"roomId\", args.roomId))\n .order(\"asc\")\n .collect();\n },\n});\n\n/**\n * Send a new message to a room.\n *\n * Supports both authenticated and demo modes:\n * - If authenticated, userId is set from the auth context\n * - If demo mode, sessionId should be provided for tracking\n *\n * Security:\n * - Content is validated and trimmed\n * - Room ID is validated\n * - User name is sanitized\n */\nexport const send = mutation({\n args: {\n roomId: v.string(),\n content: v.string(),\n userName: v.string(),\n sessionId: v.optional(v.string()),\n },\n returns: v.id(\"messages\"),\n handler: async (\n ctx: any,\n args: {\n roomId: string;\n content: string;\n userName: string;\n sessionId?: string;\n },\n ) => {\n // Validate room ID\n if (!args.roomId || args.roomId.length > MAX_ROOM_ID_LENGTH) {\n throw new ConvexError({\n code: \"INVALID_INPUT\",\n message: \"Invalid room ID\",\n });\n }\n\n // Validate and sanitize content\n const content = args.content.trim();\n if (!content) {\n throw new ConvexError({\n code: \"INVALID_INPUT\",\n message: \"Message content cannot be empty\",\n });\n }\n if (content.length > MAX_MESSAGE_LENGTH) {\n throw new ConvexError({\n code: \"INVALID_INPUT\",\n message: `Message too long. Maximum ${MAX_MESSAGE_LENGTH} characters.`,\n });\n }\n\n // Sanitize user name\n const userName = args.userName.trim().slice(0, 50) || \"Anonymous\";\n\n // Try to get authenticated user ID (optional - supports demo mode)\n let userId: any = undefined;\n try {\n // Dynamic import to make auth optional\n const { getAuthUserId } = await import(\"@convex-dev/auth/server\");\n userId = (await getAuthUserId(ctx)) ?? undefined;\n } catch {\n // Auth not configured - demo mode only\n userId = undefined;\n }\n\n return await ctx.db.insert(\"messages\", {\n roomId: args.roomId,\n userId,\n content,\n userName,\n sessionId: args.sessionId,\n });\n },\n});\n\n/**\n * Delete a message.\n *\n * Security:\n * - Only allows deletion if:\n * 1. User owns the message (userId matches), OR\n * 2. Session owns the message (sessionId matches), OR\n * 3. Message has no owner (demo message)\n * - Idempotent - returns success even if message doesn't exist\n */\nexport const remove = mutation({\n args: {\n messageId: v.id(\"messages\"),\n sessionId: v.optional(v.string()),\n },\n returns: v.null(),\n handler: async (ctx: any, args: { messageId: any; sessionId?: string }) => {\n const message = await ctx.db.get(args.messageId);\n if (!message) {\n return null; // Idempotent - already deleted\n }\n\n // Try to get authenticated user ID\n let userId: any = null;\n try {\n const { getAuthUserId } = await import(\"@convex-dev/auth/server\");\n userId = await getAuthUserId(ctx);\n } catch {\n // Auth not configured\n }\n\n // Check ownership\n const canDelete =\n // No owner (demo message)\n !message.userId ||\n // User owns it\n message.userId === userId ||\n // Session owns it\n (message.sessionId && message.sessionId === args.sessionId);\n\n if (canDelete) {\n await ctx.db.delete(args.messageId);\n }\n\n return null;\n },\n});\n", "type": "registry:file", "target": "convex/messages.ts" }, { "path": "src/registry/convex/clients/react/lib/convex/client.ts", "content": "import { ConvexReactClient } from \"convex/react\";\n\nconst convexUrl = (import.meta as any).env.VITE_CONVEX_URL as string;\n\nexport const convex = new ConvexReactClient(convexUrl);\n", "type": "registry:lib", "target": "lib/convex/client.ts" }, { "path": "src/registry/convex/clients/react/lib/convex/provider.tsx", "content": "import { ConvexAuthProvider } from \"@convex-dev/auth/react\";\nimport { ConvexReactClient } from \"convex/react\";\nimport { ReactNode } from \"react\";\n\nconst convex = new ConvexReactClient(\n (import.meta as any).env.VITE_CONVEX_URL as string,\n);\n\nexport function ConvexClientProvider({ children }: { children: ReactNode }) {\n return {children};\n}\n", "type": "registry:lib", "target": "lib/convex/provider.tsx" } ], "envVars": { "CONVEX_DEPLOYMENT": "", "VITE_CONVEX_URL": "" }, "docs": "## Post-Install Setup\n\n1. Run `npx convex dev` to start the Convex dev server.\n\n2. Copy the deployment URL to your `.env` file as `VITE_CONVEX_URL`.\n\n3. The Convex schema and functions will be automatically deployed when you run `npx convex dev`.\n\n## Post-Install Setup\n\n1. Run `npx convex dev` to start the Convex dev server and get your deployment URL.\n\n2. Copy the deployment URL to your `.env` file as `VITE_CONVEX_URL`.\n\n3. Wrap your app with the `ConvexAuthProvider` from `lib/convex/provider.tsx`.", "type": "registry:component" }