@hamzasaleemorg/convex-comments 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/LICENSE +201 -0
- package/README.md +581 -0
- package/dist/client/_generated/_ignore.d.ts +1 -0
- package/dist/client/_generated/_ignore.d.ts.map +1 -0
- package/dist/client/_generated/_ignore.js +3 -0
- package/dist/client/_generated/_ignore.js.map +1 -0
- package/dist/client/index.d.ts +745 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +579 -0
- package/dist/client/index.js.map +1 -0
- package/dist/component/_generated/api.d.ts +44 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +673 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +3 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/lib.d.ts +17 -0
- package/dist/component/lib.d.ts.map +1 -0
- package/dist/component/lib.js +18 -0
- package/dist/component/lib.js.map +1 -0
- package/dist/component/messages.d.ts +173 -0
- package/dist/component/messages.d.ts.map +1 -0
- package/dist/component/messages.js +410 -0
- package/dist/component/messages.js.map +1 -0
- package/dist/component/reactions.d.ts +51 -0
- package/dist/component/reactions.d.ts.map +1 -0
- package/dist/component/reactions.js +191 -0
- package/dist/component/reactions.js.map +1 -0
- package/dist/component/schema.d.ts +274 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +159 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/component/threads.d.ts +110 -0
- package/dist/component/threads.d.ts.map +1 -0
- package/dist/component/threads.js +276 -0
- package/dist/component/threads.js.map +1 -0
- package/dist/component/typing.d.ts +31 -0
- package/dist/component/typing.d.ts.map +1 -0
- package/dist/component/typing.js +147 -0
- package/dist/component/typing.js.map +1 -0
- package/dist/component/zones.d.ts +63 -0
- package/dist/component/zones.d.ts.map +1 -0
- package/dist/component/zones.js +159 -0
- package/dist/component/zones.js.map +1 -0
- package/dist/react/AddComment.d.ts +57 -0
- package/dist/react/AddComment.d.ts.map +1 -0
- package/dist/react/AddComment.js +285 -0
- package/dist/react/AddComment.js.map +1 -0
- package/dist/react/Comment.d.ts +70 -0
- package/dist/react/Comment.d.ts.map +1 -0
- package/dist/react/Comment.js +259 -0
- package/dist/react/Comment.js.map +1 -0
- package/dist/react/Comments.d.ts +74 -0
- package/dist/react/Comments.d.ts.map +1 -0
- package/dist/react/Comments.js +108 -0
- package/dist/react/Comments.js.map +1 -0
- package/dist/react/CommentsProvider.d.ts +104 -0
- package/dist/react/CommentsProvider.d.ts.map +1 -0
- package/dist/react/CommentsProvider.js +98 -0
- package/dist/react/CommentsProvider.js.map +1 -0
- package/dist/react/ReactionPicker.d.ts +28 -0
- package/dist/react/ReactionPicker.d.ts.map +1 -0
- package/dist/react/ReactionPicker.js +56 -0
- package/dist/react/ReactionPicker.js.map +1 -0
- package/dist/react/Thread.d.ts +84 -0
- package/dist/react/Thread.d.ts.map +1 -0
- package/dist/react/Thread.js +124 -0
- package/dist/react/Thread.js.map +1 -0
- package/dist/react/TypingIndicator.d.ts +25 -0
- package/dist/react/TypingIndicator.d.ts.map +1 -0
- package/dist/react/TypingIndicator.js +99 -0
- package/dist/react/TypingIndicator.js.map +1 -0
- package/dist/react/index.d.ts +15 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +15 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +106 -0
- package/src/client/_generated/_ignore.ts +1 -0
- package/src/client/index.ts +813 -0
- package/src/component/_generated/api.ts +60 -0
- package/src/component/_generated/component.ts +784 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +156 -0
- package/src/component/convex.config.ts +3 -0
- package/src/component/lib.ts +57 -0
- package/src/component/messages.ts +476 -0
- package/src/component/reactions.ts +222 -0
- package/src/component/schema.ts +169 -0
- package/src/component/threads.ts +319 -0
- package/src/component/typing.ts +168 -0
- package/src/component/zones.ts +180 -0
- package/src/react/AddComment.tsx +463 -0
- package/src/react/Comment.tsx +519 -0
- package/src/react/Comments.tsx +276 -0
- package/src/react/CommentsProvider.tsx +197 -0
- package/src/react/ReactionPicker.tsx +95 -0
- package/src/react/Thread.tsx +336 -0
- package/src/react/TypingIndicator.tsx +144 -0
- package/src/react/index.ts +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
# Convex Comments Component
|
|
2
|
+
|
|
3
|
+
A comments system for Convex with threads, mentions, reactions, and typing indicators. Includes backend functions and optional React UI components.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @hamzasaleemorg/convex-comments
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Add the component to your Convex app:
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
// convex/convex.config.ts
|
|
15
|
+
import { defineApp } from "convex/server";
|
|
16
|
+
import comments from "@hamzasaleemorg/convex-comments/convex.config.js";
|
|
17
|
+
|
|
18
|
+
const app = defineApp();
|
|
19
|
+
app.use(comments);
|
|
20
|
+
|
|
21
|
+
export default app;
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start (5 minutes)
|
|
25
|
+
|
|
26
|
+
Get comments working in your Convex app:
|
|
27
|
+
|
|
28
|
+
**1. Create backend functions:**
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
// convex/comments.ts
|
|
32
|
+
import { v } from "convex/values";
|
|
33
|
+
import { Comments } from "@hamzasaleemorg/convex-comments";
|
|
34
|
+
import { mutation, query } from "./_generated/server";
|
|
35
|
+
import { components } from "./_generated/api";
|
|
36
|
+
|
|
37
|
+
const comments = new Comments(components.comments);
|
|
38
|
+
|
|
39
|
+
// Create a zone for an entity (e.g., a document)
|
|
40
|
+
export const createZone = mutation({
|
|
41
|
+
args: { documentId: v.string() },
|
|
42
|
+
handler: async (ctx, args) => {
|
|
43
|
+
return await comments.getOrCreateZone(ctx, {
|
|
44
|
+
entityId: args.documentId
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Add a comment
|
|
50
|
+
export const addComment = mutation({
|
|
51
|
+
args: {
|
|
52
|
+
threadId: v.string(),
|
|
53
|
+
userId: v.string(),
|
|
54
|
+
body: v.string()
|
|
55
|
+
},
|
|
56
|
+
handler: async (ctx, args) => {
|
|
57
|
+
return await comments.addComment(ctx, {
|
|
58
|
+
threadId: args.threadId,
|
|
59
|
+
authorId: args.userId,
|
|
60
|
+
body: args.body,
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Get messages in a thread
|
|
66
|
+
export const getMessages = query({
|
|
67
|
+
args: { threadId: v.string() },
|
|
68
|
+
handler: async (ctx, args) => {
|
|
69
|
+
return await comments.getMessages(ctx, {
|
|
70
|
+
threadId: args.threadId
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**2. Use in React:**
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { useMutation, useQuery } from "convex/react";
|
|
80
|
+
import { api } from "../convex/_generated/api";
|
|
81
|
+
|
|
82
|
+
function CommentThread({ threadId, userId }) {
|
|
83
|
+
const messages = useQuery(api.comments.getMessages, { threadId });
|
|
84
|
+
const addComment = useMutation(api.comments.addComment);
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div>
|
|
88
|
+
{messages?.messages.map((msg) => (
|
|
89
|
+
<div key={msg.message._id}>
|
|
90
|
+
<strong>{msg.message.authorId}:</strong> {msg.message.body}
|
|
91
|
+
</div>
|
|
92
|
+
))}
|
|
93
|
+
<button onClick={() =>
|
|
94
|
+
addComment({ threadId, userId, body: "Hello!" })
|
|
95
|
+
}>
|
|
96
|
+
Add Comment
|
|
97
|
+
</button>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**That's it!** You now have threaded comments with mentions, reactions, and typing indicators available.
|
|
104
|
+
|
|
105
|
+
See below for the complete API reference and React components.
|
|
106
|
+
|
|
107
|
+
## Data Model
|
|
108
|
+
|
|
109
|
+
The component organizes comments into three levels:
|
|
110
|
+
|
|
111
|
+
- **Zones** - Containers for threads, tied to your entities (documents, tasks, etc.)
|
|
112
|
+
- **Threads** - Groups of messages within a zone
|
|
113
|
+
- **Messages** - Individual comments with mentions, reactions, and attachments
|
|
114
|
+
|
|
115
|
+
Each message can have:
|
|
116
|
+
- Body text with automatic mention and link parsing
|
|
117
|
+
- Attachments (URLs, files, images)
|
|
118
|
+
- Emoji reactions
|
|
119
|
+
- Resolved state
|
|
120
|
+
- Edit history
|
|
121
|
+
|
|
122
|
+
## Backend Usage
|
|
123
|
+
|
|
124
|
+
### Method 1: Comments Class
|
|
125
|
+
|
|
126
|
+
The recommended approach. Provides type-safe methods and optional callbacks.
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
import { Comments } from "@hamzasaleemorg/convex-comments";
|
|
130
|
+
import { components } from "./_generated/api";
|
|
131
|
+
import { mutation, query } from "./_generated/server";
|
|
132
|
+
|
|
133
|
+
const comments = new Comments(components.comments);
|
|
134
|
+
|
|
135
|
+
export const createZone = mutation({
|
|
136
|
+
args: { entityId: v.string() },
|
|
137
|
+
handler: async (ctx, args) => {
|
|
138
|
+
return await comments.getOrCreateZone(ctx, {
|
|
139
|
+
entityId: args.entityId,
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
export const addComment = mutation({
|
|
145
|
+
args: { threadId: v.string(), body: v.string() },
|
|
146
|
+
handler: async (ctx, args) => {
|
|
147
|
+
const userId = await getAuthUserId(ctx);
|
|
148
|
+
return await comments.addComment(ctx, {
|
|
149
|
+
threadId: args.threadId,
|
|
150
|
+
authorId: userId,
|
|
151
|
+
body: args.body,
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Method 2: Direct Component Calls
|
|
158
|
+
|
|
159
|
+
Call component functions directly through `ctx.runMutation` or `ctx.runQuery`.
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import { components } from "./_generated/api";
|
|
163
|
+
|
|
164
|
+
export const addComment = mutation({
|
|
165
|
+
args: { threadId: v.string(), body: v.string() },
|
|
166
|
+
handler: async (ctx, args) => {
|
|
167
|
+
return await ctx.runMutation(components.comments.lib.addComment, {
|
|
168
|
+
threadId: args.threadId,
|
|
169
|
+
authorId: await getAuthUserId(ctx),
|
|
170
|
+
body: args.body,
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Method 3: Exposed API
|
|
177
|
+
|
|
178
|
+
Generate wrapper functions for frontend use.
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import { exposeApi } from "@hamzasaleemorg/convex-comments";
|
|
182
|
+
import { components } from "./_generated/api";
|
|
183
|
+
|
|
184
|
+
export const {
|
|
185
|
+
getThreads,
|
|
186
|
+
addThread,
|
|
187
|
+
addComment,
|
|
188
|
+
toggleReaction,
|
|
189
|
+
setIsTyping,
|
|
190
|
+
getTypingUsers,
|
|
191
|
+
} = exposeApi(components.comments, {
|
|
192
|
+
auth: async (ctx, operation) => {
|
|
193
|
+
return await getAuthUserId(ctx);
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## API Reference
|
|
199
|
+
|
|
200
|
+
### Zones
|
|
201
|
+
|
|
202
|
+
**getOrCreateZone(ctx, args)**
|
|
203
|
+
- Args: `{ entityId: string, metadata?: any }`
|
|
204
|
+
- Returns: `Id<"zones">`
|
|
205
|
+
- Creates a zone if it doesn't exist, otherwise returns existing zone
|
|
206
|
+
|
|
207
|
+
**getZone(ctx, args)**
|
|
208
|
+
- Args: `{ entityId: string }`
|
|
209
|
+
- Returns: Zone or null
|
|
210
|
+
- Get zone by entity ID
|
|
211
|
+
|
|
212
|
+
**deleteZone(ctx, args)**
|
|
213
|
+
- Args: `{ zoneId: Id<"zones"> }`
|
|
214
|
+
- Deletes zone and all threads/messages within it
|
|
215
|
+
|
|
216
|
+
### Threads
|
|
217
|
+
|
|
218
|
+
**addThread(ctx, args)**
|
|
219
|
+
- Args: `{ zoneId: Id<"zones">, position?: { x: number, y: number, anchor?: string }, metadata?: any }`
|
|
220
|
+
- Returns: `Id<"threads">`
|
|
221
|
+
- Creates a new thread in the zone
|
|
222
|
+
- The `position` field is **optional** - use it for positioned comments (document editors, design tools, video timestamps). See [#positioned-comments-optional](#positioned-comments-optional) for examples.
|
|
223
|
+
|
|
224
|
+
**getThreads(ctx, args)**
|
|
225
|
+
- Args: `{ zoneId: Id<"zones">, limit?: number, cursor?: string, includeResolved?: boolean }`
|
|
226
|
+
- Returns: `{ threads: Thread[], nextCursor?: string, hasMore: boolean }`
|
|
227
|
+
- Lists threads with first message preview and pagination
|
|
228
|
+
|
|
229
|
+
**resolveThread(ctx, args)**
|
|
230
|
+
- Args: `{ threadId: Id<"threads">, userId: string }`
|
|
231
|
+
- Marks thread as resolved
|
|
232
|
+
|
|
233
|
+
**unresolveThread(ctx, args)**
|
|
234
|
+
- Args: `{ threadId: Id<"threads"> }`
|
|
235
|
+
- Reopens a resolved thread
|
|
236
|
+
|
|
237
|
+
**deleteThread(ctx, args)**
|
|
238
|
+
- Args: `{ threadId: Id<"threads"> }`
|
|
239
|
+
- Deletes thread and all messages
|
|
240
|
+
|
|
241
|
+
### Messages
|
|
242
|
+
|
|
243
|
+
**addComment(ctx, args)**
|
|
244
|
+
- Args: `{ threadId: Id<"threads">, authorId: string, body: string, attachments?: Attachment[] }`
|
|
245
|
+
- Returns: `{ messageId: Id<"messages">, mentions: Mention[], links: Link[] }`
|
|
246
|
+
- Creates message and parses mentions/links automatically
|
|
247
|
+
|
|
248
|
+
**getMessages(ctx, args)**
|
|
249
|
+
- Args: `{ threadId: Id<"threads">, limit?: number, cursor?: string, currentUserId?: string }`
|
|
250
|
+
- Returns: `{ messages: Message[], nextCursor?: string, hasMore: boolean }`
|
|
251
|
+
- Lists messages with reactions, supports pagination
|
|
252
|
+
|
|
253
|
+
**editMessage(ctx, args)**
|
|
254
|
+
- Args: `{ messageId: Id<"messages">, body: string, authorId?: string }`
|
|
255
|
+
- Updates message body
|
|
256
|
+
|
|
257
|
+
**deleteMessage(ctx, args)**
|
|
258
|
+
- Args: `{ messageId: Id<"messages">, authorId?: string }`
|
|
259
|
+
- Soft deletes message (marks as deleted, preserves data)
|
|
260
|
+
|
|
261
|
+
### Reactions
|
|
262
|
+
|
|
263
|
+
**toggleReaction(ctx, args)**
|
|
264
|
+
- Args: `{ messageId: Id<"messages">, userId: string, emoji: string }`
|
|
265
|
+
- Returns: `{ added: boolean }`
|
|
266
|
+
- Adds reaction if not present, removes if already exists
|
|
267
|
+
|
|
268
|
+
**addReaction(ctx, args)**
|
|
269
|
+
- Args: `{ messageId: Id<"messages">, userId: string, emoji: string }`
|
|
270
|
+
- Adds reaction (idempotent)
|
|
271
|
+
|
|
272
|
+
**removeReaction(ctx, args)**
|
|
273
|
+
- Args: `{ messageId: Id<"messages">, userId: string, emoji: string }`
|
|
274
|
+
- Removes reaction
|
|
275
|
+
|
|
276
|
+
**getReactions(ctx, args)**
|
|
277
|
+
- Args: `{ messageId: Id<"messages">, currentUserId?: string }`
|
|
278
|
+
- Returns grouped reactions with counts and user lists
|
|
279
|
+
|
|
280
|
+
### Typing Indicators
|
|
281
|
+
|
|
282
|
+
**setIsTyping(ctx, args)**
|
|
283
|
+
- Args: `{ threadId: Id<"threads">, userId: string, isTyping: boolean }`
|
|
284
|
+
- Sets typing state, automatically expires after 3 seconds
|
|
285
|
+
|
|
286
|
+
**getTypingUsers(ctx, args)**
|
|
287
|
+
- Args: `{ threadId: Id<"threads">, excludeUserId?: string }`
|
|
288
|
+
- Returns list of users currently typing (filters expired)
|
|
289
|
+
|
|
290
|
+
**clearUserTyping(ctx, args)**
|
|
291
|
+
- Args: `{ userId: string }`
|
|
292
|
+
- Clears all typing indicators for user
|
|
293
|
+
|
|
294
|
+
## React Components
|
|
295
|
+
|
|
296
|
+
Optional UI components for displaying comments.
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import {
|
|
300
|
+
CommentsProvider,
|
|
301
|
+
Comments,
|
|
302
|
+
Thread,
|
|
303
|
+
Comment,
|
|
304
|
+
AddComment,
|
|
305
|
+
} from "@hamzasaleemorg/convex-comments/react";
|
|
306
|
+
|
|
307
|
+
function App() {
|
|
308
|
+
return (
|
|
309
|
+
<CommentsProvider
|
|
310
|
+
userId={currentUser.id}
|
|
311
|
+
resolveUser={(id) => ({ name: users[id].name })}
|
|
312
|
+
reactionChoices={["👍", "❤️", "😄", "🎉"]}
|
|
313
|
+
>
|
|
314
|
+
<Comments threads={threads} />
|
|
315
|
+
</CommentsProvider>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### CommentsProvider
|
|
321
|
+
|
|
322
|
+
Required wrapper that provides configuration to child components.
|
|
323
|
+
|
|
324
|
+
Props:
|
|
325
|
+
- `userId: string | null` - Current user ID
|
|
326
|
+
- `resolveUser?: (userId: string) => Promise<{ name: string, avatar?: string }>` - Function to fetch user details
|
|
327
|
+
- `reactionChoices?: string[]` - Available emoji reactions
|
|
328
|
+
- `canModerate?: boolean` - Whether user can moderate comments
|
|
329
|
+
- `styles?: CommentsStyles` - Custom styling
|
|
330
|
+
|
|
331
|
+
### Comments
|
|
332
|
+
|
|
333
|
+
Displays list of threads.
|
|
334
|
+
|
|
335
|
+
Props:
|
|
336
|
+
- `threads: Thread[]` - Array of threads to display
|
|
337
|
+
- `hasMore?: boolean` - Whether more threads exist
|
|
338
|
+
- `onThreadClick?: (threadId: string) => void` - Thread click handler
|
|
339
|
+
- `onNewThread?: () => void` - New thread button handler
|
|
340
|
+
|
|
341
|
+
### Thread
|
|
342
|
+
|
|
343
|
+
Displays single thread with messages.
|
|
344
|
+
|
|
345
|
+
Props:
|
|
346
|
+
- `thread: Thread` - Thread data
|
|
347
|
+
- `messages: Message[]` - Array of messages
|
|
348
|
+
- `typingUsers?: TypingUser[]` - Users currently typing
|
|
349
|
+
- `onSubmit?: (body: string) => void` - Submit handler
|
|
350
|
+
- `onToggleReaction?: (messageId: string, emoji: string) => void`
|
|
351
|
+
- `onResolve?: () => void`
|
|
352
|
+
|
|
353
|
+
### Comment
|
|
354
|
+
|
|
355
|
+
Displays single message.
|
|
356
|
+
|
|
357
|
+
Props:
|
|
358
|
+
- `comment: Message` - Message data
|
|
359
|
+
- `mine?: boolean` - Whether current user authored message
|
|
360
|
+
- `onToggleReaction?: (emoji: string) => void`
|
|
361
|
+
- `onEdit?: (newBody: string) => void`
|
|
362
|
+
- `onDelete?: () => void`
|
|
363
|
+
|
|
364
|
+
### AddComment
|
|
365
|
+
|
|
366
|
+
Message composer with mention autocomplete.
|
|
367
|
+
|
|
368
|
+
Props:
|
|
369
|
+
- `onSubmit?: (body: string, attachments?: Attachment[]) => void`
|
|
370
|
+
- `onTypingChange?: (isTyping: boolean) => void`
|
|
371
|
+
- `mentionableUsers?: MentionableUser[]` - Users for autocomplete
|
|
372
|
+
- `placeholder?: string`
|
|
373
|
+
- `allowEditing?: boolean`
|
|
374
|
+
|
|
375
|
+
## Positioned Comments (Optional)
|
|
376
|
+
|
|
377
|
+
The `position` field in `addThread()` is an **optional feature** for anchoring comments to specific locations. This is useful for:
|
|
378
|
+
|
|
379
|
+
- Document editors (comment on specific paragraphs)
|
|
380
|
+
- Design tools (comment at x/y coordinates on canvas)
|
|
381
|
+
- Video players (comment at specific timestamps)
|
|
382
|
+
- Code review (comment on specific line numbers)
|
|
383
|
+
|
|
384
|
+
### Position Object
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
position?: {
|
|
388
|
+
x: number; // X coordinate (pixels, percentage, line number, etc.)
|
|
389
|
+
y: number; // Y coordinate (pixels, percentage, timestamp, etc.)
|
|
390
|
+
anchor?: string; // Optional identifier (element ID, paragraph, filename)
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### When to Use Position
|
|
395
|
+
|
|
396
|
+
**Use positioned comments if:**
|
|
397
|
+
- Your UI needs to show comments at specific visual locations
|
|
398
|
+
- You want to anchor threads to content that can move (paragraphs, code blocks)
|
|
399
|
+
- You're building collaborative editing tools
|
|
400
|
+
- Comments need to appear as overlays or annotations
|
|
401
|
+
|
|
402
|
+
**Skip position if:**
|
|
403
|
+
- You only need general discussions (like GitHub issue comments)
|
|
404
|
+
- All comments appear in a single list/feed
|
|
405
|
+
- Location doesn't matter for your use case
|
|
406
|
+
|
|
407
|
+
### Examples
|
|
408
|
+
|
|
409
|
+
**Document editor (like Google Docs):**
|
|
410
|
+
```typescript
|
|
411
|
+
await comments.addThread(ctx, {
|
|
412
|
+
zoneId,
|
|
413
|
+
position: {
|
|
414
|
+
x: 120, // Pixels from left
|
|
415
|
+
y: 450, // Pixels from top
|
|
416
|
+
anchor: "para-3" // Paragraph identifier
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
**Design tool (like Figma):**
|
|
422
|
+
```typescript
|
|
423
|
+
await comments.addThread(ctx, {
|
|
424
|
+
zoneId,
|
|
425
|
+
position: {
|
|
426
|
+
x: 500, // Canvas X coordinate
|
|
427
|
+
y: 300, // Canvas Y coordinate
|
|
428
|
+
anchor: "layer-5" // Layer name
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
**Video player:**
|
|
434
|
+
```typescript
|
|
435
|
+
await comments.addThread(ctx, {
|
|
436
|
+
zoneId,
|
|
437
|
+
position: {
|
|
438
|
+
x: 0, // Not used for video
|
|
439
|
+
y: 125, // Timestamp in seconds
|
|
440
|
+
anchor: "timecode" // Indicates this is a timestamp
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**Code review:**
|
|
446
|
+
```typescript
|
|
447
|
+
await comments.addThread(ctx, {
|
|
448
|
+
zoneId,
|
|
449
|
+
position: {
|
|
450
|
+
x: 0, // Not used
|
|
451
|
+
y: 42, // Line number
|
|
452
|
+
anchor: "src/main.ts" // File path
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### No Position Needed
|
|
458
|
+
|
|
459
|
+
For simple comment threads (like chat, issue tracking, general discussions), just omit the position field:
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
// Simple thread without position
|
|
463
|
+
await comments.addThread(ctx, { zoneId });
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
**Key Point:** The component stores position data but doesn't render it. Your UI decides how to display positioned threads based on the stored coordinates.
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
## Mention Parsing
|
|
471
|
+
|
|
472
|
+
Mentions use `@userId` format and are parsed automatically when creating messages.
|
|
473
|
+
|
|
474
|
+
Supported characters in user IDs:
|
|
475
|
+
- Letters and numbers
|
|
476
|
+
- Underscores, hyphens, colons
|
|
477
|
+
|
|
478
|
+
Examples:
|
|
479
|
+
- `@alice`
|
|
480
|
+
- `@user_123`
|
|
481
|
+
- `@clerk:user_abc`
|
|
482
|
+
|
|
483
|
+
The `addComment` function returns parsed mentions with their positions in the text:
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
{
|
|
487
|
+
mentions: [
|
|
488
|
+
{ userId: "alice", start: 0, end: 6 },
|
|
489
|
+
{ userId: "bob", start: 11, end: 15 }
|
|
490
|
+
]
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
## Attachments
|
|
495
|
+
|
|
496
|
+
Messages support attachments with metadata:
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
await comments.addComment(ctx, {
|
|
500
|
+
threadId: "...",
|
|
501
|
+
authorId: "...",
|
|
502
|
+
body: "Attached files",
|
|
503
|
+
attachments: [
|
|
504
|
+
{
|
|
505
|
+
type: "image",
|
|
506
|
+
url: "https://example.com/image.png",
|
|
507
|
+
name: "Screenshot.png",
|
|
508
|
+
mimeType: "image/png",
|
|
509
|
+
size: 145678,
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
type: "file",
|
|
513
|
+
url: "https://example.com/doc.pdf",
|
|
514
|
+
name: "Document.pdf",
|
|
515
|
+
},
|
|
516
|
+
],
|
|
517
|
+
});
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
Supported types: `"url"`, `"file"`, `"image"`
|
|
521
|
+
|
|
522
|
+
## Callbacks
|
|
523
|
+
|
|
524
|
+
The Comments class accepts optional callbacks for notifications:
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
const comments = new Comments(components.comments, {
|
|
528
|
+
onNewMessage: async ({ messageId, threadId, authorId, body, mentions }) => {
|
|
529
|
+
// Send notification about new message
|
|
530
|
+
},
|
|
531
|
+
onMention: async ({ messageId, mentionedUserId, authorId, body }) => {
|
|
532
|
+
// Send notification to mentioned user
|
|
533
|
+
},
|
|
534
|
+
});
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
Both callbacks are triggered automatically when messages are created through the Comments class methods.
|
|
538
|
+
|
|
539
|
+
## HTTP Routes
|
|
540
|
+
|
|
541
|
+
Expose comments data through HTTP endpoints:
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
// convex/http.ts
|
|
545
|
+
import { httpRouter } from "convex/server";
|
|
546
|
+
import { registerRoutes } from "@hamzasaleemorg/convex-comments";
|
|
547
|
+
import { components } from "./_generated/api";
|
|
548
|
+
|
|
549
|
+
const http = httpRouter();
|
|
550
|
+
registerRoutes(http, components.comments, {
|
|
551
|
+
pathPrefix: "/api/comments",
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
export default http;
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
Endpoints:
|
|
558
|
+
- `GET /api/comments/zones?entityId=...`
|
|
559
|
+
- `GET /api/comments/threads?zoneId=...`
|
|
560
|
+
- `GET /api/comments/messages?threadId=...`
|
|
561
|
+
|
|
562
|
+
## Development
|
|
563
|
+
|
|
564
|
+
```bash
|
|
565
|
+
npm install
|
|
566
|
+
npm run dev
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
Runs:
|
|
570
|
+
- Component build watcher
|
|
571
|
+
- Example app with Vite and Convex dev
|
|
572
|
+
|
|
573
|
+
## Testing
|
|
574
|
+
|
|
575
|
+
```bash
|
|
576
|
+
npm test
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
## License
|
|
580
|
+
|
|
581
|
+
Apache-2.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=_ignore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_ignore.d.ts","sourceRoot":"","sources":["../../../src/client/_generated/_ignore.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_ignore.js","sourceRoot":"","sources":["../../../src/client/_generated/_ignore.ts"],"names":[],"mappings":";AAAA,kEAAkE"}
|