@dxos/plugin-thread 0.6.12-staging.e11e696 → 0.6.13-main.041e8aa

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.
Files changed (72) hide show
  1. package/dist/lib/browser/{ThreadArticle-4VU752F4.mjs → ThreadArticle-GBPIH47W.mjs} +3 -3
  2. package/dist/lib/browser/{ThreadMain-ML3P4XSD.mjs → ThreadMain-PGF7W5EV.mjs} +3 -3
  3. package/dist/lib/browser/{chunk-7NUYW5IW.mjs → chunk-5G3KL6TC.mjs} +4 -6
  4. package/dist/lib/browser/chunk-5G3KL6TC.mjs.map +7 -0
  5. package/dist/lib/browser/{chunk-GJRBBPXI.mjs → chunk-JFQZ5W2I.mjs} +2 -2
  6. package/dist/lib/browser/{chunk-GJRBBPXI.mjs.map → chunk-JFQZ5W2I.mjs.map} +1 -1
  7. package/dist/lib/browser/{chunk-XF5QKI75.mjs → chunk-O4YRHIZC.mjs} +2 -2
  8. package/dist/lib/browser/index.mjs +234 -215
  9. package/dist/lib/browser/index.mjs.map +4 -4
  10. package/dist/lib/browser/meta.json +1 -1
  11. package/dist/lib/browser/meta.mjs +1 -1
  12. package/dist/lib/browser/types.mjs +2 -2
  13. package/dist/lib/node/{ThreadArticle-ERUHPHVT.cjs → ThreadArticle-GH5UBWFU.cjs} +7 -7
  14. package/dist/lib/node/{ThreadMain-6DN7FDNB.cjs → ThreadMain-CO6I3DNX.cjs} +7 -7
  15. package/dist/lib/node/{chunk-PZVUW4W2.cjs → chunk-FDYRG7HE.cjs} +9 -9
  16. package/dist/lib/node/{chunk-FXD3DYMO.cjs → chunk-FOZUQZF4.cjs} +6 -6
  17. package/dist/lib/node/{chunk-FXD3DYMO.cjs.map → chunk-FOZUQZF4.cjs.map} +1 -1
  18. package/dist/lib/node/{chunk-6EVXCSDS.cjs → chunk-JIJ7Z6NO.cjs} +6 -8
  19. package/dist/lib/node/chunk-JIJ7Z6NO.cjs.map +7 -0
  20. package/dist/lib/node/index.cjs +272 -255
  21. package/dist/lib/node/index.cjs.map +4 -4
  22. package/dist/lib/node/meta.cjs +4 -4
  23. package/dist/lib/node/meta.cjs.map +1 -1
  24. package/dist/lib/node/meta.json +1 -1
  25. package/dist/lib/node/types.cjs +3 -3
  26. package/dist/lib/node/types.cjs.map +1 -1
  27. package/dist/lib/node-esm/{ThreadArticle-NKI5F4W5.mjs → ThreadArticle-ZTYD52TR.mjs} +3 -3
  28. package/dist/lib/node-esm/{ThreadMain-2A3VQLCW.mjs → ThreadMain-LEHGOF7R.mjs} +3 -3
  29. package/dist/lib/node-esm/{chunk-Y475KZTX.mjs → chunk-DC5ZA6H5.mjs} +4 -6
  30. package/dist/lib/node-esm/chunk-DC5ZA6H5.mjs.map +7 -0
  31. package/dist/lib/node-esm/{chunk-A44C4MTQ.mjs → chunk-XOVJ65DG.mjs} +2 -2
  32. package/dist/lib/node-esm/{chunk-YO46JGAE.mjs → chunk-ZXIPFXSA.mjs} +2 -2
  33. package/dist/lib/node-esm/{chunk-YO46JGAE.mjs.map → chunk-ZXIPFXSA.mjs.map} +1 -1
  34. package/dist/lib/node-esm/index.mjs +234 -215
  35. package/dist/lib/node-esm/index.mjs.map +4 -4
  36. package/dist/lib/node-esm/meta.json +1 -1
  37. package/dist/lib/node-esm/meta.mjs +1 -1
  38. package/dist/lib/node-esm/types.mjs +2 -2
  39. package/dist/types/src/ThreadPlugin.d.ts.map +1 -1
  40. package/dist/types/src/components/ThreadComplementary.d.ts +2 -2
  41. package/dist/types/src/components/ThreadComplementary.d.ts.map +1 -1
  42. package/dist/types/src/components/index.d.ts +3 -2
  43. package/dist/types/src/components/index.d.ts.map +1 -1
  44. package/dist/types/src/components/testing.d.ts.map +1 -1
  45. package/dist/types/src/extensions/index.d.ts +2 -0
  46. package/dist/types/src/extensions/index.d.ts.map +1 -0
  47. package/dist/types/src/extensions/threads.d.ts +9 -0
  48. package/dist/types/src/extensions/threads.d.ts.map +1 -0
  49. package/dist/types/src/meta.d.ts +3 -6
  50. package/dist/types/src/meta.d.ts.map +1 -1
  51. package/dist/types/src/types.d.ts +5 -0
  52. package/dist/types/src/types.d.ts.map +1 -1
  53. package/package.json +28 -28
  54. package/src/ThreadPlugin.tsx +68 -146
  55. package/src/components/ThreadComplementary.tsx +4 -4
  56. package/src/components/index.ts +3 -2
  57. package/src/extensions/index.ts +5 -0
  58. package/src/extensions/threads.ts +117 -0
  59. package/src/{meta.tsx → meta.ts} +3 -4
  60. package/src/types.ts +7 -2
  61. package/dist/lib/browser/chunk-7NUYW5IW.mjs.map +0 -7
  62. package/dist/lib/node/chunk-6EVXCSDS.cjs.map +0 -7
  63. package/dist/lib/node-esm/chunk-Y475KZTX.mjs.map +0 -7
  64. /package/dist/lib/browser/{ThreadArticle-4VU752F4.mjs.map → ThreadArticle-GBPIH47W.mjs.map} +0 -0
  65. /package/dist/lib/browser/{ThreadMain-ML3P4XSD.mjs.map → ThreadMain-PGF7W5EV.mjs.map} +0 -0
  66. /package/dist/lib/browser/{chunk-XF5QKI75.mjs.map → chunk-O4YRHIZC.mjs.map} +0 -0
  67. /package/dist/lib/node/{ThreadArticle-ERUHPHVT.cjs.map → ThreadArticle-GH5UBWFU.cjs.map} +0 -0
  68. /package/dist/lib/node/{ThreadMain-6DN7FDNB.cjs.map → ThreadMain-CO6I3DNX.cjs.map} +0 -0
  69. /package/dist/lib/node/{chunk-PZVUW4W2.cjs.map → chunk-FDYRG7HE.cjs.map} +0 -0
  70. /package/dist/lib/node-esm/{ThreadArticle-NKI5F4W5.mjs.map → ThreadArticle-ZTYD52TR.mjs.map} +0 -0
  71. /package/dist/lib/node-esm/{ThreadMain-2A3VQLCW.mjs.map → ThreadMain-LEHGOF7R.mjs.map} +0 -0
  72. /package/dist/lib/node-esm/{chunk-A44C4MTQ.mjs.map → chunk-XOVJ65DG.mjs.map} +0 -0
@@ -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 { ThreadMain, ThreadSettings, ChatContainer, ChatHeading, ThreadArticle } from './components';
43
- import { ThreadComplementary } from './components/ThreadComplementary';
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
- // TODO(Zan): More robust runtime check.
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>({ staging: {} });
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
- // TODO(Zan): Find util (or make one).
142
- const subjectId = id.split('~').at(0);
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
- stagedThreads={state.staging[fullyQualifiedId(data.subject)]}
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 (e) {
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 stagingArea = state.staging[subjectId];
339
- if (stagingArea) {
340
- stagingArea.push(thread);
334
+ const draft = state.drafts[subjectId];
335
+ if (draft) {
336
+ draft.push(thread);
341
337
  } else {
342
- state.staging[subjectId] = [thread];
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: LayoutAction.SET_LAYOUT,
352
+ action: NavigationAction.OPEN,
355
353
  data: {
356
- element: 'complementary',
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 { data: create(ChannelType, { threads: [create(ThreadType, { messages: [] })] }) };
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
- return { data: true };
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 stagingArea = state.staging[subjectId];
413
- if (stagingArea) {
414
- // Check if we're deleting a thread that's in the staging area.
415
- // If so, remove it from the staging area without ceremony.
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
- stagingArea.splice(index, 1);
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.staging[subjectId]?.find((t) => t === thread)) {
486
- // Move thread from staging to document.
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.staging[subjectId] = state.staging[subjectId]?.filter((t) => t.id !== thread.id);
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
- const space = doc && getSpace(doc);
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
- stagedThreads,
24
+ drafts,
25
25
  current,
26
26
  showResolvedThreads,
27
27
  }: {
28
28
  role: string;
29
29
  subject: any;
30
- stagedThreads: ThreadType[] | undefined;
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(stagedThreads ?? []).filter(nonNullable) as ThreadType[];
46
- }, [JSON.stringify(subject.threads), JSON.stringify(stagedThreads)]);
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));
@@ -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,5 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './threads';
@@ -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
+ };
@@ -2,15 +2,14 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { pluginMeta } from '@dxos/app-framework';
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 pluginMeta({
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
- }