@dxos/plugin-thread 0.6.12-staging.e11e696 → 0.6.13-main.09887cd
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/lib/browser/{ThreadArticle-4VU752F4.mjs → ThreadArticle-GBPIH47W.mjs} +3 -3
- package/dist/lib/browser/{ThreadMain-ML3P4XSD.mjs → ThreadMain-PGF7W5EV.mjs} +3 -3
- package/dist/lib/browser/{chunk-7NUYW5IW.mjs → chunk-5G3KL6TC.mjs} +4 -6
- package/dist/lib/browser/chunk-5G3KL6TC.mjs.map +7 -0
- package/dist/lib/browser/{chunk-GJRBBPXI.mjs → chunk-JFQZ5W2I.mjs} +2 -2
- package/dist/lib/browser/{chunk-GJRBBPXI.mjs.map → chunk-JFQZ5W2I.mjs.map} +1 -1
- package/dist/lib/browser/{chunk-XF5QKI75.mjs → chunk-O4YRHIZC.mjs} +2 -2
- package/dist/lib/browser/index.mjs +234 -215
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/meta.mjs +1 -1
- package/dist/lib/browser/types.mjs +2 -2
- package/dist/lib/node/{ThreadArticle-ERUHPHVT.cjs → ThreadArticle-GH5UBWFU.cjs} +7 -7
- package/dist/lib/node/{ThreadMain-6DN7FDNB.cjs → ThreadMain-CO6I3DNX.cjs} +7 -7
- package/dist/lib/node/{chunk-PZVUW4W2.cjs → chunk-FDYRG7HE.cjs} +9 -9
- package/dist/lib/node/{chunk-FXD3DYMO.cjs → chunk-FOZUQZF4.cjs} +6 -6
- package/dist/lib/node/{chunk-FXD3DYMO.cjs.map → chunk-FOZUQZF4.cjs.map} +1 -1
- package/dist/lib/node/{chunk-6EVXCSDS.cjs → chunk-JIJ7Z6NO.cjs} +6 -8
- package/dist/lib/node/chunk-JIJ7Z6NO.cjs.map +7 -0
- package/dist/lib/node/index.cjs +272 -255
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.cjs +4 -4
- package/dist/lib/node/meta.cjs.map +1 -1
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/types.cjs +3 -3
- package/dist/lib/node/types.cjs.map +1 -1
- package/dist/lib/node-esm/{ThreadArticle-NKI5F4W5.mjs → ThreadArticle-ZTYD52TR.mjs} +3 -3
- package/dist/lib/node-esm/{ThreadMain-2A3VQLCW.mjs → ThreadMain-LEHGOF7R.mjs} +3 -3
- package/dist/lib/node-esm/{chunk-Y475KZTX.mjs → chunk-DC5ZA6H5.mjs} +4 -6
- package/dist/lib/node-esm/chunk-DC5ZA6H5.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-A44C4MTQ.mjs → chunk-XOVJ65DG.mjs} +2 -2
- package/dist/lib/node-esm/{chunk-YO46JGAE.mjs → chunk-ZXIPFXSA.mjs} +2 -2
- package/dist/lib/node-esm/{chunk-YO46JGAE.mjs.map → chunk-ZXIPFXSA.mjs.map} +1 -1
- package/dist/lib/node-esm/index.mjs +234 -215
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/meta.mjs +1 -1
- package/dist/lib/node-esm/types.mjs +2 -2
- package/dist/types/src/ThreadPlugin.d.ts.map +1 -1
- package/dist/types/src/components/ThreadComplementary.d.ts +2 -2
- package/dist/types/src/components/ThreadComplementary.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +3 -2
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +2 -0
- package/dist/types/src/extensions/index.d.ts.map +1 -0
- package/dist/types/src/extensions/threads.d.ts +9 -0
- package/dist/types/src/extensions/threads.d.ts.map +1 -0
- package/dist/types/src/meta.d.ts +3 -6
- package/dist/types/src/meta.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +5 -0
- package/dist/types/src/types.d.ts.map +1 -1
- package/package.json +28 -28
- package/src/ThreadPlugin.tsx +68 -146
- package/src/components/ThreadComplementary.tsx +4 -4
- package/src/components/index.ts +3 -2
- package/src/extensions/index.ts +5 -0
- package/src/extensions/threads.ts +117 -0
- package/src/{meta.tsx → meta.ts} +3 -4
- package/src/types.ts +7 -2
- package/dist/lib/browser/chunk-7NUYW5IW.mjs.map +0 -7
- package/dist/lib/node/chunk-6EVXCSDS.cjs.map +0 -7
- package/dist/lib/node-esm/chunk-Y475KZTX.mjs.map +0 -7
- /package/dist/lib/browser/{ThreadArticle-4VU752F4.mjs.map → ThreadArticle-GBPIH47W.mjs.map} +0 -0
- /package/dist/lib/browser/{ThreadMain-ML3P4XSD.mjs.map → ThreadMain-PGF7W5EV.mjs.map} +0 -0
- /package/dist/lib/browser/{chunk-XF5QKI75.mjs.map → chunk-O4YRHIZC.mjs.map} +0 -0
- /package/dist/lib/node/{ThreadArticle-ERUHPHVT.cjs.map → ThreadArticle-GH5UBWFU.cjs.map} +0 -0
- /package/dist/lib/node/{ThreadMain-6DN7FDNB.cjs.map → ThreadMain-CO6I3DNX.cjs.map} +0 -0
- /package/dist/lib/node/{chunk-PZVUW4W2.cjs.map → chunk-FDYRG7HE.cjs.map} +0 -0
- /package/dist/lib/node-esm/{ThreadArticle-NKI5F4W5.mjs.map → ThreadArticle-ZTYD52TR.mjs.map} +0 -0
- /package/dist/lib/node-esm/{ThreadMain-2A3VQLCW.mjs.map → ThreadMain-LEHGOF7R.mjs.map} +0 -0
- /package/dist/lib/node-esm/{chunk-A44C4MTQ.mjs.map → chunk-XOVJ65DG.mjs.map} +0 -0
package/src/ThreadPlugin.tsx
CHANGED
|
@@ -2,25 +2,22 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { computed, effect } from '@preact/signals-core';
|
|
6
5
|
import React from 'react';
|
|
7
6
|
|
|
8
7
|
import {
|
|
9
8
|
type IntentPluginProvides,
|
|
10
|
-
LayoutAction,
|
|
11
|
-
NavigationAction,
|
|
12
9
|
type LocationProvides,
|
|
10
|
+
NavigationAction,
|
|
13
11
|
type Plugin,
|
|
14
12
|
type PluginDefinition,
|
|
13
|
+
isLayoutParts,
|
|
15
14
|
parseIntentPlugin,
|
|
16
15
|
parseNavigationPlugin,
|
|
17
|
-
resolvePlugin,
|
|
18
|
-
isLayoutParts,
|
|
19
16
|
parseMetadataResolverPlugin,
|
|
17
|
+
resolvePlugin,
|
|
20
18
|
} from '@dxos/app-framework';
|
|
21
19
|
import { type UnsubscribeCallback } from '@dxos/async';
|
|
22
|
-
import { type EchoReactiveObject, getTypename } from '@dxos/echo-schema';
|
|
23
|
-
import { create } from '@dxos/echo-schema';
|
|
20
|
+
import { create, type EchoReactiveObject, getTypename } from '@dxos/echo-schema';
|
|
24
21
|
import { LocalStorageStore } from '@dxos/local-storage';
|
|
25
22
|
import { log } from '@dxos/log';
|
|
26
23
|
import { parseClientPlugin } from '@dxos/plugin-client';
|
|
@@ -28,39 +25,33 @@ import { type ActionGroup, createExtension, isActionGroup, toSignal } from '@dxo
|
|
|
28
25
|
import { ObservabilityAction } from '@dxos/plugin-observability/meta';
|
|
29
26
|
import { SpaceAction } from '@dxos/plugin-space';
|
|
30
27
|
import { ThreadType, MessageType, ChannelType } from '@dxos/plugin-space/types';
|
|
31
|
-
import {
|
|
32
|
-
getSpace,
|
|
33
|
-
getTextInRange,
|
|
34
|
-
createDocAccessor,
|
|
35
|
-
fullyQualifiedId,
|
|
36
|
-
loadObjectReferences,
|
|
37
|
-
} from '@dxos/react-client/echo';
|
|
38
|
-
import { comments, createExternalCommentSync, listener } from '@dxos/react-ui-editor';
|
|
28
|
+
import { getSpace, fullyQualifiedId, loadObjectReferences, parseFullyQualifiedId } from '@dxos/react-client/echo';
|
|
39
29
|
import { translations as threadTranslations } from '@dxos/react-ui-thread';
|
|
40
|
-
import { nonNullable } from '@dxos/util';
|
|
41
30
|
|
|
42
|
-
import {
|
|
43
|
-
|
|
31
|
+
import {
|
|
32
|
+
ChatContainer,
|
|
33
|
+
ChatHeading,
|
|
34
|
+
ThreadArticle,
|
|
35
|
+
ThreadComplementary,
|
|
36
|
+
ThreadMain,
|
|
37
|
+
ThreadSettings,
|
|
38
|
+
} from './components';
|
|
39
|
+
import { threads } from './extensions';
|
|
44
40
|
import meta, { THREAD_ITEM, THREAD_PLUGIN } from './meta';
|
|
45
41
|
import translations from './translations';
|
|
46
|
-
import { ThreadAction, type ThreadPluginProvides, type ThreadSettingsProps } from './types';
|
|
47
|
-
|
|
48
|
-
type ThreadState = {
|
|
49
|
-
/** An in-memory staging area for threads that are being drafted. */
|
|
50
|
-
staging: Record<string, ThreadType[]>;
|
|
51
|
-
current?: string | undefined;
|
|
52
|
-
};
|
|
42
|
+
import { ThreadAction, type ThreadPluginProvides, type ThreadSettingsProps, type ThreadState } from './types';
|
|
53
43
|
|
|
54
44
|
type SubjectId = string;
|
|
45
|
+
|
|
55
46
|
const initialViewState = { showResolvedThreads: false };
|
|
56
|
-
type ViewStore = Record<SubjectId, typeof initialViewState>;
|
|
57
47
|
|
|
58
|
-
|
|
48
|
+
type ViewStore = Record<SubjectId, typeof initialViewState>;
|
|
59
49
|
|
|
60
50
|
// TODO(Zan): Every instance of `cursor` should be replaced with `anchor`.
|
|
51
|
+
// NOTE(burdon): Review/discuss CursorConverter semantics.
|
|
61
52
|
export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
62
53
|
const settings = new LocalStorageStore<ThreadSettingsProps>(THREAD_PLUGIN);
|
|
63
|
-
const state = create<ThreadState>({
|
|
54
|
+
const state = create<ThreadState>({ drafts: {} });
|
|
64
55
|
|
|
65
56
|
const viewStore = create<ViewStore>({});
|
|
66
57
|
const getViewState = (subjectId: string) => {
|
|
@@ -134,13 +125,13 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
134
125
|
createExtension({
|
|
135
126
|
id: `${THREAD_PLUGIN}/comments-for-subject`,
|
|
136
127
|
resolver: ({ id }) => {
|
|
128
|
+
// TODO(Zan): Find util (or make one).
|
|
137
129
|
if (!id.endsWith('~comments')) {
|
|
138
130
|
return;
|
|
139
131
|
}
|
|
140
132
|
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
const [spaceId, objectId] = subjectId?.split(':') ?? [];
|
|
133
|
+
const [subjectId] = id.split('~');
|
|
134
|
+
const [spaceId, objectId] = parseFullyQualifiedId(subjectId);
|
|
144
135
|
const space = client.spaces.get().find((space) => space.id === spaceId);
|
|
145
136
|
const object = toSignal(
|
|
146
137
|
(onChange) => {
|
|
@@ -262,8 +253,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
262
253
|
return data.plugin === meta.id ? <ThreadSettings settings={settings.values} /> : null;
|
|
263
254
|
}
|
|
264
255
|
|
|
265
|
-
case 'article':
|
|
266
|
-
case 'complementary': {
|
|
256
|
+
case 'article': {
|
|
267
257
|
const location = navigationPlugin?.provides.location;
|
|
268
258
|
|
|
269
259
|
if (data.object instanceof ChannelType && data.object.threads[0]) {
|
|
@@ -291,6 +281,10 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
291
281
|
);
|
|
292
282
|
}
|
|
293
283
|
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
case 'complementary--comments': {
|
|
294
288
|
if (
|
|
295
289
|
data.subject &&
|
|
296
290
|
typeof data.subject === 'object' &&
|
|
@@ -303,12 +297,14 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
303
297
|
<ThreadComplementary
|
|
304
298
|
role={role}
|
|
305
299
|
subject={data.subject}
|
|
306
|
-
|
|
300
|
+
drafts={state.drafts[fullyQualifiedId(data.subject)]}
|
|
307
301
|
current={state.current}
|
|
308
302
|
showResolvedThreads={showResolvedThreads}
|
|
309
303
|
/>
|
|
310
304
|
);
|
|
311
305
|
}
|
|
306
|
+
|
|
307
|
+
break;
|
|
312
308
|
}
|
|
313
309
|
}
|
|
314
310
|
|
|
@@ -327,7 +323,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
327
323
|
try {
|
|
328
324
|
// Static schema will throw an error if subject does not support threads array property.
|
|
329
325
|
subject.threads = [];
|
|
330
|
-
} catch (
|
|
326
|
+
} catch (err) {
|
|
331
327
|
log.error('Subject does not support threads array', subject?.typename);
|
|
332
328
|
return;
|
|
333
329
|
}
|
|
@@ -335,11 +331,11 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
335
331
|
|
|
336
332
|
const subjectId = fullyQualifiedId(subject);
|
|
337
333
|
const thread = create(ThreadType, { name, anchor: cursor, messages: [], status: 'staged' });
|
|
338
|
-
const
|
|
339
|
-
if (
|
|
340
|
-
|
|
334
|
+
const draft = state.drafts[subjectId];
|
|
335
|
+
if (draft) {
|
|
336
|
+
draft.push(thread);
|
|
341
337
|
} else {
|
|
342
|
-
state.
|
|
338
|
+
state.drafts[subjectId] = [thread];
|
|
343
339
|
}
|
|
344
340
|
|
|
345
341
|
return {
|
|
@@ -350,12 +346,12 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
350
346
|
action: ThreadAction.SELECT,
|
|
351
347
|
data: { current: fullyQualifiedId(thread) },
|
|
352
348
|
},
|
|
349
|
+
],
|
|
350
|
+
[
|
|
353
351
|
{
|
|
354
|
-
action:
|
|
352
|
+
action: NavigationAction.OPEN,
|
|
355
353
|
data: {
|
|
356
|
-
|
|
357
|
-
subject: subjectId,
|
|
358
|
-
state: true,
|
|
354
|
+
activeParts: { complementary: 'comments' },
|
|
359
355
|
},
|
|
360
356
|
},
|
|
361
357
|
],
|
|
@@ -363,13 +359,30 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
363
359
|
};
|
|
364
360
|
} else {
|
|
365
361
|
// NOTE: This is the standalone thread creation case.
|
|
366
|
-
return {
|
|
362
|
+
return {
|
|
363
|
+
data: create(ChannelType, { threads: [create(ThreadType, { messages: [] })] }),
|
|
364
|
+
};
|
|
367
365
|
}
|
|
368
366
|
}
|
|
369
367
|
|
|
370
368
|
case ThreadAction.SELECT: {
|
|
371
369
|
state.current = intent.data?.current;
|
|
372
|
-
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
data: true,
|
|
373
|
+
intents: !intent.data?.skipOpen
|
|
374
|
+
? [
|
|
375
|
+
[
|
|
376
|
+
{
|
|
377
|
+
action: NavigationAction.OPEN,
|
|
378
|
+
data: {
|
|
379
|
+
activeParts: { complementary: 'comments' },
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
]
|
|
384
|
+
: [],
|
|
385
|
+
};
|
|
373
386
|
}
|
|
374
387
|
|
|
375
388
|
case ThreadAction.TOGGLE_RESOLVED: {
|
|
@@ -388,6 +401,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
388
401
|
const spaceId = space?.id;
|
|
389
402
|
|
|
390
403
|
return {
|
|
404
|
+
data: true,
|
|
391
405
|
intents: [
|
|
392
406
|
[
|
|
393
407
|
{
|
|
@@ -409,13 +423,12 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
409
423
|
}
|
|
410
424
|
|
|
411
425
|
const subjectId = fullyQualifiedId(subject);
|
|
412
|
-
const
|
|
413
|
-
if (
|
|
414
|
-
// Check if we're deleting a
|
|
415
|
-
|
|
416
|
-
const index = stagingArea.findIndex((t) => t.id === thread.id);
|
|
426
|
+
const draft = state.drafts[subjectId];
|
|
427
|
+
if (draft) {
|
|
428
|
+
// Check if we're deleting a draft; if so, remove it.
|
|
429
|
+
const index = draft.findIndex((t) => t.id === thread.id);
|
|
417
430
|
if (index !== -1) {
|
|
418
|
-
|
|
431
|
+
draft.splice(index, 1);
|
|
419
432
|
return { data: true };
|
|
420
433
|
}
|
|
421
434
|
}
|
|
@@ -482,12 +495,11 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
482
495
|
const intents = [];
|
|
483
496
|
const analyticsProperties = { threadId: thread.id, spaceId: space?.id };
|
|
484
497
|
|
|
485
|
-
if (state.
|
|
486
|
-
// Move
|
|
498
|
+
if (state.drafts[subjectId]?.find((t) => t === thread)) {
|
|
499
|
+
// Move draft to document.
|
|
487
500
|
thread.status = 'active';
|
|
488
501
|
subject.threads ? subject.threads.push(thread) : (subject.threads = [thread]);
|
|
489
|
-
state.
|
|
490
|
-
|
|
502
|
+
state.drafts[subjectId] = state.drafts[subjectId]?.filter(({ id }) => id !== thread.id);
|
|
491
503
|
intents.push({
|
|
492
504
|
action: ObservabilityAction.SEND_EVENT,
|
|
493
505
|
data: { name: 'threads.thread-created', properties: analyticsProperties },
|
|
@@ -555,13 +567,11 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
555
567
|
} else if (intent.undo) {
|
|
556
568
|
const message = intent.data?.message;
|
|
557
569
|
const messageIndex = intent.data?.messageIndex;
|
|
558
|
-
|
|
559
570
|
if (!(message instanceof MessageType) || !(typeof messageIndex === 'number')) {
|
|
560
571
|
return;
|
|
561
572
|
}
|
|
562
573
|
|
|
563
574
|
thread.messages.splice(messageIndex, 0, message);
|
|
564
|
-
|
|
565
575
|
return {
|
|
566
576
|
data: true,
|
|
567
577
|
intents: [
|
|
@@ -582,96 +592,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
582
592
|
},
|
|
583
593
|
},
|
|
584
594
|
markdown: {
|
|
585
|
-
// TODO(burdon): Factor out extension factory into separate file (for simplicity).
|
|
586
595
|
extensions: ({ document: doc }) => {
|
|
587
|
-
|
|
588
|
-
if (!doc || !space) {
|
|
589
|
-
// Include no-op comments extension here to ensure that the facets are always present when they are expected.
|
|
590
|
-
// TODO(wittjosiah): The Editor should only look for these facets when comments are available.
|
|
591
|
-
return [comments()];
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// TODO(Zan): When we have the deepsignal specific equivalent of this we should use that instead.
|
|
595
|
-
const threads = computed(() =>
|
|
596
|
-
[...doc.threads.filter(nonNullable), ...(state.staging[fullyQualifiedId(doc)] ?? [])].filter(
|
|
597
|
-
(thread) => !(thread?.status === 'resolved'),
|
|
598
|
-
),
|
|
599
|
-
);
|
|
600
|
-
|
|
601
|
-
return [
|
|
602
|
-
listener({
|
|
603
|
-
onChange: () => {
|
|
604
|
-
doc.threads.forEach((thread) => {
|
|
605
|
-
if (thread?.anchor) {
|
|
606
|
-
const [start, end] = thread.anchor.split(':');
|
|
607
|
-
const name = doc.content && getTextInRange(createDocAccessor(doc.content, ['content']), start, end);
|
|
608
|
-
// TODO(burdon): This seems unsafe; review.
|
|
609
|
-
// Only update if the name has changed, otherwise this will cause an infinite loop.
|
|
610
|
-
// Skip if the name is empty - this means comment text was deleted, but thread name should remain.
|
|
611
|
-
if (name && name !== thread.name) {
|
|
612
|
-
thread.name = name;
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
});
|
|
616
|
-
},
|
|
617
|
-
}),
|
|
618
|
-
createExternalCommentSync(
|
|
619
|
-
fullyQualifiedId(doc),
|
|
620
|
-
(sink) => effect(() => sink()),
|
|
621
|
-
() =>
|
|
622
|
-
threads.value
|
|
623
|
-
.filter((thread) => thread?.anchor)
|
|
624
|
-
.map((thread) => ({ id: fullyQualifiedId(thread), cursor: thread.anchor! })),
|
|
625
|
-
),
|
|
626
|
-
comments({
|
|
627
|
-
id: fullyQualifiedId(doc),
|
|
628
|
-
onCreate: ({ cursor }) => {
|
|
629
|
-
const [start, end] = cursor.split(':');
|
|
630
|
-
const name = doc.content && getTextInRange(createDocAccessor(doc.content, ['content']), start, end);
|
|
631
|
-
|
|
632
|
-
void intentPlugin?.provides.intent.dispatch({
|
|
633
|
-
action: ThreadAction.CREATE,
|
|
634
|
-
data: {
|
|
635
|
-
cursor,
|
|
636
|
-
name,
|
|
637
|
-
subject: doc,
|
|
638
|
-
},
|
|
639
|
-
});
|
|
640
|
-
},
|
|
641
|
-
onDelete: ({ id }) => {
|
|
642
|
-
// If the thread is in the staging area, remove it.
|
|
643
|
-
const stagingArea = state.staging[fullyQualifiedId(doc)];
|
|
644
|
-
if (stagingArea) {
|
|
645
|
-
const index = stagingArea.findIndex((thread) => fullyQualifiedId(thread) === id);
|
|
646
|
-
if (index !== -1) {
|
|
647
|
-
stagingArea.splice(index, 1);
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
const thread = doc.threads.find((thread) => thread?.id === id);
|
|
652
|
-
if (thread) {
|
|
653
|
-
thread.anchor = undefined;
|
|
654
|
-
}
|
|
655
|
-
},
|
|
656
|
-
onUpdate: ({ id, cursor }) => {
|
|
657
|
-
const thread =
|
|
658
|
-
state.staging[fullyQualifiedId(doc)]?.find((thread) => fullyQualifiedId(thread) === id) ??
|
|
659
|
-
doc.threads.find((thread) => thread?.id === id);
|
|
660
|
-
|
|
661
|
-
if (thread instanceof ThreadType && thread.anchor) {
|
|
662
|
-
const [start, end] = thread.anchor.split(':');
|
|
663
|
-
thread.name = doc.content && getTextInRange(createDocAccessor(doc.content, ['content']), start, end);
|
|
664
|
-
thread.anchor = cursor;
|
|
665
|
-
}
|
|
666
|
-
},
|
|
667
|
-
onSelect: ({ selection: { current, closest } }) => {
|
|
668
|
-
const dispatch = intentPlugin?.provides.intent.dispatch;
|
|
669
|
-
if (dispatch) {
|
|
670
|
-
void dispatch([{ action: ThreadAction.SELECT, data: { current: current ?? closest } }]);
|
|
671
|
-
}
|
|
672
|
-
},
|
|
673
|
-
}),
|
|
674
|
-
];
|
|
596
|
+
return threads(state, doc, intentPlugin?.provides.intent.dispatch);
|
|
675
597
|
},
|
|
676
598
|
},
|
|
677
599
|
},
|
|
@@ -21,13 +21,13 @@ const providesThreadsConfig = (plugin: any): Plugin<ThreadProvides<any>> | undef
|
|
|
21
21
|
export const ThreadComplementary = ({
|
|
22
22
|
role,
|
|
23
23
|
subject,
|
|
24
|
-
|
|
24
|
+
drafts,
|
|
25
25
|
current,
|
|
26
26
|
showResolvedThreads,
|
|
27
27
|
}: {
|
|
28
28
|
role: string;
|
|
29
29
|
subject: any;
|
|
30
|
-
|
|
30
|
+
drafts: ThreadType[] | undefined;
|
|
31
31
|
current?: string;
|
|
32
32
|
showResolvedThreads?: boolean;
|
|
33
33
|
}) => {
|
|
@@ -42,8 +42,8 @@ export const ThreadComplementary = ({
|
|
|
42
42
|
const sort = useMemo(() => createSort?.(subject), [createSort, subject]);
|
|
43
43
|
|
|
44
44
|
const threads = useMemo(() => {
|
|
45
|
-
return subject.threads.concat(
|
|
46
|
-
}, [JSON.stringify(subject.threads), JSON.stringify(
|
|
45
|
+
return subject.threads.concat(drafts ?? []).filter(nonNullable) as ThreadType[];
|
|
46
|
+
}, [JSON.stringify(subject.threads), JSON.stringify(drafts)]);
|
|
47
47
|
|
|
48
48
|
const detachedIds = useMemo(() => {
|
|
49
49
|
return threads.filter(({ anchor }) => !anchor).map((thread) => fullyQualifiedId(thread));
|
package/src/components/index.ts
CHANGED
|
@@ -9,8 +9,9 @@ export const ThreadMain = React.lazy(() => import('./ThreadMain'));
|
|
|
9
9
|
export const ThreadArticle = React.lazy(() => import('./ThreadArticle'));
|
|
10
10
|
|
|
11
11
|
// TODO(wittjosiah): Suspense boundary for sidebar?
|
|
12
|
-
export * from './ThreadSettings';
|
|
13
12
|
export * from './CommentContainer';
|
|
14
|
-
export * from './MessageContainer';
|
|
15
13
|
export * from './CommentsContainer';
|
|
16
14
|
export * from './ChatContainer';
|
|
15
|
+
export * from './MessageContainer';
|
|
16
|
+
export * from './ThreadComplementary';
|
|
17
|
+
export * from './ThreadSettings';
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension } from '@codemirror/state';
|
|
6
|
+
import { EditorView } from '@codemirror/view';
|
|
7
|
+
import { computed, effect } from '@preact/signals-core';
|
|
8
|
+
|
|
9
|
+
import { type IntentDispatcher } from '@dxos/app-framework';
|
|
10
|
+
import { type DocumentType } from '@dxos/plugin-markdown/types';
|
|
11
|
+
import { ThreadType } from '@dxos/plugin-space/types';
|
|
12
|
+
import { getSpace, getTextInRange, createDocAccessor, fullyQualifiedId } from '@dxos/react-client/echo';
|
|
13
|
+
import { comments, createExternalCommentSync } from '@dxos/react-ui-editor';
|
|
14
|
+
import { nonNullable } from '@dxos/util';
|
|
15
|
+
|
|
16
|
+
import { ThreadAction, type ThreadState } from '../types';
|
|
17
|
+
|
|
18
|
+
// TODO(burdon): Factor out.
|
|
19
|
+
const getName = (doc: DocumentType, anchor: string): string | undefined => {
|
|
20
|
+
if (doc.content) {
|
|
21
|
+
const [start, end] = anchor.split(':');
|
|
22
|
+
return getTextInRange(createDocAccessor(doc.content, ['content']), start, end);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Construct plugins.
|
|
28
|
+
*/
|
|
29
|
+
export const threads = (state: ThreadState, doc?: DocumentType, dispatch?: IntentDispatcher): Extension => {
|
|
30
|
+
const space = doc && getSpace(doc);
|
|
31
|
+
if (!doc || !space || !dispatch) {
|
|
32
|
+
// Include no-op comments extension here to ensure that the facets are always present when they are expected.
|
|
33
|
+
// TODO(wittjosiah): The Editor should only look for these facets when comments are available.
|
|
34
|
+
return [comments()];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// TODO(Zan): When we have the deepsignal specific equivalent of this we should use that instead.
|
|
38
|
+
const threads = computed(() =>
|
|
39
|
+
[...doc.threads.filter(nonNullable), ...(state.drafts[fullyQualifiedId(doc)] ?? [])].filter(
|
|
40
|
+
(thread) => !(thread?.status === 'resolved'),
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return [
|
|
45
|
+
EditorView.updateListener.of((update) => {
|
|
46
|
+
if (update.docChanged) {
|
|
47
|
+
doc.threads.forEach((thread) => {
|
|
48
|
+
if (thread?.anchor) {
|
|
49
|
+
// Only update if the name has changed, otherwise this will cause an infinite loop.
|
|
50
|
+
// Skip if the name is empty; this means comment text was deleted, but thread name should remain.
|
|
51
|
+
const name = getName(doc, thread.anchor);
|
|
52
|
+
if (name && name !== thread.name) {
|
|
53
|
+
thread.name = name;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}),
|
|
59
|
+
|
|
60
|
+
createExternalCommentSync(
|
|
61
|
+
fullyQualifiedId(doc),
|
|
62
|
+
(sink) => effect(() => sink()),
|
|
63
|
+
() =>
|
|
64
|
+
threads.value
|
|
65
|
+
.filter((thread) => thread?.anchor)
|
|
66
|
+
.map((thread) => ({ id: fullyQualifiedId(thread), cursor: thread.anchor! })),
|
|
67
|
+
),
|
|
68
|
+
|
|
69
|
+
comments({
|
|
70
|
+
id: fullyQualifiedId(doc),
|
|
71
|
+
onCreate: ({ cursor }) => {
|
|
72
|
+
const name = getName(doc, cursor);
|
|
73
|
+
void dispatch({
|
|
74
|
+
action: ThreadAction.CREATE,
|
|
75
|
+
data: {
|
|
76
|
+
cursor,
|
|
77
|
+
name,
|
|
78
|
+
subject: doc,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
onDelete: ({ id }) => {
|
|
83
|
+
const draft = state.drafts[fullyQualifiedId(doc)];
|
|
84
|
+
if (draft) {
|
|
85
|
+
const index = draft.findIndex((thread) => fullyQualifiedId(thread) === id);
|
|
86
|
+
if (index !== -1) {
|
|
87
|
+
draft.splice(index, 1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const thread = doc.threads.find((thread) => thread?.id === id);
|
|
92
|
+
if (thread) {
|
|
93
|
+
thread.anchor = undefined;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
onUpdate: ({ id, cursor }) => {
|
|
97
|
+
const thread =
|
|
98
|
+
state.drafts[fullyQualifiedId(doc)]?.find((thread) => fullyQualifiedId(thread) === id) ??
|
|
99
|
+
doc.threads.find((thread) => thread?.id === id);
|
|
100
|
+
|
|
101
|
+
if (thread instanceof ThreadType && thread.anchor) {
|
|
102
|
+
thread.name = getName(doc, thread.anchor);
|
|
103
|
+
thread.anchor = cursor;
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
onSelect: ({ selection: { current, closest } }) => {
|
|
107
|
+
void dispatch({
|
|
108
|
+
action: ThreadAction.SELECT,
|
|
109
|
+
data: {
|
|
110
|
+
current: current ?? closest,
|
|
111
|
+
skipOpen: true,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
}),
|
|
116
|
+
];
|
|
117
|
+
};
|
package/src/{meta.tsx → meta.ts}
RENAMED
|
@@ -2,15 +2,14 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { type PluginMeta } from '@dxos/app-framework';
|
|
6
6
|
|
|
7
7
|
export const THREAD_PLUGIN = 'dxos.org/plugin/thread';
|
|
8
8
|
export const THREAD_ITEM = `${THREAD_PLUGIN}/item`;
|
|
9
9
|
|
|
10
|
-
export default
|
|
10
|
+
export default {
|
|
11
11
|
id: THREAD_PLUGIN,
|
|
12
12
|
name: 'Threads',
|
|
13
|
-
// TODO(wittjosiah): Update once scope of threads expands.
|
|
14
13
|
description: 'Comment threads on documents.',
|
|
15
14
|
icon: 'ph--chat--regular',
|
|
16
|
-
}
|
|
15
|
+
} satisfies PluginMeta;
|
package/src/types.ts
CHANGED
|
@@ -26,8 +26,7 @@ export enum ThreadAction {
|
|
|
26
26
|
TOGGLE_RESOLVED = `${THREAD_ACTION}/toggle-resolved`,
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
// TODO(Zan): Move this to the plugin-space plugin or another common location
|
|
30
|
-
// when we implement threads in sheets.
|
|
29
|
+
// TODO(Zan): Move this to the plugin-space plugin or another common location when we implement threads in sheets.
|
|
31
30
|
export type ThreadProvides<T> = {
|
|
32
31
|
thread: {
|
|
33
32
|
predicate: (obj: any) => obj is T;
|
|
@@ -49,3 +48,9 @@ export type ThreadSettingsProps = { standalone?: boolean };
|
|
|
49
48
|
export interface ThreadModel {
|
|
50
49
|
root: ThreadType;
|
|
51
50
|
}
|
|
51
|
+
|
|
52
|
+
export type ThreadState = {
|
|
53
|
+
/** In-memory draft threads. */
|
|
54
|
+
drafts: Record<string, ThreadType[]>;
|
|
55
|
+
current?: string | undefined;
|
|
56
|
+
};
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../../src/meta.tsx"],
|
|
4
|
-
"sourcesContent": ["//\n// Copyright 2023 DXOS.org\n//\n\nimport { pluginMeta } from '@dxos/app-framework';\n\nexport const THREAD_PLUGIN = 'dxos.org/plugin/thread';\nexport const THREAD_ITEM = `${THREAD_PLUGIN}/item`;\n\nexport default pluginMeta({\n id: THREAD_PLUGIN,\n name: 'Threads',\n // TODO(wittjosiah): Update once scope of threads expands.\n description: 'Comment threads on documents.',\n icon: 'ph--chat--regular',\n});\n"],
|
|
5
|
-
"mappings": ";AAIA,SAASA,kBAAkB;AAEpB,IAAMC,gBAAgB;AACtB,IAAMC,cAAc,GAAGD,aAAAA;AAE9B,IAAA,eAAeE,WAAW;EACxBC,IAAIH;EACJI,MAAM;;EAENC,aAAa;EACbC,MAAM;AACR,CAAA;",
|
|
6
|
-
"names": ["pluginMeta", "THREAD_PLUGIN", "THREAD_ITEM", "pluginMeta", "id", "name", "description", "icon"]
|
|
7
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../../src/meta.tsx"],
|
|
4
|
-
"sourcesContent": ["//\n// Copyright 2023 DXOS.org\n//\n\nimport { pluginMeta } from '@dxos/app-framework';\n\nexport const THREAD_PLUGIN = 'dxos.org/plugin/thread';\nexport const THREAD_ITEM = `${THREAD_PLUGIN}/item`;\n\nexport default pluginMeta({\n id: THREAD_PLUGIN,\n name: 'Threads',\n // TODO(wittjosiah): Update once scope of threads expands.\n description: 'Comment threads on documents.',\n icon: 'ph--chat--regular',\n});\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;AAIA,2BAA2B;AAEpB,IAAMA,gBAAgB;AACtB,IAAMC,cAAc,GAAGD,aAAAA;AAE9B,IAAA,mBAAeE,iCAAW;EACxBC,IAAIH;EACJI,MAAM;;EAENC,aAAa;EACbC,MAAM;AACR,CAAA;",
|
|
6
|
-
"names": ["THREAD_PLUGIN", "THREAD_ITEM", "pluginMeta", "id", "name", "description", "icon"]
|
|
7
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../../src/meta.tsx"],
|
|
4
|
-
"sourcesContent": ["//\n// Copyright 2023 DXOS.org\n//\n\nimport { pluginMeta } from '@dxos/app-framework';\n\nexport const THREAD_PLUGIN = 'dxos.org/plugin/thread';\nexport const THREAD_ITEM = `${THREAD_PLUGIN}/item`;\n\nexport default pluginMeta({\n id: THREAD_PLUGIN,\n name: 'Threads',\n // TODO(wittjosiah): Update once scope of threads expands.\n description: 'Comment threads on documents.',\n icon: 'ph--chat--regular',\n});\n"],
|
|
5
|
-
"mappings": ";;;AAIA,SAASA,kBAAkB;AAEpB,IAAMC,gBAAgB;AACtB,IAAMC,cAAc,GAAGD,aAAAA;AAE9B,IAAA,eAAeE,WAAW;EACxBC,IAAIH;EACJI,MAAM;;EAENC,aAAa;EACbC,MAAM;AACR,CAAA;",
|
|
6
|
-
"names": ["pluginMeta", "THREAD_PLUGIN", "THREAD_ITEM", "pluginMeta", "id", "name", "description", "icon"]
|
|
7
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/dist/lib/node-esm/{ThreadArticle-NKI5F4W5.mjs.map → ThreadArticle-ZTYD52TR.mjs.map}
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|