@hocuspocus/provider 2.13.7 → 2.15.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.
@@ -88,6 +88,8 @@ export declare class HocuspocusProvider extends EventEmitter {
88
88
  intervals: any;
89
89
  isConnected: boolean;
90
90
  constructor(configuration: HocuspocusProviderConfiguration);
91
+ boundDocumentUpdateHandler: (update: Uint8Array, origin: any) => void;
92
+ boundAwarenessUpdateHandler: ({ added, updated, removed }: any, origin: any) => void;
91
93
  boundBroadcastChannelSubscriber: (data: ArrayBuffer) => void;
92
94
  boundPageHide: () => void;
93
95
  boundOnOpen: (event: Event) => Promise<void>;
@@ -2,8 +2,13 @@ import type { AbstractType, YArrayEvent } from 'yjs';
2
2
  import * as Y from 'yjs';
3
3
  import { HocuspocusProvider, HocuspocusProviderConfiguration } from './HocuspocusProvider.js';
4
4
  import { TiptapCollabProviderWebsocket } from './TiptapCollabProviderWebsocket.js';
5
- import type { TCollabComment, TCollabThread, THistoryVersion } from './types.js';
6
- export type TiptapCollabProviderConfiguration = Required<Pick<HocuspocusProviderConfiguration, 'name'>> & Partial<HocuspocusProviderConfiguration> & (Required<Pick<AdditionalTiptapCollabProviderConfiguration, 'websocketProvider'>> | Required<Pick<AdditionalTiptapCollabProviderConfiguration, 'appId'>> | Required<Pick<AdditionalTiptapCollabProviderConfiguration, 'baseUrl'>>) & Pick<AdditionalTiptapCollabProviderConfiguration, 'user'>;
5
+ import { type DeleteCommentOptions, type DeleteThreadOptions, type GetThreadsOptions, type TCollabComment, type TCollabThread, type THistoryVersion } from './types.js';
6
+ export type TiptapCollabProviderConfiguration = Required<Pick<HocuspocusProviderConfiguration, 'name'>> & Partial<HocuspocusProviderConfiguration> & (Required<Pick<AdditionalTiptapCollabProviderConfiguration, 'websocketProvider'>> | Required<Pick<AdditionalTiptapCollabProviderConfiguration, 'appId'>> | Required<Pick<AdditionalTiptapCollabProviderConfiguration, 'baseUrl'>>) & Pick<AdditionalTiptapCollabProviderConfiguration, 'user'> & {
7
+ /**
8
+ * Pass `true` if you want to delete a thread when the first comment is deleted.
9
+ */
10
+ deleteThreadOnFirstCommentDelete?: boolean;
11
+ };
7
12
  export interface AdditionalTiptapCollabProviderConfiguration {
8
13
  /**
9
14
  * A Hocuspocus Cloud App ID, get one here: https://cloud.tiptap.dev
@@ -43,21 +48,114 @@ export declare class TiptapCollabProvider extends HocuspocusProvider {
43
48
  isAutoVersioning(): boolean;
44
49
  enableAutoVersioning(): 1;
45
50
  disableAutoVersioning(): 0;
51
+ /**
52
+ * Returns all users in the document as Y.Map objects
53
+ * @returns An array of Y.Map objects
54
+ */
46
55
  private getYThreads;
47
- getThreads<Data, CommentData>(): TCollabThread<Data, CommentData>[];
56
+ /**
57
+ * Finds all threads in the document and returns them as JSON objects
58
+ * @options Options to control the output of the threads (e.g. include deleted threads)
59
+ * @returns An array of threads as JSON objects
60
+ */
61
+ getThreads<Data, CommentData>(options?: GetThreadsOptions): TCollabThread<Data, CommentData>[];
62
+ /**
63
+ * Find the index of a thread by its id
64
+ * @param id The thread id
65
+ * @returns The index of the thread or null if not found
66
+ */
48
67
  private getThreadIndex;
68
+ /**
69
+ * Gets a single thread by its id
70
+ * @param id The thread id
71
+ * @returns The thread as a JSON object or null if not found
72
+ */
49
73
  getThread<Data, CommentData>(id: string): TCollabThread<Data, CommentData> | null;
74
+ /**
75
+ * Gets a single thread by its id as a Y.Map object
76
+ * @param id The thread id
77
+ * @returns The thread as a Y.Map object or null if not found
78
+ */
50
79
  private getYThread;
51
- createThread(data: Omit<TCollabThread, 'id' | 'createdAt' | 'updatedAt' | 'comments'>): TCollabThread;
80
+ /**
81
+ * Create a new thread
82
+ * @param data The thread data
83
+ * @returns The created thread
84
+ */
85
+ createThread(data: Omit<TCollabThread, 'id' | 'createdAt' | 'updatedAt' | 'deletedAt' | 'comments' | 'deletedComments'>): TCollabThread;
86
+ /**
87
+ * Update a specific thread
88
+ * @param id The thread id
89
+ * @param data New data for the thread
90
+ * @returns The updated thread or null if the thread is not found
91
+ */
52
92
  updateThread(id: TCollabThread['id'], data: Partial<Pick<TCollabThread, 'data'> & {
53
93
  resolvedAt: TCollabThread['resolvedAt'] | null;
54
94
  }>): TCollabThread;
55
- deleteThread(id: TCollabThread['id']): void;
56
- getThreadComments(threadId: TCollabThread['id']): TCollabComment[] | null;
57
- getThreadComment(threadId: TCollabThread['id'], commentId: TCollabComment['id']): TCollabComment | null;
95
+ /**
96
+ * Handle the deletion of a thread. By default, the thread and it's comments are not deleted, but marked as deleted
97
+ * via the `deletedAt` property. Forceful deletion can be enabled by setting the `force` option to `true`.
98
+ *
99
+ * If you only want to delete the comments of a thread, you can set the `deleteComments` option to `true`.
100
+ * @param id The thread id
101
+ * @param options A set of options that control how the thread is deleted
102
+ * @returns The deleted thread or null if the thread is not found
103
+ */
104
+ deleteThread(id: TCollabThread['id'], options?: DeleteThreadOptions): TCollabThread | null | undefined;
105
+ /**
106
+ * Tries to restore a deleted thread
107
+ * @param id The thread id
108
+ * @returns The restored thread or null if the thread is not found
109
+ */
110
+ restoreThread(id: TCollabThread['id']): TCollabThread | null;
111
+ /**
112
+ * Returns comments from a thread, either deleted or not
113
+ * @param threadId The thread id
114
+ * @param includeDeleted If you want to include deleted comments, defaults to `false`
115
+ * @returns The comments or null if the thread is not found
116
+ */
117
+ getThreadComments(threadId: TCollabThread['id'], includeDeleted?: boolean): TCollabComment[] | null;
118
+ /**
119
+ * Get a single comment from a specific thread
120
+ * @param threadId The thread id
121
+ * @param commentId The comment id
122
+ * @param includeDeleted If you want to include deleted comments in the search
123
+ * @returns The comment or null if not found
124
+ */
125
+ getThreadComment(threadId: TCollabThread['id'], commentId: TCollabComment['id'], includeDeleted?: boolean): TCollabComment | null;
126
+ /**
127
+ * Adds a comment to a thread
128
+ * @param threadId The thread id
129
+ * @param data The comment data
130
+ * @returns The updated thread or null if the thread is not found
131
+ * @example addComment('123', { content: 'Hello world', data: { author: 'Maria Doe' } })
132
+ */
58
133
  addComment(threadId: TCollabThread['id'], data: Omit<TCollabComment, 'id' | 'updatedAt' | 'createdAt'>): TCollabThread;
134
+ /**
135
+ * Update a comment in a thread
136
+ * @param threadId The thread id
137
+ * @param commentId The comment id
138
+ * @param data The new comment data
139
+ * @returns The updated thread or null if the thread or comment is not found
140
+ * @example updateComment('123', { content: 'The new content', data: { attachments: ['file1.jpg'] }})
141
+ */
59
142
  updateComment(threadId: TCollabThread['id'], commentId: TCollabComment['id'], data: Partial<Pick<TCollabComment, 'data' | 'content'>>): TCollabThread;
60
- deleteComment(threadId: TCollabThread['id'], commentId: TCollabComment['id']): TCollabThread | null | undefined;
143
+ /**
144
+ * Deletes a comment from a thread
145
+ * @param threadId The thread id
146
+ * @param commentId The comment id
147
+ * @param options A set of options that control how the comment is deleted
148
+ * @returns The updated thread or null if the thread or comment is not found
149
+ */
150
+ deleteComment(threadId: TCollabThread['id'], commentId: TCollabComment['id'], options: DeleteCommentOptions): TCollabThread | null | undefined;
151
+ /**
152
+ * Start watching threads for changes
153
+ * @param callback The callback function to be called when a thread changes
154
+ */
61
155
  watchThreads(callback: () => void): void;
156
+ /**
157
+ * Stop watching threads for changes
158
+ * @param callback The callback function to be removed
159
+ */
62
160
  unwatchThreads(callback: () => void): void;
63
161
  }
@@ -88,14 +88,17 @@ export type TCollabThread<Data = any, CommentData = any> = {
88
88
  id: string;
89
89
  createdAt: number;
90
90
  updatedAt: number;
91
+ deletedAt: number | null;
91
92
  resolvedAt?: string;
92
93
  comments: TCollabComment<CommentData>[];
94
+ deletedComments: TCollabComment<CommentData>[];
93
95
  data: Data;
94
96
  };
95
97
  export type TCollabComment<Data = any> = {
96
98
  id: string;
97
- createdAt: number;
98
- updatedAt: number;
99
+ createdAt: string;
100
+ updatedAt: string;
101
+ deletedAt?: string;
99
102
  data: Data;
100
103
  content: any;
101
104
  };
@@ -144,3 +147,40 @@ export type THistoryDocumentRevertedEvent = {
144
147
  event: 'document.reverted';
145
148
  version: number;
146
149
  };
150
+ export type DeleteCommentOptions = {
151
+ /**
152
+ * If `true`, the thread will also be deleted if the deleted comment was the first comment in the thread.
153
+ */
154
+ deleteThread?: boolean;
155
+ /**
156
+ * If `true`, will remove the content of the deleted comment
157
+ */
158
+ deleteContent?: boolean;
159
+ };
160
+ export type DeleteThreadOptions = {
161
+ /**
162
+ * If `true`, will remove the comments on the thread,
163
+ * otherwise will only mark the thread as deleted
164
+ * and keep the comments
165
+ * @default false
166
+ */
167
+ deleteComments?: boolean;
168
+ /**
169
+ * If `true`, will forcefully remove the thread and all comments,
170
+ * otherwise will only mark the thread as deleted
171
+ * and keep the comments
172
+ * @default false
173
+ */
174
+ force?: boolean;
175
+ };
176
+ /**
177
+ * The type of thread
178
+ */
179
+ export type ThreadType = 'archived' | 'unarchived';
180
+ export type GetThreadsOptions = {
181
+ /**
182
+ * The types of threads to get
183
+ * @default ['unarchived']
184
+ */
185
+ types?: Array<ThreadType>;
186
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hocuspocus/provider",
3
- "version": "2.13.7",
3
+ "version": "2.15.0",
4
4
  "description": "hocuspocus provider",
5
5
  "homepage": "https://hocuspocus.dev",
6
6
  "keywords": [
@@ -29,7 +29,7 @@
29
29
  "dist"
30
30
  ],
31
31
  "dependencies": {
32
- "@hocuspocus/common": "^2.13.7",
32
+ "@hocuspocus/common": "^2.15.0",
33
33
  "@lifeomic/attempt": "^3.0.2",
34
34
  "lib0": "^0.2.87",
35
35
  "ws": "^8.17.1"
@@ -212,8 +212,8 @@ export class HocuspocusProvider extends EventEmitter {
212
212
  this.emit('awarenessChange', { states: awarenessStatesToArray(this.awareness!.getStates()) })
213
213
  })
214
214
 
215
- this.document.on('update', this.documentUpdateHandler.bind(this))
216
- this.awareness?.on('update', this.awarenessUpdateHandler.bind(this))
215
+ this.document.on('update', this.boundDocumentUpdateHandler)
216
+ this.awareness?.on('update', this.boundAwarenessUpdateHandler)
217
217
  this.registerEventListeners()
218
218
 
219
219
  if (
@@ -229,6 +229,10 @@ export class HocuspocusProvider extends EventEmitter {
229
229
  this.configuration.websocketProvider.attach(this)
230
230
  }
231
231
 
232
+ boundDocumentUpdateHandler = this.documentUpdateHandler.bind(this)
233
+
234
+ boundAwarenessUpdateHandler = this.awarenessUpdateHandler.bind(this)
235
+
232
236
  boundBroadcastChannelSubscriber = this.broadcastChannelSubscriber.bind(this)
233
237
 
234
238
  boundPageHide = this.pageHide.bind(this)
@@ -490,11 +494,11 @@ export class HocuspocusProvider extends EventEmitter {
490
494
 
491
495
  if (this.awareness) {
492
496
  removeAwarenessStates(this.awareness, [this.document.clientID], 'provider destroy')
493
- this.awareness.off('update', this.awarenessUpdateHandler)
497
+ this.awareness.off('update', this.boundAwarenessUpdateHandler)
494
498
  this.awareness.destroy()
495
499
  }
496
500
 
497
- this.document.off('update', this.documentUpdateHandler)
501
+ this.document.off('update', this.boundDocumentUpdateHandler)
498
502
 
499
503
  this.removeAllListeners()
500
504
 
@@ -7,17 +7,39 @@ import {
7
7
  } from './HocuspocusProvider.js'
8
8
 
9
9
  import { TiptapCollabProviderWebsocket } from './TiptapCollabProviderWebsocket.js'
10
- import type {
11
- TCollabComment, TCollabThread, THistoryVersion,
10
+ import {
11
+ type DeleteCommentOptions,
12
+ type DeleteThreadOptions,
13
+ type GetThreadsOptions,
14
+ type TCollabComment, type TCollabThread, type THistoryVersion,
12
15
  } from './types.js'
13
16
 
17
+ const defaultDeleteCommentOptions: DeleteCommentOptions = {
18
+ deleteContent: false,
19
+ deleteThread: false,
20
+ }
21
+
22
+ const defaultGetThreadsOptions: GetThreadsOptions = {
23
+ types: ['unarchived'],
24
+ }
25
+
26
+ const defaultDeleteThreadOptions: DeleteThreadOptions = {
27
+ deleteComments: false,
28
+ force: false,
29
+ }
30
+
14
31
  export type TiptapCollabProviderConfiguration =
15
32
  Required<Pick<HocuspocusProviderConfiguration, 'name'>> &
16
33
  Partial<HocuspocusProviderConfiguration> &
17
34
  (Required<Pick<AdditionalTiptapCollabProviderConfiguration, 'websocketProvider'>> |
18
35
  Required<Pick<AdditionalTiptapCollabProviderConfiguration, 'appId'>>|
19
36
  Required<Pick<AdditionalTiptapCollabProviderConfiguration, 'baseUrl'>>) &
20
- Pick<AdditionalTiptapCollabProviderConfiguration, 'user'>
37
+ Pick<AdditionalTiptapCollabProviderConfiguration, 'user'> & {
38
+ /**
39
+ * Pass `true` if you want to delete a thread when the first comment is deleted.
40
+ */
41
+ deleteThreadOnFirstCommentDelete?: boolean,
42
+ }
21
43
 
22
44
  export interface AdditionalTiptapCollabProviderConfiguration {
23
45
  /**
@@ -107,20 +129,52 @@ export class TiptapCollabProvider extends HocuspocusProvider {
107
129
  return this.configuration.document.getMap<number>(`${this.tiptapCollabConfigurationPrefix}config`).set('autoVersioning', 0)
108
130
  }
109
131
 
132
+ /**
133
+ * Returns all users in the document as Y.Map objects
134
+ * @returns An array of Y.Map objects
135
+ */
110
136
  private getYThreads() {
111
137
  return this.configuration.document.getArray<Y.Map<any>>(`${this.tiptapCollabConfigurationPrefix}threads`)
112
138
  }
113
139
 
114
- getThreads<Data, CommentData>(): TCollabThread<Data, CommentData>[] {
115
- return this.getYThreads().toJSON() as TCollabThread<Data, CommentData>[]
140
+ /**
141
+ * Finds all threads in the document and returns them as JSON objects
142
+ * @options Options to control the output of the threads (e.g. include deleted threads)
143
+ * @returns An array of threads as JSON objects
144
+ */
145
+ getThreads<Data, CommentData>(options?: GetThreadsOptions): TCollabThread<Data, CommentData>[] {
146
+ const { types } = { ...defaultGetThreadsOptions, ...options } as GetThreadsOptions
147
+
148
+ const threads = this.getYThreads().toJSON() as TCollabThread<Data, CommentData>[]
149
+
150
+ if (types?.includes('archived') && types?.includes('unarchived')) {
151
+ return threads
152
+ }
153
+
154
+ return threads.filter(currentThead => {
155
+ if (types?.includes('archived') && currentThead.deletedAt) {
156
+ return true
157
+ }
158
+
159
+ if (types?.includes('unarchived') && !currentThead.deletedAt) {
160
+ return true
161
+ }
162
+
163
+ return false
164
+ })
116
165
  }
117
166
 
167
+ /**
168
+ * Find the index of a thread by its id
169
+ * @param id The thread id
170
+ * @returns The index of the thread or null if not found
171
+ */
118
172
  private getThreadIndex(id: string): number | null {
119
173
  let index = null
120
174
 
121
175
  let i = 0
122
176
  // eslint-disable-next-line no-restricted-syntax
123
- for (const thread of this.getThreads()) {
177
+ for (const thread of this.getThreads({ types: ['archived', 'unarchived'] })) {
124
178
  if (thread.id === id) {
125
179
  index = i
126
180
  break
@@ -131,6 +185,11 @@ export class TiptapCollabProvider extends HocuspocusProvider {
131
185
  return index
132
186
  }
133
187
 
188
+ /**
189
+ * Gets a single thread by its id
190
+ * @param id The thread id
191
+ * @returns The thread as a JSON object or null if not found
192
+ */
134
193
  getThread<Data, CommentData>(id: string): TCollabThread<Data, CommentData> | null {
135
194
  const index = this.getThreadIndex(id)
136
195
 
@@ -141,6 +200,11 @@ export class TiptapCollabProvider extends HocuspocusProvider {
141
200
  return this.getYThreads().get(index).toJSON() as TCollabThread<Data, CommentData>
142
201
  }
143
202
 
203
+ /**
204
+ * Gets a single thread by its id as a Y.Map object
205
+ * @param id The thread id
206
+ * @returns The thread as a Y.Map object or null if not found
207
+ */
144
208
  private getYThread(id: string) {
145
209
  const index = this.getThreadIndex(id)
146
210
 
@@ -151,7 +215,12 @@ export class TiptapCollabProvider extends HocuspocusProvider {
151
215
  return this.getYThreads().get(index)
152
216
  }
153
217
 
154
- createThread(data: Omit<TCollabThread, 'id' | 'createdAt' | 'updatedAt' | 'comments'>) {
218
+ /**
219
+ * Create a new thread
220
+ * @param data The thread data
221
+ * @returns The created thread
222
+ */
223
+ createThread(data: Omit<TCollabThread, 'id' | 'createdAt' | 'updatedAt' | 'deletedAt' | 'comments' | 'deletedComments'>) {
155
224
  let createdThread: TCollabThread = {} as TCollabThread
156
225
 
157
226
  this.document.transact(() => {
@@ -159,6 +228,8 @@ export class TiptapCollabProvider extends HocuspocusProvider {
159
228
  thread.set('id', uuidv4())
160
229
  thread.set('createdAt', (new Date()).toISOString())
161
230
  thread.set('comments', new Y.Array())
231
+ thread.set('deletedComments', new Y.Array())
232
+ thread.set('deletedAt', null)
162
233
 
163
234
  this.getYThreads().push([thread])
164
235
  createdThread = this.updateThread(String(thread.get('id')), data)
@@ -167,6 +238,12 @@ export class TiptapCollabProvider extends HocuspocusProvider {
167
238
  return createdThread
168
239
  }
169
240
 
241
+ /**
242
+ * Update a specific thread
243
+ * @param id The thread id
244
+ * @param data New data for the thread
245
+ * @returns The updated thread or null if the thread is not found
246
+ */
170
247
  updateThread(id: TCollabThread['id'], data: Partial<Pick<TCollabThread, 'data'> & {
171
248
  resolvedAt: TCollabThread['resolvedAt'] | null
172
249
  }>) {
@@ -195,36 +272,106 @@ export class TiptapCollabProvider extends HocuspocusProvider {
195
272
  return updatedThread
196
273
  }
197
274
 
198
- deleteThread(id: TCollabThread['id']) {
275
+ /**
276
+ * Handle the deletion of a thread. By default, the thread and it's comments are not deleted, but marked as deleted
277
+ * via the `deletedAt` property. Forceful deletion can be enabled by setting the `force` option to `true`.
278
+ *
279
+ * If you only want to delete the comments of a thread, you can set the `deleteComments` option to `true`.
280
+ * @param id The thread id
281
+ * @param options A set of options that control how the thread is deleted
282
+ * @returns The deleted thread or null if the thread is not found
283
+ */
284
+ deleteThread(id: TCollabThread['id'], options?: DeleteThreadOptions) {
285
+ const { deleteComments, force } = { ...defaultDeleteThreadOptions, ...options }
286
+
199
287
  const index = this.getThreadIndex(id)
200
288
 
201
289
  if (index === null) {
290
+ return null
291
+ }
292
+
293
+ if (force) {
294
+ this.getYThreads().delete(index, 1)
202
295
  return
203
296
  }
204
297
 
205
- this.getYThreads().delete(index, 1)
298
+ const thread = this.getYThreads().get(index)
299
+
300
+ thread.set('deletedAt', (new Date()).toISOString())
301
+
302
+ if (deleteComments) {
303
+ thread.set('comments', new Y.Array())
304
+ thread.set('deletedComments', new Y.Array())
305
+ }
306
+
307
+ return thread.toJSON() as TCollabThread
308
+ }
309
+
310
+ /**
311
+ * Tries to restore a deleted thread
312
+ * @param id The thread id
313
+ * @returns The restored thread or null if the thread is not found
314
+ */
315
+ restoreThread(id: TCollabThread['id']) {
316
+ const index = this.getThreadIndex(id)
317
+
318
+ if (index === null) {
319
+ return null
320
+ }
321
+
322
+ const thread = this.getYThreads().get(index)
323
+
324
+ thread.set('deletedAt', null)
325
+
326
+ return thread.toJSON() as TCollabThread
206
327
  }
207
328
 
208
- getThreadComments(threadId: TCollabThread['id']): TCollabComment[] | null {
329
+ /**
330
+ * Returns comments from a thread, either deleted or not
331
+ * @param threadId The thread id
332
+ * @param includeDeleted If you want to include deleted comments, defaults to `false`
333
+ * @returns The comments or null if the thread is not found
334
+ */
335
+ getThreadComments(threadId: TCollabThread['id'], includeDeleted?: boolean): TCollabComment[] | null {
209
336
  const index = this.getThreadIndex(threadId)
210
337
 
211
338
  if (index === null) {
212
339
  return null
213
340
  }
214
341
 
215
- return this.getThread(threadId)?.comments ?? []
342
+ const comments = !includeDeleted ? this.getThread(threadId)?.comments : [...(this.getThread(threadId)?.comments || []), ...(this.getThread(threadId)?.deletedComments || [])].sort((a, b) => {
343
+ return a.createdAt.localeCompare(b.createdAt)
344
+ })
345
+
346
+ return comments ?? []
216
347
  }
217
348
 
218
- getThreadComment(threadId: TCollabThread['id'], commentId: TCollabComment['id']): TCollabComment | null {
349
+ /**
350
+ * Get a single comment from a specific thread
351
+ * @param threadId The thread id
352
+ * @param commentId The comment id
353
+ * @param includeDeleted If you want to include deleted comments in the search
354
+ * @returns The comment or null if not found
355
+ */
356
+ getThreadComment(threadId: TCollabThread['id'], commentId: TCollabComment['id'], includeDeleted?: boolean): TCollabComment | null {
219
357
  const index = this.getThreadIndex(threadId)
220
358
 
221
359
  if (index === null) {
222
360
  return null
223
361
  }
224
362
 
225
- return this.getThread(threadId)?.comments.find(comment => comment.id === commentId) ?? null
363
+ const comments = this.getThreadComments(threadId, includeDeleted)
364
+
365
+ return comments?.find(comment => comment.id === commentId) ?? null
226
366
  }
227
367
 
368
+ /**
369
+ * Adds a comment to a thread
370
+ * @param threadId The thread id
371
+ * @param data The comment data
372
+ * @returns The updated thread or null if the thread is not found
373
+ * @example addComment('123', { content: 'Hello world', data: { author: 'Maria Doe' } })
374
+ */
228
375
  addComment(threadId: TCollabThread['id'], data: Omit<TCollabComment, 'id' | 'updatedAt' | 'createdAt'>) {
229
376
  let updatedThread: TCollabThread = {} as TCollabThread
230
377
 
@@ -246,6 +393,14 @@ export class TiptapCollabProvider extends HocuspocusProvider {
246
393
  return updatedThread
247
394
  }
248
395
 
396
+ /**
397
+ * Update a comment in a thread
398
+ * @param threadId The thread id
399
+ * @param commentId The comment id
400
+ * @param data The new comment data
401
+ * @returns The updated thread or null if the thread or comment is not found
402
+ * @example updateComment('123', { content: 'The new content', data: { attachments: ['file1.jpg'] }})
403
+ */
249
404
  updateComment(threadId: TCollabThread['id'], commentId: TCollabComment['id'], data: Partial<Pick<TCollabComment, 'data' | 'content'>>) {
250
405
  let updatedThread: TCollabThread = {} as TCollabThread
251
406
 
@@ -281,7 +436,16 @@ export class TiptapCollabProvider extends HocuspocusProvider {
281
436
  return updatedThread
282
437
  }
283
438
 
284
- deleteComment(threadId: TCollabThread['id'], commentId: TCollabComment['id']) {
439
+ /**
440
+ * Deletes a comment from a thread
441
+ * @param threadId The thread id
442
+ * @param commentId The comment id
443
+ * @param options A set of options that control how the comment is deleted
444
+ * @returns The updated thread or null if the thread or comment is not found
445
+ */
446
+ deleteComment(threadId: TCollabThread['id'], commentId: TCollabComment['id'], options: DeleteCommentOptions) {
447
+ const { deleteContent, deleteThread } = { ...defaultDeleteCommentOptions, ...options }
448
+
285
449
  const thread = this.getYThread(threadId)
286
450
 
287
451
  if (thread === null) return null
@@ -297,22 +461,39 @@ export class TiptapCollabProvider extends HocuspocusProvider {
297
461
 
298
462
  // if the first comment of a thread is deleted we also
299
463
  // delete the thread itself as the source comment is gone
300
- if (commentIndex === 0) {
464
+ if (commentIndex === 0 && (deleteThread || (this.configuration as TiptapCollabProviderConfiguration).deleteThreadOnFirstCommentDelete)) {
301
465
  this.deleteThread(threadId)
302
466
  return
303
467
  }
304
468
 
305
- if (commentIndex > 0) {
306
- thread.get('comments').delete(commentIndex)
307
- }
469
+ const comment = thread.get('comments').get(commentIndex)
470
+ const newComment = new Y.Map()
471
+
472
+ newComment.set('id', comment.get('id'))
473
+ newComment.set('createdAt', comment.get('createdAt'))
474
+ newComment.set('updatedAt', (new Date()).toISOString())
475
+ newComment.set('deletedAt', (new Date()).toISOString())
476
+ newComment.set('data', comment.get('data'))
477
+ newComment.set('content', deleteContent ? null : comment.get('content'))
478
+
479
+ thread.get('deletedComments').push([newComment])
480
+ thread.get('comments').delete(commentIndex)
308
481
 
309
482
  return thread.toJSON() as TCollabThread
310
483
  }
311
484
 
485
+ /**
486
+ * Start watching threads for changes
487
+ * @param callback The callback function to be called when a thread changes
488
+ */
312
489
  watchThreads(callback: () => void) {
313
490
  this.getYThreads().observeDeep(callback)
314
491
  }
315
492
 
493
+ /**
494
+ * Stop watching threads for changes
495
+ * @param callback The callback function to be removed
496
+ */
316
497
  unwatchThreads(callback: () => void) {
317
498
  this.getYThreads().unobserveDeep(callback)
318
499
  }
package/src/types.ts CHANGED
@@ -110,15 +110,18 @@ export type TCollabThread<Data = any, CommentData = any> = {
110
110
  id: string;
111
111
  createdAt: number;
112
112
  updatedAt: number;
113
+ deletedAt: number | null;
113
114
  resolvedAt?: string; // (new Date()).toISOString()
114
115
  comments: TCollabComment<CommentData>[];
116
+ deletedComments: TCollabComment<CommentData>[];
115
117
  data: Data
116
118
  }
117
119
 
118
120
  export type TCollabComment<Data = any> = {
119
121
  id: string;
120
- createdAt: number;
121
- updatedAt: number;
122
+ createdAt: string;
123
+ updatedAt: string;
124
+ deletedAt?: string;
122
125
  data: Data
123
126
  content: any
124
127
  }
@@ -183,3 +186,46 @@ export type THistoryDocumentRevertedEvent = {
183
186
  event: 'document.reverted';
184
187
  version: number;
185
188
  };
189
+
190
+ export type DeleteCommentOptions = {
191
+ /**
192
+ * If `true`, the thread will also be deleted if the deleted comment was the first comment in the thread.
193
+ */
194
+ deleteThread?: boolean
195
+
196
+ /**
197
+ * If `true`, will remove the content of the deleted comment
198
+ */
199
+ deleteContent?: boolean
200
+ }
201
+
202
+ export type DeleteThreadOptions = {
203
+ /**
204
+ * If `true`, will remove the comments on the thread,
205
+ * otherwise will only mark the thread as deleted
206
+ * and keep the comments
207
+ * @default false
208
+ */
209
+ deleteComments?: boolean
210
+
211
+ /**
212
+ * If `true`, will forcefully remove the thread and all comments,
213
+ * otherwise will only mark the thread as deleted
214
+ * and keep the comments
215
+ * @default false
216
+ */
217
+ force?: boolean,
218
+ }
219
+
220
+ /**
221
+ * The type of thread
222
+ */
223
+ export type ThreadType = 'archived' | 'unarchived'
224
+
225
+ export type GetThreadsOptions = {
226
+ /**
227
+ * The types of threads to get
228
+ * @default ['unarchived']
229
+ */
230
+ types?: Array<ThreadType>
231
+ }