@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
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comments Component - Client Library
|
|
3
|
+
*
|
|
4
|
+
* This module provides a class-based client for interacting with the
|
|
5
|
+
* comments component, plus helper functions for re-exporting APIs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
httpActionGeneric,
|
|
10
|
+
mutationGeneric,
|
|
11
|
+
queryGeneric,
|
|
12
|
+
} from "convex/server";
|
|
13
|
+
import type {
|
|
14
|
+
Auth,
|
|
15
|
+
|
|
16
|
+
GenericDataModel,
|
|
17
|
+
GenericMutationCtx,
|
|
18
|
+
GenericQueryCtx,
|
|
19
|
+
HttpRouter,
|
|
20
|
+
} from "convex/server";
|
|
21
|
+
import { v } from "convex/values";
|
|
22
|
+
import type { ComponentApi } from "../component/_generated/component.js";
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Types
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
/** Minimal context types for flexibility */
|
|
29
|
+
type QueryCtx = Pick<GenericQueryCtx<GenericDataModel>, "runQuery">;
|
|
30
|
+
type MutationCtx = Pick<GenericMutationCtx<GenericDataModel>, "runQuery" | "runMutation">;
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
/** Authentication operation types */
|
|
34
|
+
type AuthOperation =
|
|
35
|
+
| { type: "read"; zoneId?: string; threadId?: string }
|
|
36
|
+
| { type: "create"; zoneId?: string; threadId?: string }
|
|
37
|
+
| { type: "update"; messageId?: string; threadId?: string }
|
|
38
|
+
| { type: "delete"; messageId?: string; threadId?: string }
|
|
39
|
+
| { type: "react"; messageId?: string };
|
|
40
|
+
|
|
41
|
+
/** Authentication callback that returns the user ID or throws */
|
|
42
|
+
type AuthCallback = (
|
|
43
|
+
ctx: { auth: Auth },
|
|
44
|
+
operation: AuthOperation
|
|
45
|
+
) => Promise<string>;
|
|
46
|
+
|
|
47
|
+
/** Attachment metadata */
|
|
48
|
+
interface Attachment {
|
|
49
|
+
type: "url" | "file" | "image";
|
|
50
|
+
url: string;
|
|
51
|
+
name?: string;
|
|
52
|
+
mimeType?: string;
|
|
53
|
+
size?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Mention callbacks */
|
|
57
|
+
interface MentionCallbacks {
|
|
58
|
+
/** Called when a new message is created */
|
|
59
|
+
onNewMessage?: (params: {
|
|
60
|
+
messageId: string;
|
|
61
|
+
threadId: string;
|
|
62
|
+
authorId: string;
|
|
63
|
+
body: string;
|
|
64
|
+
mentions: Array<{ userId: string; start: number; end: number }>;
|
|
65
|
+
}) => Promise<void>;
|
|
66
|
+
|
|
67
|
+
/** Called for each mention in a message */
|
|
68
|
+
onMention?: (params: {
|
|
69
|
+
messageId: string;
|
|
70
|
+
mentionedUserId: string;
|
|
71
|
+
authorId: string;
|
|
72
|
+
body: string;
|
|
73
|
+
}) => Promise<void>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ============================================================================
|
|
77
|
+
// Comments Client Class
|
|
78
|
+
// ============================================================================
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Class-based client for the Comments component.
|
|
82
|
+
*
|
|
83
|
+
* Usage:
|
|
84
|
+
* ```ts
|
|
85
|
+
* import { Comments } from "@your-org/comments";
|
|
86
|
+
* import { components } from "./_generated/api";
|
|
87
|
+
*
|
|
88
|
+
* const comments = new Comments(components.comments);
|
|
89
|
+
*
|
|
90
|
+
* // In a mutation/query handler:
|
|
91
|
+
* const zoneId = await comments.getOrCreateZone(ctx, { entityId: "doc_123" });
|
|
92
|
+
* const threads = await comments.getThreads(ctx, { zoneId });
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export class Comments {
|
|
96
|
+
constructor(
|
|
97
|
+
public readonly component: ComponentApi,
|
|
98
|
+
private readonly callbacks?: MentionCallbacks
|
|
99
|
+
) { }
|
|
100
|
+
|
|
101
|
+
// ==========================================================================
|
|
102
|
+
// Zone Methods
|
|
103
|
+
// ==========================================================================
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get or create a zone for an entity.
|
|
107
|
+
*/
|
|
108
|
+
async getOrCreateZone(
|
|
109
|
+
ctx: MutationCtx,
|
|
110
|
+
args: { entityId: string; metadata?: unknown }
|
|
111
|
+
): Promise<string> {
|
|
112
|
+
return await ctx.runMutation(this.component.lib.getOrCreateZone, args);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get a zone by entity ID (without creating).
|
|
117
|
+
*/
|
|
118
|
+
async getZone(ctx: QueryCtx, args: { entityId: string }) {
|
|
119
|
+
return await ctx.runQuery(this.component.lib.getZone, args);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get a zone by its ID.
|
|
124
|
+
*/
|
|
125
|
+
async getZoneById(ctx: QueryCtx, args: { zoneId: string }) {
|
|
126
|
+
return await ctx.runQuery(this.component.lib.getZoneById, { zoneId: args.zoneId });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Delete a zone and all its contents.
|
|
131
|
+
*/
|
|
132
|
+
async deleteZone(ctx: MutationCtx, args: { zoneId: string }) {
|
|
133
|
+
return await ctx.runMutation(this.component.lib.deleteZone, { zoneId: args.zoneId });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ==========================================================================
|
|
137
|
+
// Thread Methods
|
|
138
|
+
// ==========================================================================
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Create a new thread in a zone.
|
|
142
|
+
*/
|
|
143
|
+
async addThread(
|
|
144
|
+
ctx: MutationCtx,
|
|
145
|
+
args: {
|
|
146
|
+
zoneId: string;
|
|
147
|
+
position?: { x: number; y: number; anchor?: string };
|
|
148
|
+
metadata?: unknown;
|
|
149
|
+
}
|
|
150
|
+
): Promise<string> {
|
|
151
|
+
return await ctx.runMutation(this.component.lib.addThread, {
|
|
152
|
+
zoneId: args.zoneId,
|
|
153
|
+
position: args.position,
|
|
154
|
+
metadata: args.metadata,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get a thread by ID.
|
|
160
|
+
*/
|
|
161
|
+
async getThread(ctx: QueryCtx, args: { threadId: string }) {
|
|
162
|
+
return await ctx.runQuery(this.component.lib.getThread, { threadId: args.threadId });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get all threads in a zone with pagination.
|
|
167
|
+
*/
|
|
168
|
+
async getThreads(
|
|
169
|
+
ctx: QueryCtx,
|
|
170
|
+
args: {
|
|
171
|
+
zoneId: string;
|
|
172
|
+
limit?: number;
|
|
173
|
+
includeResolved?: boolean;
|
|
174
|
+
cursor?: string;
|
|
175
|
+
}
|
|
176
|
+
) {
|
|
177
|
+
return await ctx.runQuery(this.component.lib.getThreads, {
|
|
178
|
+
zoneId: args.zoneId,
|
|
179
|
+
limit: args.limit,
|
|
180
|
+
includeResolved: args.includeResolved,
|
|
181
|
+
cursor: args.cursor,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Resolve a thread.
|
|
187
|
+
*/
|
|
188
|
+
async resolveThread(
|
|
189
|
+
ctx: MutationCtx,
|
|
190
|
+
args: { threadId: string; userId: string }
|
|
191
|
+
) {
|
|
192
|
+
return await ctx.runMutation(this.component.lib.resolveThread, args);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Unresolve a thread.
|
|
197
|
+
*/
|
|
198
|
+
async unresolveThread(ctx: MutationCtx, args: { threadId: string }) {
|
|
199
|
+
return await ctx.runMutation(this.component.lib.unresolveThread, args);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Update thread position.
|
|
204
|
+
*/
|
|
205
|
+
async updateThreadPosition(
|
|
206
|
+
ctx: MutationCtx,
|
|
207
|
+
args: {
|
|
208
|
+
threadId: string;
|
|
209
|
+
position?: { x: number; y: number; anchor?: string };
|
|
210
|
+
}
|
|
211
|
+
) {
|
|
212
|
+
return await ctx.runMutation(this.component.lib.updateThreadPosition, args);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Delete a thread and all its messages.
|
|
217
|
+
*/
|
|
218
|
+
async deleteThread(ctx: MutationCtx, args: { threadId: string }) {
|
|
219
|
+
return await ctx.runMutation(this.component.lib.deleteThread, { threadId: args.threadId });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ==========================================================================
|
|
223
|
+
// Message Methods
|
|
224
|
+
// ==========================================================================
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Add a comment to a thread.
|
|
228
|
+
* Returns the message ID and parsed mentions/links.
|
|
229
|
+
*/
|
|
230
|
+
async addComment(
|
|
231
|
+
ctx: MutationCtx,
|
|
232
|
+
args: {
|
|
233
|
+
threadId: string;
|
|
234
|
+
authorId: string;
|
|
235
|
+
body: string;
|
|
236
|
+
attachments?: Attachment[];
|
|
237
|
+
}
|
|
238
|
+
) {
|
|
239
|
+
const result = await ctx.runMutation(this.component.lib.addComment, {
|
|
240
|
+
threadId: args.threadId,
|
|
241
|
+
authorId: args.authorId,
|
|
242
|
+
body: args.body,
|
|
243
|
+
attachments: args.attachments,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Fire callbacks if configured
|
|
247
|
+
if (this.callbacks?.onNewMessage) {
|
|
248
|
+
await this.callbacks.onNewMessage({
|
|
249
|
+
messageId: result.messageId,
|
|
250
|
+
threadId: args.threadId,
|
|
251
|
+
authorId: args.authorId,
|
|
252
|
+
body: args.body,
|
|
253
|
+
mentions: result.mentions,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (this.callbacks?.onMention) {
|
|
258
|
+
for (const mention of result.mentions) {
|
|
259
|
+
await this.callbacks.onMention({
|
|
260
|
+
messageId: result.messageId,
|
|
261
|
+
mentionedUserId: mention.userId,
|
|
262
|
+
authorId: args.authorId,
|
|
263
|
+
body: args.body,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Get a single message with reactions.
|
|
273
|
+
*/
|
|
274
|
+
async getMessage(
|
|
275
|
+
ctx: QueryCtx,
|
|
276
|
+
args: { messageId: string; currentUserId?: string }
|
|
277
|
+
) {
|
|
278
|
+
return await ctx.runQuery(this.component.lib.getMessage, args);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Get messages in a thread with pagination.
|
|
283
|
+
*/
|
|
284
|
+
async getMessages(
|
|
285
|
+
ctx: QueryCtx,
|
|
286
|
+
args: {
|
|
287
|
+
threadId: string;
|
|
288
|
+
limit?: number;
|
|
289
|
+
cursor?: string;
|
|
290
|
+
order?: "asc" | "desc";
|
|
291
|
+
currentUserId?: string;
|
|
292
|
+
includeDeleted?: boolean;
|
|
293
|
+
}
|
|
294
|
+
) {
|
|
295
|
+
return await ctx.runQuery(this.component.lib.getMessages, args);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Edit a message.
|
|
300
|
+
*/
|
|
301
|
+
async editMessage(
|
|
302
|
+
ctx: MutationCtx,
|
|
303
|
+
args: { messageId: string; body: string; authorId?: string }
|
|
304
|
+
) {
|
|
305
|
+
return await ctx.runMutation(this.component.lib.editMessage, args);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Soft delete a message.
|
|
310
|
+
*/
|
|
311
|
+
async deleteMessage(
|
|
312
|
+
ctx: MutationCtx,
|
|
313
|
+
args: { messageId: string; authorId?: string }
|
|
314
|
+
) {
|
|
315
|
+
return await ctx.runMutation(this.component.lib.deleteMessage, args);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ==========================================================================
|
|
319
|
+
// Reaction Methods
|
|
320
|
+
// ==========================================================================
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Add a reaction to a message.
|
|
324
|
+
*/
|
|
325
|
+
async addReaction(
|
|
326
|
+
ctx: MutationCtx,
|
|
327
|
+
args: { messageId: string; userId: string; emoji: string }
|
|
328
|
+
) {
|
|
329
|
+
return await ctx.runMutation(this.component.lib.addReaction, args);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Remove a reaction from a message.
|
|
334
|
+
*/
|
|
335
|
+
async removeReaction(
|
|
336
|
+
ctx: MutationCtx,
|
|
337
|
+
args: { messageId: string; userId: string; emoji: string }
|
|
338
|
+
) {
|
|
339
|
+
return await ctx.runMutation(this.component.lib.removeReaction, args);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Toggle a reaction (add if not present, remove if present).
|
|
344
|
+
*/
|
|
345
|
+
async toggleReaction(
|
|
346
|
+
ctx: MutationCtx,
|
|
347
|
+
args: { messageId: string; userId: string; emoji: string }
|
|
348
|
+
) {
|
|
349
|
+
return await ctx.runMutation(this.component.lib.toggleReaction, args);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Get all reactions for a message.
|
|
354
|
+
*/
|
|
355
|
+
async getReactions(
|
|
356
|
+
ctx: QueryCtx,
|
|
357
|
+
args: { messageId: string; currentUserId?: string }
|
|
358
|
+
) {
|
|
359
|
+
return await ctx.runQuery(this.component.lib.getReactions, args);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ==========================================================================
|
|
363
|
+
// Typing Indicator Methods
|
|
364
|
+
// ==========================================================================
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Set typing indicator for a user in a thread.
|
|
368
|
+
*/
|
|
369
|
+
async setIsTyping(
|
|
370
|
+
ctx: MutationCtx,
|
|
371
|
+
args: { threadId: string; userId: string; isTyping: boolean }
|
|
372
|
+
) {
|
|
373
|
+
return await ctx.runMutation(this.component.lib.setIsTyping, args);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Get all users currently typing in a thread.
|
|
378
|
+
*/
|
|
379
|
+
async getTypingUsers(
|
|
380
|
+
ctx: QueryCtx,
|
|
381
|
+
args: { threadId: string; excludeUserId?: string }
|
|
382
|
+
) {
|
|
383
|
+
return await ctx.runQuery(this.component.lib.getTypingUsers, args);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Clear all typing indicators for a user.
|
|
388
|
+
*/
|
|
389
|
+
async clearUserTyping(ctx: MutationCtx, args: { userId: string }) {
|
|
390
|
+
return await ctx.runMutation(this.component.lib.clearUserTyping, args);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Resolve a message.
|
|
395
|
+
*/
|
|
396
|
+
async resolveMessage(
|
|
397
|
+
ctx: MutationCtx,
|
|
398
|
+
args: { messageId: string; userId: string }
|
|
399
|
+
) {
|
|
400
|
+
return await ctx.runMutation(this.component.lib.resolveMessage, args);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Unresolve a message.
|
|
405
|
+
*/
|
|
406
|
+
async unresolveMessage(ctx: MutationCtx, args: { messageId: string }) {
|
|
407
|
+
return await ctx.runMutation(this.component.lib.unresolveMessage, args);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ============================================================================
|
|
412
|
+
// API Re-export Functions
|
|
413
|
+
// ============================================================================
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Create a re-exportable API for the Comments component.
|
|
417
|
+
*
|
|
418
|
+
* This allows apps to expose comments functionality directly to React clients
|
|
419
|
+
* with authentication. The auth callback is called before each operation.
|
|
420
|
+
*
|
|
421
|
+
* Usage:
|
|
422
|
+
* ```ts
|
|
423
|
+
* // In convex/comments.ts
|
|
424
|
+
* import { exposeApi } from "@your-org/comments";
|
|
425
|
+
* import { components } from "./_generated/api";
|
|
426
|
+
*
|
|
427
|
+
* export const { getThreads, addComment, toggleReaction } = exposeApi(
|
|
428
|
+
* components.comments,
|
|
429
|
+
* {
|
|
430
|
+
* auth: async (ctx, operation) => {
|
|
431
|
+
* const userId = await getAuthUserId(ctx);
|
|
432
|
+
* if (!userId && operation.type !== "read") {
|
|
433
|
+
* throw new Error("Authentication required");
|
|
434
|
+
* }
|
|
435
|
+
* return userId ?? "anonymous";
|
|
436
|
+
* },
|
|
437
|
+
* }
|
|
438
|
+
* );
|
|
439
|
+
* ```
|
|
440
|
+
*/
|
|
441
|
+
export function exposeApi(
|
|
442
|
+
component: ComponentApi,
|
|
443
|
+
options: {
|
|
444
|
+
/** Authentication callback - must return user ID or throw */
|
|
445
|
+
auth: AuthCallback;
|
|
446
|
+
/** Optional notification callbacks */
|
|
447
|
+
callbacks?: MentionCallbacks;
|
|
448
|
+
}
|
|
449
|
+
) {
|
|
450
|
+
return {
|
|
451
|
+
// Zone queries/mutations
|
|
452
|
+
getOrCreateZone: mutationGeneric({
|
|
453
|
+
args: { entityId: v.string(), metadata: v.optional(v.any()) },
|
|
454
|
+
handler: async (ctx, args) => {
|
|
455
|
+
await options.auth(ctx, { type: "create" });
|
|
456
|
+
return await ctx.runMutation(component.lib.getOrCreateZone, args);
|
|
457
|
+
},
|
|
458
|
+
}),
|
|
459
|
+
|
|
460
|
+
getZone: queryGeneric({
|
|
461
|
+
args: { entityId: v.string() },
|
|
462
|
+
handler: async (ctx, args) => {
|
|
463
|
+
await options.auth(ctx, { type: "read" });
|
|
464
|
+
return await ctx.runQuery(component.lib.getZone, args);
|
|
465
|
+
},
|
|
466
|
+
}),
|
|
467
|
+
|
|
468
|
+
// Thread queries/mutations
|
|
469
|
+
addThread: mutationGeneric({
|
|
470
|
+
args: {
|
|
471
|
+
zoneId: v.string(),
|
|
472
|
+
position: v.optional(
|
|
473
|
+
v.object({
|
|
474
|
+
x: v.number(),
|
|
475
|
+
y: v.number(),
|
|
476
|
+
anchor: v.optional(v.string()),
|
|
477
|
+
})
|
|
478
|
+
),
|
|
479
|
+
metadata: v.optional(v.any()),
|
|
480
|
+
},
|
|
481
|
+
handler: async (ctx, args) => {
|
|
482
|
+
await options.auth(ctx, { type: "create", zoneId: args.zoneId });
|
|
483
|
+
return await ctx.runMutation(component.lib.addThread, {
|
|
484
|
+
zoneId: args.zoneId,
|
|
485
|
+
position: args.position,
|
|
486
|
+
metadata: args.metadata,
|
|
487
|
+
});
|
|
488
|
+
},
|
|
489
|
+
}),
|
|
490
|
+
|
|
491
|
+
getThread: queryGeneric({
|
|
492
|
+
args: { threadId: v.string() },
|
|
493
|
+
handler: async (ctx, args) => {
|
|
494
|
+
await options.auth(ctx, { type: "read", threadId: args.threadId });
|
|
495
|
+
return await ctx.runQuery(component.lib.getThread, { threadId: args.threadId });
|
|
496
|
+
},
|
|
497
|
+
}),
|
|
498
|
+
|
|
499
|
+
getThreads: queryGeneric({
|
|
500
|
+
args: {
|
|
501
|
+
zoneId: v.string(),
|
|
502
|
+
limit: v.optional(v.number()),
|
|
503
|
+
includeResolved: v.optional(v.boolean()),
|
|
504
|
+
cursor: v.optional(v.string()),
|
|
505
|
+
},
|
|
506
|
+
handler: async (ctx, args) => {
|
|
507
|
+
await options.auth(ctx, { type: "read", zoneId: args.zoneId });
|
|
508
|
+
return await ctx.runQuery(component.lib.getThreads, {
|
|
509
|
+
zoneId: args.zoneId,
|
|
510
|
+
limit: args.limit,
|
|
511
|
+
includeResolved: args.includeResolved,
|
|
512
|
+
cursor: args.cursor,
|
|
513
|
+
});
|
|
514
|
+
},
|
|
515
|
+
}),
|
|
516
|
+
|
|
517
|
+
resolveThread: mutationGeneric({
|
|
518
|
+
args: { threadId: v.string() },
|
|
519
|
+
handler: async (ctx, args) => {
|
|
520
|
+
const userId = await options.auth(ctx, { type: "update", threadId: args.threadId });
|
|
521
|
+
return await ctx.runMutation(component.lib.resolveThread, {
|
|
522
|
+
threadId: args.threadId,
|
|
523
|
+
userId,
|
|
524
|
+
});
|
|
525
|
+
},
|
|
526
|
+
}),
|
|
527
|
+
|
|
528
|
+
unresolveThread: mutationGeneric({
|
|
529
|
+
args: { threadId: v.string() },
|
|
530
|
+
handler: async (ctx, args) => {
|
|
531
|
+
await options.auth(ctx, { type: "update", threadId: args.threadId });
|
|
532
|
+
return await ctx.runMutation(component.lib.unresolveThread, { threadId: args.threadId });
|
|
533
|
+
},
|
|
534
|
+
}),
|
|
535
|
+
|
|
536
|
+
// Message queries/mutations
|
|
537
|
+
addComment: mutationGeneric({
|
|
538
|
+
args: {
|
|
539
|
+
threadId: v.string(),
|
|
540
|
+
body: v.string(),
|
|
541
|
+
attachments: v.optional(
|
|
542
|
+
v.array(
|
|
543
|
+
v.object({
|
|
544
|
+
type: v.union(v.literal("url"), v.literal("file"), v.literal("image")),
|
|
545
|
+
url: v.string(),
|
|
546
|
+
name: v.optional(v.string()),
|
|
547
|
+
mimeType: v.optional(v.string()),
|
|
548
|
+
size: v.optional(v.number()),
|
|
549
|
+
})
|
|
550
|
+
)
|
|
551
|
+
),
|
|
552
|
+
},
|
|
553
|
+
handler: async (ctx, args) => {
|
|
554
|
+
const userId = await options.auth(ctx, { type: "create", threadId: args.threadId });
|
|
555
|
+
const result = await ctx.runMutation(component.lib.addComment, {
|
|
556
|
+
threadId: args.threadId,
|
|
557
|
+
authorId: userId,
|
|
558
|
+
body: args.body,
|
|
559
|
+
attachments: args.attachments,
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
// Fire callbacks
|
|
563
|
+
if (options.callbacks?.onNewMessage) {
|
|
564
|
+
await options.callbacks.onNewMessage({
|
|
565
|
+
messageId: result.messageId,
|
|
566
|
+
threadId: args.threadId,
|
|
567
|
+
authorId: userId,
|
|
568
|
+
body: args.body,
|
|
569
|
+
mentions: result.mentions,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (options.callbacks?.onMention) {
|
|
574
|
+
for (const mention of result.mentions) {
|
|
575
|
+
await options.callbacks.onMention({
|
|
576
|
+
messageId: result.messageId,
|
|
577
|
+
mentionedUserId: mention.userId,
|
|
578
|
+
authorId: userId,
|
|
579
|
+
body: args.body,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return result;
|
|
585
|
+
},
|
|
586
|
+
}),
|
|
587
|
+
|
|
588
|
+
getMessage: queryGeneric({
|
|
589
|
+
args: { messageId: v.string() },
|
|
590
|
+
handler: async (ctx, args) => {
|
|
591
|
+
const userId = await options.auth(ctx, { type: "read" }).catch(() => undefined);
|
|
592
|
+
return await ctx.runQuery(component.lib.getMessage, {
|
|
593
|
+
messageId: args.messageId,
|
|
594
|
+
currentUserId: userId,
|
|
595
|
+
});
|
|
596
|
+
},
|
|
597
|
+
}),
|
|
598
|
+
|
|
599
|
+
getMessages: queryGeneric({
|
|
600
|
+
args: {
|
|
601
|
+
threadId: v.string(),
|
|
602
|
+
limit: v.optional(v.number()),
|
|
603
|
+
cursor: v.optional(v.string()),
|
|
604
|
+
order: v.optional(v.union(v.literal("asc"), v.literal("desc"))),
|
|
605
|
+
includeDeleted: v.optional(v.boolean()),
|
|
606
|
+
},
|
|
607
|
+
handler: async (ctx, args) => {
|
|
608
|
+
const userId = await options.auth(ctx, { type: "read", threadId: args.threadId }).catch(() => undefined);
|
|
609
|
+
return await ctx.runQuery(component.lib.getMessages, {
|
|
610
|
+
threadId: args.threadId,
|
|
611
|
+
limit: args.limit,
|
|
612
|
+
cursor: args.cursor,
|
|
613
|
+
order: args.order,
|
|
614
|
+
currentUserId: userId,
|
|
615
|
+
includeDeleted: args.includeDeleted,
|
|
616
|
+
});
|
|
617
|
+
},
|
|
618
|
+
}),
|
|
619
|
+
|
|
620
|
+
editMessage: mutationGeneric({
|
|
621
|
+
args: { messageId: v.string(), body: v.string() },
|
|
622
|
+
handler: async (ctx, args) => {
|
|
623
|
+
const userId = await options.auth(ctx, { type: "update", messageId: args.messageId });
|
|
624
|
+
return await ctx.runMutation(component.lib.editMessage, {
|
|
625
|
+
messageId: args.messageId,
|
|
626
|
+
body: args.body,
|
|
627
|
+
authorId: userId,
|
|
628
|
+
});
|
|
629
|
+
},
|
|
630
|
+
}),
|
|
631
|
+
|
|
632
|
+
deleteMessage: mutationGeneric({
|
|
633
|
+
args: { messageId: v.string() },
|
|
634
|
+
handler: async (ctx, args) => {
|
|
635
|
+
const userId = await options.auth(ctx, { type: "delete", messageId: args.messageId });
|
|
636
|
+
return await ctx.runMutation(component.lib.deleteMessage, {
|
|
637
|
+
messageId: args.messageId,
|
|
638
|
+
authorId: userId,
|
|
639
|
+
});
|
|
640
|
+
},
|
|
641
|
+
}),
|
|
642
|
+
|
|
643
|
+
// Reactions
|
|
644
|
+
toggleReaction: mutationGeneric({
|
|
645
|
+
args: { messageId: v.string(), emoji: v.string() },
|
|
646
|
+
handler: async (ctx, args) => {
|
|
647
|
+
const userId = await options.auth(ctx, { type: "react", messageId: args.messageId });
|
|
648
|
+
return await ctx.runMutation(component.lib.toggleReaction, {
|
|
649
|
+
messageId: args.messageId,
|
|
650
|
+
userId,
|
|
651
|
+
emoji: args.emoji,
|
|
652
|
+
});
|
|
653
|
+
},
|
|
654
|
+
}),
|
|
655
|
+
|
|
656
|
+
getReactions: queryGeneric({
|
|
657
|
+
args: { messageId: v.string() },
|
|
658
|
+
handler: async (ctx, args) => {
|
|
659
|
+
const userId = await options.auth(ctx, { type: "read" }).catch(() => undefined);
|
|
660
|
+
return await ctx.runQuery(component.lib.getReactions, {
|
|
661
|
+
messageId: args.messageId,
|
|
662
|
+
currentUserId: userId,
|
|
663
|
+
});
|
|
664
|
+
},
|
|
665
|
+
}),
|
|
666
|
+
|
|
667
|
+
// Typing indicators
|
|
668
|
+
setIsTyping: mutationGeneric({
|
|
669
|
+
args: { threadId: v.string(), isTyping: v.boolean() },
|
|
670
|
+
handler: async (ctx, args) => {
|
|
671
|
+
const userId = await options.auth(ctx, { type: "create", threadId: args.threadId });
|
|
672
|
+
return await ctx.runMutation(component.lib.setIsTyping, {
|
|
673
|
+
threadId: args.threadId,
|
|
674
|
+
userId,
|
|
675
|
+
isTyping: args.isTyping,
|
|
676
|
+
});
|
|
677
|
+
},
|
|
678
|
+
}),
|
|
679
|
+
|
|
680
|
+
getTypingUsers: queryGeneric({
|
|
681
|
+
args: { threadId: v.string() },
|
|
682
|
+
handler: async (ctx, args) => {
|
|
683
|
+
const userId = await options.auth(ctx, { type: "read", threadId: args.threadId }).catch(() => undefined);
|
|
684
|
+
return await ctx.runQuery(component.lib.getTypingUsers, {
|
|
685
|
+
threadId: args.threadId,
|
|
686
|
+
excludeUserId: userId,
|
|
687
|
+
});
|
|
688
|
+
},
|
|
689
|
+
}),
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// ============================================================================
|
|
694
|
+
// HTTP Route Registration
|
|
695
|
+
// ============================================================================
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Register HTTP routes for the Comments component.
|
|
699
|
+
*
|
|
700
|
+
* Provides REST-like endpoints for the comments API:
|
|
701
|
+
* - GET /comments/zones/:entityId - Get zone by entity ID
|
|
702
|
+
* - GET /comments/threads/:zoneId - Get threads in a zone
|
|
703
|
+
* - GET /comments/messages/:threadId - Get messages in a thread
|
|
704
|
+
*
|
|
705
|
+
* Usage:
|
|
706
|
+
* ```ts
|
|
707
|
+
* // In convex/http.ts
|
|
708
|
+
* import { registerRoutes } from "@your-org/comments";
|
|
709
|
+
* import { components } from "./_generated/api";
|
|
710
|
+
*
|
|
711
|
+
* const http = httpRouter();
|
|
712
|
+
* registerRoutes(http, components.comments, { pathPrefix: "/api/comments" });
|
|
713
|
+
* export default http;
|
|
714
|
+
* ```
|
|
715
|
+
*/
|
|
716
|
+
export function registerRoutes(
|
|
717
|
+
http: HttpRouter,
|
|
718
|
+
component: ComponentApi,
|
|
719
|
+
options: { pathPrefix?: string } = {}
|
|
720
|
+
) {
|
|
721
|
+
const prefix = options.pathPrefix ?? "/comments";
|
|
722
|
+
|
|
723
|
+
// Get zone by entity ID
|
|
724
|
+
http.route({
|
|
725
|
+
path: `${prefix}/zones`,
|
|
726
|
+
method: "GET",
|
|
727
|
+
handler: httpActionGeneric(async (ctx, request) => {
|
|
728
|
+
const entityId = new URL(request.url).searchParams.get("entityId");
|
|
729
|
+
if (!entityId) {
|
|
730
|
+
return new Response(
|
|
731
|
+
JSON.stringify({ error: "entityId parameter required" }),
|
|
732
|
+
{ status: 400, headers: { "Content-Type": "application/json" } }
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const zone = await ctx.runQuery(component.lib.getZone, { entityId });
|
|
737
|
+
return new Response(JSON.stringify(zone), {
|
|
738
|
+
status: 200,
|
|
739
|
+
headers: { "Content-Type": "application/json" },
|
|
740
|
+
});
|
|
741
|
+
}),
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
// Get threads in a zone
|
|
745
|
+
http.route({
|
|
746
|
+
path: `${prefix}/threads`,
|
|
747
|
+
method: "GET",
|
|
748
|
+
handler: httpActionGeneric(async (ctx, request) => {
|
|
749
|
+
const url = new URL(request.url);
|
|
750
|
+
const zoneId = url.searchParams.get("zoneId");
|
|
751
|
+
if (!zoneId) {
|
|
752
|
+
return new Response(
|
|
753
|
+
JSON.stringify({ error: "zoneId parameter required" }),
|
|
754
|
+
{ status: 400, headers: { "Content-Type": "application/json" } }
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const limit = url.searchParams.get("limit");
|
|
759
|
+
const cursor = url.searchParams.get("cursor");
|
|
760
|
+
const includeResolved = url.searchParams.get("includeResolved");
|
|
761
|
+
|
|
762
|
+
const result = await ctx.runQuery(component.lib.getThreads, {
|
|
763
|
+
zoneId,
|
|
764
|
+
limit: limit ? parseInt(limit, 10) : undefined,
|
|
765
|
+
cursor: cursor ?? undefined,
|
|
766
|
+
includeResolved: includeResolved === "true",
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
return new Response(JSON.stringify(result), {
|
|
770
|
+
status: 200,
|
|
771
|
+
headers: { "Content-Type": "application/json" },
|
|
772
|
+
});
|
|
773
|
+
}),
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
// Get messages in a thread
|
|
777
|
+
http.route({
|
|
778
|
+
path: `${prefix}/messages`,
|
|
779
|
+
method: "GET",
|
|
780
|
+
handler: httpActionGeneric(async (ctx, request) => {
|
|
781
|
+
const url = new URL(request.url);
|
|
782
|
+
const threadId = url.searchParams.get("threadId");
|
|
783
|
+
if (!threadId) {
|
|
784
|
+
return new Response(
|
|
785
|
+
JSON.stringify({ error: "threadId parameter required" }),
|
|
786
|
+
{ status: 400, headers: { "Content-Type": "application/json" } }
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const limit = url.searchParams.get("limit");
|
|
791
|
+
const cursor = url.searchParams.get("cursor");
|
|
792
|
+
const order = url.searchParams.get("order") as "asc" | "desc" | null;
|
|
793
|
+
|
|
794
|
+
const result = await ctx.runQuery(component.lib.getMessages, {
|
|
795
|
+
threadId,
|
|
796
|
+
limit: limit ? parseInt(limit, 10) : undefined,
|
|
797
|
+
cursor: cursor ?? undefined,
|
|
798
|
+
order: order ?? undefined,
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
return new Response(JSON.stringify(result), {
|
|
802
|
+
status: 200,
|
|
803
|
+
headers: { "Content-Type": "application/json" },
|
|
804
|
+
});
|
|
805
|
+
}),
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// ============================================================================
|
|
810
|
+
// Default Export
|
|
811
|
+
// ============================================================================
|
|
812
|
+
|
|
813
|
+
export default Comments;
|