@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.
- package/dist/hocuspocus-provider.cjs +172 -20
- package/dist/hocuspocus-provider.cjs.map +1 -1
- package/dist/hocuspocus-provider.esm.js +172 -20
- package/dist/hocuspocus-provider.esm.js.map +1 -1
- package/dist/packages/provider/src/HocuspocusProvider.d.ts +2 -0
- package/dist/packages/provider/src/TiptapCollabProvider.d.ts +106 -8
- package/dist/packages/provider/src/types.d.ts +42 -2
- package/package.json +2 -2
- package/src/HocuspocusProvider.ts +8 -4
- package/src/TiptapCollabProvider.ts +199 -18
- package/src/types.ts +48 -2
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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:
|
|
98
|
-
updatedAt:
|
|
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.
|
|
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.
|
|
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.
|
|
216
|
-
this.awareness?.on('update', 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.
|
|
497
|
+
this.awareness.off('update', this.boundAwarenessUpdateHandler)
|
|
494
498
|
this.awareness.destroy()
|
|
495
499
|
}
|
|
496
500
|
|
|
497
|
-
this.document.off('update', this.
|
|
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
|
|
11
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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().
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
306
|
-
|
|
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:
|
|
121
|
-
updatedAt:
|
|
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
|
+
}
|