@dxos/plugin-thread 0.6.12-main.7907542 → 0.6.12-main.ac23639

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 (68) 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 +168 -148
  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 +196 -178
  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 +168 -148
  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/extensions/index.d.ts +2 -0
  43. package/dist/types/src/extensions/index.d.ts.map +1 -0
  44. package/dist/types/src/extensions/threads.d.ts +9 -0
  45. package/dist/types/src/extensions/threads.d.ts.map +1 -0
  46. package/dist/types/src/meta.d.ts +3 -6
  47. package/dist/types/src/meta.d.ts.map +1 -1
  48. package/dist/types/src/types.d.ts +5 -0
  49. package/dist/types/src/types.d.ts.map +1 -1
  50. package/package.json +28 -28
  51. package/src/ThreadPlugin.tsx +53 -136
  52. package/src/components/ThreadComplementary.tsx +4 -4
  53. package/src/extensions/index.ts +5 -0
  54. package/src/extensions/threads.ts +117 -0
  55. package/src/{meta.tsx → meta.ts} +3 -4
  56. package/src/types.ts +7 -2
  57. package/dist/lib/browser/chunk-7NUYW5IW.mjs.map +0 -7
  58. package/dist/lib/node/chunk-6EVXCSDS.cjs.map +0 -7
  59. package/dist/lib/node-esm/chunk-Y475KZTX.mjs.map +0 -7
  60. /package/dist/lib/browser/{ThreadArticle-4VU752F4.mjs.map → ThreadArticle-GBPIH47W.mjs.map} +0 -0
  61. /package/dist/lib/browser/{ThreadMain-ML3P4XSD.mjs.map → ThreadMain-PGF7W5EV.mjs.map} +0 -0
  62. /package/dist/lib/browser/{chunk-XF5QKI75.mjs.map → chunk-O4YRHIZC.mjs.map} +0 -0
  63. /package/dist/lib/node/{ThreadArticle-ERUHPHVT.cjs.map → ThreadArticle-GH5UBWFU.cjs.map} +0 -0
  64. /package/dist/lib/node/{ThreadMain-6DN7FDNB.cjs.map → ThreadMain-CO6I3DNX.cjs.map} +0 -0
  65. /package/dist/lib/node/{chunk-PZVUW4W2.cjs.map → chunk-FDYRG7HE.cjs.map} +0 -0
  66. /package/dist/lib/node-esm/{ThreadArticle-NKI5F4W5.mjs.map → ThreadArticle-ZTYD52TR.mjs.map} +0 -0
  67. /package/dist/lib/node-esm/{ThreadMain-2A3VQLCW.mjs.map → ThreadMain-LEHGOF7R.mjs.map} +0 -0
  68. /package/dist/lib/node-esm/{chunk-A44C4MTQ.mjs.map → chunk-XOVJ65DG.mjs.map} +0 -0
@@ -2,12 +2,10 @@
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
9
  NavigationAction,
12
10
  type LocationProvides,
13
11
  type Plugin,
@@ -28,39 +26,27 @@ import { type ActionGroup, createExtension, isActionGroup, toSignal } from '@dxo
28
26
  import { ObservabilityAction } from '@dxos/plugin-observability/meta';
29
27
  import { SpaceAction } from '@dxos/plugin-space';
30
28
  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';
29
+ import { getSpace, fullyQualifiedId, loadObjectReferences } from '@dxos/react-client/echo';
39
30
  import { translations as threadTranslations } from '@dxos/react-ui-thread';
40
- import { nonNullable } from '@dxos/util';
41
31
 
42
32
  import { ThreadMain, ThreadSettings, ChatContainer, ChatHeading, ThreadArticle } from './components';
43
33
  import { ThreadComplementary } from './components/ThreadComplementary';
34
+ import { threads } from './extensions';
44
35
  import meta, { THREAD_ITEM, THREAD_PLUGIN } from './meta';
45
36
  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
- };
37
+ import { ThreadAction, type ThreadPluginProvides, type ThreadSettingsProps, type ThreadState } from './types';
53
38
 
54
39
  type SubjectId = string;
40
+
55
41
  const initialViewState = { showResolvedThreads: false };
56
- type ViewStore = Record<SubjectId, typeof initialViewState>;
57
42
 
58
- // TODO(Zan): More robust runtime check.
43
+ type ViewStore = Record<SubjectId, typeof initialViewState>;
59
44
 
60
45
  // TODO(Zan): Every instance of `cursor` should be replaced with `anchor`.
46
+ // NOTE(burdon): Review/discuss CursorConverter semantics.
61
47
  export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
62
48
  const settings = new LocalStorageStore<ThreadSettingsProps>(THREAD_PLUGIN);
63
- const state = create<ThreadState>({ staging: {} });
49
+ const state = create<ThreadState>({ drafts: {} });
64
50
 
65
51
  const viewStore = create<ViewStore>({});
66
52
  const getViewState = (subjectId: string) => {
@@ -262,8 +248,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
262
248
  return data.plugin === meta.id ? <ThreadSettings settings={settings.values} /> : null;
263
249
  }
264
250
 
265
- case 'article':
266
- case 'complementary': {
251
+ case 'article': {
267
252
  const location = navigationPlugin?.provides.location;
268
253
 
269
254
  if (data.object instanceof ChannelType && data.object.threads[0]) {
@@ -291,6 +276,10 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
291
276
  );
292
277
  }
293
278
 
279
+ break;
280
+ }
281
+
282
+ case 'complementary--comments': {
294
283
  if (
295
284
  data.subject &&
296
285
  typeof data.subject === 'object' &&
@@ -303,12 +292,14 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
303
292
  <ThreadComplementary
304
293
  role={role}
305
294
  subject={data.subject}
306
- stagedThreads={state.staging[fullyQualifiedId(data.subject)]}
295
+ drafts={state.drafts[fullyQualifiedId(data.subject)]}
307
296
  current={state.current}
308
297
  showResolvedThreads={showResolvedThreads}
309
298
  />
310
299
  );
311
300
  }
301
+
302
+ break;
312
303
  }
313
304
  }
314
305
 
@@ -327,7 +318,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
327
318
  try {
328
319
  // Static schema will throw an error if subject does not support threads array property.
329
320
  subject.threads = [];
330
- } catch (e) {
321
+ } catch (err) {
331
322
  log.error('Subject does not support threads array', subject?.typename);
332
323
  return;
333
324
  }
@@ -335,11 +326,11 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
335
326
 
336
327
  const subjectId = fullyQualifiedId(subject);
337
328
  const thread = create(ThreadType, { name, anchor: cursor, messages: [], status: 'staged' });
338
- const stagingArea = state.staging[subjectId];
339
- if (stagingArea) {
340
- stagingArea.push(thread);
329
+ const draft = state.drafts[subjectId];
330
+ if (draft) {
331
+ draft.push(thread);
341
332
  } else {
342
- state.staging[subjectId] = [thread];
333
+ state.drafts[subjectId] = [thread];
343
334
  }
344
335
 
345
336
  return {
@@ -350,12 +341,12 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
350
341
  action: ThreadAction.SELECT,
351
342
  data: { current: fullyQualifiedId(thread) },
352
343
  },
344
+ ],
345
+ [
353
346
  {
354
- action: LayoutAction.SET_LAYOUT,
347
+ action: NavigationAction.OPEN,
355
348
  data: {
356
- element: 'complementary',
357
- subject: subjectId,
358
- state: true,
349
+ activeParts: { complementary: 'comments' },
359
350
  },
360
351
  },
361
352
  ],
@@ -363,13 +354,30 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
363
354
  };
364
355
  } else {
365
356
  // NOTE: This is the standalone thread creation case.
366
- return { data: create(ChannelType, { threads: [create(ThreadType, { messages: [] })] }) };
357
+ return {
358
+ data: create(ChannelType, { threads: [create(ThreadType, { messages: [] })] }),
359
+ };
367
360
  }
368
361
  }
369
362
 
370
363
  case ThreadAction.SELECT: {
371
364
  state.current = intent.data?.current;
372
- return { data: true };
365
+
366
+ return {
367
+ data: true,
368
+ intents: !intent.data?.skipOpen
369
+ ? [
370
+ [
371
+ {
372
+ action: NavigationAction.OPEN,
373
+ data: {
374
+ activeParts: { complementary: 'comments' },
375
+ },
376
+ },
377
+ ],
378
+ ]
379
+ : [],
380
+ };
373
381
  }
374
382
 
375
383
  case ThreadAction.TOGGLE_RESOLVED: {
@@ -388,6 +396,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
388
396
  const spaceId = space?.id;
389
397
 
390
398
  return {
399
+ data: true,
391
400
  intents: [
392
401
  [
393
402
  {
@@ -409,13 +418,12 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
409
418
  }
410
419
 
411
420
  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);
421
+ const draft = state.drafts[subjectId];
422
+ if (draft) {
423
+ // Check if we're deleting a draft; if so, remove it.
424
+ const index = draft.findIndex((t) => t.id === thread.id);
417
425
  if (index !== -1) {
418
- stagingArea.splice(index, 1);
426
+ draft.splice(index, 1);
419
427
  return { data: true };
420
428
  }
421
429
  }
@@ -482,12 +490,11 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
482
490
  const intents = [];
483
491
  const analyticsProperties = { threadId: thread.id, spaceId: space?.id };
484
492
 
485
- if (state.staging[subjectId]?.find((t) => t === thread)) {
486
- // Move thread from staging to document.
493
+ if (state.drafts[subjectId]?.find((t) => t === thread)) {
494
+ // Move draft to document.
487
495
  thread.status = 'active';
488
496
  subject.threads ? subject.threads.push(thread) : (subject.threads = [thread]);
489
- state.staging[subjectId] = state.staging[subjectId]?.filter((t) => t.id !== thread.id);
490
-
497
+ state.drafts[subjectId] = state.drafts[subjectId]?.filter(({ id }) => id !== thread.id);
491
498
  intents.push({
492
499
  action: ObservabilityAction.SEND_EVENT,
493
500
  data: { name: 'threads.thread-created', properties: analyticsProperties },
@@ -555,13 +562,11 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
555
562
  } else if (intent.undo) {
556
563
  const message = intent.data?.message;
557
564
  const messageIndex = intent.data?.messageIndex;
558
-
559
565
  if (!(message instanceof MessageType) || !(typeof messageIndex === 'number')) {
560
566
  return;
561
567
  }
562
568
 
563
569
  thread.messages.splice(messageIndex, 0, message);
564
-
565
570
  return {
566
571
  data: true,
567
572
  intents: [
@@ -582,96 +587,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
582
587
  },
583
588
  },
584
589
  markdown: {
585
- // TODO(burdon): Factor out extension factory into separate file (for simplicity).
586
590
  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
- ];
591
+ return threads(state, doc, intentPlugin?.provides.intent.dispatch);
675
592
  },
676
593
  },
677
594
  },
@@ -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));
@@ -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
- }