@hamzasaleemorg/convex-comments 1.0.0 → 1.0.1

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.
@@ -82,10 +82,15 @@ interface MentionCallbacks {
82
82
  *
83
83
  * Usage:
84
84
  * ```ts
85
- * import { Comments } from "@your-org/comments";
85
+ * import { Comments } from "@hamzasaleemorg/convex-comments";
86
86
  * import { components } from "./_generated/api";
87
87
  *
88
- * const comments = new Comments(components.comments);
88
+ * const comments = new Comments(components.comments, {
89
+ * onMention: async ({ mentionedUserId, authorId, body }) => {
90
+ * // Send email or push notification to the mentioned user
91
+ * console.log(`${authorId} mentioned ${mentionedUserId}: ${body}`);
92
+ * }
93
+ * });
89
94
  *
90
95
  * // In a mutation/query handler:
91
96
  * const zoneId = await comments.getOrCreateZone(ctx, { entityId: "doc_123" });
@@ -103,7 +108,18 @@ export class Comments {
103
108
  // ==========================================================================
104
109
 
105
110
  /**
106
- * Get or create a zone for an entity.
111
+ * Get or create a "Zone" for a specific entity.
112
+ *
113
+ * A Zone is the top-level container for all threads and messages related to a specific
114
+ * resource in your app (e.g., a specific document ID, a task ID, or a project page).
115
+ *
116
+ * This method uses a "get or create" pattern, making it ideal for lazy-initializing
117
+ * the comment system for an entity the first time a user interacts with it.
118
+ *
119
+ * @param ctx - The mutation context (requires runMutation)
120
+ * @param args.entityId - A unique string identifying your resource (e.g., "doc_123")
121
+ * @param args.metadata - Optional arbitrary data to store with the zone
122
+ * @returns The unique ID of the zone
107
123
  */
108
124
  async getOrCreateZone(
109
125
  ctx: MutationCtx,
@@ -113,21 +129,33 @@ export class Comments {
113
129
  }
114
130
 
115
131
  /**
116
- * Get a zone by entity ID (without creating).
132
+ * Retrieves an existing zone by its entity ID.
133
+ *
134
+ * Unlike `getOrCreateZone`, this will return `null` if the zone doesn't exist yet.
135
+ * Use this when you want to check if an entity has any comments without creating a container.
136
+ *
137
+ * @param ctx - The query context
138
+ * @param args.entityId - The unique resource identifier
117
139
  */
118
140
  async getZone(ctx: QueryCtx, args: { entityId: string }) {
119
141
  return await ctx.runQuery(this.component.lib.getZone, args);
120
142
  }
121
143
 
122
144
  /**
123
- * Get a zone by its ID.
145
+ * Retrieves a zone by its internal Convex ID.
146
+ *
147
+ * Useful when you already have a `zoneId` (e.g., from a thread reference) and
148
+ * need to fetch its associated metadata or entity identifier.
124
149
  */
125
150
  async getZoneById(ctx: QueryCtx, args: { zoneId: string }) {
126
151
  return await ctx.runQuery(this.component.lib.getZoneById, { zoneId: args.zoneId });
127
152
  }
128
153
 
129
154
  /**
130
- * Delete a zone and all its contents.
155
+ * Permanently deletes a zone and every thread, message, and reaction within it.
156
+ *
157
+ * This is a destructive operation often used when the parent resource
158
+ * (e.g., a document) is being deleted from your system.
131
159
  */
132
160
  async deleteZone(ctx: MutationCtx, args: { zoneId: string }) {
133
161
  return await ctx.runMutation(this.component.lib.deleteZone, { zoneId: args.zoneId });
@@ -138,7 +166,14 @@ export class Comments {
138
166
  // ==========================================================================
139
167
 
140
168
  /**
141
- * Create a new thread in a zone.
169
+ * Creates a new conversation thread within a specific zone.
170
+ *
171
+ * Threads act as a grouping for related messages. They support "positioned"
172
+ * comments (e.g., pins on a PDF or coordinates on a canvas) via the `position` argument.
173
+ *
174
+ * @param args.zoneId - The ID of the zone to contain this thread
175
+ * @param args.position - Optional coordinates {x, y} and an anchor point for UI placement
176
+ * @param args.metadata - Optional data (e.g., the specific version of a document)
142
177
  */
143
178
  async addThread(
144
179
  ctx: MutationCtx,
@@ -156,14 +191,19 @@ export class Comments {
156
191
  }
157
192
 
158
193
  /**
159
- * Get a thread by ID.
194
+ * Retrieves full details for a specific thread, including its position and resolution status.
160
195
  */
161
196
  async getThread(ctx: QueryCtx, args: { threadId: string }) {
162
197
  return await ctx.runQuery(this.component.lib.getThread, { threadId: args.threadId });
163
198
  }
164
199
 
165
200
  /**
166
- * Get all threads in a zone with pagination.
201
+ * Lists threads within a zone, typically used for a "Sidebar" or "Activity" view.
202
+ *
203
+ * Supports pagination and filtering by resolution status.
204
+ *
205
+ * @param args.includeResolved - If true, returns both open and resolved threads. Defaults to false.
206
+ * @param args.limit - Maximum number of threads to return in one page.
167
207
  */
168
208
  async getThreads(
169
209
  ctx: QueryCtx,
@@ -183,7 +223,11 @@ export class Comments {
183
223
  }
184
224
 
185
225
  /**
186
- * Resolve a thread.
226
+ * Marks a thread as resolved.
227
+ *
228
+ * Resolved threads are effectively "closed" and are hidden from the default `getThreads` view.
229
+ *
230
+ * @param args.userId - The ID of the user who resolved the thread.
187
231
  */
188
232
  async resolveThread(
189
233
  ctx: MutationCtx,
@@ -193,14 +237,16 @@ export class Comments {
193
237
  }
194
238
 
195
239
  /**
196
- * Unresolve a thread.
240
+ * Re-opens a previously resolved thread.
197
241
  */
198
242
  async unresolveThread(ctx: MutationCtx, args: { threadId: string }) {
199
243
  return await ctx.runMutation(this.component.lib.unresolveThread, args);
200
244
  }
201
245
 
202
246
  /**
203
- * Update thread position.
247
+ * Updates the visual position of a thread.
248
+ *
249
+ * Useful for "draggable" comment pins or when the underlying content changes layout.
204
250
  */
205
251
  async updateThreadPosition(
206
252
  ctx: MutationCtx,
@@ -213,7 +259,7 @@ export class Comments {
213
259
  }
214
260
 
215
261
  /**
216
- * Delete a thread and all its messages.
262
+ * Permanently deletes a thread and all its messages.
217
263
  */
218
264
  async deleteThread(ctx: MutationCtx, args: { threadId: string }) {
219
265
  return await ctx.runMutation(this.component.lib.deleteThread, { threadId: args.threadId });
@@ -224,8 +270,16 @@ export class Comments {
224
270
  // ==========================================================================
225
271
 
226
272
  /**
227
- * Add a comment to a thread.
228
- * Returns the message ID and parsed mentions/links.
273
+ * Adds a new comment (message) to a thread.
274
+ *
275
+ * This method automatically parses the body for `@user` mentions and URLs.
276
+ * If the `Comments` client was initialized with callbacks, `onNewMessage` and
277
+ * `onMention` will be triggered automatically.
278
+ *
279
+ * @param args.authorId - The ID of the user sending the comment
280
+ * @param args.body - The text content (supports markdown)
281
+ * @param args.attachments - Optional array of file/url attachments
282
+ * @returns The generated message ID and the list of detected mentions/links
229
283
  */
230
284
  async addComment(
231
285
  ctx: MutationCtx,
@@ -269,7 +323,10 @@ export class Comments {
269
323
  }
270
324
 
271
325
  /**
272
- * Get a single message with reactions.
326
+ * Fetches a specific message, including its reaction and resolution status.
327
+ *
328
+ * @param args.currentUserId - Optional. If provided, the response will include
329
+ * `includesMe` flags for reactions.
273
330
  */
274
331
  async getMessage(
275
332
  ctx: QueryCtx,
@@ -279,7 +336,12 @@ export class Comments {
279
336
  }
280
337
 
281
338
  /**
282
- * Get messages in a thread with pagination.
339
+ * Retrieves a paginated list of messages for a thread.
340
+ *
341
+ * Typically used to populate the main comment list for a conversation.
342
+ *
343
+ * @param args.order - "asc" for chronological (chat style) or "desc" for newest first.
344
+ * @param args.includeDeleted - If true, includes placeholders for deleted messages.
283
345
  */
284
346
  async getMessages(
285
347
  ctx: QueryCtx,
@@ -296,7 +358,9 @@ export class Comments {
296
358
  }
297
359
 
298
360
  /**
299
- * Edit a message.
361
+ * Edits the content of an existing message.
362
+ *
363
+ * The message will be marked as `isEdited: true`.
300
364
  */
301
365
  async editMessage(
302
366
  ctx: MutationCtx,
@@ -306,7 +370,9 @@ export class Comments {
306
370
  }
307
371
 
308
372
  /**
309
- * Soft delete a message.
373
+ * Soft-deletes a message.
374
+ *
375
+ * The message record remains but its `body` is cleared and `isDeleted` is set to true.
310
376
  */
311
377
  async deleteMessage(
312
378
  ctx: MutationCtx,
@@ -320,7 +386,7 @@ export class Comments {
320
386
  // ==========================================================================
321
387
 
322
388
  /**
323
- * Add a reaction to a message.
389
+ * Adds an emoji reaction to a message for a specific user.
324
390
  */
325
391
  async addReaction(
326
392
  ctx: MutationCtx,
@@ -330,7 +396,7 @@ export class Comments {
330
396
  }
331
397
 
332
398
  /**
333
- * Remove a reaction from a message.
399
+ * Removes an emoji reaction from a message.
334
400
  */
335
401
  async removeReaction(
336
402
  ctx: MutationCtx,
@@ -340,7 +406,8 @@ export class Comments {
340
406
  }
341
407
 
342
408
  /**
343
- * Toggle a reaction (add if not present, remove if present).
409
+ * Toggles a reaction. If the user already reacted with this emoji,
410
+ * it removes it; otherwise, it adds it.
344
411
  */
345
412
  async toggleReaction(
346
413
  ctx: MutationCtx,
@@ -350,7 +417,7 @@ export class Comments {
350
417
  }
351
418
 
352
419
  /**
353
- * Get all reactions for a message.
420
+ * Retrieves a summary of all reactions for a message.
354
421
  */
355
422
  async getReactions(
356
423
  ctx: QueryCtx,
@@ -364,7 +431,17 @@ export class Comments {
364
431
  // ==========================================================================
365
432
 
366
433
  /**
367
- * Set typing indicator for a user in a thread.
434
+ * Updates the "isTyping" status for a user in a specific thread.
435
+ *
436
+ * This is used to drive real-time typing indicators in your UI. The typing status
437
+ * automatically expires after a short period (typically 5-10 seconds of inactivity).
438
+ *
439
+ * For the best user experience, call this whenever the user types a character
440
+ * (debounced) or when the input field loses focus.
441
+ *
442
+ * @param args.threadId - Internal ID of the thread
443
+ * @param args.userId - The ID of the user who is typing
444
+ * @param args.isTyping - true to show as typing, false to immediately clear
368
445
  */
369
446
  async setIsTyping(
370
447
  ctx: MutationCtx,
@@ -374,7 +451,12 @@ export class Comments {
374
451
  }
375
452
 
376
453
  /**
377
- * Get all users currently typing in a thread.
454
+ * Returns a list of user IDs currently typing in a thread.
455
+ *
456
+ * Use this in a reactive query to show "User A, User B are typing..." in your UI.
457
+ *
458
+ * @param args.excludeUserId - Optional. Exclude the current user from the list to avoid
459
+ * showing an "I am typing" indicator to yourself.
378
460
  */
379
461
  async getTypingUsers(
380
462
  ctx: QueryCtx,
@@ -384,14 +466,23 @@ export class Comments {
384
466
  }
385
467
 
386
468
  /**
387
- * Clear all typing indicators for a user.
469
+ * Immediately clears all typing indicators for a specific user across all threads.
470
+ *
471
+ * Useful to call when a user logs out or closes their browser tab to ensure
472
+ * typing indicators don't linger for the timeout duration.
388
473
  */
389
474
  async clearUserTyping(ctx: MutationCtx, args: { userId: string }) {
390
475
  return await ctx.runMutation(this.component.lib.clearUserTyping, args);
391
476
  }
392
477
 
393
478
  /**
394
- * Resolve a message.
479
+ * Resolves an individual message.
480
+ *
481
+ * While `resolveThread` marks an entire conversation as closed, `resolveMessage`
482
+ * is useful for "task-style" comments where each message might represent a
483
+ * specific action item that can be checked off.
484
+ *
485
+ * @param args.userId - The ID of the user who resolved the message.
395
486
  */
396
487
  async resolveMessage(
397
488
  ctx: MutationCtx,
@@ -401,7 +492,7 @@ export class Comments {
401
492
  }
402
493
 
403
494
  /**
404
- * Unresolve a message.
495
+ * Re-opens a previously resolved message.
405
496
  */
406
497
  async unresolveMessage(ctx: MutationCtx, args: { messageId: string }) {
407
498
  return await ctx.runMutation(this.component.lib.unresolveMessage, args);
@@ -421,19 +512,24 @@ export class Comments {
421
512
  * Usage:
422
513
  * ```ts
423
514
  * // In convex/comments.ts
424
- * import { exposeApi } from "@your-org/comments";
515
+ * import { exposeApi } from "@hamzasaleemorg/convex-comments";
425
516
  * import { components } from "./_generated/api";
426
517
  *
427
- * export const { getThreads, addComment, toggleReaction } = exposeApi(
518
+ * export const { getThreads, addComment, toggleReaction, setIsTyping } = exposeApi(
428
519
  * components.comments,
429
520
  * {
430
521
  * auth: async (ctx, operation) => {
431
- * const userId = await getAuthUserId(ctx);
432
- * if (!userId && operation.type !== "read") {
522
+ * const identity = await ctx.auth.getUserIdentity();
523
+ * if (!identity && operation.type !== "read") {
433
524
  * throw new Error("Authentication required");
434
525
  * }
435
- * return userId ?? "anonymous";
526
+ * return identity?.subject ?? "anonymous";
436
527
  * },
528
+ * callbacks: {
529
+ * onMention: async ({ mentionedUserId }) => {
530
+ * // Handle notification logic here
531
+ * }
532
+ * }
437
533
  * }
438
534
  * );
439
535
  * ```
@@ -448,7 +544,7 @@ export function exposeApi(
448
544
  }
449
545
  ) {
450
546
  return {
451
- // Zone queries/mutations
547
+ /** Initialize or fetch a zone for an entity. Access: create/admin. */
452
548
  getOrCreateZone: mutationGeneric({
453
549
  args: { entityId: v.string(), metadata: v.optional(v.any()) },
454
550
  handler: async (ctx, args) => {
@@ -457,6 +553,7 @@ export function exposeApi(
457
553
  },
458
554
  }),
459
555
 
556
+ /** Get zone by entity ID. Access: read. */
460
557
  getZone: queryGeneric({
461
558
  args: { entityId: v.string() },
462
559
  handler: async (ctx, args) => {
@@ -465,7 +562,7 @@ export function exposeApi(
465
562
  },
466
563
  }),
467
564
 
468
- // Thread queries/mutations
565
+ /** Start a new thread. Access: create/write. */
469
566
  addThread: mutationGeneric({
470
567
  args: {
471
568
  zoneId: v.string(),
@@ -488,6 +585,7 @@ export function exposeApi(
488
585
  },
489
586
  }),
490
587
 
588
+ /** Fetch single thread details. Access: read. */
491
589
  getThread: queryGeneric({
492
590
  args: { threadId: v.string() },
493
591
  handler: async (ctx, args) => {
@@ -496,6 +594,7 @@ export function exposeApi(
496
594
  },
497
595
  }),
498
596
 
597
+ /** List threads in a zone. Access: read. */
499
598
  getThreads: queryGeneric({
500
599
  args: {
501
600
  zoneId: v.string(),
@@ -514,6 +613,7 @@ export function exposeApi(
514
613
  },
515
614
  }),
516
615
 
616
+ /** Close a thread. Access: update/owner. */
517
617
  resolveThread: mutationGeneric({
518
618
  args: { threadId: v.string() },
519
619
  handler: async (ctx, args) => {
@@ -525,6 +625,7 @@ export function exposeApi(
525
625
  },
526
626
  }),
527
627
 
628
+ /** Re-open a thread. Access: update/owner. */
528
629
  unresolveThread: mutationGeneric({
529
630
  args: { threadId: v.string() },
530
631
  handler: async (ctx, args) => {
@@ -533,7 +634,7 @@ export function exposeApi(
533
634
  },
534
635
  }),
535
636
 
536
- // Message queries/mutations
637
+ /** Post a new comment. Access: write. Mentions are parsed automatically. */
537
638
  addComment: mutationGeneric({
538
639
  args: {
539
640
  threadId: v.string(),
@@ -585,6 +686,7 @@ export function exposeApi(
585
686
  },
586
687
  }),
587
688
 
689
+ /** Fetch single message. Access: read. */
588
690
  getMessage: queryGeneric({
589
691
  args: { messageId: v.string() },
590
692
  handler: async (ctx, args) => {
@@ -596,6 +698,7 @@ export function exposeApi(
596
698
  },
597
699
  }),
598
700
 
701
+ /** Fetch conversation history. Access: read. */
599
702
  getMessages: queryGeneric({
600
703
  args: {
601
704
  threadId: v.string(),
@@ -617,6 +720,7 @@ export function exposeApi(
617
720
  },
618
721
  }),
619
722
 
723
+ /** Update message content. Access: update/owner. */
620
724
  editMessage: mutationGeneric({
621
725
  args: { messageId: v.string(), body: v.string() },
622
726
  handler: async (ctx, args) => {
@@ -629,6 +733,7 @@ export function exposeApi(
629
733
  },
630
734
  }),
631
735
 
736
+ /** Soft-delete message. Access: delete/owner. */
632
737
  deleteMessage: mutationGeneric({
633
738
  args: { messageId: v.string() },
634
739
  handler: async (ctx, args) => {
@@ -640,7 +745,7 @@ export function exposeApi(
640
745
  },
641
746
  }),
642
747
 
643
- // Reactions
748
+ /** Add/remove reaction. Access: react. */
644
749
  toggleReaction: mutationGeneric({
645
750
  args: { messageId: v.string(), emoji: v.string() },
646
751
  handler: async (ctx, args) => {
@@ -653,6 +758,7 @@ export function exposeApi(
653
758
  },
654
759
  }),
655
760
 
761
+ /** List reactions for message. Access: read. */
656
762
  getReactions: queryGeneric({
657
763
  args: { messageId: v.string() },
658
764
  handler: async (ctx, args) => {
@@ -664,7 +770,7 @@ export function exposeApi(
664
770
  },
665
771
  }),
666
772
 
667
- // Typing indicators
773
+ /** Signal typing intent. Access: write. Indicators expire automatically. */
668
774
  setIsTyping: mutationGeneric({
669
775
  args: { threadId: v.string(), isTyping: v.boolean() },
670
776
  handler: async (ctx, args) => {
@@ -677,6 +783,7 @@ export function exposeApi(
677
783
  },
678
784
  }),
679
785
 
786
+ /** List current typists. Access: read. */
680
787
  getTypingUsers: queryGeneric({
681
788
  args: { threadId: v.string() },
682
789
  handler: async (ctx, args) => {
@@ -698,18 +805,22 @@ export function exposeApi(
698
805
  * Register HTTP routes for the Comments component.
699
806
  *
700
807
  * 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
808
+ * - GET /comments/zones?entityId=...
809
+ * - GET /comments/threads?zoneId=...
810
+ * - GET /comments/messages?threadId=...
704
811
  *
705
812
  * Usage:
706
813
  * ```ts
707
814
  * // In convex/http.ts
708
- * import { registerRoutes } from "@your-org/comments";
815
+ * import { httpRouter } from "convex/server";
816
+ * import { registerRoutes } from "@hamzasaleemorg/convex-comments";
709
817
  * import { components } from "./_generated/api";
710
818
  *
711
819
  * const http = httpRouter();
820
+ *
821
+ * // Mount the comments API at /api/comments/*
712
822
  * registerRoutes(http, components.comments, { pathPrefix: "/api/comments" });
823
+ *
713
824
  * export default http;
714
825
  * ```
715
826
  */