@dxos/plugin-thread 0.6.13 → 0.6.14-main.2b6a0f3

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 (100) hide show
  1. package/dist/lib/browser/{ThreadArticle-O64ZECMF.mjs → ThreadArticle-43QSPQ2B.mjs} +3 -3
  2. package/dist/lib/browser/{ThreadMain-ZDCRC6RG.mjs → ThreadMain-VXHMJDQC.mjs} +3 -3
  3. package/dist/lib/browser/chunk-5G3KL6TC.mjs +16 -0
  4. package/dist/lib/browser/chunk-5G3KL6TC.mjs.map +7 -0
  5. package/dist/lib/browser/{chunk-GKPZHUT3.mjs → chunk-JFQZ5W2I.mjs} +2 -2
  6. package/dist/lib/browser/{chunk-GKPZHUT3.mjs.map → chunk-JFQZ5W2I.mjs.map} +1 -1
  7. package/dist/lib/browser/{chunk-P7ZE5N5M.mjs → chunk-TDPY6D4W.mjs} +16 -16
  8. package/dist/lib/browser/chunk-TDPY6D4W.mjs.map +7 -0
  9. package/dist/lib/browser/index.mjs +409 -382
  10. package/dist/lib/browser/index.mjs.map +4 -4
  11. package/dist/lib/browser/meta.json +1 -1
  12. package/dist/lib/browser/meta.mjs +1 -1
  13. package/dist/lib/browser/types.mjs +2 -2
  14. package/dist/lib/node/{ThreadArticle-DO56JXBU.cjs → ThreadArticle-ILLKCW6I.cjs} +7 -7
  15. package/dist/lib/node/{ThreadMain-XJX75GHG.cjs → ThreadMain-7UVE5D34.cjs} +7 -7
  16. package/dist/lib/node/{chunk-D5GOVXHS.cjs → chunk-FOZUQZF4.cjs} +6 -6
  17. package/dist/lib/node/{chunk-D5GOVXHS.cjs.map → chunk-FOZUQZF4.cjs.map} +1 -1
  18. package/dist/lib/node/{chunk-BWBONZGU.cjs → chunk-JIJ7Z6NO.cjs} +7 -22
  19. package/dist/lib/node/chunk-JIJ7Z6NO.cjs.map +7 -0
  20. package/dist/lib/node/{chunk-5PZNPPQF.cjs → chunk-QVOXOTRV.cjs} +65 -65
  21. package/dist/lib/node/chunk-QVOXOTRV.cjs.map +7 -0
  22. package/dist/lib/node/index.cjs +458 -435
  23. package/dist/lib/node/index.cjs.map +4 -4
  24. package/dist/lib/node/meta.cjs +4 -4
  25. package/dist/lib/node/meta.cjs.map +1 -1
  26. package/dist/lib/node/meta.json +1 -1
  27. package/dist/lib/node/types.cjs +3 -3
  28. package/dist/lib/node/types.cjs.map +1 -1
  29. package/dist/lib/node-esm/ThreadArticle-MN2KCRY4.mjs +19 -0
  30. package/dist/lib/node-esm/ThreadArticle-MN2KCRY4.mjs.map +7 -0
  31. package/dist/lib/node-esm/ThreadMain-VKPAHZK7.mjs +27 -0
  32. package/dist/lib/node-esm/ThreadMain-VKPAHZK7.mjs.map +7 -0
  33. package/dist/lib/node-esm/chunk-5XQXCOZP.mjs +411 -0
  34. package/dist/lib/node-esm/chunk-5XQXCOZP.mjs.map +7 -0
  35. package/dist/lib/node-esm/chunk-DC5ZA6H5.mjs +18 -0
  36. package/dist/lib/node-esm/chunk-DC5ZA6H5.mjs.map +7 -0
  37. package/dist/lib/node-esm/chunk-ZXIPFXSA.mjs +21 -0
  38. package/dist/lib/node-esm/chunk-ZXIPFXSA.mjs.map +7 -0
  39. package/dist/lib/node-esm/index.mjs +1130 -0
  40. package/dist/lib/node-esm/index.mjs.map +7 -0
  41. package/dist/lib/node-esm/meta.json +1 -0
  42. package/dist/lib/node-esm/meta.mjs +12 -0
  43. package/dist/lib/node-esm/meta.mjs.map +7 -0
  44. package/dist/lib/node-esm/types.mjs +9 -0
  45. package/dist/lib/node-esm/types.mjs.map +7 -0
  46. package/dist/types/src/ThreadPlugin.d.ts.map +1 -1
  47. package/dist/types/src/components/Chat.stories.d.ts +3 -42
  48. package/dist/types/src/components/Chat.stories.d.ts.map +1 -1
  49. package/dist/types/src/components/ChatContainer.d.ts.map +1 -1
  50. package/dist/types/src/components/CommentContainer.d.ts +1 -1
  51. package/dist/types/src/components/CommentContainer.d.ts.map +1 -1
  52. package/dist/types/src/components/Comments.stories.d.ts +3 -41
  53. package/dist/types/src/components/Comments.stories.d.ts.map +1 -1
  54. package/dist/types/src/components/CommentsContainer.d.ts +1 -5
  55. package/dist/types/src/components/CommentsContainer.d.ts.map +1 -1
  56. package/dist/types/src/components/MessageContainer.d.ts.map +1 -1
  57. package/dist/types/src/components/ThreadComplementary.d.ts +10 -0
  58. package/dist/types/src/components/ThreadComplementary.d.ts.map +1 -0
  59. package/dist/types/src/components/index.d.ts +6 -6
  60. package/dist/types/src/components/index.d.ts.map +1 -1
  61. package/dist/types/src/components/testing.d.ts +2 -0
  62. package/dist/types/src/components/testing.d.ts.map +1 -1
  63. package/dist/types/src/components/types.d.ts +2 -2
  64. package/dist/types/src/components/types.d.ts.map +1 -1
  65. package/dist/types/src/extensions/index.d.ts +2 -0
  66. package/dist/types/src/extensions/index.d.ts.map +1 -0
  67. package/dist/types/src/extensions/threads.d.ts +9 -0
  68. package/dist/types/src/extensions/threads.d.ts.map +1 -0
  69. package/dist/types/src/meta.d.ts +3 -9
  70. package/dist/types/src/meta.d.ts.map +1 -1
  71. package/dist/types/src/types.d.ts +6 -1
  72. package/dist/types/src/types.d.ts.map +1 -1
  73. package/package.json +41 -37
  74. package/src/ThreadPlugin.tsx +125 -305
  75. package/src/components/Chat.stories.tsx +7 -4
  76. package/src/components/ChatContainer.tsx +7 -8
  77. package/src/components/CommentContainer.tsx +18 -19
  78. package/src/components/Comments.stories.tsx +9 -6
  79. package/src/components/CommentsContainer.tsx +18 -31
  80. package/src/components/MessageContainer.tsx +13 -23
  81. package/src/components/ThreadComplementary.tsx +113 -0
  82. package/src/components/index.ts +6 -6
  83. package/src/components/types.ts +2 -2
  84. package/src/extensions/index.ts +5 -0
  85. package/src/extensions/threads.ts +117 -0
  86. package/src/meta.ts +15 -0
  87. package/src/types.ts +8 -3
  88. package/dist/lib/browser/chunk-5IBJO53M.mjs +0 -21
  89. package/dist/lib/browser/chunk-5IBJO53M.mjs.map +0 -7
  90. package/dist/lib/browser/chunk-P7ZE5N5M.mjs.map +0 -7
  91. package/dist/lib/node/chunk-5PZNPPQF.cjs.map +0 -7
  92. package/dist/lib/node/chunk-BWBONZGU.cjs.map +0 -7
  93. package/dist/types/src/types.test.d.ts +0 -2
  94. package/dist/types/src/types.test.d.ts.map +0 -1
  95. package/src/meta.tsx +0 -20
  96. package/src/types.test.ts +0 -13
  97. /package/dist/lib/browser/{ThreadArticle-O64ZECMF.mjs.map → ThreadArticle-43QSPQ2B.mjs.map} +0 -0
  98. /package/dist/lib/browser/{ThreadMain-ZDCRC6RG.mjs.map → ThreadMain-VXHMJDQC.mjs.map} +0 -0
  99. /package/dist/lib/node/{ThreadArticle-DO56JXBU.cjs.map → ThreadArticle-ILLKCW6I.cjs.map} +0 -0
  100. /package/dist/lib/node/{ThreadMain-XJX75GHG.cjs.map → ThreadMain-7UVE5D34.cjs.map} +0 -0
@@ -2,85 +2,57 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { Chat, type IconProps } from '@phosphor-icons/react';
6
- import { computed, effect } from '@preact/signals-core';
7
5
  import React from 'react';
8
6
 
9
7
  import {
10
8
  type IntentPluginProvides,
11
- LayoutAction,
12
- NavigationAction,
13
9
  type LocationProvides,
10
+ NavigationAction,
14
11
  type Plugin,
15
12
  type PluginDefinition,
13
+ isLayoutParts,
16
14
  parseIntentPlugin,
17
15
  parseNavigationPlugin,
18
- resolvePlugin,
19
- parseGraphPlugin,
20
- SLUG_PATH_SEPARATOR,
21
- SLUG_COLLECTION_INDICATOR,
22
- isLayoutParts,
23
16
  parseMetadataResolverPlugin,
24
- type IntentDispatcher,
25
- useResolvePlugins,
17
+ resolvePlugin,
18
+ LayoutAction,
26
19
  } from '@dxos/app-framework';
27
20
  import { type UnsubscribeCallback } from '@dxos/async';
28
- import { type EchoReactiveObject, getTypename } from '@dxos/echo-schema';
29
- import { create } from '@dxos/echo-schema';
30
21
  import { LocalStorageStore } from '@dxos/local-storage';
31
- import { type AttentionPluginProvides, parseAttentionPlugin } from '@dxos/plugin-attention';
22
+ import { log } from '@dxos/log';
32
23
  import { parseClientPlugin } from '@dxos/plugin-client';
33
- import { type ActionGroup, createExtension, isActionGroup } from '@dxos/plugin-graph';
34
- import { DocumentType } from '@dxos/plugin-markdown/types';
24
+ import { type ActionGroup, createExtension, isActionGroup, toSignal } from '@dxos/plugin-graph';
35
25
  import { ObservabilityAction } from '@dxos/plugin-observability/meta';
36
26
  import { SpaceAction } from '@dxos/plugin-space';
37
27
  import { ThreadType, MessageType, ChannelType } from '@dxos/plugin-space/types';
38
- import {
39
- getSpace,
40
- getTextInRange,
41
- Filter,
42
- createDocAccessor,
43
- fullyQualifiedId,
44
- loadObjectReferences,
45
- } from '@dxos/react-client/echo';
46
- import { ScrollArea } from '@dxos/react-ui';
47
- import { createAttendableAttributes } from '@dxos/react-ui-attention';
48
- import { comments, createExternalCommentSync, listener } from '@dxos/react-ui-editor';
28
+ import { create, type EchoReactiveObject, getTypename } from '@dxos/react-client/echo';
29
+ import { getSpace, fullyQualifiedId, loadObjectReferences, parseFullyQualifiedId } from '@dxos/react-client/echo';
49
30
  import { translations as threadTranslations } from '@dxos/react-ui-thread';
50
- import { nonNullable } from '@dxos/util';
51
31
 
52
32
  import {
53
- ThreadMain,
54
- ThreadSettings,
55
- CommentsContainer,
56
- CommentsHeading,
57
33
  ChatContainer,
58
34
  ChatHeading,
59
35
  ThreadArticle,
36
+ ThreadComplementary,
37
+ ThreadMain,
38
+ ThreadSettings,
60
39
  } from './components';
40
+ import { threads } from './extensions';
61
41
  import meta, { THREAD_ITEM, THREAD_PLUGIN } from './meta';
62
42
  import translations from './translations';
63
- import { ThreadAction, type ThreadProvides, type ThreadPluginProvides, type ThreadSettingsProps } from './types';
64
-
65
- type ThreadState = {
66
- /** An in-memory staging area for threads that are being drafted. */
67
- staging: Record<string, ThreadType[]>;
68
- current?: string | undefined;
69
- focus?: boolean;
70
- };
43
+ import { ThreadAction, type ThreadPluginProvides, type ThreadSettingsProps, type ThreadState } from './types';
71
44
 
72
45
  type SubjectId = string;
46
+
73
47
  const initialViewState = { showResolvedThreads: false };
74
- type ViewStore = Record<SubjectId, typeof initialViewState>;
75
48
 
76
- // TODO(Zan): More robust runtime check.
77
- const providesThreadsConfig = (plugin: any): Plugin<ThreadProvides<any>> | undefined =>
78
- 'thread' in plugin.provides ? (plugin as Plugin<ThreadProvides<any>>) : undefined;
49
+ type ViewStore = Record<SubjectId, typeof initialViewState>;
79
50
 
80
51
  // TODO(Zan): Every instance of `cursor` should be replaced with `anchor`.
52
+ // NOTE(burdon): Review/discuss CursorConverter semantics.
81
53
  export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
82
54
  const settings = new LocalStorageStore<ThreadSettingsProps>(THREAD_PLUGIN);
83
- const state = create<ThreadState>({ staging: {} });
55
+ const state = create<ThreadState>({ drafts: {} });
84
56
 
85
57
  const viewStore = create<ViewStore>({});
86
58
  const getViewState = (subjectId: string) => {
@@ -90,10 +62,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
90
62
  return viewStore[subjectId];
91
63
  };
92
64
 
93
- let attentionPlugin: Plugin<AttentionPluginProvides> | undefined;
94
65
  let navigationPlugin: Plugin<LocationProvides> | undefined;
95
66
  let intentPlugin: Plugin<IntentPluginProvides> | undefined;
96
- let dispatch: IntentDispatcher | undefined;
97
67
 
98
68
  const unsubscribeCallbacks = [] as UnsubscribeCallback[];
99
69
 
@@ -102,45 +72,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
102
72
  ready: async (plugins) => {
103
73
  settings.prop({ key: 'standalone', type: LocalStorageStore.bool({ allowUndefined: true }) });
104
74
 
105
- attentionPlugin = resolvePlugin(plugins, parseAttentionPlugin);
106
75
  navigationPlugin = resolvePlugin(plugins, parseNavigationPlugin);
107
76
  intentPlugin = resolvePlugin(plugins, parseIntentPlugin)!;
108
- dispatch = intentPlugin?.provides.intent.dispatch;
109
-
110
- const graphPlugin = resolvePlugin(plugins, parseGraphPlugin);
111
- const client = resolvePlugin(plugins, parseClientPlugin)?.provides.client;
112
- if (!client) {
113
- return;
114
- }
115
-
116
- // TODO(wittjosiah): This is a hack to make standalone threads work in the c11y sidebar.
117
- // This should have a better solution when deck is introduced.
118
- const channelsQuery = client.spaces.query(Filter.schema(ChannelType));
119
- const queryUnsubscribe = channelsQuery.subscribe();
120
- const unsubscribe = effect(() => {
121
- const attention = attentionPlugin?.provides.attention;
122
- if (!attention?.attended) {
123
- return;
124
- }
125
-
126
- const firstAttendedNodeWithComments = Array.from(attention.attended)
127
- .map((id) => graphPlugin?.provides.graph.findNode(id))
128
- .find((node) => node?.data instanceof DocumentType && (node.data.threads?.length ?? 0) > 0);
129
-
130
- if (firstAttendedNodeWithComments) {
131
- void intentPlugin?.provides.intent.dispatch({
132
- action: NavigationAction.OPEN,
133
- data: {
134
- activeParts: {
135
- complementary: `${firstAttendedNodeWithComments.id}${SLUG_PATH_SEPARATOR}comments`,
136
- },
137
- },
138
- });
139
- }
140
- });
141
-
142
- unsubscribeCallbacks.push(queryUnsubscribe);
143
- unsubscribeCallbacks.push(unsubscribe);
144
77
  },
145
78
  unload: async () => {
146
79
  unsubscribeCallbacks.forEach((unsubscribe) => unsubscribe());
@@ -151,8 +84,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
151
84
  records: {
152
85
  [ChannelType.typename]: {
153
86
  placeholder: ['channel title placeholder', { ns: THREAD_PLUGIN }],
154
- icon: (props: IconProps) => <Chat {...props} />,
155
- iconSymbol: 'ph--chat--regular',
87
+ icon: 'ph--chat--regular',
156
88
  // TODO(wittjosiah): Move out of metadata.
157
89
  loadReferences: (channel: ChannelType) => loadObjectReferences(channel, (channel) => channel.threads),
158
90
  },
@@ -194,35 +126,46 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
194
126
  createExtension({
195
127
  id: `${THREAD_PLUGIN}/comments-for-subject`,
196
128
  resolver: ({ id }) => {
129
+ // TODO(Zan): Find util (or make one).
197
130
  if (!id.endsWith('~comments')) {
198
131
  return;
199
132
  }
200
133
 
201
- // TODO(Zan): Find util (or make one)
202
- const docId = id.split('~').at(0);
203
- const [spaceId, objectId] = docId?.split(':') ?? [];
134
+ const [subjectId] = id.split('~');
135
+ const [spaceId, objectId] = parseFullyQualifiedId(subjectId);
204
136
  const space = client.spaces.get().find((space) => space.id === spaceId);
205
- const doc = space?.db.getObjectById(objectId);
206
- if (!doc || !docId) {
137
+ const object = toSignal(
138
+ (onChange) => {
139
+ const timeout = setTimeout(async () => {
140
+ await space?.db.loadObjectById(objectId);
141
+ onChange();
142
+ });
143
+
144
+ return () => clearTimeout(timeout);
145
+ },
146
+ () => space?.db.getObjectById(objectId),
147
+ subjectId,
148
+ );
149
+ if (!object || !subjectId) {
207
150
  return;
208
151
  }
209
152
 
210
- const docMeta = metadataResolver(getTypename(doc) ?? '');
211
- const label = docMeta.label?.(doc) ||
212
- doc.name ||
213
- docMeta.placeholder || ['unnamed object threads label', { ns: THREAD_PLUGIN }];
214
- // const label = docMeta.label?.(doc) || doc.name || docMeta.placeholder;
153
+ const meta = metadataResolver(getTypename(object) ?? '');
154
+ const label = meta.label?.(object) ||
155
+ object.name ||
156
+ meta.placeholder || ['unnamed object threads label', { ns: THREAD_PLUGIN }];
215
157
 
216
- const viewState = getViewState(docId);
158
+ const viewState = getViewState(subjectId);
217
159
 
218
160
  return {
219
161
  id,
220
162
  type: 'orphan-comments-for-subject',
221
- data: doc,
163
+ data: null,
222
164
  properties: {
223
- icon: meta.iconComponent,
165
+ icon: 'ph--chat-text--regular',
224
166
  label,
225
167
  showResolvedThreads: viewState.showResolvedThreads,
168
+ object,
226
169
  },
227
170
  };
228
171
  },
@@ -256,6 +199,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
256
199
  menuItemType: 'toggle',
257
200
  isChecked: viewState.showResolvedThreads,
258
201
  testId: 'threadPlugin.toggleShowResolved',
202
+ icon: viewState.showResolvedThreads ? 'ph--eye-slash--regular' : 'ph--eye--regular',
259
203
  },
260
204
  },
261
205
  ];
@@ -287,8 +231,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
287
231
  },
288
232
  properties: {
289
233
  label: ['create channel label', { ns: THREAD_PLUGIN }],
290
- icon: (props: IconProps) => <Chat {...props} />,
291
- iconSymbol: 'ph--chat--regular',
234
+ icon: 'ph--chat--regular',
292
235
  testId: 'threadPlugin.createObject',
293
236
  },
294
237
  },
@@ -311,11 +254,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
311
254
  return data.plugin === meta.id ? <ThreadSettings settings={settings.values} /> : null;
312
255
  }
313
256
 
314
- case 'article':
315
- case 'complementary': {
257
+ case 'article': {
316
258
  const location = navigationPlugin?.provides.location;
317
- const threadsIntegrators = useResolvePlugins(providesThreadsConfig);
318
- const threadProvides = threadsIntegrators.map((p) => p.provides.thread);
319
259
 
320
260
  if (data.object instanceof ChannelType && data.object.threads[0]) {
321
261
  const channel = data.object;
@@ -342,93 +282,30 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
342
282
  );
343
283
  }
344
284
 
285
+ break;
286
+ }
287
+
288
+ case 'complementary--comments': {
345
289
  if (
346
290
  data.subject &&
347
291
  typeof data.subject === 'object' &&
348
292
  'threads' in data.subject &&
349
293
  Array.isArray(data.subject.threads)
350
294
  ) {
351
- const threads = data.subject.threads
352
- .concat(state.staging[fullyQualifiedId(data.subject)])
353
- .filter(nonNullable);
354
-
355
- const createSort = threadProvides.find((p) => p.predicate(data.subject))?.createSort;
356
- if (createSort) {
357
- const sort = createSort(data.subject);
358
- threads.sort((a, b) => sort(a.anchor, b.anchor));
359
- }
360
-
361
- const detached = data.subject.threads
362
- .filter(nonNullable)
363
- .filter(({ anchor }) => !anchor)
364
- .map((thread) => thread.id);
365
-
366
- const qualifiedSubjectId = fullyQualifiedId(data.subject);
367
- const attention = attentionPlugin?.provides.attention?.attended ?? new Set([qualifiedSubjectId]);
368
- const attendableAttrs = createAttendableAttributes(qualifiedSubjectId);
369
- const { showResolvedThreads } = getViewState(qualifiedSubjectId);
295
+ const { showResolvedThreads } = getViewState(fullyQualifiedId(data.subject));
370
296
 
371
297
  return (
372
- <div role='none' className='contents group/attention' {...attendableAttrs}>
373
- {role === 'complementary' && <CommentsHeading attendableId={qualifiedSubjectId} />}
374
- <ScrollArea.Root classNames='row-span-2'>
375
- <ScrollArea.Viewport>
376
- <CommentsContainer
377
- threads={threads}
378
- detached={detached}
379
- currentId={attention.has(qualifiedSubjectId) ? state.current : undefined}
380
- autoFocusCurrentTextbox={state.focus}
381
- showResolvedThreads={showResolvedThreads}
382
- onThreadAttend={(thread) => {
383
- if (state.current !== thread.id) {
384
- state.current = thread.id;
385
- void dispatch?.({
386
- action: LayoutAction.SCROLL_INTO_VIEW,
387
- data: {
388
- id: fullyQualifiedId(data.subject!),
389
- thread: fullyQualifiedId(thread),
390
- cursor: thread.anchor,
391
- },
392
- });
393
- }
394
- }}
395
- onThreadDelete={(thread) =>
396
- dispatch?.({
397
- plugin: THREAD_PLUGIN,
398
- action: ThreadAction.DELETE,
399
- data: { subject: data.subject, thread },
400
- })
401
- }
402
- onMessageDelete={(thread, messageId) =>
403
- dispatch?.({
404
- plugin: THREAD_PLUGIN,
405
- action: ThreadAction.DELETE_MESSAGE,
406
- data: { subject: data.subject, thread, messageId },
407
- })
408
- }
409
- onThreadToggleResolved={(thread) =>
410
- dispatch?.({
411
- plugin: THREAD_PLUGIN,
412
- action: ThreadAction.TOGGLE_RESOLVED,
413
- data: { thread },
414
- })
415
- }
416
- onComment={(thread) => {
417
- void intentPlugin?.provides.intent.dispatch({
418
- action: ThreadAction.ON_MESSAGE_ADD,
419
- data: { thread, subject: data.subject },
420
- });
421
- }}
422
- />
423
- <div role='none' className='bs-10' />
424
- <ScrollArea.Scrollbar>
425
- <ScrollArea.Thumb />
426
- </ScrollArea.Scrollbar>
427
- </ScrollArea.Viewport>
428
- </ScrollArea.Root>
429
- </div>
298
+ <ThreadComplementary
299
+ role={role}
300
+ subject={data.subject}
301
+ drafts={state.drafts[fullyQualifiedId(data.subject)]}
302
+ current={state.current}
303
+ showResolvedThreads={showResolvedThreads}
304
+ />
430
305
  );
431
306
  }
307
+
308
+ break;
432
309
  }
433
310
  }
434
311
 
@@ -442,36 +319,48 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
442
319
  if (intent.data && intent.data.cursor !== undefined) {
443
320
  const { cursor, name, subject } = intent.data;
444
321
 
322
+ // Seed the threads array if it does not exist.
323
+ if (subject?.threads === undefined) {
324
+ try {
325
+ // Static schema will throw an error if subject does not support threads array property.
326
+ subject.threads = [];
327
+ } catch (err) {
328
+ log.error('Subject does not support threads array', subject?.typename);
329
+ return;
330
+ }
331
+ }
332
+
445
333
  const subjectId = fullyQualifiedId(subject);
446
334
  const thread = create(ThreadType, { name, anchor: cursor, messages: [], status: 'staged' });
447
- const stagingArea = state.staging[subjectId];
448
- if (stagingArea) {
449
- stagingArea.push(thread);
335
+ const draft = state.drafts[subjectId];
336
+ if (draft) {
337
+ draft.push(thread);
450
338
  } else {
451
- state.staging[subjectId] = [thread];
339
+ state.drafts[subjectId] = [thread];
452
340
  }
453
341
 
454
342
  return {
455
343
  data: thread,
456
344
  intents: [
345
+ [
346
+ {
347
+ action: ThreadAction.SELECT,
348
+ data: { current: fullyQualifiedId(thread) },
349
+ },
350
+ ],
457
351
  [
458
352
  {
459
353
  action: NavigationAction.OPEN,
460
354
  data: {
461
- activeParts: {
462
- complementary: `${subjectId}${SLUG_PATH_SEPARATOR}comments${SLUG_COLLECTION_INDICATOR}`,
463
- },
355
+ activeParts: { complementary: 'comments' },
464
356
  },
465
357
  },
466
- {
467
- action: ThreadAction.SELECT,
468
- data: { current: thread.id, focus: true },
469
- },
358
+ ],
359
+ [
470
360
  {
471
361
  action: LayoutAction.SET_LAYOUT,
472
362
  data: {
473
363
  element: 'complementary',
474
- subject: subjectId,
475
364
  state: true,
476
365
  },
477
366
  },
@@ -480,14 +369,30 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
480
369
  };
481
370
  } else {
482
371
  // NOTE: This is the standalone thread creation case.
483
- return { data: create(ChannelType, { threads: [create(ThreadType, { messages: [] })] }) };
372
+ return {
373
+ data: create(ChannelType, { threads: [create(ThreadType, { messages: [] })] }),
374
+ };
484
375
  }
485
376
  }
486
377
 
487
378
  case ThreadAction.SELECT: {
488
- state.focus = intent.data?.current === state.current ? state.focus : intent.data?.focus;
489
379
  state.current = intent.data?.current;
490
- return { data: true };
380
+
381
+ return {
382
+ data: true,
383
+ intents: !intent.data?.skipOpen
384
+ ? [
385
+ [
386
+ {
387
+ action: NavigationAction.OPEN,
388
+ data: {
389
+ activeParts: { complementary: 'comments' },
390
+ },
391
+ },
392
+ ],
393
+ ]
394
+ : [],
395
+ };
491
396
  }
492
397
 
493
398
  case ThreadAction.TOGGLE_RESOLVED: {
@@ -506,6 +411,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
506
411
  const spaceId = space?.id;
507
412
 
508
413
  return {
414
+ data: true,
509
415
  intents: [
510
416
  [
511
417
  {
@@ -519,19 +425,21 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
519
425
 
520
426
  case ThreadAction.DELETE: {
521
427
  const { subject, thread } = intent.data ?? {};
522
- if (!(subject instanceof DocumentType) || !(thread instanceof ThreadType)) {
428
+ if (
429
+ !(thread instanceof ThreadType) ||
430
+ !(subject && typeof subject === 'object' && 'threads' in subject)
431
+ ) {
523
432
  return;
524
433
  }
525
434
 
526
435
  const subjectId = fullyQualifiedId(subject);
527
- const stagingArea = state.staging[subjectId];
528
- if (stagingArea) {
529
- // Check if we're deleting a thread that's in the staging area.
530
- // If so, remove it from the staging area without ceremony.
531
- const index = stagingArea.findIndex((t) => t.id === thread.id);
436
+ const draft = state.drafts[subjectId];
437
+ if (draft) {
438
+ // Check if we're deleting a draft; if so, remove it.
439
+ const index = draft.findIndex((t) => t.id === thread.id);
532
440
  if (index !== -1) {
533
- stagingArea.splice(index, 1);
534
- return;
441
+ draft.splice(index, 1);
442
+ return { data: true };
535
443
  }
536
444
  }
537
445
 
@@ -541,7 +449,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
541
449
  }
542
450
 
543
451
  if (!intent.undo) {
544
- const index = subject.threads.findIndex((t) => t?.id === thread.id);
452
+ const index = subject.threads.findIndex((t: any) => t?.id === thread.id);
545
453
  const cursor = subject.threads[index]?.anchor;
546
454
  if (index !== -1) {
547
455
  subject.threads?.splice(index, 1);
@@ -587,7 +495,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
587
495
  const { thread, subject } = intent.data ?? {};
588
496
  if (
589
497
  !(thread instanceof ThreadType) ||
590
- !(subject && typeof subject === 'object' && 'threads' in subject)
498
+ !(subject && typeof subject === 'object' && 'threads' in subject && Array.isArray(subject.threads))
591
499
  ) {
592
500
  return;
593
501
  }
@@ -597,12 +505,11 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
597
505
  const intents = [];
598
506
  const analyticsProperties = { threadId: thread.id, spaceId: space?.id };
599
507
 
600
- if (state.staging[subjectId]?.find((t) => t === thread)) {
601
- // Move thread from staging to document.
508
+ if (state.drafts[subjectId]?.find((t) => t === thread)) {
509
+ // Move draft to document.
602
510
  thread.status = 'active';
603
511
  subject.threads ? subject.threads.push(thread) : (subject.threads = [thread]);
604
- state.staging[subjectId] = state.staging[subjectId]?.filter((t) => t.id !== thread.id);
605
-
512
+ state.drafts[subjectId] = state.drafts[subjectId]?.filter(({ id }) => id !== thread.id);
606
513
  intents.push({
607
514
  action: ObservabilityAction.SEND_EVENT,
608
515
  data: { name: 'threads.thread-created', properties: analyticsProperties },
@@ -627,7 +534,10 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
627
534
  const { subject, thread, messageId } = intent.data ?? {};
628
535
  const space = getSpace(subject);
629
536
 
630
- if (!(subject instanceof DocumentType) || !(thread instanceof ThreadType) || !space) {
537
+ if (
538
+ !(thread instanceof ThreadType) ||
539
+ !(subject && typeof subject === 'object' && 'threads' in subject)
540
+ ) {
631
541
  return;
632
542
  }
633
543
 
@@ -658,7 +568,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
658
568
  action: ObservabilityAction.SEND_EVENT,
659
569
  data: {
660
570
  name: 'threads.message.delete',
661
- properties: { threadId: thread.id, spaceId: space.id },
571
+ properties: { threadId: thread.id, spaceId: space?.id },
662
572
  },
663
573
  },
664
574
  ],
@@ -667,13 +577,11 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
667
577
  } else if (intent.undo) {
668
578
  const message = intent.data?.message;
669
579
  const messageIndex = intent.data?.messageIndex;
670
-
671
580
  if (!(message instanceof MessageType) || !(typeof messageIndex === 'number')) {
672
581
  return;
673
582
  }
674
583
 
675
584
  thread.messages.splice(messageIndex, 0, message);
676
-
677
585
  return {
678
586
  data: true,
679
587
  intents: [
@@ -682,7 +590,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
682
590
  action: ObservabilityAction.SEND_EVENT,
683
591
  data: {
684
592
  name: 'threads.message.undo-delete',
685
- properties: { threadId: thread.id, spaceId: space.id },
593
+ properties: { threadId: thread.id, spaceId: space?.id },
686
594
  },
687
595
  },
688
596
  ],
@@ -694,96 +602,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
694
602
  },
695
603
  },
696
604
  markdown: {
697
- // TODO(burdon): Factor out extension factory into separate file (for simplicity).
698
605
  extensions: ({ document: doc }) => {
699
- const space = doc && getSpace(doc);
700
- if (!doc || !space) {
701
- // Include no-op comments extension here to ensure that the facets are always present when they are expected.
702
- // TODO(wittjosiah): The Editor should only look for these facets when comments are available.
703
- return [comments()];
704
- }
705
-
706
- // TODO(Zan): When we have the deepsignal specific equivalent of this we should use that instead.
707
- const threads = computed(() =>
708
- [...doc.threads.filter(nonNullable), ...(state.staging[fullyQualifiedId(doc)] ?? [])].filter(
709
- (thread) => !(thread?.status === 'resolved'),
710
- ),
711
- );
712
-
713
- return [
714
- listener({
715
- onChange: () => {
716
- doc.threads.forEach((thread) => {
717
- if (thread?.anchor) {
718
- const [start, end] = thread.anchor.split(':');
719
- const name = doc.content && getTextInRange(createDocAccessor(doc.content, ['content']), start, end);
720
- // TODO(burdon): This seems unsafe; review.
721
- // Only update if the name has changed, otherwise this will cause an infinite loop.
722
- // Skip if the name is empty - this means comment text was deleted, but thread name should remain.
723
- if (name && name !== thread.name) {
724
- thread.name = name;
725
- }
726
- }
727
- });
728
- },
729
- }),
730
- createExternalCommentSync(
731
- doc.id,
732
- (sink) => effect(() => sink()),
733
- () =>
734
- threads.value
735
- .filter((thread) => thread?.anchor)
736
- .map((thread) => ({ id: thread.id, cursor: thread.anchor! })),
737
- ),
738
- comments({
739
- id: doc.id,
740
- onCreate: ({ cursor }) => {
741
- const [start, end] = cursor.split(':');
742
- const name = doc.content && getTextInRange(createDocAccessor(doc.content, ['content']), start, end);
743
-
744
- void intentPlugin?.provides.intent.dispatch({
745
- action: ThreadAction.CREATE,
746
- data: {
747
- cursor,
748
- name,
749
- subject: doc,
750
- },
751
- });
752
- },
753
- onDelete: ({ id }) => {
754
- // If the thread is in the staging area, remove it.
755
- const stagingArea = state.staging[fullyQualifiedId(doc)];
756
- if (stagingArea) {
757
- const index = stagingArea.findIndex((thread) => thread.id === id);
758
- if (index !== -1) {
759
- stagingArea.splice(index, 1);
760
- }
761
- }
762
-
763
- const thread = doc.threads.find((thread) => thread?.id === id);
764
- if (thread) {
765
- thread.anchor = undefined;
766
- }
767
- },
768
- onUpdate: ({ id, cursor }) => {
769
- const thread =
770
- state.staging[fullyQualifiedId(doc)]?.find((thread) => thread.id === id) ??
771
- doc.threads.find((thread) => thread?.id === id);
772
-
773
- if (thread instanceof ThreadType && thread.anchor) {
774
- const [start, end] = thread.anchor.split(':');
775
- thread.name = doc.content && getTextInRange(createDocAccessor(doc.content, ['content']), start, end);
776
- thread.anchor = cursor;
777
- }
778
- },
779
- onSelect: ({ selection: { current, closest } }) => {
780
- const dispatch = intentPlugin?.provides.intent.dispatch;
781
- if (dispatch) {
782
- void dispatch([{ action: ThreadAction.SELECT, data: { current: current ?? closest } }]);
783
- }
784
- },
785
- }),
786
- ];
606
+ return threads(state, doc, intentPlugin?.provides.intent.dispatch);
787
607
  },
788
608
  },
789
609
  },